diff options
| author | Stephen Blott | 2015-05-11 12:48:08 +0100 | 
|---|---|---|
| committer | Stephen Blott | 2015-05-11 12:59:44 +0100 | 
| commit | c3134f6496f9b0136f1fa454a2c5f81683713a3a (patch) | |
| tree | 4d1cfb05e2b13f8cd2fa8f80cfe9ca2c9f57154e /pages/vomnibar.coffee | |
| parent | 4f8aeea11d86ac6f7051680edf0b45bf15ce2c14 (diff) | |
| download | vimium-c3134f6496f9b0136f1fa454a2c5f81683713a3a.tar.bz2 | |
Search completion; pre-merge tweaks.
Diffstat (limited to 'pages/vomnibar.coffee')
| -rw-r--r-- | pages/vomnibar.coffee | 186 | 
1 files changed, 45 insertions, 141 deletions
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index fed8680b..d0abe9da 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -91,8 +91,9 @@ class VomnibarUI        @suppressedLeadingKeyword = queryTerms[0]        @input.value = queryTerms[1..].join " " -    # 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. +    # For suggestions from search-engine completion, 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 @@ -112,125 +113,37 @@ class VomnibarUI      for i in [0...@completionList.children.length]        @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") -  # This identifies the common part of all of the (relevant) suggestions which has yet to be typed, adds that -  # text to the input and selects it. Tab (or just Enter) can then be used to accept the new text, or the user -  # can just continue typing. -  selectCommonMatches: (response) -> +  # This adds prompted text to the vomnibar input.  The propted text is a continuation of the text the user +  # has typed already, 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: (response) ->      # 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 then @input and the completion state are out of sync).      return if @updateTimer? -    @previousLength ?= @input.value.length -    previousLength = @previousLength -    currentLength = @input.value.length -    @previousLength = currentLength - -    # We only highlight matches when the query gets longer (so, not on deletions). -    return unless previousLength < currentLength - -    # Get the completions from which we can select text to highlight. -    completions = @completions.filter (completion) -> -      completion.selectCommonMatches? and completion.selectCommonMatches - -    # Bail on leading whitespace or on redundant whitespace.  This provides users with a way to force this -    # feature off. -    value = @input.value -    return if /^\s/.test(value) or /\s\s/.test value - -    # Fetch the query and the suggestion texts. -    query = value.ltrim().split(/\s+/).join(" ").toLowerCase() -    suggestions = completions.map (completion) -> completion.title - -    # Some completion engines add text at the start of the suggestion; for example, Bing takes "they might be" -    # and suggests "Ana Ng They Might be Giants".  In such cases, we should still be able to complete -    # "giants". So, if the query string is present in the suggestion but there is extra text at the start, we -    # strip the prefix. -    suggestions = -      for suggestion in suggestions -        index = Math.max 0, suggestion.toLowerCase().indexOf query -        suggestion[index..] - -    # Strip suggestions which aren't longer than the query (they can't help). -    suggestions = suggestions.filter (suggestion) -> query.length < suggestion.length - -    # Ensure that the query is a prefix of all remaining suggestions. -    for suggestion in suggestions -      return unless 0 == suggestion.toLowerCase().indexOf query - -    # Bail if these aren't any remaining completions. -    return unless 0 < completions.length - -    # Calculate the length of the shortest suggestion. -    length = suggestions[0].length -    length = Math.min length, suggestion.length for suggestion in suggestions - -    # Find the the length of the longest common continuation. -    length = do (suggestions) -> -      suggestions = suggestions.map (s) -> s.toLowerCase() -      [ first, suggestions... ] = suggestions -      for index in [query.length...length] -        for suggestion in suggestions -          return index if first[index] != suggestion[index] -      length - -    # Bail if there's nothing to complete. -    return unless query.length < length - -    completion = suggestions[0].slice query.length, length - -    # Don't complete trailing whitespace, strip it.  Then, verify that the completion is still long enough. -    completion = completion.replace /\s+$/, "" -    return unless 0 < completion.length - -    # If the typed text is all lower case, then make the completion lower case too. -    completion = completion.toLowerCase() unless /[A-Z]/.test @input.value - -    # Insert the completion and highlight it. -    @input.value = query + completion -    @input.setSelectionRange query.length, query.length + completion.length - -  selectFirstSuggestion: (response) -> -    # 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 then @input and the completion state are out of sync). -    return if @updateTimer? - -    value = @getInputWithoutSelectionRange() +    value = @getInputWithoutPromptedText()      @previousLength ?= value.length      previousLength = @previousLength      currentLength = value.length      @previousLength = currentLength -    # We only highlight matches when the query gets longer (so, not on deletions).      return unless previousLength < currentLength - -    # Bail on leading whitespace or on redundant whitespace.  This provides users with a way to force this -    # feature off.      return if /^\s/.test(value) or /\s\s/.test value -    completion = do (completion) => -      for completion in @completions -        continue if completion.custonSearchEnginePrimarySuggestion -        return completion if completion.customSearchEngineCompletionSuggestion -      null - -    return unless completion +    completions = @completions.filter (completion) -> completion.customSearchEngineCompletionSuggestion +    return unless 0 < completions.length -    # Fetch the query and the suggestion texts.      query = value.ltrim().split(/\s+/).join(" ").toLowerCase() -    suggestion = completion.title +    suggestion = completions[0].title      index = suggestion.toLowerCase().indexOf query -    return unless 0 <= index +    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..] -    return unless query.length < suggestion.length - -    # If the typed text is all lower case, then make the completion lower case too. -    suggestion = suggestion.toLowerCase() unless /[A-Z]/.test @getInputWithoutSelectionRange() +    suggestion = suggestion.toLowerCase() unless /[A-Z]/.test @getInputWithoutPromptedText()      suggestion = suggestion[query.length..]      @input.value = query + suggestion @@ -267,20 +180,9 @@ class VomnibarUI      if (action == "dismiss")        @hide()      else if action in [ "tab", "down" ] -      if action == "tab" -        if @inputContainsASelectionRange() -          # Tab moves the start of the selection to the end of the current word. -          text = @input.value[@input.selectionStart..] -          length = text.length -          text = text.replace /^\s*\S+/, "" -          @input.setSelectionRange @input.selectionStart + (length - text.length), @input.selectionEnd -        else -          # Other tabs behave the same as "down". -          action = "down" -      if action == "down" -        @selection += 1 -        @selection = @initialSelectionValue if @selection == @completions.length -        @updateSelection() +      @selection += 1 +      @selection = @initialSelectionValue if @selection == @completions.length +      @updateSelection()      else if (action == "up")        @selection -= 1        @selection = @completions.length - 1 if @selection < @initialSelectionValue @@ -292,10 +194,11 @@ class VomnibarUI          # <Enter> on an empty vomnibar is a no-op.          return unless 0 < query.length          if @suppressedLeadingKeyword? -          # This is a custom search engine completion.  Because of the way we add and highlight the text -          # common to all completions in the input (selectCommonMatches), the text in the input might not -          # correspond to any of the completions.  So we fire off the query to the background page and use the -          # completion at the top of the list (which will be the right one). +          # This is a custom search engine completion.  Because of the way we add prompted text to the input +          # (addPromptedText), the text in the input might not correspond to any of the completions.  So we +          # fire off the query to the background page and use the completion at the top of the list (which +          # will be the right one). +          window.getSelection()?.collapseToEnd() if @inputContainsASelectionRange()            @update true, =>              if @completions[0]                completion = @completions[0] @@ -313,6 +216,8 @@ class VomnibarUI          @hide -> completion.performAction openInNewTab      else if action == "delete"        if @suppressedLeadingKeyword? and @input.value.length == 0 +        # Normally, with custom search engines, the keyword (e,g, the "w" of "w query terms") suppressed.  If +        # the input is empty, then show the keyword again.          @input.value = @suppressedLeadingKeyword          @suppressedLeadingKeyword = null          @updateCompletions() @@ -326,34 +231,34 @@ class VomnibarUI      true    onKeypress: (event) => -    if @inputContainsASelectionRange() -      # As the user types characters which match a highlighted completion suggestion (in the text input), we -      # suppress the keyboard event and "simulate" it by advancing the start of the highlighted selection (but -      # only if the typed character matches).  This avoids flicker as the selection is first collapsed then -      # replaced. -      if @input.value[@input.selectionStart][0].toLowerCase() == (String.fromCharCode event.charCode).toLowerCase() -        console.log "extend selection:", @getInputWithoutSelectionRange() -        @input.setSelectionRange @input.selectionStart + 1, @input.selectionEnd -        @updateOnInput() -        event.stopImmediatePropagation() -        event.preventDefault() +    unless event.altKey or event.ctrlKey or event.metaKey +      if @inputContainsASelectionRange() +        # As the user types characters which the match prompted text, we suppress the keyboard event and +        # simulate it by advancing the start of the selection (but only if the typed character matches).  This +        # avoids flicker (if we were to allow the event through) as the selection is first collapsed then +        # 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 selected text. +  # 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 selected text removed. -  getInputWithoutSelectionRange: -> +  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 stripped from the input, and strip any selection. +  # search engine keyword which is currently being suppressed, and strip any propted text.    getInputValueAsQuery: -> -    (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutSelectionRange() +    (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutPromptedText()    updateCompletions: (callback = null) ->      @completer.filter @getInputValueAsQuery(), (response) => @@ -365,12 +270,12 @@ class VomnibarUI        @selection = Math.min @completions.length - 1, Math.max @initialSelectionValue, @selection        @previousAutoSelect = null if @completions[0]?.autoSelect and @completions[0]?.forceAutoSelect        @updateSelection() -      @selectFirstSuggestion response +      @addPromptedText response        callback?()    updateOnInput: =>      @completer.cancel() -    # If the user types, then don't reset any previous text, and re-enable auto-select. +    # If the user types, then don't reset any previous text, and restart auto select.      if @previousInputValue?        @previousInputValue = null        @previousAutoSelect = null @@ -388,13 +293,13 @@ 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 state is updated immediately). +    # synchronous update (so that the state is updated immediately).      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 +      # Update asynchronously for a better user experience, and to take some load off the CPU (not every        # keystroke will cause a dedicated update).        @updateTimer = Utils.setTimeout @refreshInterval, =>          @updateTimer = null @@ -424,7 +329,7 @@ class VomnibarUI  # Sends requests to a Vomnibox completer on the background page.  #  class BackgroundCompleter -  debug: true +  debug: false    # The "name" is the background-page completer to connect to: "omni", "tabs", or "bookmarks".    constructor: (@name) -> @@ -461,7 +366,6 @@ class BackgroundCompleter            @mostRecentCallback msg if msg.id == @messageId    filter: (query, @mostRecentCallback) -> -    queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length      cacheKey = query.ltrim().split(/\s+/).join " "      if cacheKey of @cache @@ -473,7 +377,7 @@ class BackgroundCompleter          handler: "filter"          name: @name          id: @messageId = Utils.createUniqueId() -        queryTerms: queryTerms +        queryTerms: query.trim().split(/\s+/).filter (s) -> 0 < s.length          query: query          cacheKey: cacheKey  | 
