aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2015-06-06 20:19:33 +0100
committerStephen Blott2015-06-06 20:19:33 +0100
commit616261ef8364cbd99765d651dfe8acd5ff55b453 (patch)
treec7b75f7aa9a960132ba484935a4c1ed1cfb63a29
parentc20e5d44455be8ac885f0d7c42e05ec9857bd203 (diff)
parentcb900a255113b8304d8931f7c6294e20f7f9f36d (diff)
downloadvimium-616261ef8364cbd99765d651dfe8acd5ff55b453.tar.bz2
Merge branch 'rework-completions'
Conflicts: background_scripts/completion.coffee background_scripts/completion_engines.coffee
-rw-r--r--background_scripts/completion.coffee29
-rw-r--r--background_scripts/completion_engines.coffee192
-rw-r--r--background_scripts/completion_search.coffee78
-rw-r--r--manifest.json3
-rw-r--r--pages/completion_engines.coffee35
-rw-r--r--pages/completion_engines.css15
-rw-r--r--pages/completion_engines.html32
-rw-r--r--pages/options.css2
-rw-r--r--pages/options.html5
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>