diff options
| author | Stephen Blott | 2015-05-10 15:19:25 +0100 | 
|---|---|---|
| committer | Stephen Blott | 2015-05-10 15:54:42 +0100 | 
| commit | 6b52c9e6397ac0a040c1bd46b7e6825a0a8415d2 (patch) | |
| tree | bfd7281a66e456f50892251c59c0a6c473c4fc72 /background_scripts/completion_search.coffee | |
| parent | 8493811a4279950194cc8b1f5941cf9730cda1f0 (diff) | |
| download | vimium-6b52c9e6397ac0a040c1bd46b7e6825a0a8415d2.tar.bz2 | |
Search completion; move completion engines to their own file.
Diffstat (limited to 'background_scripts/completion_search.coffee')
| -rw-r--r-- | background_scripts/completion_search.coffee | 140 | 
1 files changed, 140 insertions, 0 deletions
| diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee new file mode 100644 index 00000000..eb27c076 --- /dev/null +++ b/background_scripts/completion_search.coffee @@ -0,0 +1,140 @@ + +CompletionSearch = +  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 + +  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) -> +    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 just return the results.  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 callback @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 the user appears to be typing a continuation of the characters of the most recent query, then we can +    # re-use the previous suggestions. +    if @mostRecentQuery? and @mostRecentSuggestions? +      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 + +    # That's all of the caches we can try.  Bail if the caller is looking for synchronous results. +    return callback 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 +          # 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 cache failures too, but remove them after just thirty minutes. +          Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null +          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 +        @mostRecentHandler = null + +        # Share duplicate requests. First fetch the suggestions... +        @inTransit ?= {} +        @inTransit[completionCacheKey] ?= new AsyncDataFetcher (callback) => +          fetchSuggestions @lookupEngine(searchUrl), callback + +        # ... then use the suggestions. +        @inTransit[completionCacheKey].use (suggestions) => +          @mostRecentQuery = queryTerms.join " " +          @mostRecentSuggestions = suggestions +          callback @completionCache.set completionCacheKey, suggestions +          delete @inTransit[completionCacheKey] + +  # 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.CompletionSearch = CompletionSearch | 
