diff options
| author | Stephen Blott | 2015-05-08 14:02:21 +0100 | 
|---|---|---|
| committer | Stephen Blott | 2015-05-08 14:15:21 +0100 | 
| commit | b882213019792a7fb47352a920a54d468d352c86 (patch) | |
| tree | 3d74271999b209af9a0c015434e40e2d3be452b6 | |
| parent | 44378220093ee5bd873b553f5be556921c778663 (diff) | |
| download | vimium-b882213019792a7fb47352a920a54d468d352c86.tar.bz2 | |
Search completion; exclusivity.
If we have a custom search engine with a completer, then exclude
suggestions from other completion engines.
| -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: => | 
