aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/completion.coffee27
-rw-r--r--background_scripts/completion_engines.coffee17
-rw-r--r--pages/vomnibar.coffee45
3 files changed, 58 insertions, 31 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 4f0401f0..4fa91b9d 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -384,7 +384,7 @@ class SearchEngineCompleter
handler: "keywords"
keywords: key for own key of engines
- filter: ({ queryTerms, query }, onComplete) ->
+ filter: ({ queryTerms, query, maxResults }, onComplete) ->
return onComplete [] if queryTerms.length == 0
@searchEngines.use (engines) =>
@@ -421,11 +421,11 @@ class SearchEngineCompleter
# This distinguishes two very different kinds of vomnibar baviours, the newer bahviour (true) and the
# legacy behavior (false). We retain the latter for the default search engine, and for custom search
# engines for which we do not have a completion engine.
- version2 = custom and haveCompletionEngine
+ useExclusiveVomnibar = custom and haveCompletionEngine
# If this is a custom search engine and we have a completer, then we exclude results from other
# completers.
- filter = if version2 then (suggestion) -> suggestion.type == description else null
+ filter = if useExclusiveVomnibar then (suggestion) -> suggestion.type == description else null
suggestions = []
@@ -439,17 +439,17 @@ class SearchEngineCompleter
url: Utils.createSearchUrl queryTerms, searchUrl
title: query
relevancy: 1
- insertText: if version2 then query else null
+ insertText: if useExclusiveVomnibar then query else null
# We suppress the leading keyword, for example "w something" becomes "something" in the vomnibar.
suppressLeadingKeyword: true
# Should we highlight (via the selection) the longest continuation of the current query which is
# contained in all completions?
- completeSuggestions: version2
+ completeSuggestions: useExclusiveVomnibar
# Toggles for the legacy behaviour.
- autoSelect: not version2
- forceAutoSelect: not version2
- highlightTerms: not version2
- # Do not use this entry for vomnibar completion.
+ autoSelect: not useExclusiveVomnibar
+ forceAutoSelect: not useExclusiveVomnibar
+ highlightTerms: not useExclusiveVomnibar
+ # Do not use this entry for vomnibar completion (highlighting the common text of the suggestions).
highlightCommonMatches: false
mkSuggestion = do ->
@@ -487,9 +487,10 @@ class SearchEngineCompleter
if 0 < existingSuggestions.length
existingSuggestionsMinScore = existingSuggestions[existingSuggestions.length-1].relevancy
- if relavancy < existingSuggestionsMinScore and MultiCompleter.maxResults <= existingSuggestions.length
+ if relavancy < existingSuggestionsMinScore and maxResults <= existingSuggestions.length
# No suggestion we propose will have a high enough relavancy to beat the existing suggestions, so bail
# immediately.
+ console.log "skip: cannot add completions" if @debug
return onComplete []
CompletionEngines.complete searchUrl, queryTerms, (completionSuggestions = []) =>
@@ -509,16 +510,15 @@ class SearchEngineCompleter
# 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
+ count = Math.min 6, Math.max 3, maxResults - existingSuggestions.length
onComplete suggestions[...count]
# A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top
# 10. Queries from the vomnibar frontend script come through a multi completer.
class MultiCompleter
- @maxResults: 10
+ maxResults: 10
constructor: (@completers) ->
- @maxResults = MultiCompleter.maxResults
refresh: (port) ->
completer.refresh? port for completer in @completers
@@ -533,6 +533,7 @@ class MultiCompleter
RegexpCache.clear()
{ queryTerms } = request
+ request.maxResults = @maxResults
@mostRecentQuery = null
@filterInProgress = true
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 51799971..ac5c86aa 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -126,7 +126,8 @@ CompletionEngines =
# The amount of time to wait for new requests before launching the HTTP request. The intention is to cut
# down on the number of HTTP requests we issue.
- delay: 100
+ # delay: 100
+ delay: 0
get: (searchUrl, url, callback) ->
xhr = new XMLHttpRequest()
@@ -190,20 +191,22 @@ CompletionEngines =
else
# We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
# suggestions are posted.
- Utils.setTimeout 75, =>
+ Utils.setTimeout 50, =>
console.log "hit", completionCacheKey if @debug
callback @completionCache.get completionCacheKey
return
if @mostRecentQuery? and @mostRecentSuggestions?
- # If the user appears to be typing a continuation of the characters of the most recent query, and those
- # characters are also common to all of the most recent suggestions, then we can re-use the previous
- # suggestions.
+ # If the user appears to be typing a continuation of the characters of the most recent query, then we
+ # can re-use the previous suggestions.
reusePreviousSuggestions = do (query) =>
query = queryTerms.join(" ").toLowerCase()
+ # Verify that the previous query is a prefix of the current query.
return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
- previousSuggestions = @mostRecentSuggestions.map (s) -> s.toLowerCase()
- return false unless query.length <= Utils.longestCommonPrefix previousSuggestions
+ # Ensure that every previous suggestion contains the text of the new query.
+ for suggestion in (@mostRecentSuggestions.map (s) -> s.toLowerCase())
+ return false unless 0 <= suggestion.indexOf query
+ # Ok. Re-use the suggestion.
true
if reusePreviousSuggestions
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 8c655b07..85930aff 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -18,7 +18,7 @@ Vomnibar =
newTab: false
selectFirst: false
extend options, userOptions
- extend options, refreshInterval: if options.completer == "omni" then 100 else 0
+ extend options, refreshInterval: if options.completer == "omni" then 150 else 0
completer = @getCompleter options.completer
@vomnibarUI ?= new VomnibarUI()
@@ -136,26 +136,40 @@ class VomnibarUI
completions = @completions.filter (completion) ->
completion.highlightCommonMatches? and completion.highlightCommonMatches
- # Bail if these aren't any completions.
- return unless 0 < completions.length
-
# Fetch the query and the suggestion texts.
query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
suggestions = completions.map (completion) -> completion.title
- # Ensure that the query is a prefix of all of the suggestions.
+ # 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 an extra prefix, 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
- # Calculate the length of the shotest suggestion.
+ # 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 ->
+ 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 suggestions[0][index].toLowerCase() != suggestion[index].toLowerCase()
+ return index if first[index] != suggestion[index]
length
# Bail if there's nothing to complete.
@@ -164,8 +178,13 @@ class VomnibarUI
# Don't highlight only whitespace (that is, the entire common text consists only of whitespace).
return if /^\s+$/.test suggestions[0].slice query.length, length
+ completion = suggestions[0].slice query.length, 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
+
# Highlight match.
- @input.value = suggestions[0].slice 0, length
+ @input.value = @input.value + completion
@input.setSelectionRange query.length, length
#
@@ -205,6 +224,7 @@ class VomnibarUI
if @inputContainsASelectionRange()
# The first tab collapses the selection to the end.
window.getSelection()?.collapseToEnd()
+ @updateOnInput()
else
# Subsequent tabs behave the same as "down".
action = "down"
@@ -258,10 +278,13 @@ class VomnibarUI
onKeypress: (event) =>
if @inputContainsASelectionRange()
- if @input.value[@input.selectionStart][0] == String.fromCharCode event.charCode
+ # 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. We
+ # do this so that the selection doesn't flicker as the user types.
+ if @input.value[@input.selectionStart][0].toLowerCase() == (String.fromCharCode event.charCode).toLowerCase()
console.log "extend selection:", @getInputWithoutSelectionRange()
@input.setSelectionRange @input.selectionStart + 1, @input.selectionEnd
- @update()
+ @updateOnInput()
event.stopImmediatePropagation()
event.preventDefault()
true