aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--background_scripts/commands.coffee64
-rw-r--r--background_scripts/completion.coffee24
-rw-r--r--background_scripts/completion_engines.coffee60
-rw-r--r--background_scripts/main.coffee14
-rw-r--r--content_scripts/vimium.css11
-rw-r--r--content_scripts/vimium_frontend.coffee5
-rw-r--r--content_scripts/vomnibar.coffee34
-rw-r--r--lib/utils.coffee33
-rw-r--r--pages/help_dialog.html1
-rw-r--r--pages/vomnibar.coffee3
-rw-r--r--tests/dom_tests/vomnibar_test.coffee1
12 files changed, 144 insertions, 111 deletions
diff --git a/README.md b/README.md
index b4393dbe..ad40f3ba 100644
--- a/README.md
+++ b/README.md
@@ -145,8 +145,9 @@ Release Notes
-------------
1.52 (not yet released)
-- Search engine completion for selected search engines (including Google, Youtube, Bing, DuckDuckGo, Wikipedia and Amazon).
-- Much improved custom search engine experience (including completion, where available).
+- Improved custom-search engine experience (including completion for Google,
+ Youtube, Bing, DuckDuckGo, Wikipedia, Amazon and a number of other search
+ engines).
- Bug fixes: bookmarklets accessed from the vomnibar.
1.51 (2015-05-02)
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index 63e9c718..a51ff075 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -24,21 +24,13 @@ Commands =
noRepeat: options.noRepeat
repeatLimit: options.repeatLimit
- mapKeyToCommand: (key, command) ->
+ mapKeyToCommand: ({ key, command, options }) ->
unless @availableCommands[command]
- console.log(command, "doesn't exist!")
+ console.log command, "doesn't exist!"
return
- commandDetails = @availableCommands[command]
-
- @keyToCommandRegistry[key] =
- command: command
- isBackgroundCommand: commandDetails.isBackgroundCommand
- passCountToFunction: commandDetails.passCountToFunction
- noRepeat: commandDetails.noRepeat
- repeatLimit: commandDetails.repeatLimit
-
- unmapKey: (key) -> delete @keyToCommandRegistry[key]
+ options ?= []
+ @keyToCommandRegistry[key] = extend { command, options }, @availableCommands[command]
# Lower-case the appropriate portions of named keys.
#
@@ -54,37 +46,29 @@ Commands =
"<" + (if optionalPrefix then optionalPrefix else "") + keyName.toLowerCase() + ">")
parseCustomKeyMappings: (customKeyMappings) ->
- lines = customKeyMappings.split("\n")
-
- for line in lines
- continue if (line[0] == "\"" || line[0] == "#")
- splitLine = line.replace(/\s+$/, "").split(/\s+/)
-
- lineCommand = splitLine[0]
-
- if (lineCommand == "map")
- continue if (splitLine.length != 3)
- key = @normalizeKey(splitLine[1])
- vimiumCommand = splitLine[2]
-
- continue unless @availableCommands[vimiumCommand]
-
- console.log("Mapping", key, "to", vimiumCommand)
- @mapKeyToCommand(key, vimiumCommand)
- else if (lineCommand == "unmap")
- continue if (splitLine.length != 2)
-
- key = @normalizeKey(splitLine[1])
- console.log("Unmapping", key)
- @unmapKey(key)
- else if (lineCommand == "unmapAll")
- @keyToCommandRegistry = {}
+ for line in customKeyMappings.split "\n"
+ unless line[0] == "\"" or line[0] == "#"
+ tokens = line.replace(/\s+$/, "").split /\s+/
+ switch tokens[0]
+ when "map"
+ [ _, key, command, options... ] = tokens
+ if command? and @availableCommands[command]
+ key = @normalizeKey key
+ console.log "Mapping", key, "to", command
+ @mapKeyToCommand { key, command, options }
+
+ when "unmap"
+ if tokens.length == 2
+ key = @normalizeKey tokens[1]
+ console.log "Unmapping", key
+ delete @keyToCommandRegistry[key]
+
+ when "unmapAll"
+ @keyToCommandRegistry = {}
clearKeyMappingsAndSetDefaults: ->
@keyToCommandRegistry = {}
-
- for key of defaultKeyMappings
- @mapKeyToCommand(key, defaultKeyMappings[key])
+ @mapKeyToCommand { key, command } for key, command of defaultKeyMappings
# An ordered listing of all available commands, grouped by type. This is the order they will
# be shown in the help page.
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index bae73b8d..c83066a6 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -413,7 +413,6 @@ class TabCompleter
class SearchEngineCompleter
@debug: false
- searchEngines: null
previousSuggestions: null
cancel: ->
@@ -422,7 +421,7 @@ class SearchEngineCompleter
# This looks up the custom search engine and, if one is found, notes it and removes its keyword from the
# query terms.
preprocessRequest: (request) ->
- @searchEngines.use (engines) =>
+ SearchEngines.use (engines) =>
{ queryTerms, query } = request
extend request, searchEngines: engines, keywords: key for own key of engines
keyword = queryTerms[0]
@@ -436,26 +435,7 @@ class SearchEngineCompleter
refresh: (port) ->
@previousSuggestions = {}
- # Parse the search-engine configuration.
- @searchEngines = new AsyncDataFetcher (callback) ->
- engines = {}
- for line in Settings.get("searchEngines").split "\n"
- line = line.trim()
- continue if /^[#"]/.test line
- tokens = line.split /\s+/
- continue unless 2 <= tokens.length
- keyword = tokens[0].split(":")[0]
- url = tokens[1]
- description = tokens[2..].join(" ") || "search (#{keyword})"
- continue unless Utils.hasFullUrlPrefix url
- engines[keyword] =
- keyword: keyword
- searchUrl: url
- description: description
- searchUrlPrefix: url.split("%s")[0]
-
- callback engines
-
+ SearchEngines.refreshAndUse Settings.get("searchEngines"), (engines) ->
# Let the front-end vomnibar know the search-engine keywords. It needs to know them so that, when the
# query goes from "w" to "w ", the vomnibar can synchronously launch the next filter() request (which
# avoids an ugly delay/flicker).
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 189f66f3..f15e6db4 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -21,12 +21,11 @@
# A base class for common regexp-based matching engines.
class RegexpEngine
- constructor: (@regexps) ->
+ constructor: (args...) -> @regexps = args.map (regexp) -> new RegExp regexp
match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
# Several Google completion engines package XML responses in this way.
class GoogleXMLRegexpEngine extends RegexpEngine
- doNotCache: false # true (disbaled, experimental)
parse: (xhr) ->
for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
continue unless suggestion = suggestion.getAttribute "data"
@@ -34,31 +33,47 @@ class GoogleXMLRegexpEngine extends RegexpEngine
class Google extends GoogleXMLRegexpEngine
# Example search URL: http://www.google.com/search?q=%s
- constructor: ->
- super [
- # We match the major English-speaking TLDs.
- new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
- new RegExp "localhost/cgi-bin/booky" # Only for smblott.
- ]
+ constructor: (regexps = null) ->
+ super regexps ? "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s"
+# A wrapper class for Google completions. This adds prefix terms to the query, and strips those terms from
+# the resulting suggestions. For example, for Google Maps, we add "map of" as a prefix, then strip "map of"
+# from the resulting suggestions.
+class GoogleWithPrefix
+ constructor: (prefix, args...) ->
+ @engine = new Google args...
+ @prefix = "#{prefix.trim()} "
+ @queryTerms = @prefix.split /\s+/
+ match: (args...) -> @engine.match args...
+ getUrl: (queryTerms) -> @engine.getUrl [ @queryTerms..., queryTerms... ]
+ parse: (xhr) ->
+ @engine.parse(xhr)
+ .filter (suggestion) => suggestion.startsWith @prefix
+ .map (suggestion) => suggestion[@prefix.length..].ltrim()
+
+# For Google Maps, we add the prefix "map of" to the query, and send it to Google's general search engine,
+# then strip "map of" from the resulting suggestions.
+class GoogleMaps extends GoogleWithPrefix
+ # Example search URL: https://www.google.com/maps?q=%s
+ constructor: -> super "map of", "https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/maps"
+
class Youtube extends GoogleXMLRegexpEngine
# Example search URL: http://www.youtube.com/results?search_query=%s
constructor: ->
- super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
+ super "^https?://[a-z]+\.youtube\.com/results"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s"
class Wikipedia extends RegexpEngine
- doNotCache: false # true (disbaled, experimental)
# Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
constructor: ->
- super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
+ super "^https?://[a-z]+\.wikipedia\.org/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
@@ -67,28 +82,15 @@ class Wikipedia extends RegexpEngine
parse: (xhr) ->
JSON.parse(xhr.responseText)[1]
-## Does not work...
-## class GoogleMaps extends RegexpEngine
-## # Example search URL: https://www.google.com/maps/search/%s
-## constructor: ->
-## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
-##
-## getUrl: (queryTerms) ->
-## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
-##
-## parse: (xhr) ->
-## data = JSON.parse xhr.responseText
-## []
-
class Bing extends RegexpEngine
# Example search URL: https://www.bing.com/search?q=%s
- constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ]
+ constructor: -> super "^https?://www\.bing\.com/search"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://api.bing.com/osjson.aspx?query=%s"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
class Amazon extends RegexpEngine
# Example search URL: http://www.amazon.com/s/?field-keywords=%s
- constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ]
+ constructor: -> super "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=%s"
@@ -96,15 +98,14 @@ class Amazon extends RegexpEngine
class DuckDuckGo extends RegexpEngine
# Example search URL: https://duckduckgo.com/?q=%s
- constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ]
- getUrl: (queryTerms) ->
+ constructor: -> super "^https?://([a-z]+\.)?duckduckgo\.com/"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "https://duckduckgo.com/ac/?q=%s"
parse: (xhr) ->
suggestion.phrase for suggestion in JSON.parse xhr.responseText
class Webster extends RegexpEngine
# Example search URL: http://www.merriam-webster.com/dictionary/%s
- constructor: -> super [ new RegExp "^https?://www.merriam-webster.com/dictionary/" ]
+ constructor: -> super "^https?://www.merriam-webster.com/dictionary/"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://www.merriam-webster.com/autocomplete?query=%s"
parse: (xhr) -> JSON.parse(xhr.responseText).suggestions
@@ -120,6 +121,7 @@ class DummyCompletionEngine
# Note: Order matters here.
CompletionEngines = [
Youtube
+ GoogleMaps
Google
DuckDuckGo
Wikipedia
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index edcdf3b2..99a5672b 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -558,13 +558,13 @@ checkKeyQueue = (keysToCheck, tabId, frameId) ->
if runCommand
if not registryEntry.isBackgroundCommand
- chrome.tabs.sendMessage(tabId,
- name: "executePageCommand",
- command: registryEntry.command,
- frameId: frameId,
- count: count,
- passCountToFunction: registryEntry.passCountToFunction,
- completionKeys: generateCompletionKeys(""))
+ chrome.tabs.sendMessage tabId,
+ name: "executePageCommand"
+ command: registryEntry.command
+ frameId: frameId
+ count: count
+ completionKeys: generateCompletionKeys ""
+ registryEntry: registryEntry
refreshedCompletionKeys = true
else
if registryEntry.passCountToFunction
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 647c8025..b4bce776 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -208,13 +208,18 @@ div#vimiumHelpDialog a {
text-decoration: underline;
}
-div#vimiumHelpDialog .optionsPage {
+div#vimiumHelpDialog .wikiPage, div#vimiumHelpDialog .optionsPage {
position: absolute;
display: block;
font-size: 11px;
line-height: 130%;
- right: 60px;
- top: 8px;
+ top: 6px;
+}
+div#vimiumHelpDialog .optionsPage {
+ right: 40px;
+}
+div#vimiumHelpDialog .wikiPage {
+ right: 83px;
}
div#vimiumHelpDialog a.closeButton:hover {
color:black;
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index b7acf433..c8c83029 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -62,6 +62,7 @@ settings =
helpDialog_showAdvancedCommands: null
smoothScroll: null
grabBackFocus: null
+ searchEngines: null
init: ->
@port = chrome.runtime.connect name: "settings"
@@ -307,14 +308,14 @@ executePageCommand = (request) ->
if DomUtils.isTopFrame()
# We pass the frameId from request. That's the frame which originated the request, so that's the frame
# which should receive the focus when the vomnibar closes.
- Utils.invokeCommandString request.command, [ request.frameId ]
+ Utils.invokeCommandString request.command, [ request.frameId, request.registryEntry ]
refreshCompletionKeys request
return
# All other commands are handled in their frame (but only if Vimium is enabled).
return unless frameId == request.frameId and isEnabledForUrl
- if (request.passCountToFunction)
+ if request.registryEntry.passCountToFunction
Utils.invokeCommandString(request.command, [request.count])
else
Utils.invokeCommandString(request.command) for i in [0...request.count]
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 2529c077..4bd8e8fd 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -4,14 +4,36 @@
Vomnibar =
vomnibarUI: null
+ # Parse any additional options from the command's registry entry. Currently, this only includes a flag of
+ # the form "keyword=X", for direct activation of a custom search engine.
+ parseRegistryEntry: (registryEntry = { options: [] }, callback = null) ->
+ options = {}
+ searchEngines = settings.get("searchEngines") ? ""
+ SearchEngines.refreshAndUse searchEngines, (engines) ->
+ for option in registryEntry.options
+ [ key, value ] = option.split "="
+ switch key
+ when "keyword"
+ if value? and engines[value]?
+ options.keyword = value
+ else
+ console.log "Vimium configuration error: no such custom search engine: #{option}."
+ else
+ console.log "Vimium configuration error: unused flag: #{option}."
+
+ callback? options
+
# sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different
# from the current frame.
- activate: (sourceFrameId) -> @open sourceFrameId, {completer:"omni"}
- activateInNewTab: (sourceFrameId) -> @open sourceFrameId, {
- completer: "omni"
- selectFirst: false
- newTab: true
- }
+
+ activate: (sourceFrameId, registryEntry) ->
+ @parseRegistryEntry registryEntry, (options) =>
+ @open sourceFrameId, extend options, completer:"omni"
+
+ activateInNewTab: (sourceFrameId, registryEntry) ->
+ @parseRegistryEntry registryEntry, (options) =>
+ @open sourceFrameId, extend options, completer:"omni", newTab: true
+
activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
completer: "tabs"
selectFirst: true
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 835b0359..93045f32 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -235,6 +235,38 @@ Utils =
# Like Nodejs's nextTick.
nextTick: (func) -> @setTimeout 0, func
+# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the
+# search-engine configuration is unchanged.
+SearchEngines =
+ previousSearchEngines: null
+ searchEngines: null
+
+ refresh: (searchEngines) ->
+ unless @previousSearchEngines? and searchEngines == @previousSearchEngines
+ @previousSearchEngines = searchEngines
+ @searchEngines = new AsyncDataFetcher (callback) ->
+ engines = {}
+ for line in searchEngines.split "\n"
+ line = line.trim()
+ continue if /^[#"]/.test line
+ tokens = line.split /\s+/
+ continue unless 2 <= tokens.length
+ keyword = tokens[0].split(":")[0]
+ searchUrl = tokens[1]
+ description = tokens[2..].join(" ") || "search (#{keyword})"
+ continue unless Utils.hasFullUrlPrefix searchUrl
+ engines[keyword] = { keyword, searchUrl, description }
+
+ callback engines
+
+ # Use the parsed search-engine configuration, possibly asynchronously.
+ use: (callback) ->
+ @searchEngines.use callback
+
+ # Both set (refresh) the search-engine configuration and use it at the same time.
+ refreshAndUse: (searchEngines, callback) ->
+ @refresh searchEngines
+ @use callback
# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
@@ -332,6 +364,7 @@ class JobRunner
root = exports ? window
root.Utils = Utils
+root.SearchEngines = SearchEngines
root.SimpleCache = SimpleCache
root.AsyncDataFetcher = AsyncDataFetcher
root.JobRunner = JobRunner
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 44464b81..5c09c0ab 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -7,6 +7,7 @@
page with the up-to-date key bindings when the dialog is shown. -->
<div id="vimiumHelpDialog" class="vimiumReset">
<a class="vimiumReset optionsPage" href="#">Options</a>
+ <a class="vimiumReset wikiPage" href="https://github.com/philc/vimium/wiki" target="_blank">Wiki</a>
<a class="vimiumReset closeButton" href="#">&times;</a>
<div id="vimiumTitle" class="vimiumReset"><span class="vimiumReset" style="color:#2f508e">Vim</span>ium {{title}}</div>
<div class="vimiumReset vimiumColumn">
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 71f22900..d5659fdc 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -17,6 +17,7 @@ Vomnibar =
query: ""
newTab: false
selectFirst: false
+ keyword: null
extend options, userOptions
extend options, refreshInterval: if options.completer == "omni" then 150 else 0
@@ -28,6 +29,7 @@ Vomnibar =
@vomnibarUI.setRefreshInterval options.refreshInterval
@vomnibarUI.setForceNewTab options.newTab
@vomnibarUI.setQuery options.query
+ @vomnibarUI.setKeyword options.keyword
@vomnibarUI.update true
hide: -> @vomnibarUI?.hide()
@@ -40,6 +42,7 @@ class VomnibarUI
@initDom()
setQuery: (query) -> @input.value = query
+ setKeyword: (keyword) -> @customSearchMode = keyword
setInitialSelectionValue: (@initialSelectionValue) ->
setRefreshInterval: (@refreshInterval) ->
setForceNewTab: (@forceNewTab) ->
diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee
index 380175f3..3eda6234 100644
--- a/tests/dom_tests/vomnibar_test.coffee
+++ b/tests/dom_tests/vomnibar_test.coffee
@@ -1,4 +1,5 @@
vomnibarFrame = null
+SearchEngines.refresh ""
context "Keep selection within bounds",