aboutsummaryrefslogtreecommitdiffstats
path: root/background_scripts/completion_engines.coffee
diff options
context:
space:
mode:
authorStephen Blott2015-05-10 15:19:25 +0100
committerStephen Blott2015-05-10 15:54:42 +0100
commit6b52c9e6397ac0a040c1bd46b7e6825a0a8415d2 (patch)
treebfd7281a66e456f50892251c59c0a6c473c4fc72 /background_scripts/completion_engines.coffee
parent8493811a4279950194cc8b1f5941cf9730cda1f0 (diff)
downloadvimium-6b52c9e6397ac0a040c1bd46b7e6825a0a8415d2.tar.bz2
Search completion; move completion engines to their own file.
Diffstat (limited to 'background_scripts/completion_engines.coffee')
-rw-r--r--background_scripts/completion_engines.coffee170
1 files changed, 9 insertions, 161 deletions
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 638d7f60..07ecfa26 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -4,18 +4,20 @@
#
# Each completion engine defines three functions:
#
-# 1. "match" - This takes a searchUrl, and returns a boolean indicating whether this completion engine can
+# 1. "match" - This takes a searchUrl and returns a boolean indicating whether this completion engine can
# perform completion for the given search engine.
#
# 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.
#
# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and
-# returns a list of suggestions (a list of strings).
+# 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.
+#
+# Each new completion engine must be add to the list "CompletionEngines" at the bottom of this file.
+#
+# The lookup logic which uses these completion engines is in "./completion_search.coffee".
#
-# The main completion entry point is CompletionEngines.complete(). This implements all lookup and caching
-# logic. It is possible to add new completion engines without changing the CompletionEngines infrastructure
-# itself.
# A base class for common regexp-based matching engines.
class RegexpEngine
@@ -106,7 +108,8 @@ class DummyCompletionEngine
getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css"
parse: -> []
-completionEngines = [
+# Note: Order matters here.
+CompletionEngines = [
Youtube
Google
DuckDuckGo
@@ -116,160 +119,5 @@ completionEngines = [
DummyCompletionEngine
]
-# A note on caching.
-# Some completion engines allow caching, and Chrome serves up cached responses to requests (e.g. Google,
-# Wikipedia, YouTube). Others do not (e.g. Bing, DuckDuckGo, Amazon). A completion engine can set
-# @doNotCache to a truthy value to disable caching in cases where it is unnecessary.
-
-CompletionEngines =
- debug: true
-
- # The amount of time to wait for new requests before launching the HTTP request. The intention is to cut
- # down on the number of HTTP requests we issue.
- # delay: 100
- delay: 0
-
- get: (searchUrl, url, callback) ->
- xhr = new XMLHttpRequest()
- xhr.open "GET", url, true
- xhr.timeout = 1000
- xhr.ontimeout = xhr.onerror = -> callback null
- xhr.send()
-
- xhr.onreadystatechange = ->
- if xhr.readyState == 4
- callback(if xhr.status == 200 then xhr else null)
-
- # Look up the completion engine for this searchUrl. Because of DummyCompletionEngine, above, we know there
- # will always be a match. Imagining that there may be many completion engines, and knowing that this is
- # called for every query, we cache the result.
- lookupEngine: (searchUrl) ->
- @engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
- if @engineCache.has searchUrl
- @engineCache.get searchUrl
- else
- for engine in completionEngines
- engine = new engine()
- return @engineCache.set searchUrl, engine if engine.match searchUrl
-
- # True if we have a completion engine for this search URL, undefined otherwise.
- haveCompletionEngine: (searchUrl) ->
- not @lookupEngine(searchUrl).dummy
-
- # This is the main entry point.
- # - searchUrl is the search engine's URL, e.g. Settings.get("searchUrl"), or a custome search engine's URL.
- # This is only used as a key for determining the relevant completion engine.
- # - queryTerms are the query terms.
- # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
- # wrong).
- complete: (searchUrl, queryTerms, callback = null) ->
- @mostRecentHandler = null
- query = queryTerms.join ""
-
- # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie.
- # from a cache). In this case we return the results and don't call callback. Return null if we cannot
- # service the request synchronously.
- returnResultsOnlyFromCache = not callback?
- callback ?= (suggestions) -> suggestions
-
- # We don't complete single characters: the results are usually useless.
- return callback [] unless 1 < query.length
-
- # We don't complete regular URLs or Javascript URLs.
- 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
- # vanishingly unlikely.
- junk = "//Zi?ei5;o//"
- completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk
- @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
- if @completionCache.has completionCacheKey
- if returnResultsOnlyFromCache
- return @completionCache.get completionCacheKey
- else
- # We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
- # suggestions are posted.
- Utils.setTimeout 50, =>
- console.log "hit", completionCacheKey if @debug
- callback @completionCache.get completionCacheKey
- return
-
- if @mostRecentQuery? and @mostRecentSuggestions?
- # If the user appears to be typing a continuation of the characters of the most recent query, then we
- # can re-use the previous suggestions.
- reusePreviousSuggestions = do (query) =>
- query = queryTerms.join(" ").toLowerCase()
- # Verify that the previous query is a prefix of the current query.
- return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
- # Ensure that every previous suggestion contains the text of the new query.
- for suggestion in (@mostRecentSuggestions.map (s) -> s.toLowerCase())
- return false unless 0 <= suggestion.indexOf query
- # Ok. Re-use the suggestion.
- true
-
- if reusePreviousSuggestions
- console.log "reuse previous query:", @mostRecentQuery if @debug
- @mostRecentQuery = queryTerms.join " "
- return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
-
- return null if returnResultsOnlyFromCache
-
- fetchSuggestions = (engine, callback) =>
- url = engine.getUrl queryTerms
- query = queryTerms.join(" ").toLowerCase()
- @get searchUrl, url, (xhr = null) =>
- # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
- # all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of
- # incorrect or out-of-date completion engines.
- try
- suggestions = engine.parse xhr
- # Make sure we really do have an iterable of strings.
- suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
- # Filter out the query itself. It's not adding anything.
- suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
- console.log "GET", url if @debug
- catch
- suggestions = []
- # We allow failures to be cached, but remove them after just ten minutes. This (it is hoped) avoids
- # repeated unnecessary XMLHttpRequest failures over a short period of time.
- removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
- setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
- console.log "fail", url if @debug
-
- callback suggestions
-
- # We pause in case the user is still typing.
- Utils.setTimeout @delay, handler = @mostRecentHandler = =>
- if handler != @mostRecentHandler
- # Bail! Another completion has begun, or the user is typing.
- # NOTE: We do *not* call the callback (because we are not providing results, and we don't want allow
- # any higher-level component to cache the results -- specifically, the vomnibar itself, via
- # callerMayCacheResults).
- console.log "bail", completionCacheKey if @debug
- return
- @mostRecentHandler = null
- # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
- # removes a space, or when they enter a character and immediately delete it.
- @inTransit ?= {}
- unless @inTransit[completionCacheKey]?.push callback
- queue = @inTransit[completionCacheKey] = []
- engine = @lookupEngine searchUrl
- fetchSuggestions engine, (suggestions) =>
- @mostRecentQuery = queryTerms.join " "
- @mostRecentSuggestions = suggestions
- @completionCache.set completionCacheKey, suggestions unless engine.doNotCache
- callback suggestions
- delete @inTransit[completionCacheKey]
- console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
- callback suggestions for callback in queue
-
- # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries.
- cancel: ->
- if @mostRecentHandler?
- @mostRecentHandler = null
- console.log "cancel (user is typing)" if @debug
-
root = exports ? window
root.CompletionEngines = CompletionEngines