aboutsummaryrefslogtreecommitdiffstats
path: root/background_scripts
diff options
context:
space:
mode:
authorStephen Blott2015-05-06 07:32:19 +0100
committerStephen Blott2015-05-06 07:32:19 +0100
commit372e65d7b44c3f0ad3f522bd6b8c9cfacd693186 (patch)
tree627a65d4ad9b9bed082da0721d735adbbab34909 /background_scripts
parent1f97221aef5cfe28200df81a68a139a3f2b07784 (diff)
downloadvimium-372e65d7b44c3f0ad3f522bd6b8c9cfacd693186.tar.bz2
Search completion; many tweaks and refactor Suggestion constructor.
Diffstat (limited to 'background_scripts')
-rw-r--r--background_scripts/completion.coffee166
-rw-r--r--background_scripts/settings.coffee4
2 files changed, 94 insertions, 76 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 8a69b645..4ae0c44b 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -5,31 +5,38 @@
# The Vomnibox frontend script makes a "filterCompleter" request to the background page, which in turn calls
# filter() on each these completers.
#
-# A completer is a class which has two functions:
+# A completer is a class which has three functions:
# - filter(query, onComplete): "query" will be whatever the user typed into the Vomnibox.
# - refresh(): (optional) refreshes the completer's data source (e.g. refetches the list of bookmarks).
-
-# A Suggestion is a bookmark or history entry which matches the current query.
-# It also has an attached "computeRelevancyFunction" which determines how well this item matches the given
-# query terms.
+# - userIsTyping(): (optional) informs the completer that the user is typing (and pending completions may no
+# longer be needed).
class Suggestion
showRelevancy: true # Set this to true to render relevancy when debugging the ranking scores.
- # - type: one of [bookmark, history, tab].
- # - computeRelevancyFunction: a function which takes a Suggestion and returns a relevancy score
- # between [0, 1]
- # - extraRelevancyData: data (like the History item itself) which may be used by the relevancy function.
- constructor: (@queryTerms, @type, @url, @title, @computeRelevancyFunction, @extraRelevancyData) ->
- @title ||= ""
+ constructor: (@options) ->
+ # Required options.
+ @queryTerms = null
+ @type = null
+ @url = null
+ @relevancyFunction = null
+ # Other options.
+ @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.
@autoSelect = false
- # If @noHighlightTerms is falsy, then we don't highlight matched terms in the title and URL.
- @noHighlightTerms = 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
# suggestion is selected.
@insertText = null
- computeRelevancy: -> @relevancy = @computeRelevancyFunction(this)
+ extend this, @options
+
+ computeRelevancy: ->
+ # We assume that, once the relevancy has been set, it won't change. Completers must set either @relevancy
+ # or @relevancyFunction.
+ @relevancy ?= @relevancyFunction this
generateHtml: ->
return @html if @html
@@ -39,10 +46,10 @@ class Suggestion
"""
<div class="vimiumReset vomnibarTopHalf">
<span class="vimiumReset vomnibarSource">#{@type}</span>
- <span class="vimiumReset vomnibarTitle">#{@highlightTerms Utils.escapeHtml @title}</span>
+ <span class="vimiumReset vomnibarTitle">#{@highlightQueryTerms Utils.escapeHtml @title}</span>
</div>
<div class="vimiumReset vomnibarBottomHalf">
- <span class="vimiumReset vomnibarUrl">#{@shortenUrl @highlightTerms Utils.escapeHtml @url}</span>
+ <span class="vimiumReset vomnibarUrl">#{@shortenUrl @highlightQueryTerms Utils.escapeHtml @url}</span>
#{relevancyHtml}
</div>
"""
@@ -82,8 +89,8 @@ class Suggestion
textPosition += matchedText.length
# Wraps each occurence of the query terms in the given string in a <span>.
- highlightTerms: (string) ->
- return string if @noHighlightTerms
+ highlightQueryTerms: (string) ->
+ return string unless @highlightTerms
ranges = []
escapedTerms = @queryTerms.map (term) -> Utils.escapeHtml(term)
for term in escapedTerms
@@ -139,11 +146,15 @@ class BookmarkCompleter
else
[]
suggestions = results.map (bookmark) =>
- suggestionTitle = if usePathAndTitle then bookmark.pathAndTitle else bookmark.title
- new Suggestion(@currentSearch.queryTerms, "bookmark", bookmark.url, suggestionTitle, @computeRelevancy)
+ new Suggestion
+ queryTerms: @currentSearch.queryTerms
+ type: "bookmark"
+ url: bookmark.url
+ title: if usePathAndTitle then bookmark.pathAndTitle else bookmark.title
+ relevancyFunction: @computeRelevancy
onComplete = @currentSearch.onComplete
@currentSearch = null
- onComplete(suggestions)
+ onComplete suggestions
refresh: ->
@bookmarks = null
@@ -188,17 +199,21 @@ class HistoryCompleter
else
[]
suggestions = results.map (entry) =>
- new Suggestion(queryTerms, "history", entry.url, entry.title, @computeRelevancy, entry)
- onComplete(suggestions)
+ new Suggestion
+ queryTerms: queryTerms
+ type: "history"
+ url: entry.url
+ title: entry.title
+ relevancyFunction: @computeRelevancy
+ relevancyData: entry
+ onComplete suggestions
computeRelevancy: (suggestion) ->
- historyEntry = suggestion.extraRelevancyData
+ historyEntry = suggestion.relevancyData
recencyScore = RankingUtils.recencyScore(historyEntry.lastVisitTime)
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.
- score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2
-
- refresh: ->
+ (wordRelevancy + Math.max recencyScore, wordRelevancy) / 2
# The domain completer is designed to match a single-word query which looks like it is a domain. This supports
# the user experience where they quickly type a partial domain, hit tab -> enter, and expect to arrive there.
@@ -222,16 +237,21 @@ class DomainCompleter
domains = @sortDomainsByRelevancy(queryTerms, domainCandidates)
return onComplete([]) if domains.length == 0
topDomain = domains[0][0]
- onComplete([new Suggestion(queryTerms, "domain", topDomain, null, @computeRelevancy)])
+ suggestion = new Suggestion
+ queryTerms: queryTerms
+ type: "domain"
+ url: topDomain
+ relevancy: 1
+ onComplete [ suggestion ]
# Returns a list of domains of the form: [ [domain, relevancy], ... ]
sortDomainsByRelevancy: (queryTerms, domainCandidates) ->
- results = []
- for domain in domainCandidates
- recencyScore = RankingUtils.recencyScore(@domains[domain].entry.lastVisitTime || 0)
- wordRelevancy = RankingUtils.wordRelevancy(queryTerms, domain, null)
- score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2
- results.push([domain, score])
+ results =
+ for domain in domainCandidates
+ recencyScore = RankingUtils.recencyScore(@domains[domain].entry.lastVisitTime || 0)
+ wordRelevancy = RankingUtils.wordRelevancy queryTerms, domain, null
+ score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2
+ [domain, score]
results.sort (a, b) -> b[1] - a[1]
results
@@ -264,9 +284,6 @@ class DomainCompleter
parseDomainAndScheme: (url) ->
Utils.hasFullUrlPrefix(url) and not Utils.hasChromePrefix(url) and url.split("/",3).join "/"
- # Suggestions from the Domain completer have the maximum relevancy. They should be shown first in the list.
- computeRelevancy: -> 1
-
# TabRecency associates a logical timestamp with each tab id. These are used to provide an initial
# recency-based ordering in the tabs vomnibar (which allows jumping quickly between recently-visited tabs).
class TabRecency
@@ -316,10 +333,14 @@ class TabCompleter
chrome.tabs.query {}, (tabs) =>
results = tabs.filter (tab) -> RankingUtils.matches(queryTerms, tab.url, tab.title)
suggestions = results.map (tab) =>
- suggestion = new Suggestion(queryTerms, "tab", tab.url, tab.title, @computeRelevancy)
- suggestion.tabId = tab.id
- suggestion
- onComplete(suggestions)
+ new Suggestion
+ queryTerms: queryTerms
+ type: "tab"
+ url: tab.url
+ title: tab.title
+ relevancyFunction: @computeRelevancy
+ tabId: tab.id
+ onComplete suggestions
computeRelevancy: (suggestion) ->
if suggestion.queryTerms.length
@@ -334,27 +355,26 @@ class SearchEngineCompleter
CompletionEngines.userIsTyping()
filter: (queryTerms, onComplete) ->
- { keyword: keyword, url: url, description: description } = @getSearchEngineMatches queryTerms
- custom = url?
suggestions = []
- mkUrl =
- if custom
- (string) -> url.replace /%s/g, Utils.createSearchQuery string.split /\s+/
- else
- (string) -> Utils.createSearchUrl string.split /\s+/
-
- haveDescription = description? and 0 < description.trim().length
- type = if haveDescription then description else "search"
- searchUrl = if custom then url else Settings.get "searchUrl"
+ { keyword, searchUrl, description } = @getSearchEngineMatches queryTerms
+ custom = searchUrl? and keyword?
+ searchUrl ?= Settings.get("searchUrl") + "%s"
+ haveDescription = description? and 0 < description.length
+ description ||= "#{if custom then "custom " else ""}search"
# For custom search engines, we add an auto-selected suggestion.
if custom
- query = queryTerms[1..].join " "
- title = if haveDescription then query else keyword + ": " + query
- suggestions.push @mkSuggestion null, queryTerms, type, mkUrl(query), title, @computeRelevancy, 1
- suggestions[0].autoSelect = true
queryTerms = queryTerms[1..]
+ query = queryTerms.join " "
+ suggestions.push new Suggestion
+ queryTerms: queryTerms
+ type: description
+ url: searchUrl.replace /%s/g, Utils.createSearchQuery query.split /\s+/
+ title: if haveDescription then query else "#{keyword}: #{query}"
+ relevancy: 1
+ autoSelect: true
+ highlightTerms: false
else
query = queryTerms.join " "
@@ -366,30 +386,36 @@ class SearchEngineCompleter
# 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).
- # Scoring:
- # - The score does not depend upon the actual suggestion (so, it does not depend upon word
+ # 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 score is higher if the query term is longer. The idea is that search suggestions are more
+ # - 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
- score = 0.6 * (Math.min(characterCount, 10.0)/10.0)
+ relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
if 0 < existingSuggestions.length
existingSuggestionMinScore = existingSuggestions[existingSuggestions.length-1].relevancy
- if score < existingSuggestionMinScore and MultiCompleter.maxResults <= existingSuggestions.length
- # No suggestion we propose will have a high enough score to beat the existing suggestions, so bail
+ if relavancy < existingSuggestionMinScore 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, (searchSuggestions = []) =>
for suggestion in searchSuggestions
- insertText = if custom then "#{keyword} #{suggestion}" else suggestion
- suggestions.push @mkSuggestion insertText, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
- score *= 0.9
+ suggestions.push new Suggestion
+ queryTerms: queryTerms
+ type: description
+ url: searchUrl.replace /%s/g, Utils.createSearchQuery suggestion.split /\s+/
+ title: suggestion
+ relevancy: relavancy
+ insertText: if custom then "#{keyword} #{suggestion}" else suggestion
+ highlightTerms: false
+ relavancy *= 0.9
# 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
@@ -398,14 +424,6 @@ class SearchEngineCompleter
count = Math.min 6, Math.max 3, MultiCompleter.maxResults - existingSuggestions.length
onComplete suggestions[...count]
- # FIXME(smblott) Refactor Suggestion constructor as per @mrmr1993's comment in #1635.
- mkSuggestion: (insertText, args...) ->
- suggestion = new Suggestion args...
- extend suggestion, insertText: insertText, noHighlightTerms: true
-
- # The score is computed in filter() and provided here via suggestion.extraRelevancyData.
- computeRelevancy: (suggestion) -> suggestion.extraRelevancyData
-
refresh: ->
@searchEngines = SearchEngineCompleter.getSearchEngines()
@@ -426,7 +444,7 @@ class SearchEngineCompleter
continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ].
searchEnginesMap[keywords[0]] =
keyword: keywords[0]
- url: tokens[1]
+ searchUrl: tokens[1]
description: tokens[2..].join(" ")
# Fetch the search-engine map, building it if necessary.
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index b802937e..ce812970 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -98,11 +98,11 @@ root.Settings = Settings =
"g: http://www.google.com/search?q=%s Google"
"l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
"w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
- "t: http://www.youtube.com/results?search_query=%s Youtube"
+ "y: http://www.youtube.com/results?search_query=%s Youtube"
+ "t: http://www.youtube.com/results?search_query=%s"
"m: https://www.google.com/maps/search/%s Google Maps"
"b: https://www.bing.com/search?q=%s Bing"
"d: https://duckduckgo.com/?q=%s DuckDuckGo"
- "y: http://www.youtube.com/results?search_query=%s Youtube"
"az: http://www.amazon.com/s/?field-keywords=%s Amazon"
].join "\n\n"
newTabUrl: "chrome://newtab"