aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/completion.coffee57
-rw-r--r--lib/utils.coffee20
-rw-r--r--pages/vomnibar.coffee54
-rw-r--r--tests/unit_tests/utils_test.coffee10
4 files changed, 99 insertions, 42 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 91fb85e1..5fc98b88 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -22,18 +22,18 @@ class Suggestion
@title = ""
# Extra data which will be available to the relevancy function.
@relevancyData = null
- # If @autoSelect is truthy, then this suggestion is automatically pre-selected in the vomnibar. There may
- # be at most one such suggestion.
+ # If @autoSelect is truthy, then this suggestion is automatically pre-selected in the vomnibar. This only
+ # affects the suggestion in slot 0 in the vomnibar.
@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.
+ # If @highlightTerms is true, then we highlight matched terms in the title and URL. Otherwise we don't.
@highlightTerms = true
- # If @insertText is a string, then the indicated text is inserted into the vomnibar input when the
- # suggestion is selected.
+ # @insertText is text to insert into the vomnibar input when the suggestion is selected.
@insertText = null
+ # Other options set by individual completers include:
+ # - tabId (TabCompleter)
+ # - isSearchSuggestion, customSearchMode (SearchEngineCompleter)
+
extend this, @options
computeRelevancy: ->
@@ -230,13 +230,16 @@ class BookmarkCompleter
RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title)
class HistoryCompleter
- filter: ({ queryTerms }, onComplete) ->
+ filter: ({ queryTerms, seenTabToOpenCompletionList }, onComplete) ->
@currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
results = []
HistoryCache.use (history) =>
results =
if queryTerms.length > 0
history.filter (entry) -> RankingUtils.matches(queryTerms, entry.url, entry.title)
+ else if seenTabToOpenCompletionList
+ # <Tab> opens the completion list, even without a query.
+ history
else
[]
onComplete results.map (entry) =>
@@ -251,6 +254,8 @@ class HistoryCompleter
computeRelevancy: (suggestion) ->
historyEntry = suggestion.relevancyData
recencyScore = RankingUtils.recencyScore(historyEntry.lastVisitTime)
+ # If there are no query terms, then relevancy is based on recency alone.
+ return recencyScore if suggestion.queryTerms.length == 0
wordRelevancy = RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title)
# Average out the word score and the recency. Recency has the ability to pull the score up, but not down.
(wordRelevancy + Math.max recencyScore, wordRelevancy) / 2
@@ -397,9 +402,10 @@ class SearchEngineCompleter
# This looks up the custom search engine and, if one is found, notes it and removes its keyword from the
# query terms.
- triageRequest: (request) ->
+ preprocessRequest: (request) ->
@searchEngines.use (engines) =>
{ queryTerms, query } = request
+ request.searchEngines = engines
keyword = queryTerms[0]
# Note. For a keyword "w", we match "w search terms" and "w ", but not "w" on its own.
if keyword and engines[keyword] and (1 < queryTerms.length or /\s$/.test query)
@@ -468,7 +474,7 @@ class SearchEngineCompleter
# We only accept suggestions:
# - from this completer, or
# - 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).
+ # 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.
@@ -483,8 +489,8 @@ class SearchEngineCompleter
title: queryTerms.join " "
relevancy: 1
autoSelect: custom
- forceAutoSelect: custom
highlightTerms: not haveCompletionEngine
+ isSearchSuggestion: true
mkSuggestion = (suggestion) =>
new Suggestion
@@ -542,6 +548,22 @@ class SearchEngineCompleter
Suggestion.boostRelevancyScore 0.5,
relevancyData * RankingUtils.wordRelevancy queryTerms, title, title
+ postProcessSuggestions: (request, suggestions) ->
+ return unless request.searchEngines
+ engines = (engine for _, engine of request.searchEngines)
+ engines.sort (a,b) -> b.searchUrl.length - a.searchUrl.length
+ engines.push keyword: null, description: "search", searchUrl: Settings.get "searchUrl"
+ for suggestion in suggestions
+ unless suggestion.isSearchSuggestion or suggestion.insertText
+ for engine in engines
+ if suggestion.insertText = Utils.extractQuery engine.searchUrl, suggestion.url
+ # suggestion.customSearchMode informs the vomnibar that, if the users edits the text from this
+ # suggestion, then custom search-engine mode should be activated.
+ suggestion.customSearchMode = engine.keyword
+ suggestion.title = suggestion.insertText
+ suggestion.type = engine.description ? "custom search"
+ break
+
# 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.
class MultiCompleter
@@ -559,7 +581,7 @@ class MultiCompleter
# Provide each completer with an opportunity to see (and possibly alter) the request before it is
# launched.
- completer.triageRequest? request for completer in @completers
+ completer.preprocessRequest? request for completer in @completers
RegexpCache.clear()
{ queryTerms } = request
@@ -585,7 +607,7 @@ class MultiCompleter
# Post results, unless there are none and we will be running a continuation. This avoids
# collapsing the vomnibar briefly before expanding it again, which looks ugly.
unless suggestions.length == 0 and shouldRunContinuations
- suggestions = @prepareSuggestions queryTerms, suggestions
+ suggestions = @prepareSuggestions request, queryTerms, suggestions
onComplete
results: suggestions
mayCacheResults: continuations.length == 0
@@ -602,7 +624,7 @@ class MultiCompleter
jobs.onReady =>
suggestions = filter suggestions for filter in filters
- suggestions = @prepareSuggestions queryTerms, suggestions
+ suggestions = @prepareSuggestions request, queryTerms, suggestions
# We post these results even if a new query has started. The vomnibar will not display them
# (because they're arriving too late), but it will cache them.
onComplete
@@ -614,7 +636,7 @@ class MultiCompleter
if @mostRecentQuery
@filter @mostRecentQuery...
- prepareSuggestions: (queryTerms, suggestions) ->
+ prepareSuggestions: (request, queryTerms, suggestions) ->
# Compute suggestion relevancies and sort.
suggestion.computeRelevancy queryTerms for suggestion in suggestions
suggestions.sort (a, b) -> b.relevancy - a.relevancy
@@ -629,6 +651,9 @@ class MultiCompleter
break if count++ == @maxResults
seenUrls[url] = suggestion
+ # Give each completer the opportunity to tweak the suggestions.
+ completer.postProcessSuggestions? request, suggestions for completer in @completers
+
# Generate HTML for the remaining suggestions and return them.
suggestion.generateHtml() for suggestion in suggestions
suggestions
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 9a5661de..65e26b7a 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -114,6 +114,26 @@ Utils =
searchUrl += "%s" unless 0 <= searchUrl.indexOf "%s"
searchUrl.replace /%s/g, @createSearchQuery query
+ # Extract a query from url if it appears to be a URL created from the given search URL.
+ # For example, map "https://www.google.ie/search?q=star+wars&foo&bar" to "star wars".
+ extractQuery: do =>
+ queryTerminator = new RegExp "[?&#/]"
+ httpProtocolRegexp = new RegExp "^https?://"
+ (searchUrl, url) ->
+ url = url.replace httpProtocolRegexp
+ searchUrl = searchUrl.replace httpProtocolRegexp
+ [ searchUrl, suffixTerms... ] = searchUrl.split "%s"
+ # We require the URL to start with the search URL.
+ return null unless url.startsWith searchUrl
+ # We require any remaining terms in the search URL to also be present in the URL.
+ for suffix in suffixTerms
+ return null unless 0 <= url.indexOf suffix
+ # We use try/catch because decodeURIComponent can throw an exception.
+ try
+ url[searchUrl.length..].split(queryTerminator)[0].split("+").map(decodeURIComponent).join " "
+ catch
+ null
+
# Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters
# as Chrome will do that for us.
convertToUrl: (string) ->
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index a20ae7f3..fd7fd3cc 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -67,22 +67,13 @@ class VomnibarUI
@completionList.style.display = ""
@input.value = ""
@completions = []
- @previousAutoSelect = null
@previousInputValue = null
@customSearchMode = null
@selection = @initialSelectionValue
@keywords = []
+ @seenTabToOpenCompletionList = false
updateSelection: ->
- # We retain global state here (previousAutoSelect) to tell if a search item (for which autoSelect is set)
- # has just appeared or disappeared. If that happens, we set @selection to 0 or -1.
- if 0 < @completions.length
- @selection = 0 if @completions[0].autoSelect and not @previousAutoSelect
- @selection = -1 if @previousAutoSelect and not @completions[0].autoSelect
- @previousAutoSelect = @completions[0].autoSelect
- else
- @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.customSearchMode and not @customSearchMode?
@@ -95,7 +86,7 @@ class VomnibarUI
# typing.
if 0 <= @selection and @completions[@selection].insertText?
@previousInputValue ?= @input.value
- @input.value = @completions[@selection].insertText + (if @selection == 0 then "" else " ")
+ @input.value = @completions[@selection].insertText
else if @previousInputValue?
@input.value = @previousInputValue
@previousInputValue = null
@@ -135,9 +126,13 @@ class VomnibarUI
if (action == "dismiss")
@hide()
else if action in [ "tab", "down" ]
- @selection += 1
- @selection = @initialSelectionValue if @selection == @completions.length
- @updateSelection()
+ if @input.value.trim().length == 0 and action == "tab" and not @seenTabToOpenCompletionList
+ @seenTabToOpenCompletionList = true
+ @update true
+ else
+ @selection += 1
+ @selection = @initialSelectionValue if @selection == @completions.length
+ @updateSelection()
else if (action == "up")
@selection -= 1
@selection = @completions.length - 1 if @selection < @initialSelectionValue
@@ -160,12 +155,16 @@ class VomnibarUI
completion = @completions[@selection]
@hide -> completion.performAction openInNewTab
else if action == "delete"
- if @customSearchMode? and @input.value.length == 0
+ inputIsEmpty = @input.value.length == 0
+ if inputIsEmpty and @customSearchMode?
# Normally, with custom search engines, the keyword (e,g, the "w" of "w query terms") is suppressed.
# If the input is empty, then reinstate the keyword (the "w").
@input.value = @customSearchMode
@customSearchMode = null
- @updateCompletions()
+ @update true
+ else if inputIsEmpty and @seenTabToOpenCompletionList
+ @seenTabToOpenCompletionList = false
+ @update true
else
return true # Do not suppress event.
@@ -182,39 +181,43 @@ class VomnibarUI
updateCompletions: (callback = null) ->
@completer.filter
query: @getInputValueAsQuery()
+ seenTabToOpenCompletionList: @seenTabToOpenCompletionList
callback: (@lastReponse) =>
{ results } = @lastReponse
@completions = results
+ @selection = if @completions[0]?.autoSelect then 0 else @initialSelectionValue
# 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()
callback?()
- updateOnInput: =>
+ onInput: =>
+ @seenTabToOpenCompletionList = false
@completer.cancel()
- # If the user types, then don't reset any previous text, and restart auto select.
+ if 0 <= @selection and @completions[@selection].customSearchMode and not @customSearchMode
+ @customSearchMode = @completions[@selection].customSearchMode
+ updateSynchronously = true
+ # If the user types, then don't reset any previous text, and reset the selection.
if @previousInputValue?
@previousInputValue = null
- @previousAutoSelect = null
@selection = -1
- @update false
+ @update updateSynchronously
clearUpdateTimer: ->
if @updateTimer?
window.clearTimeout @updateTimer
@updateTimer = null
- isCustomSearch: ->
+ shouldActivateCustomSearchMode: ->
queryTerms = @input.value.ltrim().split /\s+/
- 1 < queryTerms.length and queryTerms[0] in @keywords
+ 1 < queryTerms.length and queryTerms[0] in @keywords and not @customSearchMode
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 @customSearchMode?
+ updateSynchronously ||= @shouldActivateCustomSearchMode()
if updateSynchronously
@clearUpdateTimer()
@updateCompletions callback
@@ -231,7 +234,7 @@ class VomnibarUI
@box = document.getElementById("vomnibar")
@input = @box.querySelector("input")
- @input.addEventListener "input", @updateOnInput
+ @input.addEventListener "input", @onInput
@input.addEventListener "keydown", @onKeydown
@completionList = @box.querySelector("ul")
@completionList.style.display = ""
@@ -286,7 +289,6 @@ class BackgroundCompleter
queryTerms: query.trim().split(/\s+/).filter (s) -> 0 < s.length
# We don't send these keys.
callback: null
- mayUseVomnibarCache: null
reset: ->
@keywords = []
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index d1444af2..bfe066c3 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -49,6 +49,16 @@ context "convertToUrl",
assert.equal "https://www.google.com/search?q=go+ogle.com", Utils.convertToUrl("go ogle.com")
assert.equal "https://www.google.com/search?q=%40twitter", Utils.convertToUrl("@twitter")
+context "extractQuery",
+ should "extract queries from search URLs", ->
+ assert.equal "bbc sport 1", Utils.extractQuery "https://www.google.ie/search?q=%s", "https://www.google.ie/search?q=bbc+sport+1"
+ assert.equal "bbc sport 2", Utils.extractQuery "http://www.google.ie/search?q=%s", "https://www.google.ie/search?q=bbc+sport+2"
+ assert.equal "bbc sport 3", Utils.extractQuery "https://www.google.ie/search?q=%s", "http://www.google.ie/search?q=bbc+sport+3"
+ assert.equal "bbc sport 4", Utils.extractQuery "https://www.google.ie/search?q=%s", "http://www.google.ie/search?q=bbc+sport+4&blah"
+
+ should "extract not queries from incorrect search URLs", ->
+ assert.isFalse Utils.extractQuery "https://www.google.ie/search?q=%s&foo=bar", "https://www.google.ie/search?q=bbc+sport"
+
context "hasChromePrefix",
should "detect chrome prefixes of URLs", ->
assert.isTrue Utils.hasChromePrefix "about:foobar"