diff options
| author | Stephen Blott | 2015-05-09 11:30:59 +0100 | 
|---|---|---|
| committer | Stephen Blott | 2015-05-09 14:11:31 +0100 | 
| commit | d73775057d443a53668f6a93fe45cc4a4b412de7 (patch) | |
| tree | 34deac62bca83c11b60cfe4c1987ffe21073e8fd | |
| parent | 75051d53536ddb9f247501b4509306cae1734184 (diff) | |
| download | vimium-d73775057d443a53668f6a93fe45cc4a4b412de7.tar.bz2 | |
Search completion; complete commmon search term.
| -rw-r--r-- | background_scripts/completion.coffee | 4 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 148 | ||||
| -rw-r--r-- | pages/vomnibar.css | 4 | ||||
| -rw-r--r-- | pages/vomnibar.html | 2 | ||||
| -rw-r--r-- | tests/dom_tests/vomnibar_test.coffee | 2 | 
5 files changed, 129 insertions, 31 deletions
| diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 8c73c658..850a257d 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -547,7 +547,7 @@ class MultiCompleter                unless suggestions.length == 0 and shouldRunContinuations                  onComplete                    results: @prepareSuggestions queryTerms, suggestions -                  mayCacheResult: continuations.length == 0 +                  mayCacheResults: continuations.length == 0                # Run any continuations, unless there's a pending query.                if shouldRunContinuations @@ -560,7 +560,7 @@ class MultiCompleter                        results: @prepareSuggestions queryTerms, suggestions                        # FIXME(smblott) This currently assumes that there is at most one continuation.  We                        # should really be counting pending/completed continuations. -                      mayCacheResult: true +                      mayCacheResults: true                # Admit subsequent queries, and launch any pending query.                @filterInProgress = false diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index bb7720e9..db380063 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -69,6 +69,7 @@ class VomnibarUI      @previousAutoSelect = null      @previousInputValue = null      @suppressedLeadingKeyword = null +    @previousLength = 0      @selection = @initialSelectionValue      @keywords = [] @@ -92,7 +93,7 @@ class VomnibarUI      # For suggestions from search-engine completion, we copy the suggested text into the input when selected,      # and revert when not.  This allows the user to select a suggestion and then continue typing.      if 0 <= @selection and @completions[@selection].insertText? -      @previousInputValue ?= @input.value +      @previousInputValue ?= @getInputWithoutSelectionRange()        @input.value = @completions[@selection].insertText + " "      else if @previousInputValue?          @input.value = @previousInputValue @@ -102,8 +103,66 @@ class VomnibarUI      for i in [0...@completionList.children.length]        @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") +  highlightCommonMatches: (response) -> +    # For custom search engines, add characters to the input which are: +    #   - not in the query/input +    #   - in all completions +    # and select the added text. + +    # Bail if we don't yet have the background completer's final word on the current query. +    return unless response.mayCacheResults + +    # Bail if there's an update pending (because @input and the correct completion state are out of sync). +    return if @updateTimer? + +    @previousLength ?= @input.value.length +    previousLength = @previousLength +    currentLength = @input.value.length +    @previousLength = currentLength + +    # Bail if the query didn't get longer. +    console.log previousLength < currentLength, previousLength, currentLength, @input.value +    return unless previousLength < currentLength + +    # Bail if these aren't completions from a custom search engine. +    return unless @suppressedLeadingKeyword? + +    # Bail if there are too few suggestions. +    return unless 1 < @completions.length + +    # Fetch the query and the suggestion texts. +    query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase() +    suggestions = @completions[1..].map (completion) -> completion.title + +    # Ensure that the query is a prefix of all suggestions. +    for suggestion in suggestions +      return unless 0 == suggestion.toLowerCase().indexOf query + +    # Calculate the length of the shotest suggestion. +    length = suggestions[0].length +    length = Math.min length, suggestion.length for suggestion in suggestions + +    # Find the thenght of the longest common continuation. +    length = do -> +      for index in [query.length...length] +        for suggestion in suggestions +          return index if suggestions[0][index].toLowerCase() != suggestion[index].toLowerCase() +      length + +    # But don't complete only whitespace. +    return if /^\s+$/.test suggestions[0].slice query.length, length + +    # Bail if there's nothing to complete. +    return unless  query.length < length + +    # Install completion. +    @input.value = suggestions[0].slice 0, length +    @input.setSelectionRange query.length, length +    # @previousLength = @input.value.length +    # -  # Returns the user's action ("up", "down", "enter", "dismiss", "delete" or null) based on their keypress. +  # Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their +  # keypress.    # We support the arrow keys and other shortcuts for moving, so this method hides that complexity.    #    actionFromKeyEvent: (event) -> @@ -114,8 +173,9 @@ class VomnibarUI          (event.shiftKey && event.keyCode == keyCodes.tab) ||          (event.ctrlKey && (key == "k" || key == "p")))        return "up" +    else if (event.keyCode == keyCodes.tab && !event.shiftKey) +      return "tab"      else if (key == "down" || -        (event.keyCode == keyCodes.tab && !event.shiftKey) ||          (event.ctrlKey && (key == "j" || key == "n")))        return "down"      else if (event.keyCode == keyCodes.enter) @@ -132,19 +192,38 @@ class VomnibarUI        (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event))      if (action == "dismiss")        @hide() +    else if action in [ "tab", "down" ] +      if action == "tab" +        if @inputContainsASelectionRange() +          # There is a selection: callapse it and update the completions. +          window.getSelection().collapseToEnd() +          @update true +        else +          # There is no selection: treat "tab" as "down". +          action = "down" +      if action == "down" +        @selection += 1 +        @selection = @initialSelectionValue if @selection == @completions.length +        @updateSelection()      else if (action == "up")        @selection -= 1        @selection = @completions.length - 1 if @selection < @initialSelectionValue        @updateSelection() -    else if (action == "down") -      @selection += 1 -      @selection = @initialSelectionValue if @selection == @completions.length -      @updateSelection()      else if (action == "enter") -      # If they type something and hit enter without selecting a completion from our list of suggestions, -      # try to open their query as a URL directly. If it doesn't look like a URL, we will search using -      # google. -      if (@selection == -1) +      if @inputContainsASelectionRange() +        # There is selected completion text in the input, put there by highlightCommonMatches().  It looks to +        # the user like, if they type "enter", then that's the query which will fire.  But we don't actually +        # have a URL for this query (it doesn't actually correspond to any of the current completions).  So we +        # fire off a new query and immediately launch the first resulting URL. +        @update true, => +          if @completions[0]? +            completion = @completions[0] +            @hide -> completion.performAction openInNewTab + +      # If the user types something and hits enter without selecting a completion from the list, then try to +      # open their query as a URL directly. If it doesn't look like a URL, then use the default search +      # engine. +      else if (@selection == -1)          query = @input.value.trim()          # <Enter> on an empty vomnibar is a no-op.          return unless 0 < query.length @@ -169,23 +248,35 @@ class VomnibarUI      event.preventDefault()      true -  getInputValue: -> +  # Test whether the input contains selected text. +  inputContainsASelectionRange: -> +    @input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd + +  # Return the text of the input, with any selected text renage removed. +  getInputWithoutSelectionRange: -> +    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 +  # custom search engine keyword which is currently stripped from the input. +  getInputValueAsQuery: ->      (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @input.value    updateCompletions: (callback = null) -> -    @clearUpdateTimer() -    @completer.filter @getInputValue(), (@completions) => -      @populateUiWithCompletions @completions +    @completer.filter @getInputValueAsQuery(), (response) => +      { results, mayCacheResults } = response +      @completions = results +      # Update completion list with the new suggestions. +      @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() +      @highlightCommonMatches response        callback?() -  populateUiWithCompletions: (completions) -> -    # Update completion list with the new suggestions. -    @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: =>      @completer.cancel()      # If the user types, then don't reset any previous text, and re-enable auto-select. @@ -209,11 +300,14 @@ class VomnibarUI      # interface is snappy).      updateSynchronously ||= @isCustomSearch() and not @suppressedLeadingKeyword?      if updateSynchronously +      @clearUpdateTimer()        @updateCompletions callback      else if not @updateTimer?        # Update asynchronously for better user experience and to take some load off the CPU (not every        # keystroke will cause a dedicated update) -      @updateTimer = Utils.setTimeout @refreshInterval, => @updateCompletions callback +      @updateTimer = Utils.setTimeout @refreshInterval, => +        @updateTimer = null +        @updateCompletions callback      @input.focus() @@ -268,14 +362,14 @@ class BackgroundCompleter            # Cache the result -- if we have been told it's ok to do so (it could be that more results will be            # posted shortly).  We cache the result even if it arrives late. -          if msg.mayCacheResult +          if msg.mayCacheResults              console.log "cache set:", "-#{msg.cacheKey}-" if @debug -            @cache[msg.cacheKey] = msg.results +            @cache[msg.cacheKey] = msg            else              console.log "not setting cache:", "-#{msg.cacheKey}-" if @debug            # Handle the message, but only if it hasn't arrived too late. -          @mostRecentCallback msg.results if msg.id == @messageId +          @mostRecentCallback msg if msg.id == @messageId    filter: (query, @mostRecentCallback) ->      queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length diff --git a/pages/vomnibar.css b/pages/vomnibar.css index 2042a6c4..4b7199e3 100644 --- a/pages/vomnibar.css +++ b/pages/vomnibar.css @@ -134,3 +134,7 @@    font-weight: normal;  } +#vomnibarInput::selection { +  /* This is the light grey color of the vomnibar border. */ +  background-color: #F1F1F1; +} diff --git a/pages/vomnibar.html b/pages/vomnibar.html index 2ca463d0..87acc081 100644 --- a/pages/vomnibar.html +++ b/pages/vomnibar.html @@ -14,7 +14,7 @@    <body>      <div id="vomnibar" class="vimiumReset">        <div class="vimiumReset vomnibarSearchArea"> -        <input type="text" class="vimiumReset"> +        <input id="vomnibarInput" type="text" class="vimiumReset">        </div>        <ul class="vimiumReset"></ul>      </div> diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee index 0e02bb7b..e32c050d 100644 --- a/tests/dom_tests/vomnibar_test.coffee +++ b/tests/dom_tests/vomnibar_test.coffee @@ -14,7 +14,7 @@ context "Keep selection within bounds",      oldGetCompleter = vomnibarFrame.Vomnibar.getCompleter.bind vomnibarFrame.Vomnibar      stub vomnibarFrame.Vomnibar, 'getCompleter', (name) =>        completer = oldGetCompleter name -      stub completer, 'filter', (query, callback) => callback(@completions) +      stub completer, 'filter', (query, callback) => callback results: @completions        completer      # Shoulda.js doesn't support async tests, so we have to hack around. | 
