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: "" } |
