diff options
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | background_scripts/commands.coffee | 64 | ||||
| -rw-r--r-- | background_scripts/completion.coffee | 24 | ||||
| -rw-r--r-- | background_scripts/completion_engines.coffee | 60 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 14 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 11 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 5 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 34 | ||||
| -rw-r--r-- | lib/utils.coffee | 33 | ||||
| -rw-r--r-- | pages/help_dialog.html | 1 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 3 | ||||
| -rw-r--r-- | tests/dom_tests/vomnibar_test.coffee | 1 |
12 files changed, 144 insertions, 111 deletions
@@ -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="#">×</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", |
