diff options
| author | Stephen Blott | 2015-06-06 20:19:33 +0100 |
|---|---|---|
| committer | Stephen Blott | 2015-06-06 20:19:33 +0100 |
| commit | 616261ef8364cbd99765d651dfe8acd5ff55b453 (patch) | |
| tree | c7b75f7aa9a960132ba484935a4c1ed1cfb63a29 | |
| parent | c20e5d44455be8ac885f0d7c42e05ec9857bd203 (diff) | |
| parent | cb900a255113b8304d8931f7c6294e20f7f9f36d (diff) | |
| download | vimium-616261ef8364cbd99765d651dfe8acd5ff55b453.tar.bz2 | |
Merge branch 'rework-completions'
Conflicts:
background_scripts/completion.coffee
background_scripts/completion_engines.coffee
| -rw-r--r-- | background_scripts/completion.coffee | 29 | ||||
| -rw-r--r-- | background_scripts/completion_engines.coffee | 192 | ||||
| -rw-r--r-- | background_scripts/completion_search.coffee | 78 | ||||
| -rw-r--r-- | manifest.json | 3 | ||||
| -rw-r--r-- | pages/completion_engines.coffee | 35 | ||||
| -rw-r--r-- | pages/completion_engines.css | 15 | ||||
| -rw-r--r-- | pages/completion_engines.html | 32 | ||||
| -rw-r--r-- | pages/options.css | 2 | ||||
| -rw-r--r-- | pages/options.html | 5 |
9 files changed, 270 insertions, 121 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index cbc5e698..189929b4 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -454,29 +454,32 @@ class SearchEngineCompleter { keyword, searchUrl, description } = engine extend request, searchUrl, customSearchMode: true - factor = 0.5 + @previousSuggestions[searchUrl] ?= [] haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl # This filter is applied to all of the suggestions from all of the completers, after they have been # aggregated by the MultiCompleter. filter = (suggestions) -> - suggestions.filter (suggestion) -> - # We only keep suggestions which either *were* generated by this search engine, or *could have - # been* generated by this search engine (and match the current query). - suggestion.searchUrl == searchUrl or - ( - terms = Utils.extractQuery searchUrl, suggestion.url - terms and RankingUtils.matches queryTerms, terms - ) + # We only keep suggestions which either *were* generated by this search engine, or *could have + # been* generated by this search engine (and match the current query). + for suggestion in suggestions + if suggestion.isSearchSuggestion or suggestion.isCustomSearch + suggestion + else + terms = Utils.extractQuery searchUrl, suggestion.url + continue unless terms and RankingUtils.matches queryTerms, terms + suggestion.url = Utils.createSearchUrl terms, searchUrl + suggestion # If a previous suggestion still matches the query, then we keep it (even if the completion engine may not - # return it for the current query). This allows the user to pick suggestions by typing fragments of their - # text, without regard to whether the completion engine can complete the actual text of the query. + # return it for the current query). This allows the user to pick suggestions that they've previously seen + # by typing fragments of their text, without regard to whether the completion engine can continue to + # complete the actual text of the query. previousSuggestions = if queryTerms.length == 0 [] else - for url, suggestion of @previousSuggestions + for _, suggestion of @previousSuggestions[searchUrl] continue unless RankingUtils.matches queryTerms, suggestion.title # Reset various fields, they may not be correct wrt. the current query. extend suggestion, relevancy: null, html: null, queryTerms: queryTerms @@ -501,7 +504,7 @@ class SearchEngineCompleter count = 0 (suggestion) => url = Utils.createSearchUrl suggestion, searchUrl - @previousSuggestions[url] = new Suggestion + @previousSuggestions[searchUrl][url] = new Suggestion queryTerms: queryTerms type: description url: url diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee index dcbf99c6..b572375d 100644 --- a/background_scripts/completion_engines.coffee +++ b/background_scripts/completion_engines.coffee @@ -1,122 +1,152 @@ -# A completion engine provides search suggestions for a search engine. A search engine is identified by a -# "searchUrl", e.g. Settings.get("searchUrl"), or a custom search engine URL. +# A completion engine provides search suggestions for a custom search engine. A custom search engine is +# identified by a "searchUrl". An "engineUrl" is used for fetching suggestions, whereas a "searchUrl" is used +# for the actual search itself. # -# Each completion engine defines three functions: +# Each completion engine defines: # -# 1. "match" - This takes a searchUrl and returns a boolean indicating whether this completion engine can -# perform completion for the given search engine. +# 1. An "engineUrl". This is the URL to use for search completions and is passed as the option "engineUrl" +# to the "BaseEngine" constructor. # -# 2. "getUrl" - This takes a list of query terms (queryTerms) and generates a completion URL, that is, a URL -# which will provide completions for this completion engine. +# 2. One or more regular expressions which define the custom search engine URLs for which the completion +# engine will be used. This is passed as the "regexps" option to the "BaseEngine" constructor. # -# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and -# returns a list of suggestions (a list of strings). This method is always executed within the context -# of a try/catch block, so errors do not propagate. +# 3. A "parse" function. This takes a successful XMLHttpRequest object (the request has completed +# successfully), and returns a list of suggestions (a list of strings). This method is always executed +# within the context of a try/catch block, so errors do not propagate. +# +# 4. Each completion engine *must* include an example custom search engine. The example must include an +# example "keyword" and an example "searchUrl", and may include an example "description" and an +# "explanation". # # Each new completion engine must be added to the list "CompletionEngines" at the bottom of this file. # # The lookup logic which uses these completion engines is in "./completion_search.coffee". # -# A base class for common regexp-based matching engines. -class RegexpEngine - constructor: (args...) -> @regexps = args.map (regexp) -> new RegExp regexp +# A base class for common regexp-based matching engines. "options" must define: +# options.engineUrl: the URL to use for the completion engine. This must be a string. +# options.regexps: one or regular expressions. This may either a single string or a list of strings. +# options.example: an example object containing at least "keyword" and "searchUrl", and optional "description". +class BaseEngine + constructor: (options) -> + extend this, options + @regexps = [ @regexps ] if "string" == typeof @regexps + @regexps = @regexps.map (regexp) -> new RegExp regexp + match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl + getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, @engineUrl -# Several Google completion engines package XML responses in this way. -class GoogleXMLRegexpEngine extends RegexpEngine +# Several Google completion engines package responses as XML. This parses such XML. +class GoogleXMLBaseEngine extends BaseEngine parse: (xhr) -> for suggestion in xhr.responseXML.getElementsByTagName "suggestion" continue unless suggestion = suggestion.getAttribute "data" suggestion -class Google extends GoogleXMLRegexpEngine - # Example search URL: http://www.google.com/search?q=%s - 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} " - @queryTerms = prefix.split /\s+/ - match: (args...) -> @engine.match args... - getUrl: (queryTerms) -> @engine.getUrl [ @queryTerms..., queryTerms... ] +class Google extends GoogleXMLBaseEngine + constructor: () -> + super + engineUrl: "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s" + regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.uk|ca|com\\.au)/" + example: + searchUrl: "http://www.google.com/search?q=%s" + keyword: "g" + +class GoogleMaps extends GoogleXMLBaseEngine + prefix: "map of " + constructor: () -> + super + engineUrl: "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{@prefix.split(' ').join '+'}%s" + regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.uk|ca|com\\.au)/maps" + example: + searchUrl: "https://www.google.com/maps?q=%s" + keyword: "m" + explanation: + """ + This uses regular Google completion, but prepends the text "<tt>map of</tt>" to the query. It works + well for places, countries, states, geographical regions and the like, but will not perform address + search. + """ + parse: (xhr) -> - for suggestion in @engine.parse xhr + for suggestion in super xhr continue unless suggestion.startsWith @prefix - 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" + suggestion[@prefix.length..] -class Youtube extends GoogleXMLRegexpEngine - # Example search URL: http://www.youtube.com/results?search_query=%s +class Youtube extends GoogleXMLBaseEngine constructor: -> - super "^https?://[a-z]+\\.youtube\\.com/results" + super + engineUrl: "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s" + regexps: "^https?://[a-z]+\\.youtube\\.com/results" + example: + searchUrl: "http://www.youtube.com/results?search_query=%s" + keyword: "y" + +class Wikipedia extends BaseEngine + constructor: -> + super + engineUrl: "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=%s" + regexps: "^https?://[a-z]+\\.wikipedia\\.org/" + example: + searchUrl: "http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s" + keyword: "w" - getUrl: (queryTerms) -> - Utils.createSearchUrl queryTerms, - "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s" + parse: (xhr) -> JSON.parse(xhr.responseText)[1] -class Wikipedia extends RegexpEngine - # Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s +class Bing extends BaseEngine constructor: -> - super "^https?://[a-z]+\\.wikipedia\\.org/" + super + engineUrl: "http://api.bing.com/osjson.aspx?query=%s" + regexps: "^https?://www\\.bing\\.com/search" + example: + searchUrl: "https://www.bing.com/search?q=%s" + keyword: "b" - getUrl: (queryTerms) -> - Utils.createSearchUrl queryTerms, - "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=%s" + parse: (xhr) -> JSON.parse(xhr.responseText)[1] - parse: (xhr) -> - JSON.parse(xhr.responseText)[1] +class Amazon extends BaseEngine + constructor: -> + super + engineUrl: "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=%s" + regexps: "^https?://www\\.amazon\\.(com|co\\.uk|ca|com\\.au)/s/" + example: + searchUrl: "http://www.amazon.com/s/?field-keywords=%s" + keyword: "a" -class Bing extends RegexpEngine - # Example search URL: https://www.bing.com/search?q=%s - 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 "^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" - parse: (xhr) -> JSON.parse(xhr.responseText)[1] +class DuckDuckGo extends BaseEngine + constructor: -> + super + engineUrl: "https://duckduckgo.com/ac/?q=%s" + regexps: "^https?://([a-z]+\\.)?duckduckgo\\.com/" + example: + searchUrl: "https://duckduckgo.com/?q=%s" + keyword: "d" -class DuckDuckGo extends RegexpEngine - # Example search URL: https://duckduckgo.com/?q=%s - 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 "^https?://www.merriam-webster.com/dictionary/" - getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://www.merriam-webster.com/autocomplete?query=%s" +class Webster extends BaseEngine + constructor: -> + super + engineUrl: "http://www.merriam-webster.com/autocomplete?query=%s" + regexps: "^https?://www.merriam-webster.com/dictionary/" + example: + searchUrl: "http://www.merriam-webster.com/dictionary/%s" + keyword: "dw" + description: "Dictionary" + parse: (xhr) -> JSON.parse(xhr.responseText).suggestions # A dummy search engine which is guaranteed to match any search URL, but never produces completions. This # allows the rest of the logic to be written knowing that there will always be a completion engine match. -class DummyCompletionEngine - dummy: true - match: -> true - # We return a useless URL which we know will succeed, but which won't generate any network traffic. - getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css" - parse: -> [] +class DummyCompletionEngine extends BaseEngine + constructor: -> + super + regexps: "." + dummy: true # Note: Order matters here. CompletionEngines = [ diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee index b26194e6..7926b45b 100644 --- a/background_scripts/completion_search.coffee +++ b/background_scripts/completion_search.coffee @@ -1,4 +1,39 @@ +# This is a wrapper class for completion engines. It handles the case where a custom search engine includes a +# prefix query term (or terms). For example: +# +# http://www.google.com/search?q=javascript+%s +# +# In this case, we get better suggestions if we include the term "javascript" in queries sent to the +# completion engine. This wrapper handles adding such prefixes to completion-engine queries and removing them +# from the resulting suggestions. +class EnginePrefixWrapper + constructor: (@searchUrl, @engine) -> + + getUrl: (queryTerms) -> + # This tests whether @searchUrl contains something of the form "...=abc+def+%s...", from which we extract + # a prefix of the form "abc def ". + if /\=.+\+%s/.test @searchUrl + terms = @searchUrl.replace /\+%s.*/, "" + terms = terms.replace /.*=/, "" + terms = terms.replace /\+/g, " " + + queryTerms = [ terms.split(" ")..., queryTerms... ] + prefix = "#{terms} " + + @postprocessSuggestions = + (suggestions) -> + for suggestion in suggestions + continue unless suggestion.startsWith prefix + suggestion[prefix.length..] + + @engine.getUrl queryTerms + + parse: (xhr) -> + @postprocessSuggestions @engine.parse xhr + + postprocessSuggestions: (suggestions) -> suggestions + CompletionSearch = debug: false inTransit: {} @@ -58,34 +93,30 @@ CompletionSearch = return callback [] if 1 == queryTerms.length and Utils.isUrl query return callback [] if Utils.hasJavascriptPrefix query - # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need - # to generate a key. We mix in some junk generated by pwgen. A key clash might be possible, but - # is vanishingly unlikely. - junk = "//Zi?ei5;o//" - completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk - + completionCacheKey = JSON.stringify [ searchUrl, queryTerms ] if @completionCache.has completionCacheKey console.log "hit", completionCacheKey if @debug return callback @completionCache.get completionCacheKey # If the user appears to be typing a continuation of the characters of the most recent query, then we can # sometimes re-use the previous suggestions. - if @mostRecentQuery? and @mostRecentSuggestions? - reusePreviousSuggestions = do => - # Verify that the previous query is a prefix of the current query. - return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase() - # Verify that every previous suggestion contains the text of the new query. - # Note: @mostRecentSuggestions may also be empty, in which case we drop though. The effect is that - # previous queries with no suggestions suppress subsequent no-hope HTTP requests as the user continues - # to type. - for suggestion in @mostRecentSuggestions - return false unless 0 <= suggestion.indexOf query - # Ok. Re-use the suggestion. - true - - if reusePreviousSuggestions - console.log "reuse previous query:", @mostRecentQuery, @mostRecentSuggestions.length if @debug - return callback @completionCache.set completionCacheKey, @mostRecentSuggestions + if @mostRecentQuery? and @mostRecentSuggestions? and @mostRecentSearchUrl? + if searchUrl == @mostRecentSearchUrl + reusePreviousSuggestions = do => + # Verify that the previous query is a prefix of the current query. + return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase() + # Verify that every previous suggestion contains the text of the new query. + # Note: @mostRecentSuggestions may also be empty, in which case we drop though. The effect is that + # previous queries with no suggestions suppress subsequent no-hope HTTP requests as the user continues + # to type. + for suggestion in @mostRecentSuggestions + return false unless 0 <= suggestion.indexOf query + # Ok. Re-use the suggestion. + true + + if reusePreviousSuggestions + console.log "reuse previous query:", @mostRecentQuery, @mostRecentSuggestions.length if @debug + return callback @completionCache.set completionCacheKey, @mostRecentSuggestions # That's all of the caches we can try. Bail if the caller is only requesting synchronous results. We # signal that we haven't found a match by returning null. @@ -98,7 +129,7 @@ CompletionSearch = # Elide duplicate requests. First fetch the suggestions... @inTransit[completionCacheKey] ?= new AsyncDataFetcher (callback) => - engine = @lookupEngine searchUrl + engine = new EnginePrefixWrapper searchUrl, @lookupEngine searchUrl url = engine.getUrl queryTerms @get searchUrl, url, (xhr = null) => @@ -124,6 +155,7 @@ CompletionSearch = # ... then use the suggestions. @inTransit[completionCacheKey].use (suggestions) => + @mostRecentSearchUrl = searchUrl @mostRecentQuery = query @mostRecentSuggestions = suggestions callback @completionCache.set completionCacheKey, suggestions diff --git a/manifest.json b/manifest.json index 80eef6c1..80aca4c5 100644 --- a/manifest.json +++ b/manifest.json @@ -73,6 +73,7 @@ "web_accessible_resources": [ "pages/vomnibar.html", "content_scripts/vimium.css", - "pages/hud.html" + "pages/hud.html", + "pages/completion_engines.html" ] } diff --git a/pages/completion_engines.coffee b/pages/completion_engines.coffee new file mode 100644 index 00000000..790f2968 --- /dev/null +++ b/pages/completion_engines.coffee @@ -0,0 +1,35 @@ + +cleanUpRegexp = (re) -> + re.toString() + .replace /^\//, '' + .replace /\/$/, '' + .replace /\\\//g, "/" + +DomUtils.documentReady -> + html = [] + for engine in CompletionEngines[0...CompletionEngines.length-1] + engine = new engine + html.push "<h4>#{engine.constructor.name}</h4>\n" + html.push "<div class=\"engine\">" + if engine.example.explanation + html.push "<p>#{engine.example.explanation}</p>" + if engine.regexps + html.push "<p>" + html.push "Regular expression#{if 1 < engine.regexps.length then 's' else ''}:" + html.push "<pre>" + html.push "#{cleanUpRegexp re}\n" for re in engine.regexps + html.push "</pre>" + html.push "</p>" + if engine.example.searchUrl and engine.example.keyword + engine.example.description ||= engine.constructor.name + html.push "<p>" + html.push "Example:" + html.push "<pre>" + html.push "#{engine.example.keyword}: #{engine.example.searchUrl} #{engine.example.description}" + html.push "</pre>" + html.push "</p>" + html.push "</div>" + + document.getElementById("engineList").innerHTML = html.join "" + + diff --git a/pages/completion_engines.css b/pages/completion_engines.css new file mode 100644 index 00000000..3e3aab1f --- /dev/null +++ b/pages/completion_engines.css @@ -0,0 +1,15 @@ + +div#wrapper +{ + width: 730px; +} + +h4, h5 +{ + color: #777; +} + +div.engine +{ + margin-left: 20px; +} diff --git a/pages/completion_engines.html b/pages/completion_engines.html new file mode 100644 index 00000000..69158c78 --- /dev/null +++ b/pages/completion_engines.html @@ -0,0 +1,32 @@ +<html> + <head> + <title>Vimium Search Completion</title> + <!-- We re-use some styling from the options page, so that the look and feel here is similar --> + <link rel="stylesheet" type="text/css" href="options.css"> + <link rel="stylesheet" type="text/css" href="completion_engines.css"> + <script src="content_script_loader.js"></script> + <script type="text/javascript" src="../lib/settings.js"></script> + <script src="../background_scripts/completion_engines.js"></script> + <script src="completion_engines.js"></script> + </head> + + <body> + <div id="wrapper"> + <header>Vimium Search Completion</header> + <p> + Search completion is available for custom search engines whose search URL matches one of Vimium's + built-in completion engines; that is, the search URL matches one of the regular expressions below. + Search completion is not available for the default search engine. + </p> + <p> + Custom search engines can be configured on the <a href="options.html" target="_blank">options</a> + page. <br/> + Further information is available on the <a href="https://github.com/philc/vimium/wiki/Search-Completion">wiki</a>. + </p> + <header>Available Completion Engines</header> + <p> + <dl id="engineList"></dl> + </p> + </div> + </body> +</html> diff --git a/pages/options.css b/pages/options.css index 745b655c..75bbe159 100644 --- a/pages/options.css +++ b/pages/options.css @@ -114,7 +114,7 @@ input#scrollStepSize { } textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines { width: 100%;; - min-height: 130px; + min-height: 140px; white-space: nowrap; } input#previousPatterns, input#nextPatterns { diff --git a/pages/options.html b/pages/options.html index 441bd9da..12a3ad21 100644 --- a/pages/options.html +++ b/pages/options.html @@ -43,7 +43,7 @@ unmap j unmapAll " this is a comment # this is also a comment</pre> - <a href="#" id="showCommands">Show available commands.</a> + <a href="#" id="showCommands">Show available commands</a>. </div> </div> <textarea id="keyMappings" type="text"></textarea> @@ -60,7 +60,8 @@ a: http://a.com/?q=%s b: http://b.com/?q=%s description " this is a comment # this is also a comment</pre> - %s is replaced with the search terms. + %s is replaced with the search terms. <br/> + For search completion, see <a href="completion_engines.html" target="_blank">here</a>. </div> </div> <textarea id="searchEngines"></textarea> |
