diff options
| -rw-r--r-- | background_scripts/completion.coffee | 219 | ||||
| -rw-r--r-- | background_scripts/completion_engines.coffee | 7 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 1 |
3 files changed, 131 insertions, 96 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 48039cd2..b9efb034 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -22,8 +22,12 @@ class Suggestion @title = "" # Extra data which will be available to the relevancy function. @relevancyData = null - # When @autoSelect is truthy, the suggestion is automatically pre-selected in the vomnibar. + # If @autoSelect is truthy, then this suggestion is automatically pre-selected in the vomnibar. There may + # be at most one such suggestion. @autoSelect = false + # If truthy (and @autoSelect is truthy too), then this suggestion is always pre-selected when the query + # changes. There may be at most one such suggestion. + @forceAutoSelect = false # If @highlightTerms is true, then we highlight matched terms in the title and URL. @highlightTerms = true # If @insertText is a string, then the indicated text is inserted into the vomnibar input when the @@ -356,7 +360,7 @@ class SearchEngineCompleter { keyword, searchUrl, description } = @getSearchEngineMatches queryTerms custom = searchUrl? and keyword? - searchUrl ?= Settings.get("searchUrl") + "%s" + searchUrl ?= Settings.get "searchUrl" haveDescription = description? and 0 < description.length description ||= "#{if custom then "custom " else ""}search" @@ -364,7 +368,7 @@ class SearchEngineCompleter query = queryTerms.join " " if queryTerms.length == 0 - return onComplete suggestions + return onComplete [] # For custom search engines, we add an auto-selected suggestion. if custom @@ -376,48 +380,52 @@ class SearchEngineCompleter relevancy: 1 highlightTerms: false autoSelect: true - - onComplete suggestions, (existingSuggestions, 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). - - # Relevancy: - # - Relevancy 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. Also, completion - # engines often handle spelling mistakes, in which case we wouldn't find the query terms in the - # suggestion anyway. - # - The relavancy is higher if the query term is longer. The idea is that search suggestions are more - # likely to be relevant if, after typing some number of characters, the user hasn't yet found - # a useful suggestion from another completer. - # - Scores are weighted such that they retain the order provided by the completion engine. - characterCount = query.length - queryTerms.length + 1 - relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0) - - if 0 < existingSuggestions.length - existingSuggestionsMinScore = existingSuggestions[existingSuggestions.length-1].relevancy - if relavancy < existingSuggestionsMinScore and MultiCompleter.maxResults <= existingSuggestions.length - # No suggestion we propose will have a high enough relavancy to beat the existing suggestions, so bail - # immediately. - return onComplete [] - - CompletionEngines.complete searchUrl, queryTerms, (completionSuggestions = []) => - for suggestion in completionSuggestions - suggestions.push new Suggestion - queryTerms: queryTerms - type: description - url: Utils.createSearchUrl suggestion, searchUrl - title: suggestion - relevancy: relavancy *= 0.9 - highlightTerms: false - insertText: if custom then "#{keyword} #{suggestion}" else suggestion - - # We keep at least three suggestions (if possible) and at most six. We keep more than three only if - # there are enough slots. The idea is that these suggestions shouldn't wholly displace suggestions - # from other completers. That would potentially be a problem because there is no relationship - # between the relevancy scores produced here and those produced by other completers. - count = Math.min 6, Math.max 3, MultiCompleter.maxResults - existingSuggestions.length - onComplete suggestions[...count] + # Always reset the selection to this suggestion on query change. The UX is weird otherwise. + forceAutoSelect: true + + onComplete suggestions, + exclusive: if custom and CompletionEngines.haveCompletionEngine searchUrl then description else null + continuation: (existingSuggestions, 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). + + # Relevancy: + # - Relevancy 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. Also, completion + # engines often handle spelling mistakes, in which case we wouldn't find the query terms in the + # suggestion anyway. + # - The relavancy is higher if the query term is longer. The idea is that search suggestions are more + # likely to be relevant if, after typing some number of characters, the user hasn't yet found + # a useful suggestion from another completer. + # - Scores are weighted such that they retain the order provided by the completion engine. + characterCount = query.length - queryTerms.length + 1 + relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0) + + if 0 < existingSuggestions.length + existingSuggestionsMinScore = existingSuggestions[existingSuggestions.length-1].relevancy + if relavancy < existingSuggestionsMinScore and MultiCompleter.maxResults <= existingSuggestions.length + # No suggestion we propose will have a high enough relavancy to beat the existing suggestions, so bail + # immediately. + return onComplete [] + + CompletionEngines.complete searchUrl, queryTerms, (completionSuggestions = []) => + for suggestion in completionSuggestions + suggestions.push new Suggestion + queryTerms: queryTerms + type: description + url: Utils.createSearchUrl suggestion, searchUrl + title: suggestion + relevancy: relavancy *= 0.9 + highlightTerms: false + insertText: if custom then "#{keyword} #{suggestion}" else suggestion + + # We keep at least three suggestions (if possible) and at most six. We keep more than three only if + # there are enough slots. The idea is that these suggestions shouldn't wholly displace suggestions + # from other completers. That would potentially be a problem because there is no relationship + # between the relevancy scores produced here and those produced by other completers. + count = Math.min 6, Math.max 3, MultiCompleter.maxResults - existingSuggestions.length + onComplete suggestions[...count] refresh: -> @searchEngines = SearchEngineCompleter.getSearchEngines() @@ -462,56 +470,77 @@ class MultiCompleter cancel: -> completer.cancel?() for completer in @completers - filter: (queryTerms, onComplete) -> - # Allow only one query to run at a time. - if @filterInProgress - @mostRecentQuery = [ queryTerms, onComplete ] - return - RegexpCache.clear() - @mostRecentQuery = null - @filterInProgress = true - suggestions = [] - continuation = null - activeCompleters = [0...@completers.length] - # Call filter() on every source completer and wait for them all to finish before returning results. - # At most one of the completers (SearchEngineCompleter) may pass a continuation function, which will be - # called 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 later. We don't call the - # continuation if another query is already waiting. - for completer, index in @completers - do (completer, index) => - completer.filter queryTerms, (newSuggestions, newContinuation = null) => - if index not in activeCompleters - # NOTE(smblott) I suspect one of the completers is calling onComplete more than once. (And the - # legacy code had ">=" where "==" should have sufficed.) This is just to track that case down. - console.log "XXXXXXXXXXXXXXX, onComplete called twice!" - console.log completer - activeCompleters = activeCompleters.filter (i) -> i != index - suggestions.push newSuggestions... - continuation = continuation ? newContinuation - if activeCompleters.length == 0 - shouldRunContinuation = continuation? and not @mostRecentQuery - console.log "skip continuation" if continuation? and not shouldRunContinuation - # We don't post results immediately if there are none, and we're going to run a continuation - # (ie. a SearchEngineCompleter). This collapsing the vomnibar briefly before expanding it - # again, which looks ugly. - unless shouldRunContinuation and suggestions.length == 0 - onComplete - results: @prepareSuggestions queryTerms, suggestions - callerMayCacheResults: not shouldRunContinuation - # Allow subsequent queries to begin. - @filterInProgress = false - if shouldRunContinuation - continuation suggestions, (newSuggestions) => - if 0 < newSuggestions.length - suggestions.push newSuggestions... - onComplete - results: @prepareSuggestions queryTerms, suggestions - callerMayCacheResults: true - else - if @mostRecentQuery - console.log "running pending query:", @mostRecentQuery[0] - @filter @mostRecentQuery... + filter: do -> + defaultCallbackOptions = + # At most one of the completers (SearchEngineCompleter) may pass a continuation function, which will be + # called 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 later. We don't call the + # continuation if another query is already waiting. + continuation: null + # If truthy, completions from other completers should be discarded. The truthy value should be the type + # of the completer (e.g. "custom search"). + exclusive: false + + (queryTerms, onComplete) -> + # Allow only one query to run at a time. + if @filterInProgress + @mostRecentQuery = [ queryTerms, onComplete ] + return + RegexpCache.clear() + @mostRecentQuery = null + @filterInProgress = true + suggestions = [] + continuation = null + exclusive = null + activeCompleters = [0...@completers.length] + # Call filter() on every source completer and wait for them all to finish before returning results. + for completer, index in @completers + do (completer, index) => + completer.filter queryTerms, (newSuggestions, options = defaultCallbackOptions) => + if index not in activeCompleters + # NOTE(smblott) I suspect one of the completers is calling onComplete more than once. (And the + # legacy code had ">=" where "==" should have sufficed.) This is just to track that case down. + console.log "XXXXXXXXXXXXXXX, onComplete called twice!" + console.log completer + activeCompleters = activeCompleters.filter (i) -> i != index + suggestions.push newSuggestions... + continuation = continuation ? options.continuation + exclusive = options.exclusive if options.exclusive? + + if activeCompleters.length == 0 + # All the completers have now returned; we combine the results, post them and call any + # continuation. + shouldRunContinuation = continuation? and not @mostRecentQuery + console.log "skip continuation" if continuation? and not shouldRunContinuation + + # If one completer has claimed exclusivity (SearchEngineCompleter), then filter out results from + # other completers. + if exclusive + suggestions = suggestions.filter (suggestion) -> suggestion.type == exclusive + + # We don't post results immediately if there are none, and we're going to run a continuation + # (ie. a SearchEngineCompleter). This collapsing the vomnibar briefly before expanding it + # again, which looks ugly. + unless shouldRunContinuation and suggestions.length == 0 + onComplete + results: @prepareSuggestions queryTerms, suggestions + callerMayCacheResults: not shouldRunContinuation + + # Allow subsequent queries to begin. + @filterInProgress = false + + # Launch continuation or any pending query. + if shouldRunContinuation + continuation suggestions, (newSuggestions) => + if 0 < newSuggestions.length + suggestions.push newSuggestions... + onComplete + results: @prepareSuggestions queryTerms, suggestions + callerMayCacheResults: true + else + if @mostRecentQuery + console.log "running pending query:", @mostRecentQuery[0] + @filter @mostRecentQuery... prepareSuggestions: (queryTerms, suggestions) -> suggestion.computeRelevancy queryTerms for suggestion in suggestions diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee index 52db90d0..ff10f4b5 100644 --- a/background_scripts/completion_engines.coffee +++ b/background_scripts/completion_engines.coffee @@ -100,6 +100,7 @@ class DuckDuckGo extends RegexpEngine # 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" @@ -140,7 +141,7 @@ CompletionEngines = # 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 input event in the vomnibar, we cache the result. + # 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 @@ -150,6 +151,10 @@ 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. diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 76e276a1..dd680a6a 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -156,6 +156,7 @@ class VomnibarUI @completionList.innerHTML = completions.map((completion) -> "<li>#{completion.html}</li>").join("") @completionList.style.display = if completions.length > 0 then "block" else "" @selection = Math.min completions.length - 1, Math.max @initialSelectionValue, @selection + @previousAutoSelect = null if completions[0]?.autoSelect and completions[0]?.forceAutoSelect @updateSelection() updateOnInput: => |
