aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2015-05-08 14:02:21 +0100
committerStephen Blott2015-05-08 14:15:21 +0100
commitb882213019792a7fb47352a920a54d468d352c86 (patch)
tree3d74271999b209af9a0c015434e40e2d3be452b6
parent44378220093ee5bd873b553f5be556921c778663 (diff)
downloadvimium-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.coffee219
-rw-r--r--background_scripts/completion_engines.coffee7
-rw-r--r--pages/vomnibar.coffee1
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: =>