diff options
| author | Stephen Blott | 2015-05-03 17:22:20 +0100 | 
|---|---|---|
| committer | Stephen Blott | 2015-05-03 17:22:20 +0100 | 
| commit | 776f617ece5d333fe70df903982a18d65fc2776a (patch) | |
| tree | 8d4fd8e22d128629df9a1998fc1f716c08b6791e | |
| parent | 7d59e948da154203722b442c477b452f7a393161 (diff) | |
| download | vimium-776f617ece5d333fe70df903982a18d65fc2776a.tar.bz2 | |
Search completion; make completion lookup asynchronous.
| -rw-r--r-- | background_scripts/completion.coffee | 97 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 15 | ||||
| -rw-r--r-- | background_scripts/search_engines.coffee | 3 | ||||
| -rw-r--r-- | lib/utils.coffee | 6 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 2 | 
5 files changed, 77 insertions, 46 deletions
| diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index c8650f45..fee4778a 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -346,29 +346,31 @@ class SearchEngineCompleter        suggestions[0].autoSelect = true        queryTerms = queryTerms[1..] -    # For custom search-engine queries, this adds suggestions only if we have a completer.  For other queries, -    # this adds suggestions for the default search engine (if we have a completer for that). -    SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) => - -      # Scoring: -      #   - The score does not depend upon the actual suggestion (so, it does not depend upon word relevancy). -      #     We assume that the completion engine has already factored that in. -      #   - The score is higher if the query is longer.  The idea is that search suggestions are more likely -      #     to be relevant if, after typing quite some number of characters, the user hasn't yet found a -      #     useful suggestion from another completer. -      #   - Scores are weighted such that they retain the ordering provided by the completion engine. -      characterCount = query.length - queryTerms.length + 1 -      score = 0.8 * (Math.min(characterCount, 12.0)/12.0) - -      for suggestion in searchSuggestions -        suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score -        score *= 0.9 - -      if custom -        for suggestion in suggestions -          suggestion.reinsertPrefix = "#{keyword} " if suggestion.insertText - -      onComplete suggestions +    onComplete suggestions, (onComplete) => +      suggestions = [] +      # For custom search-engine queries, this adds suggestions only if we have a completer.  For other queries, +      # this adds suggestions for the default search engine (if we have a completer for that). +      SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) => + +        # Scoring: +        #   - The score does not depend upon the actual suggestion (so, it does not depend upon word relevancy). +        #     We assume that the completion engine has already factored that in. +        #   - The score is higher if the query is longer.  The idea is that search suggestions are more likely +        #     to be relevant if, after typing quite some number of characters, the user hasn't yet found a +        #     useful suggestion from another completer. +        #   - Scores are weighted such that they retain the ordering provided by the completion engine. +        characterCount = query.length - queryTerms.length + 1 +        score = 0.8 * (Math.min(characterCount, 12.0)/12.0) + +        for suggestion in searchSuggestions +          suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score +          score *= 0.9 + +        if custom +          for suggestion in suggestions +            suggestion.reinsertPrefix = "#{keyword} " if suggestion.insertText + +        onComplete suggestions    mkSuggestion: (insertText, args...) ->      suggestion = new Suggestion args... @@ -410,9 +412,11 @@ class SearchEngineCompleter  # A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top  # 10. Queries from the vomnibar frontend script come through a multi completer.  class MultiCompleter -  constructor: (@completers) -> @maxResults = 10 +  constructor: (@completers) -> +    @maxResults = 10 -  refresh: -> completer.refresh() for completer in @completers when completer.refresh +  refresh: -> +    completer.refresh?() for completer in @completers    filter: (queryTerms, onComplete) ->      # Allow only one query to run at a time. @@ -424,21 +428,40 @@ class MultiCompleter      @filterInProgress = true      suggestions = []      completersFinished = 0 +    continuation = null      for completer in @completers        # Call filter() on every source completer and wait for them all to finish before returning results. -      completer.filter queryTerms, (newSuggestions) => -        suggestions = suggestions.concat(newSuggestions) -        completersFinished += 1 -        if completersFinished >= @completers.length -          results = @sortSuggestions(suggestions)[0...@maxResults] -          result.generateHtml() for result in results -          onComplete(results) -          @filterInProgress = false -          @filter(@mostRecentQuery.queryTerms, @mostRecentQuery.onComplete) if @mostRecentQuery - -  sortSuggestions: (suggestions) -> -    suggestion.computeRelevancy(@queryTerms) for suggestion in suggestions +      # At most one of the completers (SearchEngineCompleter) may pass a continuation function, which will be +      # called asynchronously after the results of all of the other completers have been posted.  Any +      # additional results from this continuation will be added to the existing results and posted.  We don't +      # call the continuation if another query is already waiting. +      completer.filter queryTerms, (newSuggestions, cont = null) => +        # Allow completers to execute concurrently. +        Utils.nextTick => +          suggestions = suggestions.concat newSuggestions +          continuation = cont if cont? +          completersFinished += 1 +          if completersFinished >= @completers.length +            onComplete @prepareSuggestions(suggestions), keepAlive: continuation? +            onDone = => +              @filterInProgress = false +              @filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery +            # We add a very short delay.  It is possible for all of this processing to have been handled +            # pretty-much synchronously, which would have prevented any newly-arriving queries from +            # registering. +            Utils.setTimeout 10, => +              if continuation? and not @mostRecentQuery +                continuation (newSuggestions) => +                  onComplete @prepareSuggestions suggestions.concat(newSuggestions) +                  onDone() +              else +                onDone() + +  prepareSuggestions: (suggestions) -> +    suggestion.computeRelevancy @queryTerms for suggestion in suggestions      suggestions.sort (a, b) -> b.relevancy - a.relevancy +    suggestions = suggestions[0...@maxResults] +    suggestion.generateHtml() for suggestion in suggestions      suggestions  # Utilities which help us compute a relevancy score for a given item. diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 4d2546fc..45619023 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -50,11 +50,13 @@ completionSources =    searchEngines: new SearchEngineCompleter()  completers = -  omni: new MultiCompleter([ -    completionSources.searchEngines, -    completionSources.bookmarks, -    completionSources.history, -    completionSources.domains]) +  omni: new MultiCompleter [ +    completionSources.bookmarks +    completionSources.history +    completionSources.domains +    # This comes last, because it delivers additional, asynchronous results. +    completionSources.searchEngines +    ]    bookmarks: new MultiCompleter([completionSources.bookmarks])    tabs: new MultiCompleter([completionSources.tabs]) @@ -220,7 +222,8 @@ refreshCompleter = (request) -> completers[request.name].refresh()  whitespaceRegexp = /\s+/  filterCompleter = (args, port) ->    queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp) -  completers[args.name].filter(queryTerms, (results) -> port.postMessage({ id: args.id, results: results })) +  completers[args.name].filter queryTerms, (results, extra = {}) -> +    port.postMessage extend extra, id: args.id, results: results  chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->    if (selectionChangedHandlers.length > 0) diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee index a80c218c..abf8c86e 100644 --- a/background_scripts/search_engines.coffee +++ b/background_scripts/search_engines.coffee @@ -134,8 +134,7 @@ SearchEngines =      @requests[searchUrl] = xhr = new XMLHttpRequest()      xhr.open "GET", url, true -    # We set a fairly short timeout.  If we block for too long, then we block *all* completers. -    xhr.timeout = 500 +    xhr.timeout = 750      xhr.ontimeout = => @cancel searchUrl, callback      xhr.onerror = => @cancel searchUrl, callback      xhr.send() diff --git a/lib/utils.coffee b/lib/utils.coffee index 5d9696e1..07528714 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -183,6 +183,12 @@ Utils =        return true if re.test string      false +  # Convenience wrapper for setTimeout (with the arguments around the other way). +  setTimeout: (ms, func) -> setTimeout func, ms + +  # Like Nodejs's nextTick. +  nextTick: (func) -> @setTimeout 0, func +  # 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. diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 2076fea6..3fb63177 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -239,7 +239,7 @@ class BackgroundCompleter      id = BackgroundCompleter.messageId += 1      @filterPort.onMessage.addListener handler = (msg) =>        if msg.id == id -        @filterPort.onMessage.removeListener handler +        @filterPort.onMessage.removeListener handler unless msg.keepAlive and id == BackgroundCompleter.messageId          if id == BackgroundCompleter.messageId            # The result objects coming from the background page will be of the form:            #   { html: "", type: "", url: "" } | 
