diff options
| -rw-r--r-- | background_scripts/completion.coffee | 65 | ||||
| -rw-r--r-- | background_scripts/completion_search.coffee | 14 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 123 | 
3 files changed, 43 insertions, 159 deletions
| diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index ebf56dde..68edad99 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -10,7 +10,7 @@  #  - refresh(): (optional) refreshes the completer's data source (e.g. refetches the list of bookmarks).  #  - cancel(): (optional) cancels any pending, cancelable action.  class Suggestion -  showRelevancy: false # Set this to true to render relevancy when debugging the ranking scores. +  showRelevancy: true # Set this to true to render relevancy when debugging the ranking scores.    constructor: (@options) ->      # Required options. @@ -397,20 +397,19 @@ class SearchEngineCompleter        callback engines        # Let the front-end vomnibar know the search-engine keywords.  It needs to know them so that, when the -      # query goes from "w" to "w ", the vomnibar synchronously launches the next filter() request (all of which avoids -      # an ugly delay). +      # query goes from "w" to "w ", the vomnibar can synchronously launch the next filter() request (which +      # avoids an ugly delay/flicker).        port.postMessage          handler: "keywords"          keywords: key for own key of engines    filter: (request, onComplete) ->      { queryTerms, query, engine } = request -    [ primarySuggestion, removePrimarySuggestion ] = [ null, false ]      { custom, searchUrl, description } =        if engine          { keyword, searchUrl, description } = engine -        extend request, { searchUrl, suppressLeadingKeyword: keyword } +        extend request, { searchUrl, customSearchMode: true }          custom: true          searchUrl: searchUrl          description: description @@ -421,35 +420,32 @@ class SearchEngineCompleter      return onComplete [] unless custom or 0 < queryTerms.length -    factor = Math.max 0, Math.min 1, Settings.get "omniSearchWeight" +    factor = Math.max 0.0, Math.min 1.0, Settings.get "omniSearchWeight"      haveCompletionEngine = (0.0 < factor or custom) and CompletionSearch.haveCompletionEngine searchUrl      # Relevancy:      #   - Relevancy does not depend upon the actual suggestion (so, it does not depend upon word      #     relevancy, say).  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. +    #     completion engines sometimes handle spelling mistakes, in which case we wouldn't find the query +    #     terms in the suggestion anyway.      #   - Scores are weighted such that they retain the order provided by the completion engine.      #   - 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.      #      characterCount = query.length - queryTerms.length + 1 -    relevancy = (if custom then 0.9 else factor) * (Math.min(characterCount, 12.0)/12.0) +    relevancy = (if custom then 0.5 else factor) * 12.0 / Math.max 12.0, characterCount +    console.log factor, relevancy -    # This filter is applied to all of the suggestions from all of the completers. +    # This filter is applied to all of the suggestions from all of the completers, after they have been +    # aggregated by the MultiCompleter.      filter = (suggestions) ->        return suggestions unless custom and haveCompletionEngine -      # The primary suggestion was just a guess.  If we've managed fetch actual completions (asynchronously), -      # then we now remove it. -      if removePrimarySuggestion -        suggestions = suggestions.filter (suggestion) -> suggestion != primarySuggestion -        # We only accept suggestions:        #   - from this completer, or -      #   - from other completers, but then only if their URL matches this search engine and this query -      #     (that is only if their URL could have been generated by this search engine). +      #   - from other completers, but then only if their URL matches this search engine and matches this +      #   query (that is only if their URL could have been generated by this search engine).        suggestions.filter (suggestion) ->          suggestion.type == description or            # This is a suggestion for the same search engine. @@ -466,7 +462,6 @@ class SearchEngineCompleter        autoSelect: custom        forceAutoSelect: custom        highlightTerms: not haveCompletionEngine -      searchSuggestionType: "primary"      mkSuggestion = (suggestion) ->        new Suggestion @@ -477,43 +472,27 @@ class SearchEngineCompleter          relevancy: relevancy *= 0.9          insertText: suggestion          highlightTerms: false -        searchSuggestionType: "completion" - -    deliverCompletions = (onComplete, completions, args...) -> -      # Make the first suggestion float to the top of the vomnibar (except if we would be competing with the -      # domain completer, which also assigns a relevancy of 1). -      if 0 < completions.length -        if custom # or (1 < queryTerms.length or /\S\s/.test query) -          extend completions[0], -            relevancy: 1 -            autoSelect: custom -            forceAutoSelect: custom -            insertText: null -      onComplete completions, args...      cachedSuggestions =        if haveCompletionEngine then CompletionSearch.complete searchUrl, queryTerms else null -    suggestions = -      if cachedSuggestions? and 0 < cachedSuggestions.length -        cachedSuggestions.map mkSuggestion -      else if custom -        [ primarySuggestion ] -      else -        [] +    suggestions = [] +    suggestions.push primarySuggestion if custom +    suggestions.push cachedSuggestions.map(mkSuggestion)... if custom and cachedSuggestions?      if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine        # There is no prospect of adding further completions. -      deliverCompletions onComplete, suggestions, { filter, continuation: null } +      suggestions.push cachedSuggestions.map(mkSuggestion)... if cachedSuggestions? +      onComplete suggestions, { filter, continuation: null }      else -      # Post the initial suggestions, then deliver further completions asynchronously, as a continuation. -      deliverCompletions onComplete, suggestions, +      # Post the initial suggestions, but then deliver any further completions asynchronously, as a +      # continuation. +      onComplete suggestions,          filter: filter          continuation: (onComplete) =>            CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) =>              console.log "fetched suggestions:", suggestions.length, query if SearchEngineCompleter.debug -            removePrimarySuggestion = 0 < suggestions.length -            deliverCompletions onComplete, suggestions.map mkSuggestion +            onComplete suggestions.map mkSuggestion  # A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top  # 10. All queries from the vomnibar come through a multi completer. diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee index a9521a3d..c6824594 100644 --- a/background_scripts/completion_search.coffee +++ b/background_scripts/completion_search.coffee @@ -1,6 +1,6 @@  CompletionSearch = -  debug: false +  debug: true    inTransit: {}    completionCache: new SimpleCache 2 * 60 * 60 * 1000, 5000 # Two hours, 5000 entries.    engineCache:new SimpleCache 1000 * 60 * 60 * 1000 # 1000 hours. @@ -75,13 +75,16 @@ CompletionSearch =          # Verify that the previous query is a prefix of the current query.          return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()          # Verify that every previous suggestion contains the text of the new query. -        for suggestion in (@mostRecentSuggestions.map (s) -> s.toLowerCase()) +        # Note: @mostRecentSuggestions may also be empty, in which case we drop though. The effect is that +        # previous queries with no suggestions suppress subsequent no-hope HTTP requests as the user continues +        # to type. +        for suggestion in @mostRecentSuggestions            return false unless 0 <= suggestion.indexOf query          # Ok. Re-use the suggestion.          true        if reusePreviousSuggestions -        console.log "reuse previous query:", @mostRecentQuery if @debug +        console.log "reuse previous query:", @mostRecentQuery, @mostRecentSuggestions.length if @debug          return callback @completionCache.set completionCacheKey, @mostRecentSuggestions      # That's all of the caches we can try.  Bail if the caller is only requesting synchronous results.  We @@ -104,8 +107,11 @@ CompletionSearch =              # incorrect or out-of-date completion engines.              try                suggestions = engine.parse xhr +              # Make all suggestions lower case.  It looks odd when suggestions from one completion engine are +              # upper case, and those from another are lower case. +              suggestions = (suggestion.toLowerCase() for suggestion in suggestions)                # Filter out the query itself. It's not adding anything. -              suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query) +              suggestions = (suggestion for suggestion in suggestions when suggestion != query)                console.log "GET", url if @debug              catch                suggestions = [] diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index a96a3b4f..28ecdc37 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -69,8 +69,7 @@ class VomnibarUI      @completions = []      @previousAutoSelect = null      @previousInputValue = null -    @lastUpdateTime = null -    @suppressedLeadingKeyword = null +    @customSearchMode = null      @selection = @initialSelectionValue      @keywords = [] @@ -84,71 +83,27 @@ class VomnibarUI      else        @previousAutoSelect = null -    # Notwithstanding all of the above, disable autoSelect if the user is deleting text from the query. -    if @lastAction == "delete" -      @selection = -1 -      @previousAutoSelect = null -      # For custom search engines, we suppress the leading term (e.g. the "w" of "w query terms") within the      # vomnibar input. -    if @lastReponse.suppressLeadingKeyword and not @suppressedLeadingKeyword? +    if @lastReponse.customSearchMode and not @customSearchMode?        queryTerms = @input.value.trim().split /\s+/ -      @suppressedLeadingKeyword = queryTerms[0] +      @customSearchMode = queryTerms[0]        @input.value = queryTerms[1..].join " "      # For suggestions for custom search engines, we copy the suggested text into the input when the item is      # selected, and revert when it is not.  This allows the user to select a suggestion and then continue      # typing.      if 0 <= @selection and @completions[@selection].insertText? -      @previousInputValue ?= -        value: @input.value -        selectionStart: @input.selectionStart -        selectionEnd: @input.selectionEnd +      @previousInputValue ?= @input.value        @input.value = @completions[@selection].insertText + (if @selection == 0 then "" else " ")      else if @previousInputValue? -      # Restore the text. -      @input.value = @previousInputValue.value -      # Restore the selection. -      if @previousInputValue.selectionStart? and @previousInputValue.selectionEnd? and -        @previousInputValue.selectionStart != @previousInputValue.selectionEnd -          @input.setSelectionRange @previousInputValue.selectionStart, @previousInputValue.selectionEnd +      @input.value = @previousInputValue        @previousInputValue = null      # Highlight the selected entry, and only the selected entry.      for i in [0...@completionList.children.length]        @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") -  # This adds prompted text to the vomnibar input.  The prompted text is a continuation of the text the user -  # has already typed, taken from one of the search suggestions.  It is highlight (using the selection) and -  # will be included with the query should the user type <Enter>. -  addPromptedText: -> -    # Bail if we don't yet have the background completer's final word on the current query. -    return unless @lastReponse.mayCacheResults - -    # Bail if the last action was "delete"; or we may be putting back what the user just deleted. -    return if @lastAction == "delete" - -    # Bail if there's an update pending, because @input and the completion state are out of sync. -    return if @updateTimer? - -    completions = @completions.filter (completion) -> -      completion. searchSuggestionType in [ "primary", "completion" ] -    return unless 0 < completions.length - -    query = @getInputWithoutPromptedText().ltrim().split(/\s+/).join(" ").toLowerCase() -    suggestion = completions[0].title - -    index = suggestion.toLowerCase().indexOf query -    return unless 0 <= index and index + query.length < suggestion.length - -    # If the typed text is all lower case, then make the prompted text lower case too. -    suggestion = suggestion[index..] -    suggestion = suggestion.toLowerCase() unless /[A-Z]/.test @getInputWithoutPromptedText() - -    suggestion = suggestion[query.length..] -    @input.value = query + suggestion -    @input.setSelectionRange query.length, query.length + suggestion.length -    # Returns the user's action ("up", "down", "tab", etc, or null) based on their keypress.  We support the    # arrow keys and various other shortcuts, and this function hides the event-decoding complexity.    actionFromKeyEvent: (event) -> @@ -182,12 +137,6 @@ class VomnibarUI      if (action == "dismiss")        @hide()      else if action in [ "tab", "down" ] -      # if action == "tab" -      #   if @inputContainsASelectionRange() -      #     window.getSelection().collapseToEnd() -      #   else -      #     action = "down" -      # if action == "down"        @selection += 1        @selection = @initialSelectionValue if @selection == @completions.length        @updateSelection() @@ -196,10 +145,6 @@ class VomnibarUI        @selection = @completions.length - 1 if @selection < @initialSelectionValue        @updateSelection()      else if (action == "enter") -      # <Enter> immediately after new suggestions have been posted is ignored.  It's all too common that the -      # user gets results they weren't intending. -      return if @lastUpdateTime? and new Date() - @lastUpdateTime < 250 and @inputContainsASelectionRange() -      @lastUpdateTime = null        if @selection == -1          query = @input.value.trim()          # <Enter> on an empty query is a no-op. @@ -217,67 +162,24 @@ class VomnibarUI          completion = @completions[@selection]          @hide -> completion.performAction openInNewTab      else if action == "delete" -      if @suppressedLeadingKeyword? and @input.value.length == 0 +      if @customSearchMode? and @input.value.length == 0          # Normally, with custom search engines, the keyword (e,g, the "w" of "w query terms") is suppressed. -        # If the input is empty, then show the keyword again. -        @input.value = @suppressedLeadingKeyword -        @suppressedLeadingKeyword = null +        # If the input is empty, then reinstate the keyword (the "w"). +        @input.value = @customSearchMode +        @customSearchMode = null          @updateCompletions()        else          return true # Do not suppress event. -    else if action in [ "left", "right" ] -      [ start, end ] = [ @input.selectionStart, @input.selectionEnd ] -      if event.ctrlKey and not (event.altKey or event.metaKey) -        return true unless @inputContainsASelectionRange() and end == @input.value.length -        # "Control-Right" advances the start of the selection by a word. -        text = @input.value[start...end] -        switch action -          when "right" -            newText = text.replace /^\s*\S+\s*/, "" -            @input.setSelectionRange start + (text.length - newText.length), end -          when "left" -            newText = text.replace /\S+\s*$/, "" -            @input.setSelectionRange start + (newText.length - text.length), end -      else -        return true # Do not suppress event.      # It seems like we have to manually suppress the event here and still return true.      event.stopImmediatePropagation()      event.preventDefault()      true -  onKeypress: (event) => -    # The user is typing. They know what they're doing. -    @lastUpdateTime = null -    # Handle typing together with prompted text. -    unless event.altKey or event.ctrlKey or event.metaKey -      if @inputContainsASelectionRange() -        # As the user types characters which the match the prompted text, we suppress the keyboard event and -        # simulate it by advancing the start of the selection (but only if the typed character matches). -        # If we were to allow the event through, we would get flicker, as the selection is first collapsed and -        # then (shortly afterwards) restored. -        if @input.value[@input.selectionStart][0].toLowerCase() == (String.fromCharCode event.charCode).toLowerCase() -          @input.setSelectionRange @input.selectionStart + 1, @input.selectionEnd -          @updateOnInput() -          event.stopImmediatePropagation() -          event.preventDefault() -    true - -  # Test whether the input contains prompted text. -  inputContainsASelectionRange: -> -    @input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd - -  # Return the text of the input, with any prompted text removed. -  getInputWithoutPromptedText: -> -    if @inputContainsASelectionRange() -      @input.value[0...@input.selectionStart] + @input.value[@input.selectionEnd..] -    else -      @input.value -    # Return the background-page query corresponding to the current input state.  In other words, reinstate any    # search engine keyword which is currently being suppressed, and strip any prompted text.    getInputValueAsQuery: -> -    (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutPromptedText() +    (if @customSearchMode? then @customSearchMode + " " else "") + @input.value    updateCompletions: (callback = null) ->      @completer.filter @@ -291,8 +193,6 @@ class VomnibarUI          @selection = Math.min @completions.length - 1, Math.max @initialSelectionValue, @selection          @previousAutoSelect = null if @completions[0]?.autoSelect and @completions[0]?.forceAutoSelect          @updateSelection() -        @addPromptedText() -        @lastUpdateTime = new Date()          callback?()    updateOnInput: => @@ -316,7 +216,7 @@ class VomnibarUI    update: (updateSynchronously = false, callback = null) =>      # If the query text becomes a custom search (the user enters a search keyword), then we need to force a      # synchronous update (so that the state is updated immediately). -    updateSynchronously ||= @isCustomSearch() and not @suppressedLeadingKeyword? +    updateSynchronously ||= @isCustomSearch() and not @customSearchMode?      if updateSynchronously        @clearUpdateTimer()        @updateCompletions callback @@ -335,7 +235,6 @@ class VomnibarUI      @input = @box.querySelector("input")      @input.addEventListener "input", @updateOnInput      @input.addEventListener "keydown", @onKeydown -    @input.addEventListener "keypress", @onKeypress      @completionList = @box.querySelector("ul")      @completionList.style.display = "" | 
