From 1debac63fcc71c88427da9b1ae450067c15cd2b2 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 1 May 2015 12:20:19 +0100
Subject: Rename SearchEngineCompleter to CustomSearchEngineCompleter.
The name "SearchEngineCompleter" is more appropriate for completions
actually delivered by a search engine. Also, what was
SearchEngineCompleter is usually referred to as "custom search engines"
elsewhere.
---
background_scripts/completion.coffee | 16 ++++++++--------
background_scripts/main.coffee | 2 +-
background_scripts/settings.coffee | 2 +-
tests/unit_tests/completion_test.coffee | 4 ++--
tests/unit_tests/settings_test.coffee | 2 +-
5 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 6a1c0d30..21730c4e 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -322,7 +322,7 @@ class TabCompleter
tabRecency.recencyScore(suggestion.tabId)
# A completer which will return your search engines
-class SearchEngineCompleter
+class CustomSearchEngineCompleter
searchEngines: {}
filter: (queryTerms, onComplete) ->
@@ -344,7 +344,7 @@ class SearchEngineCompleter
computeRelevancy: -> 1
refresh: ->
- @searchEngines = SearchEngineCompleter.getSearchEngines()
+ @searchEngines = CustomSearchEngineCompleter.getSearchEngines()
getSearchEngineMatches: (queryTerms) ->
(1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {}
@@ -353,9 +353,9 @@ class SearchEngineCompleter
# mapping in @searchEnginesMap.
@searchEnginesMap: null
- # Parse the custom search engines setting and cache it in SearchEngineCompleter.searchEnginesMap.
+ # Parse the custom search engines setting and cache it in CustomSearchEngineCompleter.searchEnginesMap.
@parseSearchEngines: (searchEnginesText) ->
- searchEnginesMap = SearchEngineCompleter.searchEnginesMap = {}
+ searchEnginesMap = CustomSearchEngineCompleter.searchEnginesMap = {}
for line in searchEnginesText.split /\n/
tokens = line.trim().split /\s+/
continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#")
@@ -367,9 +367,9 @@ class SearchEngineCompleter
# Fetch the search-engine map, building it if necessary.
@getSearchEngines: ->
- unless SearchEngineCompleter.searchEnginesMap?
- SearchEngineCompleter.parseSearchEngines Settings.get "searchEngines"
- SearchEngineCompleter.searchEnginesMap
+ unless CustomSearchEngineCompleter.searchEnginesMap?
+ CustomSearchEngineCompleter.parseSearchEngines Settings.get "searchEngines"
+ CustomSearchEngineCompleter.searchEnginesMap
# 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.
@@ -617,7 +617,7 @@ root.MultiCompleter = MultiCompleter
root.HistoryCompleter = HistoryCompleter
root.DomainCompleter = DomainCompleter
root.TabCompleter = TabCompleter
-root.SearchEngineCompleter = SearchEngineCompleter
+root.CustomSearchEngineCompleter = CustomSearchEngineCompleter
root.HistoryCache = HistoryCache
root.RankingUtils = RankingUtils
root.RegexpCache = RegexpCache
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 37c65592..353f0e53 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -47,7 +47,7 @@ completionSources =
history: new HistoryCompleter()
domains: new DomainCompleter()
tabs: new TabCompleter()
- seachEngines: new SearchEngineCompleter()
+ seachEngines: new CustomSearchEngineCompleter()
completers =
omni: new MultiCompleter([
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index a4d95c81..a73a9d5c 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -33,7 +33,7 @@ root.Settings = Settings =
root.refreshCompletionKeysAfterMappingSave()
searchEngines: (value) ->
- root.SearchEngineCompleter.parseSearchEngines value
+ root.CustomSearchEngineCompleter.parseSearchEngines value
exclusionRules: (value) ->
root.Exclusions.postUpdateHook value
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index 56fcc456..39437b52 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -239,10 +239,10 @@ context "search engines",
setup ->
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
- @completer = new SearchEngineCompleter()
+ @completer = new CustomSearchEngineCompleter()
# note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors
# workaround is below, would be good for someone that understands the testing system better than me to improve
- @completer.searchEngines = SearchEngineCompleter.getSearchEngines()
+ @completer.searchEngines = CustomSearchEngineCompleter.getSearchEngines()
should "return search engine suggestion without description", ->
results = filterCompleter(@completer, ["foo", "hello"])
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 346c98da..a67b69fa 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -73,7 +73,7 @@ context "settings",
should "set search engines, retrieve them correctly and check that they have been parsed correctly", ->
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
- result = SearchEngineCompleter.getSearchEngines()
+ result = CustomSearchEngineCompleter.getSearchEngines()
assert.equal Object.keys(result).length, 2
assert.equal "bar?q=%s", result["foo"].url
assert.isFalse result["foo"].description
--
cgit v1.2.3
From 3616e0d01ffb38e9b7d344b99ad6ce7bc6aa071e Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 1 May 2015 13:19:23 +0100
Subject: Search completion; initial working version.
---
background_scripts/completion.coffee | 62 +++++++++++++++++++++++++++++++++++-
background_scripts/main.coffee | 6 ++--
2 files changed, 65 insertions(+), 3 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 21730c4e..4f94f9e9 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -13,7 +13,7 @@
# It also has an attached "computeRelevancyFunction" which determines how well this item matches the given
# query terms.
class Suggestion
- showRelevancy: false # Set this to true to render relevancy when debugging the ranking scores.
+ 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
@@ -321,6 +321,65 @@ class TabCompleter
else
tabRecency.recencyScore(suggestion.tabId)
+# searchUrl is the URL that will be used for the search, either the default search URL, or a custom
+# search-engine URL. The other arguments area obvious.
+# If we know the search-suggestion URL for searchUrl, then use it to pass a list of suggestions to callback.
+# Otherwise, just call callback.
+#
+# Note: That's all TBD. For now, we just assume Google and use it.
+#
+getOnlineSuggestions = do ->
+ xhrs = {} # Maps searchUrl to outstanding HTTP request.
+ (searchUrl, queryTerms, callback) ->
+ # Cancel any outstanding requests.
+ xhrs?[searchUrl]?.abort()
+ xhrs[searchUrl] = null
+
+ sendNoSuggestions = -> xhrs[searchUrl] = null; callback []
+ return sendNoSuggestions() if queryTerms.length == 0
+
+ url = "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
+ xhrs[searchUrl] = xhr = new XMLHttpRequest()
+ xhr.open "GET", url, true
+ xhr.timeout = 500
+ xhr.ontimeout = sendNoSuggestions
+ xhr.onerror = sendNoSuggestions
+ xhr.send()
+
+ xhr.onreadystatechange = (response) =>
+ if xhr.readyState == 4
+ suggestions = xhr.responseXML?.getElementsByTagName "suggestion"
+ return sendNoSuggestions() unless xhr.status == 200 and suggestions
+ xhr[searchUrl] = null
+ suggestions =
+ for suggestion in suggestions
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
+ callback suggestions
+
+class SearchEngineCompleter
+ refresh: ->
+ filter: (queryTerms, onComplete) ->
+ return onComplete([]) if queryTerms.length == 0
+
+ getOnlineSuggestions Settings.get("searchUrl"), queryTerms, (suggestions) =>
+ completions =
+ for suggestion in suggestions
+ url = Utils.createSearchUrl suggestion.split /\s+/
+ new Suggestion queryTerms, "suggestion", url, suggestion, @computeRelevancy
+ characterCount = queryTerms.join(" ").length
+ completion.characterCount = characterCount for completion in completions
+ onComplete completions
+
+ computeRelevancy: (suggestion) ->
+ # We score search-engine completions by word relevancy, but weight increasingly as the number of
+ # characters in the query terms increases. The idea is that, the more the user has had to type, the less
+ # likely it is that one of the other suggestion types has found what they're looking for, so the more
+ # likely it is that a search suggestion will be useful.
+ # (1.0 - (1.0 / suggestion.characterCount)) *
+ (Math.min(suggestion.characterCount, 12)/12) *
+ RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title
+
# A completer which will return your search engines
class CustomSearchEngineCompleter
searchEngines: {}
@@ -617,6 +676,7 @@ root.MultiCompleter = MultiCompleter
root.HistoryCompleter = HistoryCompleter
root.DomainCompleter = DomainCompleter
root.TabCompleter = TabCompleter
+root.SearchEngineCompleter = SearchEngineCompleter
root.CustomSearchEngineCompleter = CustomSearchEngineCompleter
root.HistoryCache = HistoryCache
root.RankingUtils = RankingUtils
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 353f0e53..6f7db05c 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -47,11 +47,13 @@ completionSources =
history: new HistoryCompleter()
domains: new DomainCompleter()
tabs: new TabCompleter()
- seachEngines: new CustomSearchEngineCompleter()
+ customSearchEngines: new CustomSearchEngineCompleter()
+ searchEngines: new SearchEngineCompleter()
completers =
omni: new MultiCompleter([
- completionSources.seachEngines,
+ completionSources.searchEngines,
+ completionSources.customSearchEngines,
completionSources.bookmarks,
completionSources.history,
completionSources.domains])
--
cgit v1.2.3
From d848f50fb1c199de581bf63e18495b7f4d0c4faf Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 08:30:15 +0100
Subject: Search completion; refactor to separate file.
---
background_scripts/completion.coffee | 59 ++++---------------
background_scripts/search_engines.coffee | 98 ++++++++++++++++++++++++++++++++
lib/utils.coffee | 38 +++++++++++++
manifest.json | 1 +
4 files changed, 148 insertions(+), 48 deletions(-)
create mode 100644 background_scripts/search_engines.coffee
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 4f94f9e9..40c0d119 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -321,63 +321,26 @@ class TabCompleter
else
tabRecency.recencyScore(suggestion.tabId)
-# searchUrl is the URL that will be used for the search, either the default search URL, or a custom
-# search-engine URL. The other arguments area obvious.
-# If we know the search-suggestion URL for searchUrl, then use it to pass a list of suggestions to callback.
-# Otherwise, just call callback.
-#
-# Note: That's all TBD. For now, we just assume Google and use it.
-#
-getOnlineSuggestions = do ->
- xhrs = {} # Maps searchUrl to outstanding HTTP request.
- (searchUrl, queryTerms, callback) ->
- # Cancel any outstanding requests.
- xhrs?[searchUrl]?.abort()
- xhrs[searchUrl] = null
-
- sendNoSuggestions = -> xhrs[searchUrl] = null; callback []
- return sendNoSuggestions() if queryTerms.length == 0
-
- url = "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
- xhrs[searchUrl] = xhr = new XMLHttpRequest()
- xhr.open "GET", url, true
- xhr.timeout = 500
- xhr.ontimeout = sendNoSuggestions
- xhr.onerror = sendNoSuggestions
- xhr.send()
-
- xhr.onreadystatechange = (response) =>
- if xhr.readyState == 4
- suggestions = xhr.responseXML?.getElementsByTagName "suggestion"
- return sendNoSuggestions() unless xhr.status == 200 and suggestions
- xhr[searchUrl] = null
- suggestions =
- for suggestion in suggestions
- continue unless suggestion = suggestion.getAttribute "data"
- suggestion
- callback suggestions
-
class SearchEngineCompleter
refresh: ->
- filter: (queryTerms, onComplete) ->
- return onComplete([]) if queryTerms.length == 0
- getOnlineSuggestions Settings.get("searchUrl"), queryTerms, (suggestions) =>
- completions =
- for suggestion in suggestions
- url = Utils.createSearchUrl suggestion.split /\s+/
- new Suggestion queryTerms, "suggestion", url, suggestion, @computeRelevancy
- characterCount = queryTerms.join(" ").length
- completion.characterCount = characterCount for completion in completions
- onComplete completions
+ filter: (queryTerms, onComplete) ->
+ SearchEngines.complete Settings.get("searchUrl"), queryTerms, (suggestions = []) =>
+ console.log suggestions.length
+ characterCount = queryTerms.join("").length
+ completions =
+ for suggestion in suggestions
+ url = Utils.createSearchUrl suggestion.split /\s+/
+ new Suggestion queryTerms, "search", url, suggestion, @computeRelevancy, characterCount
+ onComplete completions
computeRelevancy: (suggestion) ->
# We score search-engine completions by word relevancy, but weight increasingly as the number of
# characters in the query terms increases. The idea is that, the more the user has had to type, the less
# likely it is that one of the other suggestion types has found what they're looking for, so the more
- # likely it is that a search suggestion will be useful.
+ # likely it is that this suggestion will be useful.
# (1.0 - (1.0 / suggestion.characterCount)) *
- (Math.min(suggestion.characterCount, 12)/12) *
+ (Math.min(suggestion.extraRelevancyData, 12)/12) *
RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title
# A completer which will return your search engines
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
new file mode 100644
index 00000000..485accc1
--- /dev/null
+++ b/background_scripts/search_engines.coffee
@@ -0,0 +1,98 @@
+
+# Each completer implements three functions:
+#
+# match: can this completer be used for this search URL?
+# getUrl: map these query terms to a completion URL.
+# parse: extract suggestions from the resulting (successful) XMLHttpRequest.
+#
+Google =
+ name: "Google"
+ match: (searchUrl) ->
+ true # TBD.
+
+ getUrl: (queryTerms) ->
+ "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
+
+ parse: (xhr, callback) ->
+ if suggestions = xhr?.responseXML?.getElementsByTagName "suggestion"
+ suggestions =
+ for suggestion in suggestions
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
+ callback suggestions
+ else
+ callback []
+
+# A dummy search engine which is guaranteed to match any search URL, but produces no completions. This allows
+# the rest of the logic to be written knowing that there will be a search engine match.
+DummySearchEngine =
+ name: "Dummy"
+ 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"
+ parse: (_, callback) -> callback []
+
+CompletionEngines = [ Google, DummySearchEngine ]
+
+SearchEngines =
+ cancel: (searchUrl, callback = null) ->
+ @requests[searchUrl]?.abort()
+ delete @requests[searchUrl]
+ callback? null
+
+ # Perform and HTTP GET.
+ # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl")
+ # url is the URL to fetch
+ # callback will be called a successful XMLHttpRequest object, or null.
+ get: (searchUrl, url, callback) ->
+ @requests ?= {} # Maps searchUrls to any outstanding HTTP request for that search engine.
+ @cancel searchUrl
+
+ # We cache the results of recent requests (with a two-hour expiry).
+ @requestCache ?= new SimpleCache 2 * 60 * 60 * 1000
+
+ if @requestCache.has url
+ callback @requestCache.get url
+ return
+
+ @requests[searchUrl] = xhr = new XMLHttpRequest()
+ xhr.open "GET", url, true
+ xhr.timeout = 500
+ xhr.ontimeout = => @cancel searchUrl, callback
+ xhr.onerror = => @cancel searchUrl, callback
+ xhr.send()
+
+ xhr.onreadystatechange = =>
+ if xhr.readyState == 4
+ if xhr.status == 200
+ @requests[searchUrl] = null
+ callback @requestCache.set url, xhr
+ else
+ callback null
+
+ # Look up the search engine for this search URL. Because of DummySearchEngine, above, we know there will
+ # always be a match. Imagining that there may be many search engines, and knowing that this is called for
+ # every character entered, we cache the result.
+ lookupEngine: (searchUrl) ->
+ @engineCache ?= new SimpleCache 24 * 60 * 60 * 1000
+ if @engineCache.has searchUrl
+ @engineCache.get searchUrl
+ else
+ for engine in CompletionEngines
+ return @engineCache.set searchUrl, engine if engine.match searchUrl
+
+ # This is the main (actually, the only) entry point.
+ # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl")
+ # queryTerms are the queryTerms
+ # callback will be applied to a list of suggestion strings (which will be an empty list, if anything goes
+ # wrong).
+ complete: (searchUrl, queryTerms, callback) ->
+ return callback [] unless 0 < queryTerms.length
+
+ engine = @lookupEngine searchUrl
+ url = engine.getUrl queryTerms
+ @get searchUrl, url, (xhr = null) ->
+ if xhr? then engine.parse xhr, callback else callback []
+
+root = exports ? window
+root.SearchEngines = SearchEngines
diff --git a/lib/utils.coffee b/lib/utils.coffee
index fba03b61..338e535d 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -194,5 +194,43 @@ globalRoot.extend = (hash1, hash2) ->
hash1[key] = hash2[key]
hash1
+# A simple cache. Entries used within an expiry period are retained (for one more expiry period), otherwise
+# they are discarded.
+class SimpleCache
+ # expiry: expiry time in milliseconds (default, one hour)
+ # entries: maximum number of entries
+ constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
+ @cache = {}
+ @previous = {}
+ setInterval (=> @rotate()), @expiry
+
+ rotate: ->
+ @previous = @cache
+ @cache = {}
+
+ has: (key) ->
+ (key of @cache) or key of @previous
+
+ get: (key) ->
+ console.log "get", key
+ if key of @cache
+ @cache[key]
+ else if key of @previous
+ @cache[key] = @previous[key]
+ else
+ null
+
+ # Set value, and return that value. If value is null, then delete key.
+ set: (key, value = null) ->
+ if value?
+ @cache[key] = value
+ delete @previous[key]
+ @rotate() if @entries < Object.keys(@cache).length
+ else
+ delete @cache[key]
+ delete @previous[key]
+ value
+
root = exports ? window
root.Utils = Utils
+root.SimpleCache = SimpleCache
diff --git a/manifest.json b/manifest.json
index 2cf453f8..d3f6249f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -14,6 +14,7 @@
"background_scripts/sync.js",
"background_scripts/settings.js",
"background_scripts/exclusions.js",
+ "background_scripts/search_engines.js",
"background_scripts/completion.js",
"background_scripts/marks.js",
"background_scripts/main.js"
--
cgit v1.2.3
From 1257fc7a4fe7b9d0bfe1ad7ab7255f8dba4b988d Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 11:34:18 +0100
Subject: Search completion; do not complete URLs.
---
background_scripts/search_engines.coffee | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 485accc1..5d69d087 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -89,6 +89,12 @@ SearchEngines =
complete: (searchUrl, queryTerms, callback) ->
return callback [] unless 0 < queryTerms.length
+ # Don't try to complete general URLs.
+ return callback [] if 1 == queryTerms.length and Utils.isUrl queryTerms[0]
+
+ # Don't try to complete Javascrip URLs.
+ return callback [] if 0 < queryTerms.length and Utils.hasJavascriptPrefix queryTerms[0]
+
engine = @lookupEngine searchUrl
url = engine.getUrl queryTerms
@get searchUrl, url, (xhr = null) ->
--
cgit v1.2.3
From 41495d11e6608767dde299223f10c8a606d4a8fb Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 12:14:16 +0100
Subject: Search completion; minor tweaks.
Including:
- Make completers classes. That way, we may be able to get better
code reuse.
---
background_scripts/completion.coffee | 9 +++----
background_scripts/search_engines.coffee | 46 +++++++++++++++++++-------------
lib/utils.coffee | 4 +--
3 files changed, 33 insertions(+), 26 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 40c0d119..d8cf1667 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -326,7 +326,6 @@ class SearchEngineCompleter
filter: (queryTerms, onComplete) ->
SearchEngines.complete Settings.get("searchUrl"), queryTerms, (suggestions = []) =>
- console.log suggestions.length
characterCount = queryTerms.join("").length
completions =
for suggestion in suggestions
@@ -335,11 +334,11 @@ class SearchEngineCompleter
onComplete completions
computeRelevancy: (suggestion) ->
- # We score search-engine completions by word relevancy, but weight increasingly as the number of
+ # We score search-engine completions by word relevancy, but weight the score increasingly as the number of
# characters in the query terms increases. The idea is that, the more the user has had to type, the less
- # likely it is that one of the other suggestion types has found what they're looking for, so the more
- # likely it is that this suggestion will be useful.
- # (1.0 - (1.0 / suggestion.characterCount)) *
+ # likely it is that one of the other suggestion types has proven useful, so the more likely it is that
+ # this suggestion will be useful.
+ # NOTE(smblott) This will require tweaking.
(Math.min(suggestion.extraRelevancyData, 12)/12) *
RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 5d69d087..3dfea180 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -5,10 +5,14 @@
# getUrl: map these query terms to a completion URL.
# parse: extract suggestions from the resulting (successful) XMLHttpRequest.
#
-Google =
+class Google
+ constructor: ->
name: "Google"
match: (searchUrl) ->
- true # TBD.
+ return true if /^https?:\/\/[a-z]+.google.com\//.test searchUrl
+ # NOTE(smblott). A temporary hack, just for me, and just for development. Will be removed.
+ return true if /localhost\/.*\/booky/.test searchUrl
+ false
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
@@ -23,16 +27,17 @@ Google =
else
callback []
-# A dummy search engine which is guaranteed to match any search URL, but produces no completions. This allows
-# the rest of the logic to be written knowing that there will be a search engine match.
-DummySearchEngine =
+# 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 be a search engine match.
+class DummySearchEngine
+ constructor: ->
name: "Dummy"
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"
parse: (_, callback) -> callback []
-CompletionEngines = [ Google, DummySearchEngine ]
+completionEngines = [ Google, DummySearchEngine ]
SearchEngines =
cancel: (searchUrl, callback = null) ->
@@ -40,16 +45,18 @@ SearchEngines =
delete @requests[searchUrl]
callback? null
- # Perform and HTTP GET.
- # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl")
- # url is the URL to fetch
- # callback will be called a successful XMLHttpRequest object, or null.
+ # Perform an HTTP GET.
+ # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl").
+ # url is the URL to fetch.
+ # callback will be called with a successful XMLHttpRequest object, or null.
get: (searchUrl, url, callback) ->
@requests ?= {} # Maps searchUrls to any outstanding HTTP request for that search engine.
@cancel searchUrl
- # We cache the results of recent requests (with a two-hour expiry).
- @requestCache ?= new SimpleCache 2 * 60 * 60 * 1000
+ # We cache the results of the most-recent 1000 requests (with a two-hour expiry).
+ # FIXME(smblott) Currently we're caching XMLHttpRequest objects, which is wasteful of memory. It would be
+ # better to handle caching at a higher level.
+ @requestCache ?= new SimpleCache 2 * 60 * 60 * 1000, 1000
if @requestCache.has url
callback @requestCache.get url
@@ -70,21 +77,22 @@ SearchEngines =
else
callback null
- # Look up the search engine for this search URL. Because of DummySearchEngine, above, we know there will
- # always be a match. Imagining that there may be many search engines, and knowing that this is called for
- # every character entered, we cache the result.
+ # Look up the search-completion engine for this search URL. Because of DummySearchEngine, above, we know
+ # there will always be a match. Imagining that there may be many search engines, and knowing that this is
+ # called for every character entered, we cache the result.
lookupEngine: (searchUrl) ->
@engineCache ?= new SimpleCache 24 * 60 * 60 * 1000
if @engineCache.has searchUrl
@engineCache.get searchUrl
else
- for engine in CompletionEngines
+ for engine in completionEngines
+ engine = new engine()
return @engineCache.set searchUrl, engine if engine.match searchUrl
# This is the main (actually, the only) entry point.
- # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl")
- # queryTerms are the queryTerms
- # callback will be applied to a list of suggestion strings (which will be an empty list, if anything goes
+ # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl").
+ # queryTerms are the queryTerms.
+ # callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
# wrong).
complete: (searchUrl, queryTerms, callback) ->
return callback [] unless 0 < queryTerms.length
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 338e535d..88fe9e2c 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -202,7 +202,8 @@ class SimpleCache
constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
@cache = {}
@previous = {}
- setInterval (=> @rotate()), @expiry
+ rotate = => @rotate()
+ setInterval rotate, @expiry
rotate: ->
@previous = @cache
@@ -212,7 +213,6 @@ class SimpleCache
(key of @cache) or key of @previous
get: (key) ->
- console.log "get", key
if key of @cache
@cache[key]
else if key of @previous
--
cgit v1.2.3
From a855cc15393fbf6296ac1ecf278c5f1b736c81b9 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 12:39:50 +0100
Subject: Search completion; cache at a higher level.
... and tweak caching constants.
---
background_scripts/search_engines.coffee | 42 +++++++++++++++++++-------------
1 file changed, 25 insertions(+), 17 deletions(-)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 3dfea180..f07a9df4 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -17,15 +17,13 @@ class Google
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
- parse: (xhr, callback) ->
- if suggestions = xhr?.responseXML?.getElementsByTagName "suggestion"
- suggestions =
- for suggestion in suggestions
- continue unless suggestion = suggestion.getAttribute "data"
- suggestion
- callback suggestions
- else
- callback []
+ # Returns a list of suggestions (strings).
+ parse: (xhr) ->
+ suggestions = xhr?.responseXML?.getElementsByTagName "suggestion"
+ return [] unless suggestions
+ for suggestion in suggestions
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
# 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 be a search engine match.
@@ -35,7 +33,7 @@ class DummySearchEngine
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"
- parse: (_, callback) -> callback []
+ parse: -> []
completionEngines = [ Google, DummySearchEngine ]
@@ -53,10 +51,8 @@ SearchEngines =
@requests ?= {} # Maps searchUrls to any outstanding HTTP request for that search engine.
@cancel searchUrl
- # We cache the results of the most-recent 1000 requests (with a two-hour expiry).
- # FIXME(smblott) Currently we're caching XMLHttpRequest objects, which is wasteful of memory. It would be
- # better to handle caching at a higher level.
- @requestCache ?= new SimpleCache 2 * 60 * 60 * 1000, 1000
+ # We cache the results of the most-recent 1000 requests with a one-minute expiry.
+ @requestCache ?= new SimpleCache 1 * 60 * 1000, 1000
if @requestCache.has url
callback @requestCache.get url
@@ -81,7 +77,7 @@ SearchEngines =
# there will always be a match. Imagining that there may be many search engines, and knowing that this is
# called for every character entered, we cache the result.
lookupEngine: (searchUrl) ->
- @engineCache ?= new SimpleCache 24 * 60 * 60 * 1000
+ @engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
if @engineCache.has searchUrl
@engineCache.get searchUrl
else
@@ -103,10 +99,22 @@ SearchEngines =
# Don't try to complete Javascrip URLs.
return callback [] if 0 < queryTerms.length and Utils.hasJavascriptPrefix queryTerms[0]
+ # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
+ # to generate a key. We mix in some nonsense generated by pwgen. There is the possibility of a key clash,
+ # but it's vanishingly small.
+ junk = "//Zi?ei5;o//"
+ completionCacheKey = searchUrl + junk + queryTerms.join junk
+ @completionCache ?= new SimpleCache 6 * 60 * 60 * 1000, 2000 # Six hours, 2000 entries.
+ if @completionCache.has completionCacheKey
+ return callback @completionCache.get completionCacheKey
+
engine = @lookupEngine searchUrl
url = engine.getUrl queryTerms
- @get searchUrl, url, (xhr = null) ->
- if xhr? then engine.parse xhr, callback else callback []
+ @get searchUrl, url, (xhr = null) =>
+ if xhr?
+ callback @completionCache.set completionCacheKey, engine.parse xhr
+ else
+ callback []
root = exports ? window
root.SearchEngines = SearchEngines
--
cgit v1.2.3
From 77d4e3b81b965eecb078db82bc92e747de1c371a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 15:00:05 +0100
Subject: Search completion; vomnibar integration.
---
background_scripts/completion.coffee | 4 +++-
pages/vomnibar.coffee | 24 ++++++++++++++++++++++--
2 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index d8cf1667..c13d0625 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -330,7 +330,9 @@ class SearchEngineCompleter
completions =
for suggestion in suggestions
url = Utils.createSearchUrl suggestion.split /\s+/
- new Suggestion queryTerms, "search", url, suggestion, @computeRelevancy, characterCount
+ suggestion = new Suggestion queryTerms, "search", url, suggestion, @computeRelevancy, characterCount
+ suggestion.insertText = true
+ suggestion
onComplete completions
computeRelevancy: (suggestion) ->
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index ce0eb61c..45a3a7db 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -93,6 +93,22 @@ class VomnibarUI
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
+ # For suggestions from search-engine completion, we copy the suggested text into the input when selected,
+ # and revert when not.
+ if 0 <= @selection
+ suggestion = @completions[@selection]
+ if suggestion.insertText
+ @previousText ?= @input.value
+ @input.value = suggestion.title
+ else
+ if @previousText?
+ @input.value = @previousText
+ @previousText = null
+ else
+ if @previousText?
+ @input.value = @previousText
+ @previousText = null
+
#
# Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
# We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
@@ -141,9 +157,9 @@ class VomnibarUI
handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
url: query
else
+ completion = @completions[@selection]
@update true, =>
# Shift+Enter will open the result in a new tab instead of the current tab.
- completion = @completions[@selection]
@hide -> completion.performAction openInNewTab
# It seems like we have to manually suppress the event here and still return true.
@@ -168,8 +184,12 @@ class VomnibarUI
update: (updateSynchronously, callback) =>
if (updateSynchronously)
+ # The use entered something. Don't reset any previous text.
+ if @previousText?
+ @previousText = null
+ @selection = -1
# cancel scheduled update
- if (@updateTimer != null)
+ if @updateTimer?
window.clearTimeout(@updateTimer)
@updateTimer = null
@updateCompletions(callback)
--
cgit v1.2.3
From 6073d820ff25f824b575a797f160b3204e0e9863 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 15:19:20 +0100
Subject: Search completion; generalize Google regexp.
---
background_scripts/search_engines.coffee | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index f07a9df4..e96e2c76 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -7,11 +7,17 @@
#
class Google
constructor: ->
+ @regexps = [
+ # We include matches for the major English-speaking TLDs.
+ new RegExp "^https?://[a-z]+\.google\.(com|ie|co.uk|ca|com.au)/"
+ # NOTE(smblott). A temporary hack, just for me, and just for development. Will be removed.
+ new RegExp "localhost/.*/booky"
+ ]
+
name: "Google"
match: (searchUrl) ->
- return true if /^https?:\/\/[a-z]+.google.com\//.test searchUrl
- # NOTE(smblott). A temporary hack, just for me, and just for development. Will be removed.
- return true if /localhost\/.*\/booky/.test searchUrl
+ for re in @regexps
+ return true if re.test searchUrl
false
getUrl: (queryTerms) ->
--
cgit v1.2.3
From 8329f3cbe95e6a39e500aa15e54c6c44fad9cb7e Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 16:29:42 +0100
Subject: Search completion; refactor, add Youtube.
Major refactoring. Unified tratment of custom search engines and
general searches.
---
background_scripts/completion.coffee | 97 +++++++++++++++++---------------
background_scripts/main.coffee | 2 -
background_scripts/search_engines.coffee | 32 +++++++++--
background_scripts/settings.coffee | 2 +-
pages/vomnibar.coffee | 2 +-
5 files changed, 82 insertions(+), 53 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index c13d0625..cb5f64b0 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -322,52 +322,61 @@ class TabCompleter
tabRecency.recencyScore(suggestion.tabId)
class SearchEngineCompleter
- refresh: ->
-
- filter: (queryTerms, onComplete) ->
- SearchEngines.complete Settings.get("searchUrl"), queryTerms, (suggestions = []) =>
- characterCount = queryTerms.join("").length
- completions =
- for suggestion in suggestions
- url = Utils.createSearchUrl suggestion.split /\s+/
- suggestion = new Suggestion queryTerms, "search", url, suggestion, @computeRelevancy, characterCount
- suggestion.insertText = true
- suggestion
- onComplete completions
-
- computeRelevancy: (suggestion) ->
- # We score search-engine completions by word relevancy, but weight the score increasingly as the number of
- # characters in the query terms increases. The idea is that, the more the user has had to type, the less
- # likely it is that one of the other suggestion types has proven useful, so the more likely it is that
- # this suggestion will be useful.
- # NOTE(smblott) This will require tweaking.
- (Math.min(suggestion.extraRelevancyData, 12)/12) *
- RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title
-
-# A completer which will return your search engines
-class CustomSearchEngineCompleter
searchEngines: {}
filter: (queryTerms, onComplete) ->
- {url: url, description: description} = @getSearchEngineMatches queryTerms
+ { keyword: keyword, url: url, description: description } = @getSearchEngineMatches queryTerms
+ custom = url?
suggestions = []
- if url
- url = url.replace(/%s/g, Utils.createSearchQuery queryTerms[1..])
- if description
- type = description
- query = queryTerms[1..].join " "
+
+ mkUrl =
+ if custom
+ (string) -> url.replace /%s/g, Utils.createSearchQuery string.split /\s+/
else
- type = "search"
- query = queryTerms[0] + ": " + queryTerms[1..].join(" ")
- suggestion = new Suggestion(queryTerms, type, url, query, @computeRelevancy)
- suggestion.autoSelect = true
- suggestions.push(suggestion)
- onComplete(suggestions)
+ (string) -> Utils.createSearchUrl string.split /\s+/
+
+ type = if description? then description else "search"
+ searchUrl = if custom then url else Settings.get "searchUrl"
+ query = queryTerms[1..].join " "
+
+ # For custom search engines, we add an auto-selected suggestion.
+ if custom
+ title = if description? then query else queryTerms[0] + ": " + query
+ suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), description, @computeRelevancy
+ suggestions[0].autoSelect = true
+ suggestions[0].relevancyScore = 1
+ queryTerms = queryTerms[1..]
+
+ # 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).
+ SearchEngines.complete searchUrl, queryTerms, (newSuggestions = []) =>
+ characterCount = query.length - queryTerms.length + 1
+ for suggestion in newSuggestions
+ suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, characterCount
+
+ if custom
+ for suggestion in suggestions
+ suggestion.reinsertPrefix = "#{keyword} " if suggestion.insertText
- computeRelevancy: -> 1
+ onComplete suggestions
+
+ mkSuggestion: (insertText, args...) ->
+ suggestion = new Suggestion args...
+ suggestion.insertText = insertText
+ suggestion
+
+ computeRelevancy: (suggestion) ->
+ suggestion.relevancyScore ?
+ # We score search-engine completions by word relevancy, but weight the score increasingly as the number of
+ # characters in the query terms increases. The idea is that, the more the user has had to type, the less
+ # likely it is that one of the other suggestion types has proven useful, so the more likely it is that
+ # this suggestion will be useful.
+ # NOTE(smblott) This will require tweaking.
+ (Math.min(suggestion.extraRelevancyData, 12)/12) *
+ RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title
refresh: ->
- @searchEngines = CustomSearchEngineCompleter.getSearchEngines()
+ @searchEngines = SearchEngineCompleter.getSearchEngines()
getSearchEngineMatches: (queryTerms) ->
(1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {}
@@ -376,23 +385,24 @@ class CustomSearchEngineCompleter
# mapping in @searchEnginesMap.
@searchEnginesMap: null
- # Parse the custom search engines setting and cache it in CustomSearchEngineCompleter.searchEnginesMap.
+ # Parse the custom search engines setting and cache it in SearchEngineCompleter.searchEnginesMap.
@parseSearchEngines: (searchEnginesText) ->
- searchEnginesMap = CustomSearchEngineCompleter.searchEnginesMap = {}
+ searchEnginesMap = SearchEngineCompleter.searchEnginesMap = {}
for line in searchEnginesText.split /\n/
tokens = line.trim().split /\s+/
continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#")
keywords = tokens[0].split ":"
continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ].
searchEnginesMap[keywords[0]] =
+ keyword: keywords[0]
url: tokens[1]
description: tokens[2..].join(" ")
# Fetch the search-engine map, building it if necessary.
@getSearchEngines: ->
- unless CustomSearchEngineCompleter.searchEnginesMap?
- CustomSearchEngineCompleter.parseSearchEngines Settings.get "searchEngines"
- CustomSearchEngineCompleter.searchEnginesMap
+ unless SearchEngineCompleter.searchEnginesMap?
+ SearchEngineCompleter.parseSearchEngines Settings.get "searchEngines"
+ SearchEngineCompleter.searchEnginesMap
# 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.
@@ -641,7 +651,6 @@ root.HistoryCompleter = HistoryCompleter
root.DomainCompleter = DomainCompleter
root.TabCompleter = TabCompleter
root.SearchEngineCompleter = SearchEngineCompleter
-root.CustomSearchEngineCompleter = CustomSearchEngineCompleter
root.HistoryCache = HistoryCache
root.RankingUtils = RankingUtils
root.RegexpCache = RegexpCache
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 6f7db05c..4d2546fc 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -47,13 +47,11 @@ completionSources =
history: new HistoryCompleter()
domains: new DomainCompleter()
tabs: new TabCompleter()
- customSearchEngines: new CustomSearchEngineCompleter()
searchEngines: new SearchEngineCompleter()
completers =
omni: new MultiCompleter([
completionSources.searchEngines,
- completionSources.customSearchEngines,
completionSources.bookmarks,
completionSources.history,
completionSources.domains])
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index e96e2c76..e68cf85d 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -1,4 +1,9 @@
+matchesAnyRegexp = (regexps, string) ->
+ for re in regexps
+ return true if re.test string
+ false
+
# Each completer implements three functions:
#
# match: can this completer be used for this search URL?
@@ -15,10 +20,7 @@ class Google
]
name: "Google"
- match: (searchUrl) ->
- for re in @regexps
- return true if re.test searchUrl
- false
+ match: (searchUrl) -> matchesAnyRegexp @regexps, searchUrl
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
@@ -31,6 +33,26 @@ class Google
continue unless suggestion = suggestion.getAttribute "data"
suggestion
+class Youtube
+ constructor: ->
+ @regexps = [ new RegExp "https?://[a-z]+\.youtube\.com/results" ]
+
+ name: "YouTube"
+ match: (searchUrl) -> matchesAnyRegexp @regexps, searchUrl
+
+ getUrl: (queryTerms) ->
+ "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=#{Utils.createSearchQuery queryTerms}"
+
+ # Returns a list of suggestions (strings).
+ parse: (xhr) ->
+ try
+ text = xhr.responseText
+ text = text.replace /^[^(]*\(/, ""
+ text = text.replace /\)[^\)]*$/, ""
+ suggestion[0] for suggestion in JSON.parse(text)[1]
+ catch
+ []
+
# 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 be a search engine match.
class DummySearchEngine
@@ -41,7 +63,7 @@ class DummySearchEngine
getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css"
parse: -> []
-completionEngines = [ Google, DummySearchEngine ]
+completionEngines = [ Google, Youtube, DummySearchEngine ]
SearchEngines =
cancel: (searchUrl, callback = null) ->
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index a73a9d5c..a4d95c81 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -33,7 +33,7 @@ root.Settings = Settings =
root.refreshCompletionKeysAfterMappingSave()
searchEngines: (value) ->
- root.CustomSearchEngineCompleter.parseSearchEngines value
+ root.SearchEngineCompleter.parseSearchEngines value
exclusionRules: (value) ->
root.Exclusions.postUpdateHook value
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 45a3a7db..c519ce3a 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -99,7 +99,7 @@ class VomnibarUI
suggestion = @completions[@selection]
if suggestion.insertText
@previousText ?= @input.value
- @input.value = suggestion.title
+ @input.value = (suggestion.reinsertPrefix ? "") + suggestion.title
else
if @previousText?
@input.value = @previousText
--
cgit v1.2.3
From ba4e8018e3d8cd80e0fa9ac541e37e7eee37028f Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 2 May 2015 17:32:28 +0100
Subject: Search completion; tweaks and refactoring.
---
background_scripts/completion.coffee | 33 +++++----
background_scripts/search_engines.coffee | 115 +++++++++++++++++--------------
lib/utils.coffee | 7 ++
pages/vomnibar.coffee | 17 ++---
4 files changed, 93 insertions(+), 79 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index cb5f64b0..5b5dc191 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -337,22 +337,32 @@ class SearchEngineCompleter
type = if description? then description else "search"
searchUrl = if custom then url else Settings.get "searchUrl"
- query = queryTerms[1..].join " "
+ query = queryTerms[(if custom then 1 else 0)..].join " "
# For custom search engines, we add an auto-selected suggestion.
if custom
title = if description? then query else queryTerms[0] + ": " + query
- suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), description, @computeRelevancy
+ suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), description, @computeRelevancy, 1
suggestions[0].autoSelect = true
- suggestions[0].relevancyScore = 1
queryTerms = queryTerms[1..]
# 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).
- SearchEngines.complete searchUrl, queryTerms, (newSuggestions = []) =>
+ SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
+
+ # Scoring:
+ # - The score 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.
+ # - The score is higher if the query is longer. The idea is that search suggestions are more likely
+ # to be relevant if, after typing quite some number of characters, the user hasn't yet found a
+ # useful suggestion from another completer.
+ # - Scores are weighted such that they retain the ordering provided by the completion engine.
characterCount = query.length - queryTerms.length + 1
- for suggestion in newSuggestions
- suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, characterCount
+ score = 0.8 * (Math.min(characterCount, 12.0)/12.0)
+
+ for suggestion in searchSuggestions
+ suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
+ score *= 0.9
if custom
for suggestion in suggestions
@@ -365,15 +375,8 @@ class SearchEngineCompleter
suggestion.insertText = insertText
suggestion
- computeRelevancy: (suggestion) ->
- suggestion.relevancyScore ?
- # We score search-engine completions by word relevancy, but weight the score increasingly as the number of
- # characters in the query terms increases. The idea is that, the more the user has had to type, the less
- # likely it is that one of the other suggestion types has proven useful, so the more likely it is that
- # this suggestion will be useful.
- # NOTE(smblott) This will require tweaking.
- (Math.min(suggestion.extraRelevancyData, 12)/12) *
- RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title
+ # The score is computed in filter() and provided here via suggestion.extraRelevancyData.
+ computeRelevancy: (suggestion) -> suggestion.extraRelevancyData
refresh: ->
@searchEngines = SearchEngineCompleter.getSearchEngines()
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index e68cf85d..3c6654e3 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -1,49 +1,54 @@
-matchesAnyRegexp = (regexps, string) ->
- for re in regexps
- return true if re.test string
- false
-
-# Each completer implements three functions:
+# A completion engine provides search suggestions for a search engine. A search engine is identified by a
+# "searchUrl", e.g. Settings.get("searchUrl"), or a custom search engine.
+#
+# Each completion engine defines three functions:
+#
+# 1. "match" - This takes a searchUrl, and returns a boolean indicating whether this completion engine can
+# perform completion for the given search engine.
#
-# match: can this completer be used for this search URL?
-# getUrl: map these query terms to a completion URL.
-# parse: extract suggestions from the resulting (successful) XMLHttpRequest.
+# 2. "getUrl" - This takes a list of query terms (queryTerms) and generates a completion URL, that is, a URL
+# which will provide completions for this completion engine.
#
-class Google
+# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and
+# returns a list of suggestions (a list of strings).
+#
+# The main (only) completion entry point is SearchEngines.complete(). This implements all lookup and caching
+# logic. It is possible to add new completion engines without changing the SearchEngines infrastructure
+# itself.
+
+# A base class for common regexp-based matching engines.
+class RegexpEngine
+ constructor: (@regexps) ->
+ match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
+
+# Completion engine for English-language Google search.
+class Google extends RegexpEngine
constructor: ->
- @regexps = [
- # We include matches for the major English-speaking TLDs.
+ super [
+ # We match the major English-speaking TLDs.
new RegExp "^https?://[a-z]+\.google\.(com|ie|co.uk|ca|com.au)/"
- # NOTE(smblott). A temporary hack, just for me, and just for development. Will be removed.
- new RegExp "localhost/.*/booky"
+ new RegExp "localhost/cgi-bin/booky" # Only for smblott.
]
- name: "Google"
- match: (searchUrl) -> matchesAnyRegexp @regexps, searchUrl
-
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
- # Returns a list of suggestions (strings).
parse: (xhr) ->
- suggestions = xhr?.responseXML?.getElementsByTagName "suggestion"
- return [] unless suggestions
- for suggestion in suggestions
- continue unless suggestion = suggestion.getAttribute "data"
- suggestion
+ try
+ for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
+ catch
+ []
-class Youtube
+class Youtube extends RegexpEngine
constructor: ->
- @regexps = [ new RegExp "https?://[a-z]+\.youtube\.com/results" ]
-
- name: "YouTube"
- match: (searchUrl) -> matchesAnyRegexp @regexps, searchUrl
+ super [ new RegExp "https?://[a-z]+\.youtube\.com/results" ]
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=#{Utils.createSearchQuery queryTerms}"
- # Returns a list of suggestions (strings).
parse: (xhr) ->
try
text = xhr.responseText
@@ -56,8 +61,6 @@ class Youtube
# 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 be a search engine match.
class DummySearchEngine
- constructor: ->
- name: "Dummy"
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"
@@ -72,15 +75,13 @@ SearchEngines =
callback? null
# Perform an HTTP GET.
- # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl").
- # url is the URL to fetch.
- # callback will be called with a successful XMLHttpRequest object, or null.
get: (searchUrl, url, callback) ->
- @requests ?= {} # Maps searchUrls to any outstanding HTTP request for that search engine.
+ @requests ?= {} # Maps a searchUrl to any outstanding HTTP request for that search engine.
@cancel searchUrl
- # We cache the results of the most-recent 1000 requests with a one-minute expiry.
- @requestCache ?= new SimpleCache 1 * 60 * 1000, 1000
+ # We cache the results of the most-recent 100 successfully XMLHttpRequests with a ten-second (ie. very
+ # short) expiry.
+ @requestCache ?= new SimpleCache 10 * 1000, 100
if @requestCache.has url
callback @requestCache.get url
@@ -88,22 +89,23 @@ SearchEngines =
@requests[searchUrl] = xhr = new XMLHttpRequest()
xhr.open "GET", url, true
- xhr.timeout = 500
+ # We set a fairly short timeout. If we block for too long, then we block *all* completers.
+ xhr.timeout = 300
xhr.ontimeout = => @cancel searchUrl, callback
xhr.onerror = => @cancel searchUrl, callback
xhr.send()
xhr.onreadystatechange = =>
if xhr.readyState == 4
+ @requests[searchUrl] = null
if xhr.status == 200
- @requests[searchUrl] = null
callback @requestCache.set url, xhr
else
callback null
- # Look up the search-completion engine for this search URL. Because of DummySearchEngine, above, we know
- # there will always be a match. Imagining that there may be many search engines, and knowing that this is
- # called for every character entered, we cache the result.
+ # Look up the search-completion engine for this searchUrl. Because of DummySearchEngine, 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.
lookupEngine: (searchUrl) ->
@engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
if @engineCache.has searchUrl
@@ -114,22 +116,24 @@ SearchEngines =
return @engineCache.set searchUrl, engine if engine.match searchUrl
# This is the main (actually, the only) entry point.
- # searchUrl is the search engine's URL, e.g. Settings.get("searchUrl").
- # queryTerms are the queryTerms.
- # callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
- # wrong).
+ # - 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.
+ # - queryTerms are the queryTerms.
+ # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
+ # wrong).
complete: (searchUrl, queryTerms, callback) ->
+ # We can't complete empty queries.
return callback [] unless 0 < queryTerms.length
- # Don't try to complete general URLs.
+ # We don't complete URLs.
return callback [] if 1 == queryTerms.length and Utils.isUrl queryTerms[0]
- # Don't try to complete Javascrip URLs.
- return callback [] if 0 < queryTerms.length and Utils.hasJavascriptPrefix queryTerms[0]
+ # We don't complete Javascript URLs.
+ return callback [] if Utils.hasJavascriptPrefix queryTerms[0]
# Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
- # to generate a key. We mix in some nonsense generated by pwgen. There is the possibility of a key clash,
- # but it's vanishingly small.
+ # to generate a key. We mix in some nonsense generated by pwgen. A key clash is possible, but vanishingly
+ # unlikely.
junk = "//Zi?ei5;o//"
completionCacheKey = searchUrl + junk + queryTerms.join junk
@completionCache ?= new SimpleCache 6 * 60 * 60 * 1000, 2000 # Six hours, 2000 entries.
@@ -140,9 +144,14 @@ SearchEngines =
url = engine.getUrl queryTerms
@get searchUrl, url, (xhr = null) =>
if xhr?
- callback @completionCache.set completionCacheKey, engine.parse xhr
+ # We keep at most three suggestions, the top three. These are most likely to be useful.
+ callback @completionCache.set completionCacheKey, engine.parse(xhr)[...3]
else
- callback []
+ callback @completionCache.set completionCacheKey, callback []
+ # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
+ # XMLHttpRequest failures over a short period of time.
+ removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
+ setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
root = exports ? window
root.SearchEngines = SearchEngines
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 88fe9e2c..5d9696e1 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -177,6 +177,13 @@ Utils =
delete obj[property] for property in properties
obj
+ # Does string match any of these regexps?
+ matchesAnyRegexp: (regexps, string) ->
+ for re in regexps
+ return true if re.test string
+ false
+
+
# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
Function::curry = ->
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index c519ce3a..3520537e 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -81,6 +81,7 @@ class VomnibarUI
@input.value = ""
@updateTimer = null
@completions = []
+ @previousText = null
@selection = @initialSelectionValue
updateSelection: ->
@@ -94,18 +95,12 @@ class VomnibarUI
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
# For suggestions from search-engine completion, we copy the suggested text into the input when selected,
- # and revert when not.
- if 0 <= @selection
+ # and revert when not. This allows the user to select a suggestion and then continue typing.
+ if 0 <= @selection and @completions[@selection].insertText
+ @previousText ?= @input.value
suggestion = @completions[@selection]
- if suggestion.insertText
- @previousText ?= @input.value
- @input.value = (suggestion.reinsertPrefix ? "") + suggestion.title
- else
- if @previousText?
- @input.value = @previousText
- @previousText = null
- else
- if @previousText?
+ @input.value = (suggestion.reinsertPrefix ? "") + suggestion.title
+ else if @previousText?
@input.value = @previousText
@previousText = null
--
cgit v1.2.3
From 6fecc4e24392650c540354918af641b29aa6b2b4 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 10:12:23 +0100
Subject: Search completion; Wikipedia.
---
background_scripts/search_engines.coffee | 53 +++++++++++++++++++++-----------
1 file changed, 35 insertions(+), 18 deletions(-)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 3c6654e3..c61baf17 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -35,12 +35,9 @@ class Google extends RegexpEngine
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
parse: (xhr) ->
- try
- for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
- continue unless suggestion = suggestion.getAttribute "data"
- suggestion
- catch
- []
+ for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
class Youtube extends RegexpEngine
constructor: ->
@@ -50,13 +47,20 @@ class Youtube extends RegexpEngine
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=#{Utils.createSearchQuery queryTerms}"
parse: (xhr) ->
- try
- text = xhr.responseText
- text = text.replace /^[^(]*\(/, ""
- text = text.replace /\)[^\)]*$/, ""
- suggestion[0] for suggestion in JSON.parse(text)[1]
- catch
- []
+ text = xhr.responseText
+ text = text.replace /^[^(]*\(/, ""
+ text = text.replace /\)[^\)]*$/, ""
+ suggestion[0] for suggestion in JSON.parse(text)[1]
+
+class Wikipedia extends RegexpEngine
+ constructor: ->
+ super [ new RegExp "https?://[a-z]+\.wikipedia\.org/" ]
+
+ getUrl: (queryTerms) ->
+ "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=#{Utils.createSearchQuery queryTerms}"
+
+ parse: (xhr) ->
+ JSON.parse(xhr.responseText)[1]
# 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 be a search engine match.
@@ -66,7 +70,12 @@ class DummySearchEngine
getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css"
parse: -> []
-completionEngines = [ Google, Youtube, DummySearchEngine ]
+completionEngines = [
+ Google
+ Youtube
+ Wikipedia
+ DummySearchEngine
+]
SearchEngines =
cancel: (searchUrl, callback = null) ->
@@ -142,11 +151,19 @@ SearchEngines =
engine = @lookupEngine searchUrl
url = engine.getUrl queryTerms
+ query = queryTerms.join(" ").toLowerCase()
@get searchUrl, url, (xhr = null) =>
- if xhr?
- # We keep at most three suggestions, the top three. These are most likely to be useful.
- callback @completionCache.set completionCacheKey, engine.parse(xhr)[...3]
- else
+ # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
+ # all cases, we fall back to the catch clause, below.
+ try
+ suggestions = engine.parse xhr
+ # Make sure we really do have an iterable of strings.
+ suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
+ # Filter out the query itself. It's not adding anything.
+ suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
+ # We keep at most three suggestions, the top three.
+ callback @completionCache.set completionCacheKey, suggestions[...3]
+ catch
callback @completionCache.set completionCacheKey, callback []
# We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
# XMLHttpRequest failures over a short period of time.
--
cgit v1.2.3
From 05431a5041913d78e44058e9e7f42ef9eee29ce9 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 10:24:37 +0100
Subject: Search completion; Google maps.
---
background_scripts/search_engines.coffee | 17 +++++++++++++++--
background_scripts/settings.coffee | 6 +++++-
2 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index c61baf17..90fcb99f 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -41,7 +41,7 @@ class Google extends RegexpEngine
class Youtube extends RegexpEngine
constructor: ->
- super [ new RegExp "https?://[a-z]+\.youtube\.com/results" ]
+ super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=#{Utils.createSearchQuery queryTerms}"
@@ -53,8 +53,9 @@ class Youtube extends RegexpEngine
suggestion[0] for suggestion in JSON.parse(text)[1]
class Wikipedia extends RegexpEngine
+ # Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
constructor: ->
- super [ new RegExp "https?://[a-z]+\.wikipedia\.org/" ]
+ super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
getUrl: (queryTerms) ->
"https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=#{Utils.createSearchQuery queryTerms}"
@@ -62,6 +63,18 @@ class Wikipedia extends RegexpEngine
parse: (xhr) ->
JSON.parse(xhr.responseText)[1]
+class GoogleMaps extends RegexpEngine
+ constructor: ->
+ super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
+
+ getUrl: (queryTerms) ->
+ "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
+
+ parse: (xhr) ->
+ console.log xhr
+ []
+
+ 'google-maps': 'https://www.google.com/maps/search/',
# 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 be a search engine match.
class DummySearchEngine
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index a4d95c81..01277741 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -90,7 +90,11 @@ root.Settings = Settings =
# default/fall back search engine
searchUrl: "http://www.google.com/search?q="
# put in an example search engine
- searchEngines: "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s wikipedia"
+ searchEngines: [
+ "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia", ""
+ "t: http://www.youtube.com/results?search_query=%s Youtube", ""
+ "m: https://www.google.com/maps/search/%s Google Maps", ""
+ ].join "\n"
newTabUrl: "chrome://newtab"
grabBackFocus: false
--
cgit v1.2.3
From 7ecc8805053cfe32549136412d16d5c62d6949c7 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 11:21:06 +0100
Subject: Search completion; Google maps out, Amazon, Bing in.
---
background_scripts/main.coffee | 9 ++--
background_scripts/search_engines.coffee | 71 +++++++++++++++++++-------------
background_scripts/settings.coffee | 12 ++++--
3 files changed, 56 insertions(+), 36 deletions(-)
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 4d2546fc..e678b2f7 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -51,10 +51,11 @@ completionSources =
completers =
omni: new MultiCompleter([
- completionSources.searchEngines,
- completionSources.bookmarks,
- completionSources.history,
- completionSources.domains])
+ completionSources.searchEngines
+ # completionSources.bookmarks,
+ # completionSources.history,
+ # completionSources.domains
+ ])
bookmarks: new MultiCompleter([completionSources.bookmarks])
tabs: new MultiCompleter([completionSources.tabs])
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 90fcb99f..362de3cc 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -22,8 +22,15 @@ class RegexpEngine
constructor: (@regexps) ->
match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
-# Completion engine for English-language Google search.
-class Google extends RegexpEngine
+# Several Google completion engines package responses in this way.
+class GoogleXMLRegexpEngine extends RegexpEngine
+ parse: (xhr) ->
+ for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
+
+class Google extends GoogleXMLRegexpEngine
+ # Example search URL: http://www.google.com/search?q=%s
constructor: ->
super [
# We match the major English-speaking TLDs.
@@ -34,23 +41,13 @@ class Google extends RegexpEngine
getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
- parse: (xhr) ->
- for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
- continue unless suggestion = suggestion.getAttribute "data"
- suggestion
-
-class Youtube extends RegexpEngine
+class Youtube extends GoogleXMLRegexpEngine
+ # Example search URL: http://www.youtube.com/results?search_query=%s
constructor: ->
super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
getUrl: (queryTerms) ->
- "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=#{Utils.createSearchQuery queryTerms}"
-
- parse: (xhr) ->
- text = xhr.responseText
- text = text.replace /^[^(]*\(/, ""
- text = text.replace /\)[^\)]*$/, ""
- suggestion[0] for suggestion in JSON.parse(text)[1]
+ "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=#{Utils.createSearchQuery queryTerms}"
class Wikipedia extends RegexpEngine
# Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
@@ -63,18 +60,34 @@ class Wikipedia extends RegexpEngine
parse: (xhr) ->
JSON.parse(xhr.responseText)[1]
-class GoogleMaps extends RegexpEngine
- constructor: ->
- super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
-
- getUrl: (queryTerms) ->
- "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
+## class GoogleMaps extends RegexpEngine
+## # Example search URL: https://www.google.com/maps/search/%s
+## constructor: ->
+## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
+##
+## getUrl: (queryTerms) ->
+## console.log "xxxxxxxxxxxxxxxxxxxxx"
+## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
+##
+## parse: (xhr) ->
+## console.log "yyy", xhr.responseText
+## data = JSON.parse xhr.responseText
+## console.log "zzz"
+## console.log data
+## []
+
+class Bing extends RegexpEngine
+ # Example search URL: https://www.bing.com/search?q=%s
+ constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ]
+ getUrl: (queryTerms) -> "http://api.bing.com/osjson.aspx?query=#{Utils.createSearchQuery queryTerms}"
+ parse: (xhr) -> JSON.parse(xhr.responseText)[1]
+
+class Amazon extends RegexpEngine
+ # Example search URL: http://www.amazon.com/s/?field-keywords=%s
+ constructor: -> super [ new RegExp "^https?://www\.amazon\.com/s/" ]
+ getUrl: (queryTerms) -> "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=#{Utils.createSearchQuery queryTerms}"
+ parse: (xhr) -> JSON.parse(xhr.responseText)[1]
- parse: (xhr) ->
- console.log xhr
- []
-
- 'google-maps': 'https://www.google.com/maps/search/',
# 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 be a search engine match.
class DummySearchEngine
@@ -84,9 +97,11 @@ class DummySearchEngine
parse: -> []
completionEngines = [
- Google
Youtube
+ Google
Wikipedia
+ Bing
+ Amazon
DummySearchEngine
]
@@ -112,7 +127,7 @@ SearchEngines =
@requests[searchUrl] = xhr = new XMLHttpRequest()
xhr.open "GET", url, true
# We set a fairly short timeout. If we block for too long, then we block *all* completers.
- xhr.timeout = 300
+ xhr.timeout = 500
xhr.ontimeout = => @cancel searchUrl, callback
xhr.onerror = => @cancel searchUrl, callback
xhr.send()
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 01277741..06b59af8 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -91,10 +91,14 @@ root.Settings = Settings =
searchUrl: "http://www.google.com/search?q="
# put in an example search engine
searchEngines: [
- "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia", ""
- "t: http://www.youtube.com/results?search_query=%s Youtube", ""
- "m: https://www.google.com/maps/search/%s Google Maps", ""
- ].join "\n"
+ # FIXME(smblott) Comment out these before merge.
+ "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
+ "t: http://www.youtube.com/results?search_query=%s Youtube"
+ "m: https://www.google.com/maps/search/%s Google Maps"
+ "b: https://www.bing.com/search?q=%s Bing"
+ "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"
grabBackFocus: false
--
cgit v1.2.3
From 07a62680008b6c86b0cf8a12969334bd7c1e389c Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 11:29:43 +0100
Subject: Search completion; DuckDuckGo.
---
background_scripts/main.coffee | 9 ++++-----
background_scripts/search_engines.coffee | 8 ++++++++
background_scripts/settings.coffee | 3 ++-
3 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index e678b2f7..4d2546fc 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -51,11 +51,10 @@ completionSources =
completers =
omni: new MultiCompleter([
- completionSources.searchEngines
- # completionSources.bookmarks,
- # completionSources.history,
- # completionSources.domains
- ])
+ completionSources.searchEngines,
+ completionSources.bookmarks,
+ completionSources.history,
+ completionSources.domains])
bookmarks: new MultiCompleter([completionSources.bookmarks])
tabs: new MultiCompleter([completionSources.tabs])
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 362de3cc..043e3268 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -88,6 +88,13 @@ class Amazon extends RegexpEngine
getUrl: (queryTerms) -> "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=#{Utils.createSearchQuery queryTerms}"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
+class DuckDuckGo extends RegexpEngine
+ # Example search URL: https://duckduckgo.com/?q=%s
+ constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ]
+ getUrl: (queryTerms) -> "https://duckduckgo.com/ac/?q=#{Utils.createSearchQuery queryTerms}"
+ parse: (xhr) ->
+ suggestion.phrase for suggestion in JSON.parse xhr.responseText
+
# 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 be a search engine match.
class DummySearchEngine
@@ -99,6 +106,7 @@ class DummySearchEngine
completionEngines = [
Youtube
Google
+ DuckDuckGo
Wikipedia
Bing
Amazon
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 06b59af8..07db3a89 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -91,11 +91,12 @@ root.Settings = Settings =
searchUrl: "http://www.google.com/search?q="
# put in an example search engine
searchEngines: [
- # FIXME(smblott) Comment out these before merge.
+ # FIXME(smblott) Comment these out before merge.
"w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
"t: http://www.youtube.com/results?search_query=%s Youtube"
"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"
--
cgit v1.2.3
From 98636859e0944b82fa6e3988e0ca943b5a27b145 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 11:34:19 +0100
Subject: Search completion; tweaks and fixes.
---
background_scripts/completion.coffee | 2 +-
pages/vomnibar.coffee | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 5b5dc191..c8650f45 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -342,7 +342,7 @@ class SearchEngineCompleter
# For custom search engines, we add an auto-selected suggestion.
if custom
title = if description? then query else queryTerms[0] + ": " + query
- suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), description, @computeRelevancy, 1
+ suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), title, @computeRelevancy, 1
suggestions[0].autoSelect = true
queryTerms = queryTerms[1..]
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 3520537e..831f8dd3 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -179,9 +179,11 @@ class VomnibarUI
update: (updateSynchronously, callback) =>
if (updateSynchronously)
- # The use entered something. Don't reset any previous text.
+ # The user entered something. Don't reset any previous text, and re-enable custom search engine auto
+ # selection.
if @previousText?
@previousText = null
+ @previousAutoSelect = null
@selection = -1
# cancel scheduled update
if @updateTimer?
--
cgit v1.2.3
From db59e05216ff0ce6bdd5af5fb6a6f7f3b1860a8e Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 11:48:06 +0100
Subject: Search completion; generalize Amazon.
---
background_scripts/search_engines.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 043e3268..a80c218c 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -84,7 +84,7 @@ class Bing extends RegexpEngine
class Amazon extends RegexpEngine
# Example search URL: http://www.amazon.com/s/?field-keywords=%s
- constructor: -> super [ new RegExp "^https?://www\.amazon\.com/s/" ]
+ constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ]
getUrl: (queryTerms) -> "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=#{Utils.createSearchQuery queryTerms}"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
--
cgit v1.2.3
From 7d59e948da154203722b442c477b452f7a393161 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 15:00:03 +0100
Subject: Search completion; add space in vomnibar.
---
pages/vomnibar.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 831f8dd3..2076fea6 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -99,7 +99,7 @@ class VomnibarUI
if 0 <= @selection and @completions[@selection].insertText
@previousText ?= @input.value
suggestion = @completions[@selection]
- @input.value = (suggestion.reinsertPrefix ? "") + suggestion.title
+ @input.value = (suggestion.reinsertPrefix ? "") + suggestion.title + " "
else if @previousText?
@input.value = @previousText
@previousText = null
--
cgit v1.2.3
From 776f617ece5d333fe70df903982a18d65fc2776a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 3 May 2015 17:22:20 +0100
Subject: Search completion; make completion lookup asynchronous.
---
background_scripts/completion.coffee | 97 ++++++++++++++++++++------------
background_scripts/main.coffee | 15 +++--
background_scripts/search_engines.coffee | 3 +-
lib/utils.coffee | 6 ++
pages/vomnibar.coffee | 2 +-
5 files changed, 77 insertions(+), 46 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index c8650f45..fee4778a 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -346,29 +346,31 @@ class SearchEngineCompleter
suggestions[0].autoSelect = true
queryTerms = queryTerms[1..]
- # 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).
- SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
-
- # Scoring:
- # - The score 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.
- # - The score is higher if the query is longer. The idea is that search suggestions are more likely
- # to be relevant if, after typing quite some number of characters, the user hasn't yet found a
- # useful suggestion from another completer.
- # - Scores are weighted such that they retain the ordering provided by the completion engine.
- characterCount = query.length - queryTerms.length + 1
- score = 0.8 * (Math.min(characterCount, 12.0)/12.0)
-
- for suggestion in searchSuggestions
- suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
- score *= 0.9
-
- if custom
- for suggestion in suggestions
- suggestion.reinsertPrefix = "#{keyword} " if suggestion.insertText
-
- onComplete suggestions
+ onComplete suggestions, (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).
+ SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
+
+ # Scoring:
+ # - The score 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.
+ # - The score is higher if the query is longer. The idea is that search suggestions are more likely
+ # to be relevant if, after typing quite some number of characters, the user hasn't yet found a
+ # useful suggestion from another completer.
+ # - Scores are weighted such that they retain the ordering provided by the completion engine.
+ characterCount = query.length - queryTerms.length + 1
+ score = 0.8 * (Math.min(characterCount, 12.0)/12.0)
+
+ for suggestion in searchSuggestions
+ suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
+ score *= 0.9
+
+ if custom
+ for suggestion in suggestions
+ suggestion.reinsertPrefix = "#{keyword} " if suggestion.insertText
+
+ onComplete suggestions
mkSuggestion: (insertText, args...) ->
suggestion = new Suggestion args...
@@ -410,9 +412,11 @@ class SearchEngineCompleter
# 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
- constructor: (@completers) -> @maxResults = 10
+ constructor: (@completers) ->
+ @maxResults = 10
- refresh: -> completer.refresh() for completer in @completers when completer.refresh
+ refresh: ->
+ completer.refresh?() for completer in @completers
filter: (queryTerms, onComplete) ->
# Allow only one query to run at a time.
@@ -424,21 +428,40 @@ class MultiCompleter
@filterInProgress = true
suggestions = []
completersFinished = 0
+ continuation = null
for completer in @completers
# Call filter() on every source completer and wait for them all to finish before returning results.
- completer.filter queryTerms, (newSuggestions) =>
- suggestions = suggestions.concat(newSuggestions)
- completersFinished += 1
- if completersFinished >= @completers.length
- results = @sortSuggestions(suggestions)[0...@maxResults]
- result.generateHtml() for result in results
- onComplete(results)
- @filterInProgress = false
- @filter(@mostRecentQuery.queryTerms, @mostRecentQuery.onComplete) if @mostRecentQuery
-
- sortSuggestions: (suggestions) ->
- suggestion.computeRelevancy(@queryTerms) for suggestion in suggestions
+ # At most one of the completers (SearchEngineCompleter) may pass a continuation function, which will be
+ # called asynchronously 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. We don't
+ # call the continuation if another query is already waiting.
+ completer.filter queryTerms, (newSuggestions, cont = null) =>
+ # Allow completers to execute concurrently.
+ Utils.nextTick =>
+ suggestions = suggestions.concat newSuggestions
+ continuation = cont if cont?
+ completersFinished += 1
+ if completersFinished >= @completers.length
+ onComplete @prepareSuggestions(suggestions), keepAlive: continuation?
+ onDone = =>
+ @filterInProgress = false
+ @filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
+ # We add a very short delay. It is possible for all of this processing to have been handled
+ # pretty-much synchronously, which would have prevented any newly-arriving queries from
+ # registering.
+ Utils.setTimeout 10, =>
+ if continuation? and not @mostRecentQuery
+ continuation (newSuggestions) =>
+ onComplete @prepareSuggestions suggestions.concat(newSuggestions)
+ onDone()
+ else
+ onDone()
+
+ prepareSuggestions: (suggestions) ->
+ suggestion.computeRelevancy @queryTerms for suggestion in suggestions
suggestions.sort (a, b) -> b.relevancy - a.relevancy
+ suggestions = suggestions[0...@maxResults]
+ suggestion.generateHtml() for suggestion in suggestions
suggestions
# Utilities which help us compute a relevancy score for a given item.
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 4d2546fc..45619023 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -50,11 +50,13 @@ completionSources =
searchEngines: new SearchEngineCompleter()
completers =
- omni: new MultiCompleter([
- completionSources.searchEngines,
- completionSources.bookmarks,
- completionSources.history,
- completionSources.domains])
+ omni: new MultiCompleter [
+ completionSources.bookmarks
+ completionSources.history
+ completionSources.domains
+ # This comes last, because it delivers additional, asynchronous results.
+ completionSources.searchEngines
+ ]
bookmarks: new MultiCompleter([completionSources.bookmarks])
tabs: new MultiCompleter([completionSources.tabs])
@@ -220,7 +222,8 @@ refreshCompleter = (request) -> completers[request.name].refresh()
whitespaceRegexp = /\s+/
filterCompleter = (args, port) ->
queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp)
- completers[args.name].filter(queryTerms, (results) -> port.postMessage({ id: args.id, results: results }))
+ completers[args.name].filter queryTerms, (results, extra = {}) ->
+ port.postMessage extend extra, id: args.id, results: results
chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->
if (selectionChangedHandlers.length > 0)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index a80c218c..abf8c86e 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -134,8 +134,7 @@ SearchEngines =
@requests[searchUrl] = xhr = new XMLHttpRequest()
xhr.open "GET", url, true
- # We set a fairly short timeout. If we block for too long, then we block *all* completers.
- xhr.timeout = 500
+ xhr.timeout = 750
xhr.ontimeout = => @cancel searchUrl, callback
xhr.onerror = => @cancel searchUrl, callback
xhr.send()
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 5d9696e1..07528714 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -183,6 +183,12 @@ Utils =
return true if re.test string
false
+ # Convenience wrapper for setTimeout (with the arguments around the other way).
+ setTimeout: (ms, func) -> setTimeout func, ms
+
+ # Like Nodejs's nextTick.
+ nextTick: (func) -> @setTimeout 0, func
+
# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 2076fea6..3fb63177 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -239,7 +239,7 @@ class BackgroundCompleter
id = BackgroundCompleter.messageId += 1
@filterPort.onMessage.addListener handler = (msg) =>
if msg.id == id
- @filterPort.onMessage.removeListener handler
+ @filterPort.onMessage.removeListener handler unless msg.keepAlive and id == BackgroundCompleter.messageId
if id == BackgroundCompleter.messageId
# The result objects coming from the background page will be of the form:
# { html: "", type: "", url: "" }
--
cgit v1.2.3
From 136b132c81ae4f7a3b296109427fd757ca05dacf Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 06:35:22 +0100
Subject: Search completion; tweak scoring and synchronization.
---
background_scripts/completion.coffee | 121 ++++++++++++++++++-------------
background_scripts/search_engines.coffee | 85 ++++++++++------------
tests/unit_tests/completion_test.coffee | 4 +-
tests/unit_tests/settings_test.coffee | 2 +-
4 files changed, 111 insertions(+), 101 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index fee4778a..37e9ea6b 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -335,42 +335,67 @@ class SearchEngineCompleter
else
(string) -> Utils.createSearchUrl string.split /\s+/
- type = if description? then description else "search"
+ haveDescription = description? and 0 < description.trim().length
+ type = if haveDescription then description else "search"
searchUrl = if custom then url else Settings.get "searchUrl"
- query = queryTerms[(if custom then 1 else 0)..].join " "
# For custom search engines, we add an auto-selected suggestion.
if custom
- title = if description? then query else queryTerms[0] + ": " + query
+ query = queryTerms[1..].join " "
+ title = if haveDescription then query else keyword + ": " + query
suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), title, @computeRelevancy, 1
suggestions[0].autoSelect = true
queryTerms = queryTerms[1..]
- onComplete suggestions, (onComplete) =>
+ if queryTerms.length == 0
+ return onComplete suggestions
+
+ 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).
- SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
-
- # Scoring:
- # - The score 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.
- # - The score is higher if the query is longer. The idea is that search suggestions are more likely
- # to be relevant if, after typing quite some number of characters, the user hasn't yet found a
- # useful suggestion from another completer.
- # - Scores are weighted such that they retain the ordering provided by the completion engine.
- characterCount = query.length - queryTerms.length + 1
- score = 0.8 * (Math.min(characterCount, 12.0)/12.0)
-
- for suggestion in searchSuggestions
- suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
- score *= 0.9
- if custom
- for suggestion in suggestions
- suggestion.reinsertPrefix = "#{keyword} " if suggestion.insertText
-
- onComplete suggestions
+ # Scoring:
+ # - The score 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 based on the length of the last query term. The idea is that the user is already
+ # happy with the earlier terms.
+ # - The score is higher if the last 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 = queryTerms[queryTerms.length - 1].length
+ score = 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
+ # immediately.
+ return onComplete []
+
+ # We pause in case the user is still typing.
+ Utils.setTimeout 250, handler = @mostRecentHandler = =>
+ return onComplete [] if handler != @mostRecentHandler # Bail if another completion has begun.
+
+ SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
+ for suggestion in searchSuggestions
+ suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
+ score *= 0.9
+
+ if custom
+ # For custom search engines, we need to tell the front end to insert the search engine's keyword
+ # when copying a suggestion into the vomnibar.
+ suggestion.reinsertPrefix = "#{keyword} " for suggestion in suggestions
+
+ # 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]
mkSuggestion: (insertText, args...) ->
suggestion = new Suggestion args...
@@ -412,8 +437,10 @@ class SearchEngineCompleter
# 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
+
constructor: (@completers) ->
- @maxResults = 10
+ @maxResults = MultiCompleter.maxResults
refresh: ->
completer.refresh?() for completer in @completers
@@ -429,36 +456,30 @@ class MultiCompleter
suggestions = []
completersFinished = 0
continuation = null
+ # 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 in @completers
- # 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 asynchronously 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. We don't
- # call the continuation if another query is already waiting.
- completer.filter queryTerms, (newSuggestions, cont = null) =>
- # Allow completers to execute concurrently.
+ do (completer) =>
Utils.nextTick =>
- suggestions = suggestions.concat newSuggestions
- continuation = cont if cont?
- completersFinished += 1
- if completersFinished >= @completers.length
- onComplete @prepareSuggestions(suggestions), keepAlive: continuation?
- onDone = =>
+ completer.filter queryTerms, (newSuggestions, cont = null) =>
+ suggestions = suggestions.concat newSuggestions
+ continuation = cont if cont?
+ if @completers.length <= ++completersFinished
+ shouldRunContinuation = continuation? and not @mostRecentQuery
+ onComplete @prepareSuggestions(queryTerms, suggestions), keepAlive: shouldRunContinuation
+ # Allow subsequent queries to begin.
@filterInProgress = false
- @filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
- # We add a very short delay. It is possible for all of this processing to have been handled
- # pretty-much synchronously, which would have prevented any newly-arriving queries from
- # registering.
- Utils.setTimeout 10, =>
- if continuation? and not @mostRecentQuery
- continuation (newSuggestions) =>
- onComplete @prepareSuggestions suggestions.concat(newSuggestions)
- onDone()
+ if shouldRunContinuation
+ continuation suggestions, (newSuggestions) =>
+ onComplete @prepareSuggestions queryTerms, suggestions.concat(newSuggestions)
else
- onDone()
+ @filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
- prepareSuggestions: (suggestions) ->
- suggestion.computeRelevancy @queryTerms for suggestion in suggestions
+ prepareSuggestions: (queryTerms, suggestions) ->
+ suggestion.computeRelevancy queryTerms for suggestion in suggestions
suggestions.sort (a, b) -> b.relevancy - a.relevancy
suggestions = suggestions[0...@maxResults]
suggestion.generateHtml() for suggestion in suggestions
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index abf8c86e..3ddbe742 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -114,38 +114,16 @@ completionEngines = [
]
SearchEngines =
- cancel: (searchUrl, callback = null) ->
- @requests[searchUrl]?.abort()
- delete @requests[searchUrl]
- callback? null
-
- # Perform an HTTP GET.
get: (searchUrl, url, callback) ->
- @requests ?= {} # Maps a searchUrl to any outstanding HTTP request for that search engine.
- @cancel searchUrl
-
- # We cache the results of the most-recent 100 successfully XMLHttpRequests with a ten-second (ie. very
- # short) expiry.
- @requestCache ?= new SimpleCache 10 * 1000, 100
-
- if @requestCache.has url
- callback @requestCache.get url
- return
-
- @requests[searchUrl] = xhr = new XMLHttpRequest()
+ xhr = new XMLHttpRequest()
xhr.open "GET", url, true
- xhr.timeout = 750
- xhr.ontimeout = => @cancel searchUrl, callback
- xhr.onerror = => @cancel searchUrl, callback
+ xhr.timeout = 1000
+ xhr.ontimeout = xhr.onerror = -> callback null
xhr.send()
- xhr.onreadystatechange = =>
+ xhr.onreadystatechange = ->
if xhr.readyState == 4
- @requests[searchUrl] = null
- if xhr.status == 200
- callback @requestCache.set url, xhr
- else
- callback null
+ callback(if xhr.status == 200 then xhr else null)
# Look up the search-completion engine for this searchUrl. Because of DummySearchEngine, above, we know
# there will always be a match. Imagining that there may be many completion engines, and knowing that this
@@ -176,7 +154,7 @@ SearchEngines =
return callback [] if Utils.hasJavascriptPrefix queryTerms[0]
# Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
- # to generate a key. We mix in some nonsense generated by pwgen. A key clash is possible, but vanishingly
+ # to generate a key. We mix in some junk generated by pwgen. A key clash is possible, but vanishingly
# unlikely.
junk = "//Zi?ei5;o//"
completionCacheKey = searchUrl + junk + queryTerms.join junk
@@ -184,26 +162,37 @@ SearchEngines =
if @completionCache.has completionCacheKey
return callback @completionCache.get completionCacheKey
- engine = @lookupEngine searchUrl
- url = engine.getUrl queryTerms
- query = queryTerms.join(" ").toLowerCase()
- @get searchUrl, url, (xhr = null) =>
- # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
- # all cases, we fall back to the catch clause, below.
- try
- suggestions = engine.parse xhr
- # Make sure we really do have an iterable of strings.
- suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
- # Filter out the query itself. It's not adding anything.
- suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
- # We keep at most three suggestions, the top three.
- callback @completionCache.set completionCacheKey, suggestions[...3]
- catch
- callback @completionCache.set completionCacheKey, callback []
- # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
- # XMLHttpRequest failures over a short period of time.
- removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
- setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
+ fetchSuggestions = (callback) =>
+ engine = @lookupEngine searchUrl
+ url = engine.getUrl queryTerms
+ query = queryTerms.join(" ").toLowerCase()
+ @get searchUrl, url, (xhr = null) =>
+ # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
+ # all cases, we fall back to the catch clause, below.
+ try
+ suggestions = engine.parse xhr
+ # Make sure we really do have an iterable of strings.
+ suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
+ # Filter out the query itself. It's not adding anything.
+ suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
+ catch
+ suggestions = []
+ # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
+ # XMLHttpRequest failures over a short period of time.
+ removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
+ setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
+
+ callback suggestions
+
+ # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
+ # removes a space, or when they enter a character and immediately delete it.
+ @inTransit ?= {}
+ unless @inTransit[completionCacheKey]?.push callback
+ queue = @inTransit[completionCacheKey] = []
+ fetchSuggestions (suggestions) =>
+ callback @completionCache.set completionCacheKey, suggestions
+ delete @inTransit[completionCacheKey]
+ callback suggestions for callback in queue
root = exports ? window
root.SearchEngines = SearchEngines
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index 39437b52..56fcc456 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -239,10 +239,10 @@ context "search engines",
setup ->
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
- @completer = new CustomSearchEngineCompleter()
+ @completer = new SearchEngineCompleter()
# note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors
# workaround is below, would be good for someone that understands the testing system better than me to improve
- @completer.searchEngines = CustomSearchEngineCompleter.getSearchEngines()
+ @completer.searchEngines = SearchEngineCompleter.getSearchEngines()
should "return search engine suggestion without description", ->
results = filterCompleter(@completer, ["foo", "hello"])
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index a67b69fa..346c98da 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -73,7 +73,7 @@ context "settings",
should "set search engines, retrieve them correctly and check that they have been parsed correctly", ->
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
- result = CustomSearchEngineCompleter.getSearchEngines()
+ result = SearchEngineCompleter.getSearchEngines()
assert.equal Object.keys(result).length, 2
assert.equal "bar?q=%s", result["foo"].url
assert.isFalse result["foo"].description
--
cgit v1.2.3
From 85aa77ce1daec7bd1452cd7813a0d5d408729408 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 09:54:36 +0100
Subject: Search completion; prevent vomnibar flicker.
---
background_scripts/completion.coffee | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 37e9ea6b..a75cf6b5 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -469,7 +469,11 @@ class MultiCompleter
continuation = cont if cont?
if @completers.length <= ++completersFinished
shouldRunContinuation = continuation? and not @mostRecentQuery
- onComplete @prepareSuggestions(queryTerms, suggestions), keepAlive: shouldRunContinuation
+ # We don't post results immediately if there are none, and we're going to run a continuation
+ # (ie. a SearchEngineCompleter). This prevents hiding the vomnibar briefly before showing it
+ # again, which looks ugly.
+ unless shouldRunContinuation and suggestions.length == 0
+ onComplete @prepareSuggestions(queryTerms, suggestions), keepAlive: shouldRunContinuation
# Allow subsequent queries to begin.
@filterInProgress = false
if shouldRunContinuation
--
cgit v1.2.3
From 9de495ededee7f98504d6fe308ea73526956a7f3 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 10:17:45 +0100
Subject: Search completion; hardwire search engines.
These should be removed later.
---
background_scripts/settings.coffee | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 07db3a89..b802937e 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -5,6 +5,8 @@
root = exports ? window
root.Settings = Settings =
get: (key) ->
+ # FIXME(smblott). Remove this line.
+ return @defaults.searchEngines if key == "searchEngines"
if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key]
set: (key, value) ->
@@ -92,6 +94,9 @@ root.Settings = Settings =
# put in an example search engine
searchEngines: [
# FIXME(smblott) Comment these out before merge.
+ "# THESE ARE HARD WIRED.\n# YOU CANNOT CHANGE THEM IN THIS VERSION.\n# FOR DEVELOPMENT ONLY."
+ "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"
"m: https://www.google.com/maps/search/%s Google Maps"
--
cgit v1.2.3
From b2ae01c8604ed87f52afa3b5e769fb340dcb1b93 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 10:27:08 +0100
Subject: Search completion; force the first match to the top of the list.
This may or may not be a good idea. It means that the first completion
is just a tab away.
---
background_scripts/completion.coffee | 3 +++
1 file changed, 3 insertions(+)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index a75cf6b5..795b4658 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -385,6 +385,9 @@ class SearchEngineCompleter
suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
score *= 0.9
+ # Experimental. Force the best match to the top of the list.
+ suggestions[0].extraRelevancyData = 0.9999999 if 0 < suggestions.length
+
if custom
# For custom search engines, we need to tell the front end to insert the search engine's keyword
# when copying a suggestion into the vomnibar.
--
cgit v1.2.3
From 4e86053f9464c04b9e422625478f8e827f37e533 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 11:42:59 +0100
Subject: Search completion; do not highlight search terms.
Highlighting the search terms suggests they are in some way contributing
to the match. They are not, so don't highlight them.
This gets particularly ugly when you have short, single-letter costom
search engines (eg. w), and have all of the "w"s highlighted -- for not
useful reason.
---
background_scripts/completion.coffee | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 795b4658..b503a452 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -29,15 +29,17 @@ class Suggestion
generateHtml: ->
return @html if @html
relevancyHtml = if @showRelevancy then "#{@computeRelevancy()}" else ""
+ highlightTerms =
+ if @noHighlightTerms then ((s) -> Utils.escapeHtml s) else ((s) => @highlightTerms Utils.escapeHtml s)
# NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS.
@html =
"""
"""
@@ -402,8 +404,7 @@ class SearchEngineCompleter
mkSuggestion: (insertText, args...) ->
suggestion = new Suggestion args...
- suggestion.insertText = insertText
- suggestion
+ extend suggestion, insertText: insertText, noHighlightTerms: true
# The score is computed in filter() and provided here via suggestion.extraRelevancyData.
computeRelevancy: (suggestion) -> suggestion.extraRelevancyData
--
cgit v1.2.3
From f9d35083ad511080ee5c01c5c97ef29503f3c0ba Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 11:55:37 +0100
Subject: Search completion; simplify/document suggestion options.
---
background_scripts/completion.coffee | 15 ++++++++-------
pages/vomnibar.coffee | 4 ++--
2 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index b503a452..f09a079c 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -23,6 +23,11 @@ class Suggestion
@title ||= ""
# 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.
+ @autoSelect = 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)
@@ -345,7 +350,7 @@ class SearchEngineCompleter
if custom
query = queryTerms[1..].join " "
title = if haveDescription then query else keyword + ": " + query
- suggestions.push @mkSuggestion false, queryTerms, type, mkUrl(query), title, @computeRelevancy, 1
+ suggestions.push @mkSuggestion null, queryTerms, type, mkUrl(query), title, @computeRelevancy, 1
suggestions[0].autoSelect = true
queryTerms = queryTerms[1..]
@@ -384,17 +389,13 @@ class SearchEngineCompleter
SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
for suggestion in searchSuggestions
- suggestions.push @mkSuggestion true, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
+ insertText = if custom then "#{keyword} #{suggestion}" else suggestion
+ suggestions.push @mkSuggestion insertText, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
score *= 0.9
# Experimental. Force the best match to the top of the list.
suggestions[0].extraRelevancyData = 0.9999999 if 0 < suggestions.length
- if custom
- # For custom search engines, we need to tell the front end to insert the search engine's keyword
- # when copying a suggestion into the vomnibar.
- suggestion.reinsertPrefix = "#{keyword} " for suggestion in suggestions
-
# 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
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 3fb63177..8a262e76 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -96,10 +96,10 @@ class VomnibarUI
# 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.
- if 0 <= @selection and @completions[@selection].insertText
+ if 0 <= @selection and @completions[@selection].insertText?
@previousText ?= @input.value
suggestion = @completions[@selection]
- @input.value = (suggestion.reinsertPrefix ? "") + suggestion.title + " "
+ @input.value = @completions[@selection].insertText
else if @previousText?
@input.value = @previousText
@previousText = null
--
cgit v1.2.3
From 7f99155824edd7424bd28feae401459da6a2ba8f Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 12:38:00 +0100
Subject: Search completion; instrument for debugging.
---
background_scripts/completion.coffee | 35 +++++++++++++--------------
background_scripts/search_engines.coffee | 41 ++++++++++++++++++++++----------
pages/vomnibar.coffee | 1 +
3 files changed, 46 insertions(+), 31 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index f09a079c..1c07a71a 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -383,25 +383,21 @@ class SearchEngineCompleter
# immediately.
return onComplete []
- # We pause in case the user is still typing.
- Utils.setTimeout 250, handler = @mostRecentHandler = =>
- return onComplete [] if handler != @mostRecentHandler # Bail if another completion has begun.
-
- SearchEngines.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
-
- # Experimental. Force the best match to the top of the list.
- suggestions[0].extraRelevancyData = 0.9999999 if 0 < suggestions.length
-
- # 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]
+ SearchEngines.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
+
+ # Experimental. Force the best match to the top of the list.
+ suggestions[0].extraRelevancyData = 0.9999999 if 0 < suggestions.length
+
+ # 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]
mkSuggestion: (insertText, args...) ->
suggestion = new Suggestion args...
@@ -474,6 +470,7 @@ class MultiCompleter
continuation = cont if cont?
if @completers.length <= ++completersFinished
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 prevents hiding the vomnibar briefly before showing it
# again, which looks ugly.
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 3ddbe742..608115f3 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -114,6 +114,8 @@ completionEngines = [
]
SearchEngines =
+ debug: true
+
get: (searchUrl, url, callback) ->
xhr = new XMLHttpRequest()
xhr.open "GET", url, true
@@ -144,11 +146,18 @@ SearchEngines =
# - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
# wrong).
complete: (searchUrl, queryTerms, callback) ->
+ @mostRecentHandler = null
+
# We can't complete empty queries.
return callback [] unless 0 < queryTerms.length
- # We don't complete URLs.
- return callback [] if 1 == queryTerms.length and Utils.isUrl queryTerms[0]
+ if 1 == queryTerms.length
+ # We don't complete URLs.
+ return callback [] if Utils.isUrl queryTerms[0]
+ # We don't complete less then three characters: the results are usually useless. This also prevents
+ # one- and two-character custom search engine keywords from being sent to the default completer (e.g.
+ # the initial "w" before typing "w something").
+ return callback [] unless 2 < queryTerms[0].length
# We don't complete Javascript URLs.
return callback [] if Utils.hasJavascriptPrefix queryTerms[0]
@@ -158,13 +167,15 @@ SearchEngines =
# unlikely.
junk = "//Zi?ei5;o//"
completionCacheKey = searchUrl + junk + queryTerms.join junk
- @completionCache ?= new SimpleCache 6 * 60 * 60 * 1000, 2000 # Six hours, 2000 entries.
+ @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
if @completionCache.has completionCacheKey
+ console.log "hit", completionCacheKey if @debug
return callback @completionCache.get completionCacheKey
fetchSuggestions = (callback) =>
engine = @lookupEngine searchUrl
url = engine.getUrl queryTerms
+ console.log "get", url if @debug
query = queryTerms.join(" ").toLowerCase()
@get searchUrl, url, (xhr = null) =>
# Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
@@ -184,15 +195,21 @@ SearchEngines =
callback suggestions
- # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
- # removes a space, or when they enter a character and immediately delete it.
- @inTransit ?= {}
- unless @inTransit[completionCacheKey]?.push callback
- queue = @inTransit[completionCacheKey] = []
- fetchSuggestions (suggestions) =>
- callback @completionCache.set completionCacheKey, suggestions
- delete @inTransit[completionCacheKey]
- callback suggestions for callback in queue
+ # We pause in case the user is still typing.
+ Utils.setTimeout 200, handler = @mostRecentHandler = =>
+ if handler != @mostRecentHandler # Bail if another completion has begun.
+ console.log "bail", completionCacheKey if @debug
+ return callback []
+ # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
+ # removes a space, or when they enter a character and immediately delete it.
+ @inTransit ?= {}
+ unless @inTransit[completionCacheKey]?.push callback
+ queue = @inTransit[completionCacheKey] = []
+ fetchSuggestions (suggestions) =>
+ callback @completionCache.set completionCacheKey, suggestions
+ delete @inTransit[completionCacheKey]
+ console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
+ callback suggestions for callback in queue
root = exports ? window
root.SearchEngines = SearchEngines
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 8a262e76..c029dc20 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -81,6 +81,7 @@ class VomnibarUI
@input.value = ""
@updateTimer = null
@completions = []
+ @previousAutoSelect = null
@previousText = null
@selection = @initialSelectionValue
--
cgit v1.2.3
From 44b24f43a30e2f2ebe47e70e74413488ad3a676f Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 13:29:48 +0100
Subject: Search completion; fix mistake with autoSelect.
---
background_scripts/completion.coffee | 2 +-
pages/vomnibar.coffee | 24 ++++++++++++------------
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 1c07a71a..d206a285 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -24,7 +24,7 @@ class Suggestion
# 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.
- @autoSelect = true
+ @noHighlightTerms = false
# If @insertText is a string, then the indicated text is inserted into the vomnibar input when the
# suggestion is selected.
@insertText = null
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index c029dc20..468736bb 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -82,28 +82,29 @@ class VomnibarUI
@updateTimer = null
@completions = []
@previousAutoSelect = null
- @previousText = null
+ @previousInputValue = null
@selection = @initialSelectionValue
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 @completions[0]
+ 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
- for i in [0...@completionList.children.length]
- @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
# 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.
if 0 <= @selection and @completions[@selection].insertText?
- @previousText ?= @input.value
- suggestion = @completions[@selection]
+ @previousInputValue ?= @input.value
@input.value = @completions[@selection].insertText
- else if @previousText?
- @input.value = @previousText
- @previousText = null
+ else if @previousInputValue?
+ @input.value = @previousInputValue
+ @previousInputValue = null
+
+ # Highlight the the selected entry, and only the selected entry.
+ for i in [0...@completionList.children.length]
+ @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
#
# Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
@@ -182,9 +183,8 @@ class VomnibarUI
if (updateSynchronously)
# The user entered something. Don't reset any previous text, and re-enable custom search engine auto
# selection.
- if @previousText?
- @previousText = null
- @previousAutoSelect = null
+ if @previousInputValue? and updateSynchronously.type == "input"
+ @previousInputValue = null
@selection = -1
# cancel scheduled update
if @updateTimer?
--
cgit v1.2.3
From be41c579673b323a78f578a50930f429f9b8505e Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 14:04:19 +0100
Subject: Search completion; reinstate asynchronous Vomnibar update.
At some point in the past, vomnibar updates (in omni mode) we
asychronous. That seems to have been dropped, probably accidentally.
---
pages/vomnibar.coffee | 62 ++++++++++++++++++++++++---------------------------
1 file changed, 29 insertions(+), 33 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 468736bb..1514ab29 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -24,9 +24,8 @@ Vomnibar =
selectFirst: false
extend options, userOptions
- options.refreshInterval = switch options.completer
- when "omni" then 100
- else 0
+ options.refreshInterval =
+ if options.completer == "omni" then 125 else 50
completer = @getCompleter(options.completer)
@vomnibarUI ?= new VomnibarUI()
@@ -55,7 +54,7 @@ class VomnibarUI
setCompleter: (completer) ->
@completer = completer
@reset()
- @update(true)
+ # NEEDED? (smblott) @update(true)
setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval
@@ -164,43 +163,40 @@ class VomnibarUI
event.preventDefault()
true
- updateCompletions: (callback) ->
- query = @input.value.trim()
-
- @completer.filter query, (completions) =>
- @completions = completions
- @populateUiWithCompletions(completions)
- callback() if callback
+ updateCompletions: (callback = null) ->
+ @completer.filter @input.value.trim(), (@completions) =>
+ @populateUiWithCompletions @completions
+ callback?()
populateUiWithCompletions: (completions) ->
# update completion list with the new data
@completionList.innerHTML = completions.map((completion) -> "
#{completion.html}
").join("")
@completionList.style.display = if completions.length > 0 then "block" else ""
- @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1)
+ @selection = Math.min completions.length - 1, Math.max @initialSelectionValue, @selection
@updateSelection()
- update: (updateSynchronously, callback) =>
- if (updateSynchronously)
- # The user entered something. Don't reset any previous text, and re-enable custom search engine auto
- # selection.
- if @previousInputValue? and updateSynchronously.type == "input"
- @previousInputValue = null
- @selection = -1
- # cancel scheduled update
- if @updateTimer?
- window.clearTimeout(@updateTimer)
- @updateTimer = null
- @updateCompletions(callback)
- else if (@updateTimer != null)
- # an update is already scheduled, don't do anything
- return
+ updateOnInput: =>
+ # If the user types, then don't reset any previous text, and re-enable auto-select.
+ if @previousInputValue?
+ @previousInputValue = null
+ @previousAutoSelect = null
+ @selection = -1
+ @update()
+
+ update: (updateSynchronously = false, callback = null) =>
+ # Cancel any scheduled update.
+ if @updateTimer?
+ window.clearTimeout @updateTimer
+ @updateTimer = null
+
+ if updateSynchronously
+ @updateCompletions callback
else
- # always update asynchronously for better user experience and to take some load off the CPU
- # (not every keystroke will cause a dedicated update)
- @updateTimer = setTimeout(=>
- @updateCompletions(callback)
+ # Update asynchronously for 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
- @refreshInterval)
+ @updateCompletions callback
@input.focus()
@@ -208,7 +204,7 @@ class VomnibarUI
@box = document.getElementById("vomnibar")
@input = @box.querySelector("input")
- @input.addEventListener "input", @update
+ @input.addEventListener "input", @updateOnInput
@input.addEventListener "keydown", @onKeydown
@completionList = @box.querySelector("ul")
@completionList.style.display = ""
--
cgit v1.2.3
From b8e018df407fdca6e2418a90e8ad6cf6b3659425 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 14:21:37 +0100
Subject: Search completion; disable experimental feature.
---
background_scripts/completion.coffee | 3 ---
1 file changed, 3 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index d206a285..e76139c2 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -389,9 +389,6 @@ class SearchEngineCompleter
suggestions.push @mkSuggestion insertText, queryTerms, type, mkUrl(suggestion), suggestion, @computeRelevancy, score
score *= 0.9
- # Experimental. Force the best match to the top of the list.
- suggestions[0].extraRelevancyData = 0.9999999 if 0 < suggestions.length
-
# 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
--
cgit v1.2.3
From 93b3938e8c8b04c4cd15ad7bff83f8d34c9a604b Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 4 May 2015 14:42:46 +0100
Subject: Search completion; revert incorrect change.
---
pages/vomnibar.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 1514ab29..c27d689a 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -54,7 +54,7 @@ class VomnibarUI
setCompleter: (completer) ->
@completer = completer
@reset()
- # NEEDED? (smblott) @update(true)
+ @update(true)
setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval
--
cgit v1.2.3
From 04398df01e57923a4b19319b2b77f487c703ab30 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 06:07:59 +0100
Subject: Search completion; fix autoselect on empty vomnibar.
---
pages/vomnibar.coffee | 2 ++
1 file changed, 2 insertions(+)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index c27d689a..9566d9d9 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -91,6 +91,8 @@ class VomnibarUI
@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 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.
--
cgit v1.2.3
From 1a9ea85f683c892592d7092c361e835c8b0f4f3e Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 06:14:12 +0100
Subject: Search completion; tweak scoring.
---
background_scripts/completion.coffee | 8 ++++----
pages/vomnibar.coffee | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index e76139c2..33d8a563 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -353,6 +353,8 @@ class SearchEngineCompleter
suggestions.push @mkSuggestion null, queryTerms, type, mkUrl(query), title, @computeRelevancy, 1
suggestions[0].autoSelect = true
queryTerms = queryTerms[1..]
+ else
+ query = queryTerms.join " "
if queryTerms.length == 0
return onComplete suggestions
@@ -367,13 +369,11 @@ class SearchEngineCompleter
# 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 based on the length of the last query term. The idea is that the user is already
- # happy with the earlier terms.
- # - The score is higher if the last query term is longer. The idea is that search suggestions are more
+ # - The score 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 = queryTerms[queryTerms.length - 1].length
+ characterCount = query.length - queryTerms.length + 1
score = 0.6 * (Math.min(characterCount, 10.0)/10.0)
if 0 < existingSuggestions.length
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 9566d9d9..ff004c6e 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -25,7 +25,7 @@ Vomnibar =
extend options, userOptions
options.refreshInterval =
- if options.completer == "omni" then 125 else 50
+ if options.completer == "omni" then 125 else 0
completer = @getCompleter(options.completer)
@vomnibarUI ?= new VomnibarUI()
--
cgit v1.2.3
From e21896e2af7f28eb17b5b98a32cfce32fe171d3a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 14:27:41 +0100
Subject: Search completion; rework asynchronous lookups.
---
pages/vomnibar.coffee | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index ff004c6e..58513dc2 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -186,14 +186,13 @@ class VomnibarUI
@update()
update: (updateSynchronously = false, callback = null) =>
- # Cancel any scheduled update.
- if @updateTimer?
- window.clearTimeout @updateTimer
- @updateTimer = null
-
if updateSynchronously
+ # Cancel any scheduled update.
+ if @updateTimer?
+ window.clearTimeout @updateTimer
+ @updateTimer = null
@updateCompletions callback
- else
+ else if not @updateTimer?
# Update asynchronously for better user experience and to take some load off the CPU (not every
# keystroke will cause a dedicated update)
@updateTimer = Utils.setTimeout @refreshInterval, =>
--
cgit v1.2.3
From 7c855af38cfdf32a05b90b2da4711720ebac8865 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 14:47:02 +0100
Subject: Search completion; user is typing.
Add plumbing to allow the front end to directly inform completers when
the user is typing.
---
background_scripts/completion.coffee | 6 ++++++
background_scripts/main.coffee | 10 +++++++---
background_scripts/search_engines.coffee | 8 ++++++--
pages/vomnibar.coffee | 4 ++++
4 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 33d8a563..92936098 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -331,6 +331,9 @@ class TabCompleter
class SearchEngineCompleter
searchEngines: {}
+ userIsTyping: ->
+ SearchEngines.userIsTyping()
+
filter: (queryTerms, onComplete) ->
{ keyword: keyword, url: url, description: description } = @getSearchEngineMatches queryTerms
custom = url?
@@ -443,6 +446,9 @@ class MultiCompleter
refresh: ->
completer.refresh?() for completer in @completers
+ userIsTyping: ->
+ completer.userIsTyping?() for completer in @completers
+
filter: (queryTerms, onComplete) ->
# Allow only one query to run at a time.
if @filterInProgress
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 45619023..44644769 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -221,9 +221,13 @@ refreshCompleter = (request) -> completers[request.name].refresh()
whitespaceRegexp = /\s+/
filterCompleter = (args, port) ->
- queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp)
- completers[args.name].filter queryTerms, (results, extra = {}) ->
- port.postMessage extend extra, id: args.id, results: results
+ if args.name? and args.userIsTyping
+ completers[args.name].userIsTyping?()
+
+ if args.id? and args.name? and args.query?
+ queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp)
+ completers[args.name].filter queryTerms, (results, extra = {}) ->
+ port.postMessage extend extra, id: args.id, results: results
chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->
if (selectionChangedHandlers.length > 0)
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 608115f3..74e752e3 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -13,7 +13,7 @@
# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and
# returns a list of suggestions (a list of strings).
#
-# The main (only) completion entry point is SearchEngines.complete(). This implements all lookup and caching
+# The main completion entry point is SearchEngines.complete(). This implements all lookup and caching
# logic. It is possible to add new completion engines without changing the SearchEngines infrastructure
# itself.
@@ -197,7 +197,7 @@ SearchEngines =
# We pause in case the user is still typing.
Utils.setTimeout 200, handler = @mostRecentHandler = =>
- if handler != @mostRecentHandler # Bail if another completion has begun.
+ if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing.
console.log "bail", completionCacheKey if @debug
return callback []
# Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
@@ -211,5 +211,9 @@ SearchEngines =
console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
callback suggestions for callback in queue
+ userIsTyping: ->
+ console.log "reset (typing)" if @debug and @mostRecentHandler?
+ @mostRecentHandler = null
+
root = exports ? window
root.SearchEngines = SearchEngines
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 58513dc2..8a070df6 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -178,6 +178,7 @@ class VomnibarUI
@updateSelection()
updateOnInput: =>
+ @completer.userIsTyping()
# If the user types, then don't reset any previous text, and re-enable auto-select.
if @previousInputValue?
@previousInputValue = null
@@ -253,6 +254,9 @@ class BackgroundCompleter
@filterPort.postMessage id: id, name: @name, query: query
+ userIsTyping: ->
+ @filterPort.postMessage name: @name, userIsTyping: true
+
extend BackgroundCompleter,
#
# These are the actions we can perform when the user selects a result in the Vomnibox.
--
cgit v1.2.3
From 32895a96b3da225bd1b46d4b96ab98fd71755281 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 16:16:59 +0100
Subject: Search completion; changes in response to @mrmr1993.
---
background_scripts/completion.coffee | 8 ++++----
background_scripts/search_engines.coffee | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 92936098..39f8a140 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -34,17 +34,15 @@ class Suggestion
generateHtml: ->
return @html if @html
relevancyHtml = if @showRelevancy then "#{@computeRelevancy()}" else ""
- highlightTerms =
- if @noHighlightTerms then ((s) -> Utils.escapeHtml s) else ((s) => @highlightTerms Utils.escapeHtml s)
# NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS.
@html =
"""
"""
@@ -85,6 +83,7 @@ class Suggestion
# Wraps each occurence of the query terms in the given string in a .
highlightTerms: (string) ->
+ return string if @noHighlightTerms
ranges = []
escapedTerms = @queryTerms.map (term) -> Utils.escapeHtml(term)
for term in escapedTerms
@@ -399,6 +398,7 @@ 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
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
index 74e752e3..63c61a47 100644
--- a/background_scripts/search_engines.coffee
+++ b/background_scripts/search_engines.coffee
@@ -34,7 +34,7 @@ class Google extends GoogleXMLRegexpEngine
constructor: ->
super [
# We match the major English-speaking TLDs.
- new RegExp "^https?://[a-z]+\.google\.(com|ie|co.uk|ca|com.au)/"
+ new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
new RegExp "localhost/cgi-bin/booky" # Only for smblott.
]
--
cgit v1.2.3
From 43bdd2787f2bffc4fc9c3397937a0ce9a183beda Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 16:25:27 +0100
Subject: Search completion; better SimpleCache.
SimpleCache should reset the timer very time it's rotated (including
when the allowed number of entries is exceeded.
---
lib/utils.coffee | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 07528714..1b2a7a3f 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -211,16 +211,18 @@ globalRoot.extend = (hash1, hash2) ->
# they are discarded.
class SimpleCache
# expiry: expiry time in milliseconds (default, one hour)
- # entries: maximum number of entries
+ # entries: maximum number of entries in @cache (there may be this many entries in @previous, too)
constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
@cache = {}
- @previous = {}
- rotate = => @rotate()
- setInterval rotate, @expiry
+ @rotate() # Force starts the rotation timer.
rotate: ->
@previous = @cache
@cache = {}
+ # We reset the timer every time the cache is rotated (which could be because a previous timer expired, or
+ # because the number of @entries was exceeded.
+ clearTimeout @timer if @timer?
+ @timer = Utils.setTimeout @expiry, => @rotate()
has: (key) ->
(key of @cache) or key of @previous
--
cgit v1.2.3
From 28807bd25b27e5404228a638f2ab5e6c00f606cc Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Tue, 5 May 2015 16:43:48 +0100
Subject: Search completion; misc.
---
background_scripts/completion.coffee | 2 +-
background_scripts/completion_engines.coffee | 219 +++++++++++++++++++++++++++
background_scripts/search_engines.coffee | 219 ---------------------------
lib/utils.coffee | 6 +-
manifest.json | 2 +-
5 files changed, 224 insertions(+), 224 deletions(-)
create mode 100644 background_scripts/completion_engines.coffee
delete mode 100644 background_scripts/search_engines.coffee
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 39f8a140..729e86ab 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -385,7 +385,7 @@ class SearchEngineCompleter
# immediately.
return onComplete []
- SearchEngines.complete searchUrl, queryTerms, (searchSuggestions = []) =>
+ 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
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
new file mode 100644
index 00000000..0177806a
--- /dev/null
+++ b/background_scripts/completion_engines.coffee
@@ -0,0 +1,219 @@
+
+# A completion engine provides search suggestions for a search engine. A search engine is identified by a
+# "searchUrl", e.g. Settings.get("searchUrl"), or a custom search engine.
+#
+# Each completion engine defines three functions:
+#
+# 1. "match" - This takes a searchUrl, and returns a boolean indicating whether this completion engine can
+# perform completion for the given search engine.
+#
+# 2. "getUrl" - This takes a list of query terms (queryTerms) and generates a completion URL, that is, a URL
+# which will provide completions for this completion engine.
+#
+# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and
+# returns a list of suggestions (a list of strings).
+#
+# The main completion entry point is CompletionEngines.complete(). This implements all lookup and caching
+# logic. It is possible to add new completion engines without changing the CompletionEngines infrastructure
+# itself.
+
+# A base class for common regexp-based matching engines.
+class RegexpEngine
+ constructor: (@regexps) ->
+ match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
+
+# Several Google completion engines package responses in this way.
+class GoogleXMLRegexpEngine extends RegexpEngine
+ parse: (xhr) ->
+ for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
+ continue unless suggestion = suggestion.getAttribute "data"
+ suggestion
+
+class Google extends GoogleXMLRegexpEngine
+ # Example search URL: http://www.google.com/search?q=%s
+ constructor: ->
+ super [
+ # We match the major English-speaking TLDs.
+ new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
+ new RegExp "localhost/cgi-bin/booky" # Only for smblott.
+ ]
+
+ getUrl: (queryTerms) ->
+ "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
+
+class Youtube extends GoogleXMLRegexpEngine
+ # Example search URL: http://www.youtube.com/results?search_query=%s
+ constructor: ->
+ super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
+
+ getUrl: (queryTerms) ->
+ "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=#{Utils.createSearchQuery queryTerms}"
+
+class Wikipedia extends RegexpEngine
+ # Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
+ constructor: ->
+ super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
+
+ getUrl: (queryTerms) ->
+ "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=#{Utils.createSearchQuery queryTerms}"
+
+ parse: (xhr) ->
+ JSON.parse(xhr.responseText)[1]
+
+## class GoogleMaps extends RegexpEngine
+## # Example search URL: https://www.google.com/maps/search/%s
+## constructor: ->
+## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
+##
+## getUrl: (queryTerms) ->
+## console.log "xxxxxxxxxxxxxxxxxxxxx"
+## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
+##
+## parse: (xhr) ->
+## console.log "yyy", xhr.responseText
+## data = JSON.parse xhr.responseText
+## console.log "zzz"
+## console.log data
+## []
+
+class Bing extends RegexpEngine
+ # Example search URL: https://www.bing.com/search?q=%s
+ constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ]
+ getUrl: (queryTerms) -> "http://api.bing.com/osjson.aspx?query=#{Utils.createSearchQuery queryTerms}"
+ parse: (xhr) -> JSON.parse(xhr.responseText)[1]
+
+class Amazon extends RegexpEngine
+ # Example search URL: http://www.amazon.com/s/?field-keywords=%s
+ constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ]
+ getUrl: (queryTerms) -> "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=#{Utils.createSearchQuery queryTerms}"
+ parse: (xhr) -> JSON.parse(xhr.responseText)[1]
+
+class DuckDuckGo extends RegexpEngine
+ # Example search URL: https://duckduckgo.com/?q=%s
+ constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ]
+ getUrl: (queryTerms) -> "https://duckduckgo.com/ac/?q=#{Utils.createSearchQuery queryTerms}"
+ parse: (xhr) ->
+ suggestion.phrase for suggestion in JSON.parse xhr.responseText
+
+# 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 be a search engine match.
+class DummySearchEngine
+ 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"
+ parse: -> []
+
+completionEngines = [
+ Youtube
+ Google
+ DuckDuckGo
+ Wikipedia
+ Bing
+ Amazon
+ DummySearchEngine
+]
+
+CompletionEngines =
+ debug: true
+
+ get: (searchUrl, url, callback) ->
+ xhr = new XMLHttpRequest()
+ xhr.open "GET", url, true
+ xhr.timeout = 1000
+ xhr.ontimeout = xhr.onerror = -> callback null
+ xhr.send()
+
+ xhr.onreadystatechange = ->
+ if xhr.readyState == 4
+ callback(if xhr.status == 200 then xhr else null)
+
+ # Look up the search-completion engine for this searchUrl. Because of DummySearchEngine, 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.
+ lookupEngine: (searchUrl) ->
+ @engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
+ if @engineCache.has searchUrl
+ @engineCache.get searchUrl
+ else
+ for engine in completionEngines
+ engine = new engine()
+ return @engineCache.set searchUrl, engine if engine.match searchUrl
+
+ # This is the main (actually, the only) 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.
+ # - queryTerms are the queryTerms.
+ # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
+ # wrong).
+ complete: (searchUrl, queryTerms, callback) ->
+ @mostRecentHandler = null
+
+ # We can't complete empty queries.
+ return callback [] unless 0 < queryTerms.length
+
+ if 1 == queryTerms.length
+ # We don't complete URLs.
+ return callback [] if Utils.isUrl queryTerms[0]
+ # We don't complete less then three characters: the results are usually useless. This also prevents
+ # one- and two-character custom search engine keywords from being sent to the default completer (e.g.
+ # the initial "w" before typing "w something").
+ return callback [] unless 2 < queryTerms[0].length
+
+ # We don't complete Javascript URLs.
+ return callback [] if Utils.hasJavascriptPrefix queryTerms[0]
+
+ # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
+ # to generate a key. We mix in some junk generated by pwgen. A key clash is possible, but vanishingly
+ # unlikely.
+ junk = "//Zi?ei5;o//"
+ completionCacheKey = searchUrl + junk + queryTerms.join junk
+ @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
+ if @completionCache.has completionCacheKey
+ console.log "hit", completionCacheKey if @debug
+ return callback @completionCache.get completionCacheKey
+
+ fetchSuggestions = (callback) =>
+ engine = @lookupEngine searchUrl
+ url = engine.getUrl queryTerms
+ console.log "get", url if @debug
+ query = queryTerms.join(" ").toLowerCase()
+ @get searchUrl, url, (xhr = null) =>
+ # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
+ # all cases, we fall back to the catch clause, below.
+ try
+ suggestions = engine.parse xhr
+ # Make sure we really do have an iterable of strings.
+ suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
+ # Filter out the query itself. It's not adding anything.
+ suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
+ catch
+ suggestions = []
+ # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
+ # XMLHttpRequest failures over a short period of time.
+ removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
+ setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
+
+ callback suggestions
+
+ # We pause in case the user is still typing.
+ Utils.setTimeout 200, handler = @mostRecentHandler = =>
+ if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing.
+ console.log "bail", completionCacheKey if @debug
+ return callback []
+ # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
+ # removes a space, or when they enter a character and immediately delete it.
+ @inTransit ?= {}
+ unless @inTransit[completionCacheKey]?.push callback
+ queue = @inTransit[completionCacheKey] = []
+ fetchSuggestions (suggestions) =>
+ callback @completionCache.set completionCacheKey, suggestions
+ delete @inTransit[completionCacheKey]
+ console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
+ callback suggestions for callback in queue
+
+ userIsTyping: ->
+ console.log "reset (typing)" if @debug and @mostRecentHandler?
+ @mostRecentHandler = null
+
+root = exports ? window
+root.CompletionEngines = CompletionEngines
diff --git a/background_scripts/search_engines.coffee b/background_scripts/search_engines.coffee
deleted file mode 100644
index 63c61a47..00000000
--- a/background_scripts/search_engines.coffee
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# A completion engine provides search suggestions for a search engine. A search engine is identified by a
-# "searchUrl", e.g. Settings.get("searchUrl"), or a custom search engine.
-#
-# Each completion engine defines three functions:
-#
-# 1. "match" - This takes a searchUrl, and returns a boolean indicating whether this completion engine can
-# perform completion for the given search engine.
-#
-# 2. "getUrl" - This takes a list of query terms (queryTerms) and generates a completion URL, that is, a URL
-# which will provide completions for this completion engine.
-#
-# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and
-# returns a list of suggestions (a list of strings).
-#
-# The main completion entry point is SearchEngines.complete(). This implements all lookup and caching
-# logic. It is possible to add new completion engines without changing the SearchEngines infrastructure
-# itself.
-
-# A base class for common regexp-based matching engines.
-class RegexpEngine
- constructor: (@regexps) ->
- match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
-
-# Several Google completion engines package responses in this way.
-class GoogleXMLRegexpEngine extends RegexpEngine
- parse: (xhr) ->
- for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
- continue unless suggestion = suggestion.getAttribute "data"
- suggestion
-
-class Google extends GoogleXMLRegexpEngine
- # Example search URL: http://www.google.com/search?q=%s
- constructor: ->
- super [
- # We match the major English-speaking TLDs.
- new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
- new RegExp "localhost/cgi-bin/booky" # Only for smblott.
- ]
-
- getUrl: (queryTerms) ->
- "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
-
-class Youtube extends GoogleXMLRegexpEngine
- # Example search URL: http://www.youtube.com/results?search_query=%s
- constructor: ->
- super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
-
- getUrl: (queryTerms) ->
- "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=#{Utils.createSearchQuery queryTerms}"
-
-class Wikipedia extends RegexpEngine
- # Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
- constructor: ->
- super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
-
- getUrl: (queryTerms) ->
- "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=#{Utils.createSearchQuery queryTerms}"
-
- parse: (xhr) ->
- JSON.parse(xhr.responseText)[1]
-
-## class GoogleMaps extends RegexpEngine
-## # Example search URL: https://www.google.com/maps/search/%s
-## constructor: ->
-## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
-##
-## getUrl: (queryTerms) ->
-## console.log "xxxxxxxxxxxxxxxxxxxxx"
-## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
-##
-## parse: (xhr) ->
-## console.log "yyy", xhr.responseText
-## data = JSON.parse xhr.responseText
-## console.log "zzz"
-## console.log data
-## []
-
-class Bing extends RegexpEngine
- # Example search URL: https://www.bing.com/search?q=%s
- constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ]
- getUrl: (queryTerms) -> "http://api.bing.com/osjson.aspx?query=#{Utils.createSearchQuery queryTerms}"
- parse: (xhr) -> JSON.parse(xhr.responseText)[1]
-
-class Amazon extends RegexpEngine
- # Example search URL: http://www.amazon.com/s/?field-keywords=%s
- constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ]
- getUrl: (queryTerms) -> "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=#{Utils.createSearchQuery queryTerms}"
- parse: (xhr) -> JSON.parse(xhr.responseText)[1]
-
-class DuckDuckGo extends RegexpEngine
- # Example search URL: https://duckduckgo.com/?q=%s
- constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ]
- getUrl: (queryTerms) -> "https://duckduckgo.com/ac/?q=#{Utils.createSearchQuery queryTerms}"
- parse: (xhr) ->
- suggestion.phrase for suggestion in JSON.parse xhr.responseText
-
-# 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 be a search engine match.
-class DummySearchEngine
- 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"
- parse: -> []
-
-completionEngines = [
- Youtube
- Google
- DuckDuckGo
- Wikipedia
- Bing
- Amazon
- DummySearchEngine
-]
-
-SearchEngines =
- debug: true
-
- get: (searchUrl, url, callback) ->
- xhr = new XMLHttpRequest()
- xhr.open "GET", url, true
- xhr.timeout = 1000
- xhr.ontimeout = xhr.onerror = -> callback null
- xhr.send()
-
- xhr.onreadystatechange = ->
- if xhr.readyState == 4
- callback(if xhr.status == 200 then xhr else null)
-
- # Look up the search-completion engine for this searchUrl. Because of DummySearchEngine, 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.
- lookupEngine: (searchUrl) ->
- @engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
- if @engineCache.has searchUrl
- @engineCache.get searchUrl
- else
- for engine in completionEngines
- engine = new engine()
- return @engineCache.set searchUrl, engine if engine.match searchUrl
-
- # This is the main (actually, the only) 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.
- # - queryTerms are the queryTerms.
- # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
- # wrong).
- complete: (searchUrl, queryTerms, callback) ->
- @mostRecentHandler = null
-
- # We can't complete empty queries.
- return callback [] unless 0 < queryTerms.length
-
- if 1 == queryTerms.length
- # We don't complete URLs.
- return callback [] if Utils.isUrl queryTerms[0]
- # We don't complete less then three characters: the results are usually useless. This also prevents
- # one- and two-character custom search engine keywords from being sent to the default completer (e.g.
- # the initial "w" before typing "w something").
- return callback [] unless 2 < queryTerms[0].length
-
- # We don't complete Javascript URLs.
- return callback [] if Utils.hasJavascriptPrefix queryTerms[0]
-
- # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
- # to generate a key. We mix in some junk generated by pwgen. A key clash is possible, but vanishingly
- # unlikely.
- junk = "//Zi?ei5;o//"
- completionCacheKey = searchUrl + junk + queryTerms.join junk
- @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
- if @completionCache.has completionCacheKey
- console.log "hit", completionCacheKey if @debug
- return callback @completionCache.get completionCacheKey
-
- fetchSuggestions = (callback) =>
- engine = @lookupEngine searchUrl
- url = engine.getUrl queryTerms
- console.log "get", url if @debug
- query = queryTerms.join(" ").toLowerCase()
- @get searchUrl, url, (xhr = null) =>
- # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
- # all cases, we fall back to the catch clause, below.
- try
- suggestions = engine.parse xhr
- # Make sure we really do have an iterable of strings.
- suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
- # Filter out the query itself. It's not adding anything.
- suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
- catch
- suggestions = []
- # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
- # XMLHttpRequest failures over a short period of time.
- removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
- setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
-
- callback suggestions
-
- # We pause in case the user is still typing.
- Utils.setTimeout 200, handler = @mostRecentHandler = =>
- if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing.
- console.log "bail", completionCacheKey if @debug
- return callback []
- # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
- # removes a space, or when they enter a character and immediately delete it.
- @inTransit ?= {}
- unless @inTransit[completionCacheKey]?.push callback
- queue = @inTransit[completionCacheKey] = []
- fetchSuggestions (suggestions) =>
- callback @completionCache.set completionCacheKey, suggestions
- delete @inTransit[completionCacheKey]
- console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
- callback suggestions for callback in queue
-
- userIsTyping: ->
- console.log "reset (typing)" if @debug and @mostRecentHandler?
- @mostRecentHandler = null
-
-root = exports ? window
-root.SearchEngines = SearchEngines
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 1b2a7a3f..e97872f0 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -207,14 +207,14 @@ globalRoot.extend = (hash1, hash2) ->
hash1[key] = hash2[key]
hash1
-# A simple cache. Entries used within an expiry period are retained (for one more expiry period), otherwise
-# they are discarded.
+# A simple cache. Entries used within two expiry periods are retained, otherwise they are discarded.
+# At most 2 * @entries entries are retained.
class SimpleCache
# expiry: expiry time in milliseconds (default, one hour)
# entries: maximum number of entries in @cache (there may be this many entries in @previous, too)
constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
@cache = {}
- @rotate() # Force starts the rotation timer.
+ @rotate() # Force start the rotation timer.
rotate: ->
@previous = @cache
diff --git a/manifest.json b/manifest.json
index d3f6249f..195d16c8 100644
--- a/manifest.json
+++ b/manifest.json
@@ -14,7 +14,7 @@
"background_scripts/sync.js",
"background_scripts/settings.js",
"background_scripts/exclusions.js",
- "background_scripts/search_engines.js",
+ "background_scripts/completion_engines.js",
"background_scripts/completion.js",
"background_scripts/marks.js",
"background_scripts/main.js"
--
cgit v1.2.3
From 7d11b1699454366bf99e8e5033ba39b127687fcb Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Wed, 6 May 2015 04:55:22 +0100
Subject: Search completion; simplify messaging code.
This eliminates the need to repeatedly install and remove listeners for
@filterPort in the vomnibar. It also eleiminates the need for
"keepAlive" in reponses. All as suggested by @mrmr1993 in #1635.
---
background_scripts/completion.coffee | 4 ++--
background_scripts/main.coffee | 4 ++--
pages/vomnibar.coffee | 44 +++++++++++++++++-------------------
3 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 729e86ab..c91825b5 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -478,12 +478,12 @@ class MultiCompleter
# (ie. a SearchEngineCompleter). This prevents hiding the vomnibar briefly before showing it
# again, which looks ugly.
unless shouldRunContinuation and suggestions.length == 0
- onComplete @prepareSuggestions(queryTerms, suggestions), keepAlive: shouldRunContinuation
+ onComplete @prepareSuggestions queryTerms, suggestions
# Allow subsequent queries to begin.
@filterInProgress = false
if shouldRunContinuation
continuation suggestions, (newSuggestions) =>
- onComplete @prepareSuggestions queryTerms, suggestions.concat(newSuggestions)
+ onComplete @prepareSuggestions queryTerms, suggestions.concat newSuggestions
else
@filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 44644769..09a6b89f 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -226,8 +226,8 @@ filterCompleter = (args, port) ->
if args.id? and args.name? and args.query?
queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp)
- completers[args.name].filter queryTerms, (results, extra = {}) ->
- port.postMessage extend extra, id: args.id, results: results
+ completers[args.name].filter queryTerms, (results) ->
+ port.postMessage id: args.id, results: results
chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->
if (selectionChangedHandlers.length > 0)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 8a070df6..d9a86a3f 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -223,37 +223,35 @@ class VomnibarUI
# Sends filter and refresh requests to a Vomnibox completer on the background page.
#
class BackgroundCompleter
- # We increment this counter on each message sent, and ignore responses which arrive too late.
- @messageId: 0
-
# - name: The background page completer that you want to interface with. Either "omni", "tabs", or
# "bookmarks". */
constructor: (@name) ->
+ @messageId = null
@filterPort = chrome.runtime.connect name: "filterCompleter"
+ @filterPort.onMessage.addListener handler = @messageHandler
+
+ messageHandler: (msg) =>
+ # We ignore messages which arrive too late.
+ if msg.id == @messageId
+ # The result objects coming from the background page will be of the form:
+ # { html: "", type: "", url: "" }
+ # type will be one of [tab, bookmark, history, domain].
+ results = msg.results.map (result) ->
+ functionToCall = if result.type == "tab"
+ BackgroundCompleter.completionActions.switchToTab.curry result.tabId
+ else
+ BackgroundCompleter.completionActions.navigateToUrl.curry result.url
+ result.performAction = functionToCall
+ result
+ @mostRecentCallback results
+
+ filter: (query, @mostRecentCallback) ->
+ @messageId = Utils.createUniqueId()
+ @filterPort.postMessage id: @messageId, name: @name, query: query
refresh: ->
chrome.runtime.sendMessage handler: "refreshCompleter", name: @name
- filter: (query, callback) ->
- id = BackgroundCompleter.messageId += 1
- @filterPort.onMessage.addListener handler = (msg) =>
- if msg.id == id
- @filterPort.onMessage.removeListener handler unless msg.keepAlive and id == BackgroundCompleter.messageId
- if id == BackgroundCompleter.messageId
- # The result objects coming from the background page will be of the form:
- # { html: "", type: "", url: "" }
- # type will be one of [tab, bookmark, history, domain].
- results = msg.results.map (result) ->
- functionToCall = if (result.type == "tab")
- BackgroundCompleter.completionActions.switchToTab.curry(result.tabId)
- else
- BackgroundCompleter.completionActions.navigateToUrl.curry(result.url)
- result.performAction = functionToCall
- result
- callback(results)
-
- @filterPort.postMessage id: id, name: @name, query: query
-
userIsTyping: ->
@filterPort.postMessage name: @name, userIsTyping: true
--
cgit v1.2.3
From 1f97221aef5cfe28200df81a68a139a3f2b07784 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Wed, 6 May 2015 05:18:59 +0100
Subject: Search completion; move all filter messages to a single port.
---
background_scripts/completion.coffee | 2 +-
background_scripts/main.coffee | 26 ++++++++++++--------------
pages/vomnibar.coffee | 24 ++++++++++--------------
3 files changed, 23 insertions(+), 29 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index c91825b5..8a69b645 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -331,7 +331,7 @@ class SearchEngineCompleter
searchEngines: {}
userIsTyping: ->
- SearchEngines.userIsTyping()
+ CompletionEngines.userIsTyping()
filter: (queryTerms, onComplete) ->
{ keyword: keyword, url: url, description: description } = @getSearchEngineMatches queryTerms
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 09a6b89f..066e4cb6 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -60,6 +60,17 @@ completers =
bookmarks: new MultiCompleter([completionSources.bookmarks])
tabs: new MultiCompleter([completionSources.tabs])
+completionHandlers =
+ filter: (completer, args, port) ->
+ queryTerms = args.query.split(/\s+/).filter (s) -> 0 < s.length
+ completer.filter queryTerms, (results) -> port.postMessage id: args.id, results: results
+
+ refreshCompleter: (completer) -> completer.refresh()
+ userIsTyping: (completer) -> completer.userIsTyping()
+
+handleCompletions = (args, port) ->
+ completionHandlers[args.handler] completers[args.name], args, port
+
chrome.runtime.onConnect.addListener (port, name) ->
senderTabId = if port.sender.tab then port.sender.tab.id else null
# If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore
@@ -217,18 +228,6 @@ handleSettings = (request, port) ->
values[key] = Settings.get key for own key of values
port.postMessage { values }
-refreshCompleter = (request) -> completers[request.name].refresh()
-
-whitespaceRegexp = /\s+/
-filterCompleter = (args, port) ->
- if args.name? and args.userIsTyping
- completers[args.name].userIsTyping?()
-
- if args.id? and args.name? and args.query?
- queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp)
- completers[args.name].filter queryTerms, (results) ->
- port.postMessage id: args.id, results: results
-
chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->
if (selectionChangedHandlers.length > 0)
selectionChangedHandlers.pop().call()
@@ -643,7 +642,7 @@ bgLog = (request, sender) ->
portHandlers =
keyDown: handleKeyDown,
settings: handleSettings,
- filterCompleter: filterCompleter
+ completions: handleCompletions
sendRequestHandlers =
getCompletionKeys: getCompletionKeysRequest
@@ -661,7 +660,6 @@ sendRequestHandlers =
pasteFromClipboard: pasteFromClipboard
isEnabledForUrl: isEnabledForUrl
selectSpecificTab: selectSpecificTab
- refreshCompleter: refreshCompleter
createMark: Marks.create.bind(Marks)
gotoMark: Marks.goto.bind(Marks)
setIcon: setIcon
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index d9a86a3f..a2d4df85 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -220,15 +220,14 @@ class VomnibarUI
document.body.addEventListener "click", => @hide()
#
-# Sends filter and refresh requests to a Vomnibox completer on the background page.
+# Sends requests to a Vomnibox completer on the background page.
#
class BackgroundCompleter
- # - name: The background page completer that you want to interface with. Either "omni", "tabs", or
- # "bookmarks". */
+ # name is background-page completer to connect to: "omni", "tabs", or "bookmarks".
constructor: (@name) ->
@messageId = null
- @filterPort = chrome.runtime.connect name: "filterCompleter"
- @filterPort.onMessage.addListener handler = @messageHandler
+ @port = chrome.runtime.connect name: "completions"
+ @port.onMessage.addListener handler = @messageHandler
messageHandler: (msg) =>
# We ignore messages which arrive too late.
@@ -236,29 +235,26 @@ class BackgroundCompleter
# The result objects coming from the background page will be of the form:
# { html: "", type: "", url: "" }
# type will be one of [tab, bookmark, history, domain].
- results = msg.results.map (result) ->
+ results = msg.results.map (result) =>
functionToCall = if result.type == "tab"
- BackgroundCompleter.completionActions.switchToTab.curry result.tabId
+ @completionActions.switchToTab.curry result.tabId
else
- BackgroundCompleter.completionActions.navigateToUrl.curry result.url
+ @completionActions.navigateToUrl.curry result.url
result.performAction = functionToCall
result
@mostRecentCallback results
filter: (query, @mostRecentCallback) ->
@messageId = Utils.createUniqueId()
- @filterPort.postMessage id: @messageId, name: @name, query: query
+ @port.postMessage name: @name, handler: "filter", id: @messageId, query: query
refresh: ->
- chrome.runtime.sendMessage handler: "refreshCompleter", name: @name
+ @port.postMessage name: @name, handler: "refreshCompleter"
userIsTyping: ->
- @filterPort.postMessage name: @name, userIsTyping: true
+ @port.postMessage name: @name, handler: "userIsTyping"
-extend BackgroundCompleter,
- #
# These are the actions we can perform when the user selects a result in the Vomnibox.
- #
completionActions:
navigateToUrl: (url, openInNewTab) ->
# If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
--
cgit v1.2.3
From 372e65d7b44c3f0ad3f522bd6b8c9cfacd693186 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Wed, 6 May 2015 07:32:19 +0100
Subject: Search completion; many tweaks and refactor Suggestion constructor.
---
background_scripts/completion.coffee | 166 +++++++++++++++++++----------------
background_scripts/settings.coffee | 4 +-
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
"""
"""
@@ -82,8 +89,8 @@ class Suggestion
textPosition += matchedText.length
# Wraps each occurence of the query terms in the given string in a .
- 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"
--
cgit v1.2.3
From 5752c0ead0a65fc2329515509f66e00bd6ee2f60 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Wed, 6 May 2015 07:40:46 +0100
Subject: Search completion; more tweaks.
---
background_scripts/completion.coffee | 37 ++++++++++++++--------------
background_scripts/completion_engines.coffee | 26 +++++++++++++------
lib/utils.coffee | 11 +++++----
3 files changed, 42 insertions(+), 32 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 4ae0c44b..bffb9700 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -363,23 +363,22 @@ class SearchEngineCompleter
haveDescription = description? and 0 < description.length
description ||= "#{if custom then "custom " else ""}search"
+ queryTerms = queryTerms[1..] if custom
+ query = queryTerms.join " "
+
+ if queryTerms.length == 0
+ return onComplete suggestions
+
# For custom search engines, we add an auto-selected suggestion.
if custom
- 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}"
+ url: Utils.createSearchUrl queryTerms, searchUrl
+ title: if haveDescription then query else "#{keyword}: #{query}"
relevancy: 1
- autoSelect: true
highlightTerms: false
- else
- query = queryTerms.join " "
-
- if queryTerms.length == 0
- return onComplete suggestions
+ autoSelect: true
onComplete suggestions, (existingSuggestions, onComplete) =>
suggestions = []
@@ -399,23 +398,22 @@ class SearchEngineCompleter
relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
if 0 < existingSuggestions.length
- existingSuggestionMinScore = existingSuggestions[existingSuggestions.length-1].relevancy
- if relavancy < existingSuggestionMinScore and MultiCompleter.maxResults <= 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, (searchSuggestions = []) =>
- for suggestion in searchSuggestions
+ CompletionEngines.complete searchUrl, queryTerms, (completionSuggestions = []) =>
+ for suggestion in completionSuggestions
suggestions.push new Suggestion
queryTerms: queryTerms
type: description
- url: searchUrl.replace /%s/g, Utils.createSearchQuery suggestion.split /\s+/
+ url: Utils.createSearchUrl suggestion, searchUrl
title: suggestion
- relevancy: relavancy
- insertText: if custom then "#{keyword} #{suggestion}" else suggestion
+ relevancy: relavancy *= 0.9
highlightTerms: false
- relavancy *= 0.9
+ 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
@@ -501,7 +499,8 @@ class MultiCompleter
@filterInProgress = false
if shouldRunContinuation
continuation suggestions, (newSuggestions) =>
- onComplete @prepareSuggestions queryTerms, suggestions.concat newSuggestions
+ if 0 < newSuggestions.length
+ onComplete @prepareSuggestions queryTerms, suggestions.concat newSuggestions
else
@filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 0177806a..1386256f 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -116,6 +116,9 @@ completionEngines = [
CompletionEngines =
debug: true
+ # The amount of time to wait for new completions before launching the HTTP request.
+ delay: 250
+
get: (searchUrl, url, callback) ->
xhr = new XMLHttpRequest()
xhr.open "GET", url, true
@@ -139,7 +142,7 @@ CompletionEngines =
engine = new engine()
return @engineCache.set searchUrl, engine if engine.match searchUrl
- # This is the main (actually, the only) entry point.
+ # 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.
# - queryTerms are the queryTerms.
@@ -169,34 +172,41 @@ CompletionEngines =
completionCacheKey = searchUrl + junk + queryTerms.join junk
@completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
if @completionCache.has completionCacheKey
- console.log "hit", completionCacheKey if @debug
- return callback @completionCache.get completionCacheKey
+ # We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
+ # suggestions are posted. It also makes the vomnibar behave similarly regardless of whether there's a
+ # cache hit.
+ Utils.setTimeout @delay, =>
+ console.log "hit", completionCacheKey if @debug
+ callback @completionCache.get completionCacheKey
+ return
fetchSuggestions = (callback) =>
engine = @lookupEngine searchUrl
url = engine.getUrl queryTerms
- console.log "get", url if @debug
query = queryTerms.join(" ").toLowerCase()
@get searchUrl, url, (xhr = null) =>
# Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
- # all cases, we fall back to the catch clause, below.
+ # all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of
+ # incorrect or out-of-date completion engines.
try
suggestions = engine.parse xhr
# Make sure we really do have an iterable of strings.
suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
# Filter out the query itself. It's not adding anything.
suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
+ console.log "GET", url if @debug
catch
suggestions = []
- # We cache failures, but remove them after just ten minutes. This (it is hoped) avoids repeated
- # XMLHttpRequest failures over a short period of time.
+ # We allow failures to be cached, but remove them after just ten minutes. This (it is hoped) avoids
+ # repeated unnecessary XMLHttpRequest failures over a short period of time.
removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
+ console.log "fail", url if @debug
callback suggestions
# We pause in case the user is still typing.
- Utils.setTimeout 200, handler = @mostRecentHandler = =>
+ Utils.setTimeout @delay, handler = @mostRecentHandler = =>
if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing.
console.log "bail", completionCacheKey if @debug
return callback []
diff --git a/lib/utils.coffee b/lib/utils.coffee
index e97872f0..354d82f6 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -96,11 +96,12 @@ Utils =
query = query.split(/\s+/) if typeof(query) == "string"
query.map(encodeURIComponent).join "+"
- # Creates a search URL from the given :query.
- createSearchUrl: (query) ->
- # It would be better to pull the default search engine from chrome itself. However, unfortunately chrome
- # does not provide an API for doing so.
- Settings.get("searchUrl") + @createSearchQuery query
+ # Create a search URL from the given :query (using either the provided search URL, or the default one).
+ # It would be better to pull the default search engine from chrome itself. However, chrome does not provide
+ # an API for doing so.
+ createSearchUrl: (query, searchUrl = Settings.get("searchUrl")) ->
+ searchUrl += "%s" unless 0 <= searchUrl.indexOf "%s"
+ searchUrl.replace /%s/g, @createSearchQuery query
# 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.
--
cgit v1.2.3
From f330e3243bc27f1a19040fb386fc877fe82fbefe Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Wed, 6 May 2015 11:50:28 +0100
Subject: Search completion; yet more tweaks.
---
background_scripts/completion.coffee | 11 +++--
background_scripts/completion_engines.coffee | 63 +++++++++++++++-------------
background_scripts/main.coffee | 4 +-
pages/vomnibar.coffee | 19 ++++-----
4 files changed, 48 insertions(+), 49 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index bffb9700..18bfafd1 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -8,8 +8,7 @@
# 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).
-# - userIsTyping(): (optional) informs the completer that the user is typing (and pending completions may no
-# longer be needed).
+# - cancel(): (optional) cancels any pending, cancelable action.
class Suggestion
showRelevancy: true # Set this to true to render relevancy when debugging the ranking scores.
@@ -351,8 +350,8 @@ class TabCompleter
class SearchEngineCompleter
searchEngines: {}
- userIsTyping: ->
- CompletionEngines.userIsTyping()
+ cancel: ->
+ CompletionEngines.cancel()
filter: (queryTerms, onComplete) ->
suggestions = []
@@ -462,8 +461,8 @@ class MultiCompleter
refresh: ->
completer.refresh?() for completer in @completers
- userIsTyping: ->
- completer.userIsTyping?() for completer in @completers
+ cancel: ->
+ completer.cancel?() for completer in @completers
filter: (queryTerms, onComplete) ->
# Allow only one query to run at a time.
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 1386256f..b5caadd7 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -22,8 +22,9 @@ class RegexpEngine
constructor: (@regexps) ->
match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
-# Several Google completion engines package responses in this way.
+# Several Google completion engines package XML responses in this way.
class GoogleXMLRegexpEngine extends RegexpEngine
+ doNotCache: true
parse: (xhr) ->
for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
continue unless suggestion = suggestion.getAttribute "data"
@@ -50,6 +51,7 @@ class Youtube extends GoogleXMLRegexpEngine
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=#{Utils.createSearchQuery queryTerms}"
class Wikipedia extends RegexpEngine
+ doNotCache: true
# Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
constructor: ->
super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
@@ -96,8 +98,8 @@ class DuckDuckGo extends RegexpEngine
suggestion.phrase for suggestion in JSON.parse xhr.responseText
# 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 be a search engine match.
-class DummySearchEngine
+# allows the rest of the logic to be written knowing that there will always be a completion engine match.
+class DummyCompletionEngine
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"
@@ -110,13 +112,19 @@ completionEngines = [
Wikipedia
Bing
Amazon
- DummySearchEngine
+ DummyCompletionEngine
]
+# A note on caching.
+# Some completion engines allow caching, and Chrome serves up cached responses to requests (e.g. Google,
+# Wikipedia, YouTube). Others do not (e.g. Bing, DuckDuckGo, Amazon). A completion engine can set
+# @doNotCache to a truthy value to disable caching in cases where it is unnecessary.
+
CompletionEngines =
debug: true
- # The amount of time to wait for new completions before launching the HTTP request.
+ # 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: 250
get: (searchUrl, url, callback) ->
@@ -128,11 +136,12 @@ CompletionEngines =
xhr.onreadystatechange = ->
if xhr.readyState == 4
+ console.log xhr.getAllResponseHeaders()
callback(if xhr.status == 200 then xhr else null)
- # Look up the search-completion engine for this searchUrl. Because of DummySearchEngine, 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.
+ # 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.
lookupEngine: (searchUrl) ->
@engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
if @engineCache.has searchUrl
@@ -145,29 +154,25 @@ CompletionEngines =
# 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.
- # - queryTerms are the queryTerms.
+ # - queryTerms are the query terms.
# - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
# wrong).
complete: (searchUrl, queryTerms, callback) ->
@mostRecentHandler = null
+ query = queryTerms.join ""
- # We can't complete empty queries.
- return callback [] unless 0 < queryTerms.length
-
- if 1 == queryTerms.length
- # We don't complete URLs.
- return callback [] if Utils.isUrl queryTerms[0]
- # We don't complete less then three characters: the results are usually useless. This also prevents
- # one- and two-character custom search engine keywords from being sent to the default completer (e.g.
- # the initial "w" before typing "w something").
- return callback [] unless 2 < queryTerms[0].length
+ # We don't complete less then three characters: the results are usually useless. This also prevents
+ # one- and two-character custom search engine keywords from being sent to the default completer (e.g.
+ # the initial "w" before typing "w something" for Wikipedia).
+ return callback [] unless 3 <= query.length
- # We don't complete Javascript URLs.
- return callback [] if Utils.hasJavascriptPrefix queryTerms[0]
+ # We don't complete regular URLs or Javascript URLs.
+ return callback [] if 1 == queryTerms.length and Utils.isUrl query
+ return callback [] if Utils.hasJavascriptPrefix query
# Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
- # to generate a key. We mix in some junk generated by pwgen. A key clash is possible, but vanishingly
- # unlikely.
+ # to generate a key. We mix in some junk generated by pwgen. A key clash might be possible, but
+ # vanishingly unlikely.
junk = "//Zi?ei5;o//"
completionCacheKey = searchUrl + junk + queryTerms.join junk
@completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
@@ -180,8 +185,7 @@ CompletionEngines =
callback @completionCache.get completionCacheKey
return
- fetchSuggestions = (callback) =>
- engine = @lookupEngine searchUrl
+ fetchSuggestions = (engine, callback) =>
url = engine.getUrl queryTerms
query = queryTerms.join(" ").toLowerCase()
@get searchUrl, url, (xhr = null) =>
@@ -215,14 +219,15 @@ CompletionEngines =
@inTransit ?= {}
unless @inTransit[completionCacheKey]?.push callback
queue = @inTransit[completionCacheKey] = []
- fetchSuggestions (suggestions) =>
- callback @completionCache.set completionCacheKey, suggestions
+ engine = @lookupEngine searchUrl
+ fetchSuggestions engine, (suggestions) =>
+ callback @completionCache.set completionCacheKey, suggestions unless engine.doNotCache
delete @inTransit[completionCacheKey]
console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
callback suggestions for callback in queue
- userIsTyping: ->
- console.log "reset (typing)" if @debug and @mostRecentHandler?
+ cancel: ->
+ console.log "cancel (user is typing)" if @debug and @mostRecentHandler?
@mostRecentHandler = null
root = exports ? window
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 066e4cb6..c1be9f8f 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -65,8 +65,8 @@ completionHandlers =
queryTerms = args.query.split(/\s+/).filter (s) -> 0 < s.length
completer.filter queryTerms, (results) -> port.postMessage id: args.id, results: results
- refreshCompleter: (completer) -> completer.refresh()
- userIsTyping: (completer) -> completer.userIsTyping()
+ refresh: (completer) -> completer.refresh()
+ cancel: (completer) -> completer.cancel()
handleCompletions = (args, port) ->
completionHandlers[args.handler] completers[args.name], args, port
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index a2d4df85..ff585a5e 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -8,11 +8,6 @@ Vomnibar =
getUI: -> @vomnibarUI
completers: {}
- getCompleter: (name) ->
- if (!(name of @completers))
- @completers[name] = new BackgroundCompleter(name)
- @completers[name]
-
#
# Activate the Vomnibox.
#
@@ -27,7 +22,8 @@ Vomnibar =
options.refreshInterval =
if options.completer == "omni" then 125 else 0
- completer = @getCompleter(options.completer)
+ name = options.completer
+ completer = @completers[name] ?= new BackgroundCompleter name
@vomnibarUI ?= new VomnibarUI()
completer.refresh()
@vomnibarUI.setInitialSelectionValue(if options.selectFirst then 0 else -1)
@@ -35,7 +31,7 @@ Vomnibar =
@vomnibarUI.setRefreshInterval(options.refreshInterval)
@vomnibarUI.setForceNewTab(options.newTab)
@vomnibarUI.setQuery(options.query)
- @vomnibarUI.update()
+ @vomnibarUI.update true
hide: -> @vomnibarUI?.hide()
onHidden: -> @vomnibarUI?.onHidden()
@@ -54,7 +50,6 @@ class VomnibarUI
setCompleter: (completer) ->
@completer = completer
@reset()
- @update(true)
setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval
@@ -178,7 +173,7 @@ class VomnibarUI
@updateSelection()
updateOnInput: =>
- @completer.userIsTyping()
+ @completer.cancel()
# If the user types, then don't reset any previous text, and re-enable auto-select.
if @previousInputValue?
@previousInputValue = null
@@ -249,10 +244,10 @@ class BackgroundCompleter
@port.postMessage name: @name, handler: "filter", id: @messageId, query: query
refresh: ->
- @port.postMessage name: @name, handler: "refreshCompleter"
+ @port.postMessage name: @name, handler: "refresh"
- userIsTyping: ->
- @port.postMessage name: @name, handler: "userIsTyping"
+ cancel: ->
+ @port.postMessage name: @name, handler: "cancel"
# These are the actions we can perform when the user selects a result in the Vomnibox.
completionActions:
--
cgit v1.2.3
From c7fb211f0d1a6504bd0cbb94395f9437f858f2c0 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Wed, 6 May 2015 11:56:16 +0100
Subject: Search completion; fix yet more tweaks.
---
background_scripts/completion_engines.coffee | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index b5caadd7..8a880930 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -221,7 +221,8 @@ CompletionEngines =
queue = @inTransit[completionCacheKey] = []
engine = @lookupEngine searchUrl
fetchSuggestions engine, (suggestions) =>
- callback @completionCache.set completionCacheKey, suggestions unless engine.doNotCache
+ @completionCache.set completionCacheKey, suggestions unless engine.doNotCache
+ callback suggestions
delete @inTransit[completionCacheKey]
console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
callback suggestions for callback in queue
--
cgit v1.2.3
From 430bdc8bdf7c109a3007104fc06abeeed1891529 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Thu, 7 May 2015 09:25:15 +0100
Subject: Search completion; tweak domain completer.
---
background_scripts/completion.coffee | 25 +++++++++++--------------
background_scripts/completion_engines.coffee | 7 ++++---
2 files changed, 15 insertions(+), 17 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 18bfafd1..746e662d 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -197,7 +197,7 @@ class HistoryCompleter
history.filter (entry) -> RankingUtils.matches(queryTerms, entry.url, entry.title)
else
[]
- suggestions = results.map (entry) =>
+ onComplete results.map (entry) =>
new Suggestion
queryTerms: queryTerms
type: "history"
@@ -205,7 +205,6 @@ class HistoryCompleter
title: entry.title
relevancyFunction: @computeRelevancy
relevancyData: entry
- onComplete suggestions
computeRelevancy: (suggestion) ->
historyEntry = suggestion.relevancyData
@@ -232,16 +231,15 @@ class DomainCompleter
performSearch: (queryTerms, onComplete) ->
query = queryTerms[0]
- domainCandidates = (domain for domain of @domains when domain.indexOf(query) >= 0)
- domains = @sortDomainsByRelevancy(queryTerms, domainCandidates)
- return onComplete([]) if domains.length == 0
- topDomain = domains[0][0]
- suggestion = new Suggestion
- queryTerms: queryTerms
- type: "domain"
- url: topDomain
- relevancy: 1
- onComplete [ suggestion ]
+ domains = (domain for domain of @domains when 0 <= domain.indexOf query)
+ domains = @sortDomainsByRelevancy queryTerms, domains
+ onComplete [
+ new Suggestion
+ queryTerms: queryTerms
+ type: "domain"
+ url: domains[0]?[0] ? "" # This is the URL or an empty string, but not null.
+ relevancy: 1
+ ].filter (s) -> 0 < s.url.length
# Returns a list of domains of the form: [ [domain, relevancy], ... ]
sortDomainsByRelevancy: (queryTerms, domainCandidates) ->
@@ -656,8 +654,7 @@ HistoryCache =
@callbacks = null
use: (callback) ->
- return @fetchHistory(callback) unless @history?
- callback(@history)
+ if @history? then callback @history else @fetchHistory callback
fetchHistory: (callback) ->
return @callbacks.push(callback) if @callbacks
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 8a880930..3e762e32 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -136,7 +136,6 @@ CompletionEngines =
xhr.onreadystatechange = ->
if xhr.readyState == 4
- console.log xhr.getAllResponseHeaders()
callback(if xhr.status == 200 then xhr else null)
# Look up the completion engine for this searchUrl. Because of DummyCompletionEngine, above, we know there
@@ -214,6 +213,7 @@ CompletionEngines =
if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing.
console.log "bail", completionCacheKey if @debug
return callback []
+ @mostRecentHandler = null
# Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
# removes a space, or when they enter a character and immediately delete it.
@inTransit ?= {}
@@ -228,8 +228,9 @@ CompletionEngines =
callback suggestions for callback in queue
cancel: ->
- console.log "cancel (user is typing)" if @debug and @mostRecentHandler?
- @mostRecentHandler = null
+ if @mostRecentHandler?
+ @mostRecentHandler = null
+ console.log "cancel (user is typing)" if @debug
root = exports ? window
root.CompletionEngines = CompletionEngines
--
cgit v1.2.3
From 4bb739c30f22be39ece9813312ca7219619a9347 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 05:56:53 +0100
Subject: Search completion; do not refetch on duplicate queries.
---
pages/vomnibar.coffee | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index ff585a5e..ae61ca9d 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -240,8 +240,12 @@ class BackgroundCompleter
@mostRecentCallback results
filter: (query, @mostRecentCallback) ->
- @messageId = Utils.createUniqueId()
- @port.postMessage name: @name, handler: "filter", id: @messageId, query: query
+ # Ignore identical consecutive queries. This can happen, for example, if the user adds a to the
+ # query.
+ unless @mostRecentQuery? and query == @mostRecentQuery
+ @mostRecentQuery = query
+ @messageId = Utils.createUniqueId()
+ @port.postMessage name: @name, handler: "filter", id: @messageId, query: query
refresh: ->
@port.postMessage name: @name, handler: "refresh"
--
cgit v1.2.3
From 723b66e24f9b3f4e8dd9843e21352eeceb7c96a9 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 06:17:19 +0100
Subject: Search completion; tweak timeouts and caching.
---
background_scripts/completion_engines.coffee | 12 ++++++------
pages/vomnibar.coffee | 2 +-
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 3e762e32..19a18ecd 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -24,7 +24,7 @@ class RegexpEngine
# Several Google completion engines package XML responses in this way.
class GoogleXMLRegexpEngine extends RegexpEngine
- doNotCache: true
+ doNotCache: false # true (disbaled, experimental)
parse: (xhr) ->
for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
continue unless suggestion = suggestion.getAttribute "data"
@@ -51,7 +51,7 @@ class Youtube extends GoogleXMLRegexpEngine
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=#{Utils.createSearchQuery queryTerms}"
class Wikipedia extends RegexpEngine
- doNotCache: true
+ doNotCache: false # true (disbaled, experimental)
# Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
constructor: ->
super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
@@ -125,7 +125,7 @@ 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: 250
+ delay: 200
get: (searchUrl, url, callback) ->
xhr = new XMLHttpRequest()
@@ -177,9 +177,8 @@ CompletionEngines =
@completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
if @completionCache.has completionCacheKey
# We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
- # suggestions are posted. It also makes the vomnibar behave similarly regardless of whether there's a
- # cache hit.
- Utils.setTimeout @delay, =>
+ # suggestions are posted.
+ Utils.setTimeout 75, =>
console.log "hit", completionCacheKey if @debug
callback @completionCache.get completionCacheKey
return
@@ -227,6 +226,7 @@ CompletionEngines =
console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
callback suggestions for callback in queue
+ # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries.
cancel: ->
if @mostRecentHandler?
@mostRecentHandler = null
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index ae61ca9d..62c37443 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -20,7 +20,7 @@ Vomnibar =
extend options, userOptions
options.refreshInterval =
- if options.completer == "omni" then 125 else 0
+ if options.completer == "omni" then 100 else 0
name = options.completer
completer = @completers[name] ?= new BackgroundCompleter name
--
cgit v1.2.3
From 908eac76062a45e5c8f686fccc9f91b16d8a23bf Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 06:56:09 +0100
Subject: Search completion; refactor query terms.
---
background_scripts/main.coffee | 3 +--
pages/vomnibar.coffee | 12 +++++++-----
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index c1be9f8f..a3ddb48c 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -62,8 +62,7 @@ completers =
completionHandlers =
filter: (completer, args, port) ->
- queryTerms = args.query.split(/\s+/).filter (s) -> 0 < s.length
- completer.filter queryTerms, (results) -> port.postMessage id: args.id, results: results
+ completer.filter args.queryTerms, (results) -> port.postMessage id: args.id, results: results
refresh: (completer) -> completer.refresh()
cancel: (completer) -> completer.cancel()
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 62c37443..503ae1f5 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -161,7 +161,7 @@ class VomnibarUI
true
updateCompletions: (callback = null) ->
- @completer.filter @input.value.trim(), (@completions) =>
+ @completer.filter @input.value, (@completions) =>
@populateUiWithCompletions @completions
callback?()
@@ -229,7 +229,7 @@ class BackgroundCompleter
if msg.id == @messageId
# The result objects coming from the background page will be of the form:
# { html: "", type: "", url: "" }
- # type will be one of [tab, bookmark, history, domain].
+ # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
results = msg.results.map (result) =>
functionToCall = if result.type == "tab"
@completionActions.switchToTab.curry result.tabId
@@ -240,12 +240,14 @@ class BackgroundCompleter
@mostRecentCallback results
filter: (query, @mostRecentCallback) ->
- # Ignore identical consecutive queries. This can happen, for example, if the user adds a to the
- # query.
+ # Ignore identical consecutive queries. This can happen, for example, if the user adds whitespace to the
+ # query. We normalize the query first to ensure that differences only in whitespace are ignored.
+ queryTerms = query.trim().split(/\s+/).filter (term) -> 0 < term.length
+ query = queryTerms.join " "
unless @mostRecentQuery? and query == @mostRecentQuery
@mostRecentQuery = query
@messageId = Utils.createUniqueId()
- @port.postMessage name: @name, handler: "filter", id: @messageId, query: query
+ @port.postMessage name: @name, handler: "filter", id: @messageId, queryTerms: queryTerms
refresh: ->
@port.postMessage name: @name, handler: "refresh"
--
cgit v1.2.3
From 898c21808c709a1a551fa15bb82f6a3bb4810aae Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 07:35:31 +0100
Subject: Search completion; refactor vomnibar launch.
---
pages/vomnibar.coffee | 23 +++++++++++------------
1 file changed, 11 insertions(+), 12 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 503ae1f5..29dc4a82 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -73,8 +73,10 @@ class VomnibarUI
reset: ->
@completionList.style.display = ""
@input.value = ""
- @updateTimer = null
@completions = []
+ window.clearTimeout @updateTimer if @updateTimer?
+ @updateTimer = null
+ @mostRecentQuery = null
@previousAutoSelect = null
@previousInputValue = null
@selection = @initialSelectionValue
@@ -151,9 +153,7 @@ class VomnibarUI
url: query
else
completion = @completions[@selection]
- @update true, =>
- # Shift+Enter will open the result in a new tab instead of the current tab.
- @hide -> completion.performAction openInNewTab
+ @hide -> completion.performAction openInNewTab
# It seems like we have to manually suppress the event here and still return true.
event.stopImmediatePropagation()
@@ -230,14 +230,13 @@ class BackgroundCompleter
# The result objects coming from the background page will be of the form:
# { html: "", type: "", url: "" }
# Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
- results = msg.results.map (result) =>
- functionToCall = if result.type == "tab"
- @completionActions.switchToTab.curry result.tabId
- else
- @completionActions.navigateToUrl.curry result.url
- result.performAction = functionToCall
- result
- @mostRecentCallback results
+ for result in msg.results
+ result.performAction =
+ if result.type == "tab"
+ @completionActions.switchToTab.curry result.tabId
+ else
+ @completionActions.navigateToUrl.curry result.url
+ @mostRecentCallback msg.results
filter: (query, @mostRecentCallback) ->
# Ignore identical consecutive queries. This can happen, for example, if the user adds whitespace to the
--
cgit v1.2.3
From 2dfcd17ea485484cedf636a94b9c89c527e2e0b7 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 09:03:56 +0100
Subject: Search completion; add front end cache.
---
background_scripts/completion.coffee | 15 ++++--
background_scripts/completion_engines.coffee | 8 ++-
background_scripts/main.coffee | 18 +++----
pages/vomnibar.coffee | 78 +++++++++++++++++++---------
4 files changed, 79 insertions(+), 40 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 746e662d..f17ca28c 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -491,22 +491,27 @@ class MultiCompleter
# (ie. a SearchEngineCompleter). This prevents hiding the vomnibar briefly before showing it
# again, which looks ugly.
unless shouldRunContinuation and suggestions.length == 0
- onComplete @prepareSuggestions queryTerms, suggestions
+ onComplete
+ results: @prepareSuggestions queryTerms, suggestions
+ callerMayCacheResults: not shouldRunContinuation
# Allow subsequent queries to begin.
@filterInProgress = false
if shouldRunContinuation
continuation suggestions, (newSuggestions) =>
if 0 < newSuggestions.length
- onComplete @prepareSuggestions queryTerms, suggestions.concat newSuggestions
+ suggestions.push newSuggestions...
+ onComplete
+ results: @prepareSuggestions queryTerms, suggestions
+ callerMayCacheResults: true
else
@filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
prepareSuggestions: (queryTerms, suggestions) ->
suggestion.computeRelevancy queryTerms for suggestion in suggestions
suggestions.sort (a, b) -> b.relevancy - a.relevancy
- suggestions = suggestions[0...@maxResults]
- suggestion.generateHtml() for suggestion in suggestions
- suggestions
+ for suggestion in suggestions[0...@maxResults]
+ suggestion.generateHtml()
+ suggestion
# Utilities which help us compute a relevancy score for a given item.
RankingUtils =
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 19a18ecd..52db90d0 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -209,9 +209,13 @@ CompletionEngines =
# We pause in case the user is still typing.
Utils.setTimeout @delay, handler = @mostRecentHandler = =>
- if handler != @mostRecentHandler # Bail if another completion has begun, or the user is typing.
+ if handler != @mostRecentHandler
+ # Bail! Another completion has begun, or the user is typing.
+ # NOTE: We do *not* call the callback (because we are not providing results, and we don't want allow
+ # any higher-level component to cache the results -- specifically, the vomnibar itself, via
+ # callerMayCacheResults).
console.log "bail", completionCacheKey if @debug
- return callback []
+ return
@mostRecentHandler = null
# Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
# removes a space, or when they enter a character and immediately delete it.
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index a3ddb48c..1a3281bf 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -43,26 +43,26 @@ chrome.storage.local.set
vimiumSecret: Math.floor Math.random() * 2000000000
completionSources =
- bookmarks: new BookmarkCompleter()
- history: new HistoryCompleter()
- domains: new DomainCompleter()
- tabs: new TabCompleter()
- searchEngines: new SearchEngineCompleter()
+ bookmarks: new BookmarkCompleter
+ history: new HistoryCompleter
+ domains: new DomainCompleter
+ tabs: new TabCompleter
+ searchEngines: new SearchEngineCompleter
completers =
omni: new MultiCompleter [
completionSources.bookmarks
completionSources.history
completionSources.domains
- # This comes last, because it delivers additional, asynchronous results.
completionSources.searchEngines
]
- bookmarks: new MultiCompleter([completionSources.bookmarks])
- tabs: new MultiCompleter([completionSources.tabs])
+ bookmarks: new MultiCompleter [completionSources.bookmarks]
+ tabs: new MultiCompleter [completionSources.tabs]
completionHandlers =
filter: (completer, args, port) ->
- completer.filter args.queryTerms, (results) -> port.postMessage id: args.id, results: results
+ completer.filter args.queryTerms, (response) ->
+ port.postMessage extend args, response
refresh: (completer) -> completer.refresh()
cancel: (completer) -> completer.cancel()
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 29dc4a82..cac191de 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -218,53 +218,83 @@ class VomnibarUI
# Sends requests to a Vomnibox completer on the background page.
#
class BackgroundCompleter
+ debug: true
+
# name is background-page completer to connect to: "omni", "tabs", or "bookmarks".
constructor: (@name) ->
@messageId = null
@port = chrome.runtime.connect name: "completions"
- @port.onMessage.addListener handler = @messageHandler
+ @port.onMessage.addListener (msg) => @messageHandler msg
+ @reset()
+
+ reset: ->
+ # We only cache results for the duration of a single vomnibar activation.
+ @cache = new SimpleCache 1000 * 60 * 5
+
+ messageHandler: (msg) ->
+ # The result objects coming from the background page will be of the form:
+ # { html: "", type: "", url: "" }
+ # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
+ for result in msg.results
+ result.performAction =
+ if result.type == "tab"
+ @completionActions.switchToTab.curry result.tabId
+ else
+ @completionActions.navigateToUrl.curry result.url
+
+ # Cache the results (but only if the background completer tells us that it's ok to do so).
+ if msg.callerMayCacheResults
+ console.log "cache set:", msg.query if @debug
+ @cache.set msg.query, msg.results
+ else
+ console.log "not setting cache:", msg.query if @debug
- messageHandler: (msg) =>
# We ignore messages which arrive too late.
if msg.id == @messageId
- # The result objects coming from the background page will be of the form:
- # { html: "", type: "", url: "" }
- # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
- for result in msg.results
- result.performAction =
- if result.type == "tab"
- @completionActions.switchToTab.curry result.tabId
- else
- @completionActions.navigateToUrl.curry result.url
@mostRecentCallback msg.results
filter: (query, @mostRecentCallback) ->
- # Ignore identical consecutive queries. This can happen, for example, if the user adds whitespace to the
- # query. We normalize the query first to ensure that differences only in whitespace are ignored.
queryTerms = query.trim().split(/\s+/).filter (term) -> 0 < term.length
query = queryTerms.join " "
- unless @mostRecentQuery? and query == @mostRecentQuery
- @mostRecentQuery = query
- @messageId = Utils.createUniqueId()
- @port.postMessage name: @name, handler: "filter", id: @messageId, queryTerms: queryTerms
+ if @cache.has query
+ console.log "cache hit:", query if @debug
+ @mostRecentCallback @cache.get query
+ else
+ # Silently drop identical consecutive queries. This can happen, for example, if the user adds
+ # whitespace to the query.
+ unless @mostRecentQuery? and query == @mostRecentQuery
+ @mostRecentQuery = query
+ @messageId = Utils.createUniqueId()
+ @port.postMessage
+ name: @name
+ handler: "filter"
+ id: @messageId
+ query: query
+ queryTerms: queryTerms
refresh: ->
+ @reset()
+ # Inform the background completer that we have a new vomnibar activation.
@port.postMessage name: @name, handler: "refresh"
cancel: ->
+ # Inform the background completer that it may (should it choose to do so) abandon any pending query
+ # (because the user is typing, and there'll be another query along soon).
@port.postMessage name: @name, handler: "cancel"
- # These are the actions we can perform when the user selects a result in the Vomnibox.
+ # These are the actions we can perform when the user selects a result.
completionActions:
navigateToUrl: (url, openInNewTab) ->
- # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
- openInNewTab = false if url.startsWith("javascript:")
- chrome.runtime.sendMessage(
+ # If the URL is a bookmarklet (so, prefixed with "javascript:"), then we always open it in the current
+ # tab.
+ openInNewTab &&= not Utils.hasJavascriptPrefix url
+ chrome.runtime.sendMessage
handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
- url: url,
- selected: openInNewTab)
+ url: url
+ selected: openInNewTab
- switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId })
+ switchToTab: (tabId) ->
+ chrome.runtime.sendMessage handler: "selectSpecificTab", id: tabId
UIComponentServer.registerHandler (event) ->
switch event.data
--
cgit v1.2.3
From d7f53190f9f8bf477f2e32a1dd4fb12b07024f6a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 10:03:49 +0100
Subject: Search completion; some code touch up.
---
pages/vomnibar.coffee | 92 ++++++++++++++++++++++-----------------------------
1 file changed, 39 insertions(+), 53 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index cac191de..5f133a21 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -8,9 +8,6 @@ Vomnibar =
getUI: -> @vomnibarUI
completers: {}
- #
- # Activate the Vomnibox.
- #
activate: (userOptions) ->
options =
completer: "omni"
@@ -18,19 +15,16 @@ Vomnibar =
newTab: false
selectFirst: false
extend options, userOptions
+ extend options, refreshInterval: if options.completer == "omni" then 100 else 0
- options.refreshInterval =
- if options.completer == "omni" then 100 else 0
-
- name = options.completer
- completer = @completers[name] ?= new BackgroundCompleter name
+ completer = @completers[options.completer] ?= new BackgroundCompleter options.completer
@vomnibarUI ?= new VomnibarUI()
completer.refresh()
- @vomnibarUI.setInitialSelectionValue(if options.selectFirst then 0 else -1)
- @vomnibarUI.setCompleter(completer)
- @vomnibarUI.setRefreshInterval(options.refreshInterval)
- @vomnibarUI.setForceNewTab(options.newTab)
- @vomnibarUI.setQuery(options.query)
+ @vomnibarUI.setInitialSelectionValue if options.selectFirst then 0 else -1
+ @vomnibarUI.setCompleter completer
+ @vomnibarUI.setRefreshInterval options.refreshInterval
+ @vomnibarUI.setForceNewTab options.newTab
+ @vomnibarUI.setQuery options.query
@vomnibarUI.update true
hide: -> @vomnibarUI?.hide()
@@ -43,17 +37,10 @@ class VomnibarUI
@initDom()
setQuery: (query) -> @input.value = query
-
- setInitialSelectionValue: (initialSelectionValue) ->
- @initialSelectionValue = initialSelectionValue
-
- setCompleter: (completer) ->
- @completer = completer
- @reset()
-
- setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval
-
- setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab
+ setInitialSelectionValue: (@initialSelectionValue) ->
+ setRefreshInterval: (@refreshInterval) ->
+ setForceNewTab: (@forceNewTab) ->
+ setCompleter: (@completer) -> @reset()
# The sequence of events when the vomnibar is hidden is as follows:
# 1. Post a "hide" message to the host page.
@@ -76,7 +63,6 @@ class VomnibarUI
@completions = []
window.clearTimeout @updateTimer if @updateTimer?
@updateTimer = null
- @mostRecentQuery = null
@previousAutoSelect = null
@previousInputValue = null
@selection = @initialSelectionValue
@@ -166,7 +152,7 @@ class VomnibarUI
callback?()
populateUiWithCompletions: (completions) ->
- # update completion list with the new data
+ # Update completion list with the new suggestions.
@completionList.innerHTML = completions.map((completion) -> "
#{completion.html}
").join("")
@completionList.style.display = if completions.length > 0 then "block" else ""
@selection = Math.min completions.length - 1, Math.max @initialSelectionValue, @selection
@@ -179,7 +165,7 @@ class VomnibarUI
@previousInputValue = null
@previousAutoSelect = null
@selection = -1
- @update()
+ @update false
update: (updateSynchronously = false, callback = null) =>
if updateSynchronously
@@ -222,36 +208,31 @@ class BackgroundCompleter
# name is background-page completer to connect to: "omni", "tabs", or "bookmarks".
constructor: (@name) ->
- @messageId = null
@port = chrome.runtime.connect name: "completions"
- @port.onMessage.addListener (msg) => @messageHandler msg
+ @messageId = null
@reset()
- reset: ->
- # We only cache results for the duration of a single vomnibar activation.
- @cache = new SimpleCache 1000 * 60 * 5
-
- messageHandler: (msg) ->
- # The result objects coming from the background page will be of the form:
- # { html: "", type: "", url: "" }
- # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
- for result in msg.results
- result.performAction =
- if result.type == "tab"
- @completionActions.switchToTab.curry result.tabId
- else
- @completionActions.navigateToUrl.curry result.url
-
- # Cache the results (but only if the background completer tells us that it's ok to do so).
- if msg.callerMayCacheResults
- console.log "cache set:", msg.query if @debug
- @cache.set msg.query, msg.results
- else
- console.log "not setting cache:", msg.query if @debug
+ @port.onMessage.addListener (msg) =>
+ # The result objects coming from the background page will be of the form:
+ # { html: "", type: "", url: "" }
+ # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
+ for result in msg.results
+ result.performAction =
+ if result.type == "tab"
+ @completionActions.switchToTab.curry result.tabId
+ else
+ @completionActions.navigateToUrl.curry result.url
+
+ # Cache the results (but only if the background completer tells us that it's ok to do so).
+ if msg.callerMayCacheResults
+ console.log "cache set:", msg.query if @debug
+ @cache.set msg.query, msg.results
+ else
+ console.log "not setting cache:", msg.query if @debug
- # We ignore messages which arrive too late.
- if msg.id == @messageId
- @mostRecentCallback msg.results
+ # We ignore messages which arrive too late.
+ if msg.id == @messageId
+ @mostRecentCallback msg.results
filter: (query, @mostRecentCallback) ->
queryTerms = query.trim().split(/\s+/).filter (term) -> 0 < term.length
@@ -277,6 +258,11 @@ class BackgroundCompleter
# Inform the background completer that we have a new vomnibar activation.
@port.postMessage name: @name, handler: "refresh"
+ reset: ->
+ # We only cache results for the duration of a single vomnibar activation.
+ @cache = new SimpleCache 1000 * 60 * 5
+ @mostRecentQuery = null
+
cancel: ->
# Inform the background completer that it may (should it choose to do so) abandon any pending query
# (because the user is typing, and there'll be another query along soon).
--
cgit v1.2.3
From 571cb9447552804e57d0f453b9642de55e02f317 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 10:21:39 +0100
Subject: Search completion; add space after inserted text.
---
pages/vomnibar.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 5f133a21..de1e6452 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -81,7 +81,7 @@ class VomnibarUI
# and revert when not. This allows the user to select a suggestion and then continue typing.
if 0 <= @selection and @completions[@selection].insertText?
@previousInputValue ?= @input.value
- @input.value = @completions[@selection].insertText
+ @input.value = @completions[@selection].insertText + " "
else if @previousInputValue?
@input.value = @previousInputValue
@previousInputValue = null
--
cgit v1.2.3
From 3e890adbbf7b6a9671b42536b6ee6221389b9443 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 11:02:55 +0100
Subject: Search completion; add debug code to completer dispatch.
---
background_scripts/completion.coffee | 65 ++++++++++++++++++++----------------
1 file changed, 36 insertions(+), 29 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index f17ca28c..48039cd2 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -465,46 +465,53 @@ class MultiCompleter
filter: (queryTerms, onComplete) ->
# Allow only one query to run at a time.
if @filterInProgress
- @mostRecentQuery = { queryTerms: queryTerms, onComplete: onComplete }
+ @mostRecentQuery = [ queryTerms, onComplete ]
return
RegexpCache.clear()
@mostRecentQuery = null
@filterInProgress = true
suggestions = []
- completersFinished = 0
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 in @completers
- do (completer) =>
- Utils.nextTick =>
- completer.filter queryTerms, (newSuggestions, cont = null) =>
- suggestions = suggestions.concat newSuggestions
- continuation = cont if cont?
- if @completers.length <= ++completersFinished
- 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 prevents hiding the vomnibar briefly before showing 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
- @filter @mostRecentQuery.queryTerms, @mostRecentQuery.onComplete if @mostRecentQuery
+ 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...
prepareSuggestions: (queryTerms, suggestions) ->
suggestion.computeRelevancy queryTerms for suggestion in suggestions
--
cgit v1.2.3
From 44378220093ee5bd873b553f5be556921c778663 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 11:31:05 +0100
Subject: Search completion; clear (not replace) the cache.
---
lib/utils.coffee | 11 +++++++++++
pages/vomnibar.coffee | 5 +++--
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 354d82f6..1c24a40f 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -236,6 +236,17 @@ class SimpleCache
else
null
+ clear: ->
+ @rotate()
+ @rotate()
+
+ # Because of the timer, we can't just let these caches go out of scope and have the garbage collector
+ # harvest them. Whenever they may fall out of use, we need to remove the timer. @rotate() can be used to
+ # restart the cache.
+ suspend: ->
+ clearTimeout @timer if @timer?
+ @timer = null
+
# Set value, and return that value. If value is null, then delete key.
set: (key, value = null) ->
if value?
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index de1e6452..76e276a1 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -210,6 +210,7 @@ class BackgroundCompleter
constructor: (@name) ->
@port = chrome.runtime.connect name: "completions"
@messageId = null
+ @cache ?= new SimpleCache 1000 * 60 * 5
@reset()
@port.onMessage.addListener (msg) =>
@@ -259,8 +260,8 @@ class BackgroundCompleter
@port.postMessage name: @name, handler: "refresh"
reset: ->
- # We only cache results for the duration of a single vomnibar activation.
- @cache = new SimpleCache 1000 * 60 * 5
+ # We only cache results for the duration of a single vomnibar activation, so clear the cache now.
+ @cache.clear()
@mostRecentQuery = null
cancel: ->
--
cgit v1.2.3
From b882213019792a7fb47352a920a54d468d352c86 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 14:02:21 +0100
Subject: Search completion; exclusivity.
If we have a custom search engine with a completer, then exclude
suggestions from other completion engines.
---
background_scripts/completion.coffee | 219 +++++++++++++++------------
background_scripts/completion_engines.coffee | 7 +-
pages/vomnibar.coffee | 1 +
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) -> "
#{completion.html}
").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: =>
--
cgit v1.2.3
From 4ab7881e46084a5946bae6d29fb1d0ab9677542a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 14:41:01 +0100
Subject: Search completion; fix unit tests.
---
background_scripts/settings.coffee | 22 ++++++++------------
tests/unit_tests/completion_test.coffee | 37 ++++++++++++++++++++++++++-------
tests/unit_tests/settings_test.coffee | 6 +++---
3 files changed, 41 insertions(+), 24 deletions(-)
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index ce812970..2a21b0c9 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -5,8 +5,6 @@
root = exports ? window
root.Settings = Settings =
get: (key) ->
- # FIXME(smblott). Remove this line.
- return @defaults.searchEngines if key == "searchEngines"
if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key]
set: (key, value) ->
@@ -93,18 +91,16 @@ root.Settings = Settings =
searchUrl: "http://www.google.com/search?q="
# put in an example search engine
searchEngines: [
- # FIXME(smblott) Comment these out before merge.
- "# THESE ARE HARD WIRED.\n# YOU CANNOT CHANGE THEM IN THIS VERSION.\n# FOR DEVELOPMENT ONLY."
- "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"
- "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"
- "az: http://www.amazon.com/s/?field-keywords=%s Amazon"
- ].join "\n\n"
+ "# Examples:"
+ "# g: http://www.google.com/search?q=%s Google"
+ "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
+ "# y: http://www.youtube.com/results?search_query=%s Youtube"
+ "# 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"
+ "# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
+ ].join "\n"
newTabUrl: "chrome://newtab"
grabBackFocus: false
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index 56fcc456..b1962daf 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -1,5 +1,6 @@
require "./test_helper.js"
extend(global, require "../../lib/utils.js")
+extend(global, require "../../background_scripts/completion_engines.js")
extend(global, require "../../background_scripts/completion.js")
extend global, require "./test_chrome_stubs.js"
@@ -248,31 +249,51 @@ context "search engines",
results = filterCompleter(@completer, ["foo", "hello"])
assert.arrayEqual ["bar?q=hello"], results.map (result) -> result.url
assert.arrayEqual ["foo: hello"], results.map (result) -> result.title
- assert.arrayEqual ["search"], results.map (result) -> result.type
+ assert.arrayEqual ["custom search"], results.map (result) -> result.type
should "return search engine suggestion with description", ->
results = filterCompleter(@completer, ["baz", "hello"])
- assert.arrayEqual ["qux?q=hello"], results.map (result) -> result.url
- assert.arrayEqual ["hello"], results.map (result) -> result.title
- assert.arrayEqual ["baz description"], results.map (result) -> result.type
+ # assert.arrayEqual ["qux?q=hello"], results.map (result) -> result.searchUrl
+ # assert.arrayEqual ["hello"], results.map (result) -> result.title
+ # assert.arrayEqual ["baz description"], results.map (result) -> result.type
context "suggestions",
should "escape html in page titles", ->
- suggestion = new Suggestion(["queryterm"], "tab", "url", "title ", returns(1))
+ suggestion = new Suggestion
+ queryTerms: ["queryterm"]
+ type: "tab"
+ url: "url"
+ title: "title "
+ relevancyFunction: returns 1
assert.isTrue suggestion.generateHtml().indexOf("title <span>") >= 0
should "highlight query words", ->
- suggestion = new Suggestion(["ninj", "words"], "tab", "url", "ninjawords", returns(1))
+ suggestion = new Suggestion
+ queryTerms: ["ninj", "words"]
+ type: "tab"
+ url: "url"
+ title: "ninjawords"
+ relevancyFunction: returns 1
expected = "ninjawords"
assert.isTrue suggestion.generateHtml().indexOf(expected) >= 0
should "highlight query words correctly when whey they overlap", ->
- suggestion = new Suggestion(["ninj", "jaword"], "tab", "url", "ninjawords", returns(1))
+ suggestion = new Suggestion
+ queryTerms: ["ninj", "jaword"]
+ type: "tab"
+ url: "url"
+ title: "ninjawords"
+ relevancyFunction: returns 1
expected = "ninjawords"
assert.isTrue suggestion.generateHtml().indexOf(expected) >= 0
should "shorten urls", ->
- suggestion = new Suggestion(["queryterm"], "tab", "http://ninjawords.com", "ninjawords", returns(1))
+ suggestion = new Suggestion
+ queryTerms: ["queryterm"]
+ type: "tab"
+ url: "http://ninjawords.com"
+ title: "ninjawords"
+ relevancyFunction: returns 1
assert.equal -1, suggestion.generateHtml().indexOf("http://ninjawords.com")
context "RankingUtils.wordRelevancy",
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 346c98da..faa696a8 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -74,10 +74,10 @@ context "settings",
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
result = SearchEngineCompleter.getSearchEngines()
- assert.equal Object.keys(result).length, 2
- assert.equal "bar?q=%s", result["foo"].url
+ console.log result["foo"]
+ assert.equal "bar?q=%s", result["foo"].searchUrl
assert.isFalse result["foo"].description
- assert.equal "qux?q=%s", result["baz"].url
+ assert.equal "qux?q=%s", result["baz"].searchUrl
assert.equal "baz description", result["baz"].description
should "sync a key which is not a known setting (without crashing)", ->
--
cgit v1.2.3
From f6bd6c0fb95f5e0f2dea5088539a12baf6d3d708 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 14:47:16 +0100
Subject: Search completion; fix DOM tests.
---
pages/vomnibar.coffee | 5 ++++-
tests/unit_tests/settings_test.coffee | 1 -
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index dd680a6a..f980f3f4 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -8,6 +8,9 @@ Vomnibar =
getUI: -> @vomnibarUI
completers: {}
+ getCompleter: (name) ->
+ @completers[name] ?= new BackgroundCompleter name
+
activate: (userOptions) ->
options =
completer: "omni"
@@ -17,7 +20,7 @@ Vomnibar =
extend options, userOptions
extend options, refreshInterval: if options.completer == "omni" then 100 else 0
- completer = @completers[options.completer] ?= new BackgroundCompleter options.completer
+ completer = @getCompleter options.completer
@vomnibarUI ?= new VomnibarUI()
completer.refresh()
@vomnibarUI.setInitialSelectionValue if options.selectFirst then 0 else -1
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index faa696a8..52eec30d 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -74,7 +74,6 @@ context "settings",
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
result = SearchEngineCompleter.getSearchEngines()
- console.log result["foo"]
assert.equal "bar?q=%s", result["foo"].searchUrl
assert.isFalse result["foo"].description
assert.equal "qux?q=%s", result["baz"].searchUrl
--
cgit v1.2.3
From 9887c8d763bf7b58e459a48f34531f6877ffebf4 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 14:54:15 +0100
Subject: Search completion; tweak example text.
---
background_scripts/settings.coffee | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 2a21b0c9..89b26bff 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -92,7 +92,9 @@ root.Settings = Settings =
# put in an example search engine
searchEngines: [
"w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
- "# Examples:"
+ ""
+ "# More examples:"
+ "#"
"# g: http://www.google.com/search?q=%s Google"
"# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
"# y: http://www.youtube.com/results?search_query=%s Youtube"
--
cgit v1.2.3
From 5e6fa4ccfc103750b84df02a35f42a6acef78fa1 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 15:39:13 +0100
Subject: Search completion; suppress custom search keyword.
---
background_scripts/completion.coffee | 4 +++-
pages/vomnibar.coffee | 19 ++++++++++++++++++-
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index b9efb034..9d249198 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -382,6 +382,8 @@ class SearchEngineCompleter
autoSelect: true
# Always reset the selection to this suggestion on query change. The UX is weird otherwise.
forceAutoSelect: true
+ # Suppress the "w" from "w query terms" in the vomnibar input.
+ suppressLeadingQueryTerm: true
onComplete suggestions,
exclusive: if custom and CompletionEngines.haveCompletionEngine searchUrl then description else null
@@ -418,7 +420,7 @@ class SearchEngineCompleter
title: suggestion
relevancy: relavancy *= 0.9
highlightTerms: false
- insertText: if custom then "#{keyword} #{suggestion}" else suggestion
+ insertText: 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
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index f980f3f4..1188c411 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -68,6 +68,7 @@ class VomnibarUI
@updateTimer = null
@previousAutoSelect = null
@previousInputValue = null
+ @suppressedLeadingQueryTerm = null
@selection = @initialSelectionValue
updateSelection: ->
@@ -80,6 +81,19 @@ class VomnibarUI
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 @suppressedLeadingQueryTerm?
+ # If we have a suppressed term and the input is empty, then reinstate it.
+ if @input.value.trim().split(/\s+/).join("").length == 0
+ @input.value = @getInputValue()
+ @suppressedLeadingQueryTerm = null
+ else if @completions[0]?.suppressLeadingQueryTerm
+ # We've been asked to suppress the leading query term, and it's not already suppressed. So suppress it.
+ queryTerms = @input.value.trim().split /\s+/
+ @suppressedLeadingQueryTerm = 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.
if 0 <= @selection and @completions[@selection].insertText?
@@ -149,8 +163,11 @@ class VomnibarUI
event.preventDefault()
true
+ getInputValue: ->
+ (if @suppressedLeadingQueryTerm? then @suppressedLeadingQueryTerm + " " else "") + @input.value
+
updateCompletions: (callback = null) ->
- @completer.filter @input.value, (@completions) =>
+ @completer.filter @getInputValue(), (@completions) =>
@populateUiWithCompletions @completions
callback?()
--
cgit v1.2.3
From 82d25b5df76c8526d4ccb5352c0905cc28371199 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 16:47:24 +0100
Subject: Search completion; search keyword on SPACE.
---
background_scripts/completion.coffee | 22 +++++++++----
lib/utils.coffee | 1 +
pages/vomnibar.coffee | 64 +++++++++++++++++++++---------------
3 files changed, 53 insertions(+), 34 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 9d249198..ba19970f 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -131,7 +131,8 @@ class BookmarkCompleter
# These bookmarks are loaded asynchronously when refresh() is called.
bookmarks: null
- filter: (@queryTerms, @onComplete) ->
+ filter: (queryTerms, @onComplete) ->
+ @queryTerms = queryTerms.filter (t) -> 0 < t.length
@currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
@performSearch() if @bookmarks
@@ -193,6 +194,7 @@ class BookmarkCompleter
class HistoryCompleter
filter: (queryTerms, onComplete) ->
+ queryTerms = queryTerms.filter (t) -> 0 < t.length
@currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
results = []
HistoryCache.use (history) =>
@@ -227,6 +229,7 @@ class DomainCompleter
domains: null
filter: (queryTerms, onComplete) ->
+ queryTerms = queryTerms.filter (t) -> 0 < t.length
return onComplete([]) unless queryTerms.length == 1
if @domains
@performSearch(queryTerms, onComplete)
@@ -329,6 +332,7 @@ tabRecency = new TabRecency()
# Searches through all open tabs, matching on title and URL.
class TabCompleter
filter: (queryTerms, onComplete) ->
+ queryTerms = queryTerms.filter (t) -> 0 < t.length
# NOTE(philc): We search all tabs, not just those in the current window. I'm not sure if this is the
# correct UX.
chrome.tabs.query {}, (tabs) =>
@@ -366,9 +370,7 @@ class SearchEngineCompleter
queryTerms = queryTerms[1..] if custom
query = queryTerms.join " "
-
- if queryTerms.length == 0
- return onComplete []
+ return onComplete [] if queryTerms.length == 0
# For custom search engines, we add an auto-selected suggestion.
if custom
@@ -385,6 +387,10 @@ class SearchEngineCompleter
# Suppress the "w" from "w query terms" in the vomnibar input.
suppressLeadingQueryTerm: true
+ # We filter out the empty strings late so that we can distinguish between, for example, "w" and "w ".
+ queryTerms = queryTerms.filter (t) -> 0 < t.length
+ return onComplete suggestions if queryTerms.length == 0
+
onComplete suggestions,
exclusive: if custom and CompletionEngines.haveCompletionEngine searchUrl then description else null
continuation: (existingSuggestions, onComplete) =>
@@ -404,6 +410,7 @@ class SearchEngineCompleter
characterCount = query.length - queryTerms.length + 1
relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
+ queryTerms = queryTerms.filter (t) -> 0 < t.length
if 0 < existingSuggestions.length
existingSuggestionsMinScore = existingSuggestions[existingSuggestions.length-1].relevancy
if relavancy < existingSuggestionsMinScore and MultiCompleter.maxResults <= existingSuggestions.length
@@ -477,10 +484,11 @@ class MultiCompleter
# 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 if another query is already waiting. This is for slow tasks which should be done
+ # asynchronously (e.g. HTTP GET).
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").
+ # If truthy, completions from other completers should be suppressed. The truthy value should be the
+ # type of the completer (e.g. "custom search"). All other completion types are suppressed.
exclusive: false
(queryTerms, onComplete) ->
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 1c24a40f..033fdd2b 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -201,6 +201,7 @@ Function::curry = ->
Array.copy = (array) -> Array.prototype.slice.call(array, 0)
String::startsWith = (str) -> @indexOf(str) == 0
+String::ltrim = () -> @replace /^\s+/, ""
globalRoot = window ? global
globalRoot.extend = (hash1, hash2) ->
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 1188c411..b228a59b 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -61,11 +61,10 @@ class VomnibarUI
@postHideCallback = null
reset: ->
+ @clearUpdateTimer()
@completionList.style.display = ""
@input.value = ""
@completions = []
- window.clearTimeout @updateTimer if @updateTimer?
- @updateTimer = null
@previousAutoSelect = null
@previousInputValue = null
@suppressedLeadingQueryTerm = null
@@ -84,10 +83,7 @@ class VomnibarUI
# For custom search engines, we suppress the leading term (e.g. the "w" of "w query terms") within the
# vomnibar input.
if @suppressedLeadingQueryTerm?
- # If we have a suppressed term and the input is empty, then reinstate it.
- if @input.value.trim().split(/\s+/).join("").length == 0
- @input.value = @getInputValue()
- @suppressedLeadingQueryTerm = null
+ @restoreSuppressedQueryTerm()
else if @completions[0]?.suppressLeadingQueryTerm
# We've been asked to suppress the leading query term, and it's not already suppressed. So suppress it.
queryTerms = @input.value.trim().split /\s+/
@@ -107,8 +103,15 @@ class VomnibarUI
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
+ restoreSuppressedQueryTerm: ->
+ if @suppressedLeadingQueryTerm?
+ # If we have a suppressed term and the input is empty, then reinstate it.
+ if @input.value.length == 0
+ @input.value = @suppressedLeadingQueryTerm
+ @suppressedLeadingQueryTerm = null
+
#
- # Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
+ # Returns the user's action ("up", "down", "enter", "dismiss", "delete" or null) based on their keypress.
# We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
#
actionFromKeyEvent: (event) ->
@@ -125,6 +128,9 @@ class VomnibarUI
return "down"
else if (event.keyCode == keyCodes.enter)
return "enter"
+ else if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
+ return "delete"
+ null
onKeydown: (event) =>
action = @actionFromKeyEvent(event)
@@ -157,6 +163,13 @@ class VomnibarUI
else
completion = @completions[@selection]
@hide -> completion.performAction openInNewTab
+ else if action == "delete"
+ if @input.value.length == 0
+ @restoreSuppressedQueryTerm()
+ @updateCompletions()
+ else
+ # Don't suppress the Delete. We want it to happen.
+ return true
# It seems like we have to manually suppress the event here and still return true.
event.stopImmediatePropagation()
@@ -167,6 +180,7 @@ class VomnibarUI
(if @suppressedLeadingQueryTerm? then @suppressedLeadingQueryTerm + " " else "") + @input.value
updateCompletions: (callback = null) ->
+ @clearUpdateTimer()
@completer.filter @getInputValue(), (@completions) =>
@populateUiWithCompletions @completions
callback?()
@@ -188,19 +202,18 @@ class VomnibarUI
@selection = -1
@update false
+ clearUpdateTimer: ->
+ if @updateTimer?
+ window.clearTimeout @updateTimer
+ @updateTimer = null
+
update: (updateSynchronously = false, callback = null) =>
if updateSynchronously
- # Cancel any scheduled update.
- if @updateTimer?
- window.clearTimeout @updateTimer
- @updateTimer = null
@updateCompletions callback
else if not @updateTimer?
# Update asynchronously for 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
- @updateCompletions callback
+ @updateTimer = Utils.setTimeout @refreshInterval, => @updateCompletions callback
@input.focus()
@@ -257,23 +270,21 @@ class BackgroundCompleter
@mostRecentCallback msg.results
filter: (query, @mostRecentCallback) ->
- queryTerms = query.trim().split(/\s+/).filter (term) -> 0 < term.length
+ # We retain trailing whitespace so that we can tell the difference between "w" and "w " (for custom search
+ # engines).
+ queryTerms = query.ltrim().split(/\s+/)
query = queryTerms.join " "
if @cache.has query
console.log "cache hit:", query if @debug
@mostRecentCallback @cache.get query
else
- # Silently drop identical consecutive queries. This can happen, for example, if the user adds
- # whitespace to the query.
- unless @mostRecentQuery? and query == @mostRecentQuery
- @mostRecentQuery = query
- @messageId = Utils.createUniqueId()
- @port.postMessage
- name: @name
- handler: "filter"
- id: @messageId
- query: query
- queryTerms: queryTerms
+ @messageId = Utils.createUniqueId()
+ @port.postMessage
+ name: @name
+ handler: "filter"
+ id: @messageId
+ query: query
+ queryTerms: queryTerms
refresh: ->
@reset()
@@ -283,7 +294,6 @@ class BackgroundCompleter
reset: ->
# We only cache results for the duration of a single vomnibar activation, so clear the cache now.
@cache.clear()
- @mostRecentQuery = null
cancel: ->
# Inform the background completer that it may (should it choose to do so) abandon any pending query
--
cgit v1.2.3
From 21db0f353257e5e7848d9d884ed93e717120e88d Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 17:28:09 +0100
Subject: Search completion; search keyword on SPACE.
---
background_scripts/completion.coffee | 20 ++++++----
background_scripts/main.coffee | 14 +++----
pages/vomnibar.coffee | 74 ++++++++++++++++++------------------
3 files changed, 57 insertions(+), 51 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index ba19970f..dc5b2737 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -359,6 +359,13 @@ class SearchEngineCompleter
cancel: ->
CompletionEngines.cancel()
+ refresh: (port) ->
+ @searchEngines = SearchEngineCompleter.getSearchEngines()
+ # Let the vomnibar know the custom search engine keywords.
+ port.postMessage
+ handler: "customSearchEngineKeywords"
+ keywords: key for own key of @searchEngines
+
filter: (queryTerms, onComplete) ->
suggestions = []
@@ -385,7 +392,7 @@ class SearchEngineCompleter
# Always reset the selection to this suggestion on query change. The UX is weird otherwise.
forceAutoSelect: true
# Suppress the "w" from "w query terms" in the vomnibar input.
- suppressLeadingQueryTerm: true
+ suppressLeadingKeyword: true
# We filter out the empty strings late so that we can distinguish between, for example, "w" and "w ".
queryTerms = queryTerms.filter (t) -> 0 < t.length
@@ -436,9 +443,6 @@ class SearchEngineCompleter
count = Math.min 6, Math.max 3, MultiCompleter.maxResults - existingSuggestions.length
onComplete suggestions[...count]
- refresh: ->
- @searchEngines = SearchEngineCompleter.getSearchEngines()
-
getSearchEngineMatches: (queryTerms) ->
(1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {}
@@ -473,11 +477,11 @@ class MultiCompleter
constructor: (@completers) ->
@maxResults = MultiCompleter.maxResults
- refresh: ->
- completer.refresh?() for completer in @completers
+ refresh: (port) ->
+ completer.refresh? port for completer in @completers
- cancel: ->
- completer.cancel?() for completer in @completers
+ cancel: (port) ->
+ completer.cancel? port for completer in @completers
filter: do ->
defaultCallbackOptions =
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 1a3281bf..34db5a20 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -60,15 +60,15 @@ completers =
tabs: new MultiCompleter [completionSources.tabs]
completionHandlers =
- filter: (completer, args, port) ->
- completer.filter args.queryTerms, (response) ->
- port.postMessage extend args, response
+ filter: (completer, request, port) ->
+ completer.filter request.queryTerms, (response) ->
+ port.postMessage extend request, extend response, handler: "completions"
- refresh: (completer) -> completer.refresh()
- cancel: (completer) -> completer.cancel()
+ refresh: (completer, _, port) -> completer.refresh port
+ cancel: (completer, _, port) -> completer.cancel port
-handleCompletions = (args, port) ->
- completionHandlers[args.handler] completers[args.name], args, port
+handleCompletions = (request, port) ->
+ completionHandlers[request.handler] completers[request.name], request, port
chrome.runtime.onConnect.addListener (port, name) ->
senderTabId = if port.sender.tab then port.sender.tab.id else null
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index b228a59b..3039075c 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -22,7 +22,7 @@ Vomnibar =
completer = @getCompleter options.completer
@vomnibarUI ?= new VomnibarUI()
- completer.refresh()
+ completer.refresh @vomnibarUI
@vomnibarUI.setInitialSelectionValue if options.selectFirst then 0 else -1
@vomnibarUI.setCompleter completer
@vomnibarUI.setRefreshInterval options.refreshInterval
@@ -44,6 +44,7 @@ class VomnibarUI
setRefreshInterval: (@refreshInterval) ->
setForceNewTab: (@forceNewTab) ->
setCompleter: (@completer) -> @reset()
+ setKeywords: (@keywords) ->
# The sequence of events when the vomnibar is hidden is as follows:
# 1. Post a "hide" message to the host page.
@@ -67,8 +68,9 @@ class VomnibarUI
@completions = []
@previousAutoSelect = null
@previousInputValue = null
- @suppressedLeadingQueryTerm = null
+ @suppressedLeadingKeyword = null
@selection = @initialSelectionValue
+ @keywords = []
updateSelection: ->
# We retain global state here (previousAutoSelect) to tell if a search item (for which autoSelect is set)
@@ -82,12 +84,9 @@ class VomnibarUI
# For custom search engines, we suppress the leading term (e.g. the "w" of "w query terms") within the
# vomnibar input.
- if @suppressedLeadingQueryTerm?
- @restoreSuppressedQueryTerm()
- else if @completions[0]?.suppressLeadingQueryTerm
- # We've been asked to suppress the leading query term, and it's not already suppressed. So suppress it.
+ if @completions[0]?.suppressLeadingKeyword and not @suppressedLeadingKeyword?
queryTerms = @input.value.trim().split /\s+/
- @suppressedLeadingQueryTerm = queryTerms[0]
+ @suppressedLeadingKeyword = queryTerms[0]
@input.value = queryTerms[1..].join " "
# For suggestions from search-engine completion, we copy the suggested text into the input when selected,
@@ -103,13 +102,6 @@ class VomnibarUI
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
- restoreSuppressedQueryTerm: ->
- if @suppressedLeadingQueryTerm?
- # If we have a suppressed term and the input is empty, then reinstate it.
- if @input.value.length == 0
- @input.value = @suppressedLeadingQueryTerm
- @suppressedLeadingQueryTerm = null
-
#
# Returns the user's action ("up", "down", "enter", "dismiss", "delete" or null) based on their keypress.
# We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
@@ -164,8 +156,9 @@ class VomnibarUI
completion = @completions[@selection]
@hide -> completion.performAction openInNewTab
else if action == "delete"
- if @input.value.length == 0
- @restoreSuppressedQueryTerm()
+ if @suppressedLeadingKeyword? and @input.value.length == 0
+ @input.value = @suppressedLeadingKeyword
+ @suppressedLeadingKeyword = null
@updateCompletions()
else
# Don't suppress the Delete. We want it to happen.
@@ -177,7 +170,7 @@ class VomnibarUI
true
getInputValue: ->
- (if @suppressedLeadingQueryTerm? then @suppressedLeadingQueryTerm + " " else "") + @input.value
+ (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @input.value
updateCompletions: (callback = null) ->
@clearUpdateTimer()
@@ -208,6 +201,11 @@ class VomnibarUI
@updateTimer = null
update: (updateSynchronously = false, callback = null) =>
+ # If the query text is a custom search keyword, then we need to force a synchronous update (so that the
+ # interface is snappy).
+ if @keywords? and not @suppressedLeadingKeyword?
+ queryTerms = @input.value.ltrim().split /\s+/
+ updateSynchronously ||= 1 < queryTerms.length and queryTerms[0] in @keywords
if updateSynchronously
@updateCompletions callback
else if not @updateTimer?
@@ -248,26 +246,30 @@ class BackgroundCompleter
@reset()
@port.onMessage.addListener (msg) =>
- # The result objects coming from the background page will be of the form:
- # { html: "", type: "", url: "" }
- # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
- for result in msg.results
- result.performAction =
- if result.type == "tab"
- @completionActions.switchToTab.curry result.tabId
+ switch msg.handler
+ when "customSearchEngineKeywords"
+ @lastUI.setKeywords msg.keywords
+ when "completions"
+ # The result objects coming from the background page will be of the form:
+ # { html: "", type: "", url: "" }
+ # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
+ for result in msg.results
+ result.performAction =
+ if result.type == "tab"
+ @completionActions.switchToTab.curry result.tabId
+ else
+ @completionActions.navigateToUrl.curry result.url
+
+ # Cache the results (but only if the background completer tells us that it's ok to do so).
+ if msg.callerMayCacheResults
+ console.log "cache set:", msg.query if @debug
+ @cache.set msg.query, msg.results
else
- @completionActions.navigateToUrl.curry result.url
-
- # Cache the results (but only if the background completer tells us that it's ok to do so).
- if msg.callerMayCacheResults
- console.log "cache set:", msg.query if @debug
- @cache.set msg.query, msg.results
- else
- console.log "not setting cache:", msg.query if @debug
+ console.log "not setting cache:", msg.query if @debug
- # We ignore messages which arrive too late.
- if msg.id == @messageId
- @mostRecentCallback msg.results
+ # We ignore messages which arrive too late.
+ if msg.id == @messageId
+ @mostRecentCallback msg.results
filter: (query, @mostRecentCallback) ->
# We retain trailing whitespace so that we can tell the difference between "w" and "w " (for custom search
@@ -286,7 +288,7 @@ class BackgroundCompleter
query: query
queryTerms: queryTerms
- refresh: ->
+ refresh: (@lastUI) ->
@reset()
# Inform the background completer that we have a new vomnibar activation.
@port.postMessage name: @name, handler: "refresh"
--
cgit v1.2.3
From cf993efaf69fcf3482cf0f54881fcf87f0108a0d Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 17:53:39 +0100
Subject: Search completion; exclusion [WIP].
---
background_scripts/completion.coffee | 32 ++++++++++++++++++++++------
background_scripts/completion_engines.coffee | 6 ++----
2 files changed, 27 insertions(+), 11 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index dc5b2737..6e57f0d4 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -63,6 +63,11 @@ class Suggestion
a.href = url
a.protocol + "//" + a.hostname
+ getHostname: (url) ->
+ a = document.createElement 'a'
+ a.href = url
+ a.hostname
+
shortenUrl: (url) -> @stripTrailingSlash(url).replace(/^https?:\/\//, "")
stripTrailingSlash: (url) ->
@@ -396,10 +401,24 @@ class SearchEngineCompleter
# We filter out the empty strings late so that we can distinguish between, for example, "w" and "w ".
queryTerms = queryTerms.filter (t) -> 0 < t.length
- return onComplete suggestions if queryTerms.length == 0
+ # NOTE(smblott) I'm having difficulty figuring out to do the filtering, here. Exclusive should mean
+ # exclusive to what?
+ exclusive = if custom and CompletionEngines.haveCompletionEngine searchUrl then description else null
+ # exclusive =
+ # if custom and CompletionEngines.haveCompletionEngine searchUrl
+ # suggestions[0].getHostname suggestions[0].url
+ # else
+ # null
+ # exclusive =
+ # if custom and CompletionEngines.haveCompletionEngine searchUrl
+ # searchUrl.split("%s")?[0]
+ # else
+ # null
+ if queryTerms.length == 0
+ return onComplete suggestions, { exclusive }
onComplete suggestions,
- exclusive: if custom and CompletionEngines.haveCompletionEngine searchUrl then description else null
+ exclusive: exclusive
continuation: (existingSuggestions, onComplete) =>
suggestions = []
# For custom search-engine queries, this adds suggestions only if we have a completer. For other queries,
@@ -491,9 +510,8 @@ class MultiCompleter
# continuation if another query is already waiting. This is for slow tasks which should be done
# asynchronously (e.g. HTTP GET).
continuation: null
- # If truthy, completions from other completers should be suppressed. The truthy value should be the
- # type of the completer (e.g. "custom search"). All other completion types are suppressed.
- exclusive: false
+ # If truthy, non-matching completions from other completers should be suppressed.
+ exclusive: null
(queryTerms, onComplete) ->
# Allow only one query to run at a time.
@@ -518,8 +536,8 @@ class MultiCompleter
console.log completer
activeCompleters = activeCompleters.filter (i) -> i != index
suggestions.push newSuggestions...
- continuation = continuation ? options.continuation
- exclusive = options.exclusive if options.exclusive?
+ continuation ?= options.continuation
+ exclusive ?= options.exclusive
if activeCompleters.length == 0
# All the completers have now returned; we combine the results, post them and call any
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index ff10f4b5..badae126 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -165,10 +165,8 @@ CompletionEngines =
@mostRecentHandler = null
query = queryTerms.join ""
- # We don't complete less then three characters: the results are usually useless. This also prevents
- # one- and two-character custom search engine keywords from being sent to the default completer (e.g.
- # the initial "w" before typing "w something" for Wikipedia).
- return callback [] unless 3 <= query.length
+ # We don't complete single characters: the results are usually useless.
+ return callback [] unless 1 < query.length
# We don't complete regular URLs or Javascript URLs.
return callback [] if 1 == queryTerms.length and Utils.isUrl query
--
cgit v1.2.3
From d94e7c74d78ce788cf3deabcd1c99c3859240566 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Fri, 8 May 2015 17:57:29 +0100
Subject: Search completion; reduce delay.
---
background_scripts/completion_engines.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index badae126..85062c49 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -126,7 +126,7 @@ 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: 200
+ delay: 100
get: (searchUrl, url, callback) ->
xhr = new XMLHttpRequest()
--
cgit v1.2.3
From 8c308d47cc615b9fa0eed47e4ecddd1fd9d125eb Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 06:56:02 +0100
Subject: Search completion; refactor MultiCompleter.
---
background_scripts/completion.coffee | 129 +++++++++++++++++------------------
pages/vomnibar.coffee | 2 +-
2 files changed, 63 insertions(+), 68 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 6e57f0d4..011ff72b 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -401,24 +401,19 @@ class SearchEngineCompleter
# We filter out the empty strings late so that we can distinguish between, for example, "w" and "w ".
queryTerms = queryTerms.filter (t) -> 0 < t.length
- # NOTE(smblott) I'm having difficulty figuring out to do the filtering, here. Exclusive should mean
- # exclusive to what?
- exclusive = if custom and CompletionEngines.haveCompletionEngine searchUrl then description else null
- # exclusive =
- # if custom and CompletionEngines.haveCompletionEngine searchUrl
- # suggestions[0].getHostname suggestions[0].url
- # else
- # null
- # exclusive =
- # if custom and CompletionEngines.haveCompletionEngine searchUrl
- # searchUrl.split("%s")?[0]
- # else
- # null
+
+ # Exclude results from other completers if this is a custom search engine and we have a completer.
+ filter =
+ if custom and CompletionEngines.haveCompletionEngine searchUrl
+ (suggestion) -> suggestion.type == description
+ else
+ null
+
if queryTerms.length == 0
- return onComplete suggestions, { exclusive }
+ return onComplete suggestions, { filter }
onComplete suggestions,
- exclusive: exclusive
+ filter: filter
continuation: (existingSuggestions, onComplete) =>
suggestions = []
# For custom search-engine queries, this adds suggestions only if we have a completer. For other queries,
@@ -504,75 +499,75 @@ class MultiCompleter
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. This is for slow tasks which should be done
- # asynchronously (e.g. HTTP GET).
+ # Completers may provide a continuation function. This will be run after all completers have posted
+ # their suggestions, and is used to post additional (slow) asynchronous suggestions (e.g. search-engine
+ # completions fetched over HTTP).
continuation: null
- # If truthy, non-matching completions from other completers should be suppressed.
- exclusive: null
+ # Completers may provide a filter function. This allows one completer to filter out suggestions from
+ # other completers.
+ filter: null
(queryTerms, onComplete) ->
- # Allow only one query to run at a time.
- if @filterInProgress
- @mostRecentQuery = [ queryTerms, onComplete ]
- return
+ @debug = true
+ # Allow only one query to run at a time, and remember the most recent query.
+ return @mostRecentQuery = arguments if @filterInProgress
+
RegexpCache.clear()
@mostRecentQuery = null
@filterInProgress = true
suggestions = []
- continuation = null
- exclusive = null
+ continuations = []
+ filters = []
activeCompleters = [0...@completers.length]
- # Call filter() on every source completer and wait for them all to finish before returning results.
+
+ # Call filter() on every completer and wait for them all to finish before filtering and posting the
+ # results, then calling any continuations.
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
+ do (index) =>
+ completer.filter queryTerms, (newSuggestions = [], { continuation, filter } = defaultCallbackOptions) =>
+
+ # Store the results.
suggestions.push newSuggestions...
- continuation ?= options.continuation
- exclusive ?= options.exclusive
+ continuations.push continuation if continuation?
+ filters.push filter if filter?
+ activeCompleters = activeCompleters.filter (i) -> i != index
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
+ # All the completers have now yielded their (initial) results, we're good to go.
- # Allow subsequent queries to begin.
- @filterInProgress = false
+ # Apply filters.
+ suggestions = suggestions.filter filter for filter in filters
+
+ # Should we run continuations?
+ shouldRunContinuations = 0 < continuations.length and not @mostRecentQuery?
- # Launch continuation or any pending query.
- if shouldRunContinuation
- continuation suggestions, (newSuggestions) =>
- if 0 < newSuggestions.length
+ # 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
+ onComplete
+ results: @prepareSuggestions queryTerms, suggestions
+ mayCacheResults: continuations.length == 0
+ expectMoreResults: shouldRunContinuations
+
+ # Run any continuations, unless there's a pending query.
+ if shouldRunContinuations
+ for continuation in continuations
+ console.log "launching continuation..." if @debug
+ continuation suggestions, (newSuggestions) =>
+ console.log "posting continuation" if @debug
suggestions.push newSuggestions...
onComplete
results: @prepareSuggestions queryTerms, suggestions
- callerMayCacheResults: true
- else
- if @mostRecentQuery
- console.log "running pending query:", @mostRecentQuery[0]
- @filter @mostRecentQuery...
+ # FIXME(smblott) This currently assumes that there is at most one continuation. We
+ # should really be counting pending/completed continuations.
+ mayCacheResults: true
+ expectMoreResults: false
+
+ # Admit subsequent queries, and launch any pending query.
+ @filterInProgress = false
+ if @mostRecentQuery
+ console.log "running pending query:", @mostRecentQuery[0] if @debug
+ @filter @mostRecentQuery...
prepareSuggestions: (queryTerms, suggestions) ->
suggestion.computeRelevancy queryTerms for suggestion in suggestions
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 3039075c..4781c273 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -261,7 +261,7 @@ class BackgroundCompleter
@completionActions.navigateToUrl.curry result.url
# Cache the results (but only if the background completer tells us that it's ok to do so).
- if msg.callerMayCacheResults
+ if msg.mayCacheResults
console.log "cache set:", msg.query if @debug
@cache.set msg.query, msg.results
else
--
cgit v1.2.3
From 6fd0e15b96325222abf1a19886bd5e0fb48fcdbb Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 08:30:29 +0100
Subject: Search completion; refactor SearchCompleter activation.
---
background_scripts/completion.coffee | 39 ++++++++++++++----------------
background_scripts/main.coffee | 2 +-
lib/utils.coffee | 3 ++-
pages/vomnibar.coffee | 42 ++++++++++-----------------------
tests/unit_tests/completion_test.coffee | 2 +-
5 files changed, 35 insertions(+), 53 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 011ff72b..c1f76b81 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -136,8 +136,7 @@ class BookmarkCompleter
# These bookmarks are loaded asynchronously when refresh() is called.
bookmarks: null
- filter: (queryTerms, @onComplete) ->
- @queryTerms = queryTerms.filter (t) -> 0 < t.length
+ filter: ({ @queryTerms }, @onComplete) ->
@currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
@performSearch() if @bookmarks
@@ -198,8 +197,7 @@ class BookmarkCompleter
RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title)
class HistoryCompleter
- filter: (queryTerms, onComplete) ->
- queryTerms = queryTerms.filter (t) -> 0 < t.length
+ filter: ({ queryTerms }, onComplete) ->
@currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
results = []
HistoryCache.use (history) =>
@@ -233,8 +231,7 @@ class DomainCompleter
# If `referenceCount` goes to zero, the domain entry can and should be deleted.
domains: null
- filter: (queryTerms, onComplete) ->
- queryTerms = queryTerms.filter (t) -> 0 < t.length
+ filter: ({ queryTerms }, onComplete) ->
return onComplete([]) unless queryTerms.length == 1
if @domains
@performSearch(queryTerms, onComplete)
@@ -336,8 +333,7 @@ tabRecency = new TabRecency()
# Searches through all open tabs, matching on title and URL.
class TabCompleter
- filter: (queryTerms, onComplete) ->
- queryTerms = queryTerms.filter (t) -> 0 < t.length
+ filter: ({ queryTerms }, onComplete) ->
# NOTE(philc): We search all tabs, not just those in the current window. I'm not sure if this is the
# correct UX.
chrome.tabs.query {}, (tabs) =>
@@ -371,10 +367,11 @@ class SearchEngineCompleter
handler: "customSearchEngineKeywords"
keywords: key for own key of @searchEngines
- filter: (queryTerms, onComplete) ->
+ filter: ({ queryTerms, query }, onComplete) ->
+ return onComplete [] if queryTerms.length == 0
suggestions = []
- { keyword, searchUrl, description } = @getSearchEngineMatches queryTerms
+ { keyword, searchUrl, description } = @getSearchEngineMatches queryTerms, query
custom = searchUrl? and keyword?
searchUrl ?= Settings.get "searchUrl"
haveDescription = description? and 0 < description.length
@@ -382,7 +379,6 @@ class SearchEngineCompleter
queryTerms = queryTerms[1..] if custom
query = queryTerms.join " "
- return onComplete [] if queryTerms.length == 0
# For custom search engines, we add an auto-selected suggestion.
if custom
@@ -399,9 +395,6 @@ class SearchEngineCompleter
# Suppress the "w" from "w query terms" in the vomnibar input.
suppressLeadingKeyword: true
- # We filter out the empty strings late so that we can distinguish between, for example, "w" and "w ".
- queryTerms = queryTerms.filter (t) -> 0 < t.length
-
# Exclude results from other completers if this is a custom search engine and we have a completer.
filter =
if custom and CompletionEngines.haveCompletionEngine searchUrl
@@ -431,7 +424,6 @@ class SearchEngineCompleter
characterCount = query.length - queryTerms.length + 1
relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
- queryTerms = queryTerms.filter (t) -> 0 < t.length
if 0 < existingSuggestions.length
existingSuggestionsMinScore = existingSuggestions[existingSuggestions.length-1].relevancy
if relavancy < existingSuggestionsMinScore and MultiCompleter.maxResults <= existingSuggestions.length
@@ -457,8 +449,14 @@ class SearchEngineCompleter
count = Math.min 6, Math.max 3, MultiCompleter.maxResults - existingSuggestions.length
onComplete suggestions[...count]
- getSearchEngineMatches: (queryTerms) ->
- (1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {}
+ getSearchEngineMatches: (queryTerms, query = queryTerms.join " ") ->
+ # To allow users to write queries with leading search-engine keywords, leading whitespace disables custom
+ # search engines; for example, " w" is a regular query.
+ return {} if /^\s/.test query
+ # Trailing whitespace is significant when activating a custom search engine; for example, "w" (just a
+ # regular query) is different from "w " (a custom search engine).
+ length = queryTerms.length + (if /\s$/.test query then 1 else 0)
+ (1 < length and @searchEngines[queryTerms[0]]) or {}
# Static data and methods for parsing the configured search engines. We keep a cache of the search-engine
# mapping in @searchEnginesMap.
@@ -507,11 +505,12 @@ class MultiCompleter
# other completers.
filter: null
- (queryTerms, onComplete) ->
+ (request, onComplete) ->
@debug = true
# Allow only one query to run at a time, and remember the most recent query.
return @mostRecentQuery = arguments if @filterInProgress
+ { queryTerms } = request
RegexpCache.clear()
@mostRecentQuery = null
@filterInProgress = true
@@ -524,7 +523,7 @@ class MultiCompleter
# results, then calling any continuations.
for completer, index in @completers
do (index) =>
- completer.filter queryTerms, (newSuggestions = [], { continuation, filter } = defaultCallbackOptions) =>
+ completer.filter request, (newSuggestions = [], { continuation, filter } = defaultCallbackOptions) =>
# Store the results.
suggestions.push newSuggestions...
@@ -546,7 +545,6 @@ class MultiCompleter
unless suggestions.length == 0 and shouldRunContinuations
onComplete
results: @prepareSuggestions queryTerms, suggestions
- mayCacheResults: continuations.length == 0
expectMoreResults: shouldRunContinuations
# Run any continuations, unless there's a pending query.
@@ -560,7 +558,6 @@ class MultiCompleter
results: @prepareSuggestions queryTerms, suggestions
# FIXME(smblott) This currently assumes that there is at most one continuation. We
# should really be counting pending/completed continuations.
- mayCacheResults: true
expectMoreResults: false
# Admit subsequent queries, and launch any pending query.
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 34db5a20..612f6170 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -61,7 +61,7 @@ completers =
completionHandlers =
filter: (completer, request, port) ->
- completer.filter request.queryTerms, (response) ->
+ completer.filter request, (response) ->
port.postMessage extend request, extend response, handler: "completions"
refresh: (completer, _, port) -> completer.refresh port
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 033fdd2b..4c2a7a14 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -201,7 +201,8 @@ Function::curry = ->
Array.copy = (array) -> Array.prototype.slice.call(array, 0)
String::startsWith = (str) -> @indexOf(str) == 0
-String::ltrim = () -> @replace /^\s+/, ""
+String::ltrim = -> @replace /^\s+/, ""
+String::rtrim = -> @replace /\s+$/, ""
globalRoot = window ? global
globalRoot.extend = (hash1, hash2) ->
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 4781c273..bc773909 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -200,12 +200,14 @@ class VomnibarUI
window.clearTimeout @updateTimer
@updateTimer = null
+ isCustomSearch: ->
+ queryTerms = @input.value.ltrim().split /\s+/
+ 1 < queryTerms.length and queryTerms[0] in @keywords
+
update: (updateSynchronously = false, callback = null) =>
- # If the query text is a custom search keyword, then we need to force a synchronous update (so that the
+ # If the query text becomes a custom search, then we need to force a synchronous update (so that the
# interface is snappy).
- if @keywords? and not @suppressedLeadingKeyword?
- queryTerms = @input.value.ltrim().split /\s+/
- updateSynchronously ||= 1 < queryTerms.length and queryTerms[0] in @keywords
+ updateSynchronously ||= @isCustomSearch() and not @suppressedLeadingKeyword?
if updateSynchronously
@updateCompletions callback
else if not @updateTimer?
@@ -242,7 +244,6 @@ class BackgroundCompleter
constructor: (@name) ->
@port = chrome.runtime.connect name: "completions"
@messageId = null
- @cache ?= new SimpleCache 1000 * 60 * 5
@reset()
@port.onMessage.addListener (msg) =>
@@ -260,33 +261,18 @@ class BackgroundCompleter
else
@completionActions.navigateToUrl.curry result.url
- # Cache the results (but only if the background completer tells us that it's ok to do so).
- if msg.mayCacheResults
- console.log "cache set:", msg.query if @debug
- @cache.set msg.query, msg.results
- else
- console.log "not setting cache:", msg.query if @debug
-
# We ignore messages which arrive too late.
if msg.id == @messageId
@mostRecentCallback msg.results
filter: (query, @mostRecentCallback) ->
- # We retain trailing whitespace so that we can tell the difference between "w" and "w " (for custom search
- # engines).
- queryTerms = query.ltrim().split(/\s+/)
- query = queryTerms.join " "
- if @cache.has query
- console.log "cache hit:", query if @debug
- @mostRecentCallback @cache.get query
- else
- @messageId = Utils.createUniqueId()
- @port.postMessage
- name: @name
- handler: "filter"
- id: @messageId
- query: query
- queryTerms: queryTerms
+ queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
+ @port.postMessage
+ handler: "filter"
+ name: @name
+ id: @messageId = Utils.createUniqueId()
+ queryTerms: queryTerms
+ query: query
refresh: (@lastUI) ->
@reset()
@@ -294,8 +280,6 @@ class BackgroundCompleter
@port.postMessage name: @name, handler: "refresh"
reset: ->
- # We only cache results for the duration of a single vomnibar activation, so clear the cache now.
- @cache.clear()
cancel: ->
# Inform the background completer that it may (should it choose to do so) abandon any pending query
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index b1962daf..b9a062f2 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -486,7 +486,7 @@ context "TabRecency",
# A convenience wrapper around completer.filter() so it can be called synchronously in tests.
filterCompleter = (completer, queryTerms) ->
results = []
- completer.filter(queryTerms, (completionResults) -> results = completionResults)
+ completer.filter({ queryTerms }, (completionResults) -> results = completionResults)
results
hours = (n) -> 1000 * 60 * 60 * n
--
cgit v1.2.3
From 3bc555eb02b228f35647884575189eed40625c52 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 08:37:05 +0100
Subject: Search completion; tweaks.
---
background_scripts/completion.coffee | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index c1f76b81..f800c818 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -395,14 +395,16 @@ class SearchEngineCompleter
# Suppress the "w" from "w query terms" in the vomnibar input.
suppressLeadingKeyword: true
- # Exclude results from other completers if this is a custom search engine and we have a completer.
+ haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
+ # If this is a custom search engine and we have a completer, then exclude results from other completers.
filter =
- if custom and CompletionEngines.haveCompletionEngine searchUrl
+ if custom and haveCompletionEngine
(suggestion) -> suggestion.type == description
else
null
- if queryTerms.length == 0
+ # Post suggestions and bail if there is no prospect of adding further suggestions.
+ if queryTerms.length == 0 or not haveCompletionEngine
return onComplete suggestions, { filter }
onComplete suggestions,
--
cgit v1.2.3
From 75051d53536ddb9f247501b4509306cae1734184 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 09:50:13 +0100
Subject: Search completion; reintroduce vomnibar cache.
---
background_scripts/completion.coffee | 4 +--
pages/vomnibar.coffee | 47 +++++++++++++++++++++++++++---------
2 files changed, 37 insertions(+), 14 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index f800c818..8c73c658 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -547,7 +547,7 @@ class MultiCompleter
unless suggestions.length == 0 and shouldRunContinuations
onComplete
results: @prepareSuggestions queryTerms, suggestions
- expectMoreResults: shouldRunContinuations
+ mayCacheResult: continuations.length == 0
# Run any continuations, unless there's a pending query.
if shouldRunContinuations
@@ -560,7 +560,7 @@ class MultiCompleter
results: @prepareSuggestions queryTerms, suggestions
# FIXME(smblott) This currently assumes that there is at most one continuation. We
# should really be counting pending/completed continuations.
- expectMoreResults: false
+ mayCacheResult: true
# Admit subsequent queries, and launch any pending query.
@filterInProgress = false
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index bc773909..bb7720e9 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -244,12 +244,17 @@ class BackgroundCompleter
constructor: (@name) ->
@port = chrome.runtime.connect name: "completions"
@messageId = null
+ # @keywords and @cache are both reset in @reset().
+ # We only cache for the duration of a single vomnibar activation.
+ @keywords = []
+ @cache = {}
@reset()
@port.onMessage.addListener (msg) =>
switch msg.handler
when "customSearchEngineKeywords"
- @lastUI.setKeywords msg.keywords
+ @keywords = msg.keywords
+ @lastUI.setKeywords @keywords
when "completions"
# The result objects coming from the background page will be of the form:
# { html: "", type: "", url: "" }
@@ -261,26 +266,44 @@ class BackgroundCompleter
else
@completionActions.navigateToUrl.curry result.url
- # We ignore messages which arrive too late.
- if msg.id == @messageId
- @mostRecentCallback msg.results
+ # Cache the result -- if we have been told it's ok to do so (it could be that more results will be
+ # posted shortly). We cache the result even if it arrives late.
+ if msg.mayCacheResult
+ console.log "cache set:", "-#{msg.cacheKey}-" if @debug
+ @cache[msg.cacheKey] = msg.results
+ else
+ console.log "not setting cache:", "-#{msg.cacheKey}-" if @debug
+
+ # Handle the message, but only if it hasn't arrived too late.
+ @mostRecentCallback msg.results if msg.id == @messageId
filter: (query, @mostRecentCallback) ->
queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
- @port.postMessage
- handler: "filter"
- name: @name
- id: @messageId = Utils.createUniqueId()
- queryTerms: queryTerms
- query: query
+ cacheKey = queryTerms.join " "
+ cacheKey += " " if 0 < queryTerms.length and queryTerms[0] in @keywords and /\s$/.test query
+
+ if cacheKey of @cache
+ console.log "cache hit:", "-#{cacheKey}-" if @debug
+ @mostRecentCallback @cache[cacheKey]
+ else
+ console.log "cache miss:", "-#{cacheKey}-" if @debug
+ @port.postMessage
+ handler: "filter"
+ name: @name
+ id: @messageId = Utils.createUniqueId()
+ queryTerms: queryTerms
+ query: query
+ cacheKey: cacheKey
+
+ reset: ->
+ @keywords = []
+ @cache = {}
refresh: (@lastUI) ->
@reset()
# Inform the background completer that we have a new vomnibar activation.
@port.postMessage name: @name, handler: "refresh"
- reset: ->
-
cancel: ->
# Inform the background completer that it may (should it choose to do so) abandon any pending query
# (because the user is typing, and there'll be another query along soon).
--
cgit v1.2.3
From d73775057d443a53668f6a93fe45cc4a4b412de7 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 11:30:59 +0100
Subject: Search completion; complete commmon search term.
---
background_scripts/completion.coffee | 4 +-
pages/vomnibar.coffee | 148 ++++++++++++++++++++++++++++-------
pages/vomnibar.css | 4 +
pages/vomnibar.html | 2 +-
tests/dom_tests/vomnibar_test.coffee | 2 +-
5 files changed, 129 insertions(+), 31 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 8c73c658..850a257d 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -547,7 +547,7 @@ class MultiCompleter
unless suggestions.length == 0 and shouldRunContinuations
onComplete
results: @prepareSuggestions queryTerms, suggestions
- mayCacheResult: continuations.length == 0
+ mayCacheResults: continuations.length == 0
# Run any continuations, unless there's a pending query.
if shouldRunContinuations
@@ -560,7 +560,7 @@ class MultiCompleter
results: @prepareSuggestions queryTerms, suggestions
# FIXME(smblott) This currently assumes that there is at most one continuation. We
# should really be counting pending/completed continuations.
- mayCacheResult: true
+ mayCacheResults: true
# Admit subsequent queries, and launch any pending query.
@filterInProgress = false
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index bb7720e9..db380063 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -69,6 +69,7 @@ class VomnibarUI
@previousAutoSelect = null
@previousInputValue = null
@suppressedLeadingKeyword = null
+ @previousLength = 0
@selection = @initialSelectionValue
@keywords = []
@@ -92,7 +93,7 @@ class VomnibarUI
# 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.
if 0 <= @selection and @completions[@selection].insertText?
- @previousInputValue ?= @input.value
+ @previousInputValue ?= @getInputWithoutSelectionRange()
@input.value = @completions[@selection].insertText + " "
else if @previousInputValue?
@input.value = @previousInputValue
@@ -102,8 +103,66 @@ class VomnibarUI
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
+ highlightCommonMatches: (response) ->
+ # For custom search engines, add characters to the input which are:
+ # - not in the query/input
+ # - in all completions
+ # and select the added text.
+
+ # 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 @input and the correct completion state are out of sync).
+ return if @updateTimer?
+
+ @previousLength ?= @input.value.length
+ previousLength = @previousLength
+ currentLength = @input.value.length
+ @previousLength = currentLength
+
+ # Bail if the query didn't get longer.
+ console.log previousLength < currentLength, previousLength, currentLength, @input.value
+ return unless previousLength < currentLength
+
+ # Bail if these aren't completions from a custom search engine.
+ return unless @suppressedLeadingKeyword?
+
+ # Bail if there are too few suggestions.
+ return unless 1 < @completions.length
+
+ # Fetch the query and the suggestion texts.
+ query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
+ suggestions = @completions[1..].map (completion) -> completion.title
+
+ # Ensure that the query is a prefix of all suggestions.
+ for suggestion in suggestions
+ return unless 0 == suggestion.toLowerCase().indexOf query
+
+ # Calculate the length of the shotest suggestion.
+ length = suggestions[0].length
+ length = Math.min length, suggestion.length for suggestion in suggestions
+
+ # Find the thenght of the longest common continuation.
+ length = do ->
+ for index in [query.length...length]
+ for suggestion in suggestions
+ return index if suggestions[0][index].toLowerCase() != suggestion[index].toLowerCase()
+ length
+
+ # But don't complete only whitespace.
+ return if /^\s+$/.test suggestions[0].slice query.length, length
+
+ # Bail if there's nothing to complete.
+ return unless query.length < length
+
+ # Install completion.
+ @input.value = suggestions[0].slice 0, length
+ @input.setSelectionRange query.length, length
+ # @previousLength = @input.value.length
+
#
- # Returns the user's action ("up", "down", "enter", "dismiss", "delete" or null) based on their keypress.
+ # Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
+ # keypress.
# We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
#
actionFromKeyEvent: (event) ->
@@ -114,8 +173,9 @@ class VomnibarUI
(event.shiftKey && event.keyCode == keyCodes.tab) ||
(event.ctrlKey && (key == "k" || key == "p")))
return "up"
+ else if (event.keyCode == keyCodes.tab && !event.shiftKey)
+ return "tab"
else if (key == "down" ||
- (event.keyCode == keyCodes.tab && !event.shiftKey) ||
(event.ctrlKey && (key == "j" || key == "n")))
return "down"
else if (event.keyCode == keyCodes.enter)
@@ -132,19 +192,38 @@ class VomnibarUI
(event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event))
if (action == "dismiss")
@hide()
+ else if action in [ "tab", "down" ]
+ if action == "tab"
+ if @inputContainsASelectionRange()
+ # There is a selection: callapse it and update the completions.
+ window.getSelection().collapseToEnd()
+ @update true
+ else
+ # There is no selection: treat "tab" as "down".
+ action = "down"
+ if action == "down"
+ @selection += 1
+ @selection = @initialSelectionValue if @selection == @completions.length
+ @updateSelection()
else if (action == "up")
@selection -= 1
@selection = @completions.length - 1 if @selection < @initialSelectionValue
@updateSelection()
- else if (action == "down")
- @selection += 1
- @selection = @initialSelectionValue if @selection == @completions.length
- @updateSelection()
else if (action == "enter")
- # If they type something and hit enter without selecting a completion from our list of suggestions,
- # try to open their query as a URL directly. If it doesn't look like a URL, we will search using
- # google.
- if (@selection == -1)
+ if @inputContainsASelectionRange()
+ # There is selected completion text in the input, put there by highlightCommonMatches(). It looks to
+ # the user like, if they type "enter", then that's the query which will fire. But we don't actually
+ # have a URL for this query (it doesn't actually correspond to any of the current completions). So we
+ # fire off a new query and immediately launch the first resulting URL.
+ @update true, =>
+ if @completions[0]?
+ completion = @completions[0]
+ @hide -> completion.performAction openInNewTab
+
+ # If the user types something and hits enter without selecting a completion from the list, then try to
+ # open their query as a URL directly. If it doesn't look like a URL, then use the default search
+ # engine.
+ else if (@selection == -1)
query = @input.value.trim()
# on an empty vomnibar is a no-op.
return unless 0 < query.length
@@ -169,23 +248,35 @@ class VomnibarUI
event.preventDefault()
true
- getInputValue: ->
+ # Test whether the input contains selected text.
+ inputContainsASelectionRange: ->
+ @input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd
+
+ # Return the text of the input, with any selected text renage removed.
+ getInputWithoutSelectionRange: ->
+ 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
+ # custom search engine keyword which is currently stripped from the input.
+ getInputValueAsQuery: ->
(if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @input.value
updateCompletions: (callback = null) ->
- @clearUpdateTimer()
- @completer.filter @getInputValue(), (@completions) =>
- @populateUiWithCompletions @completions
+ @completer.filter @getInputValueAsQuery(), (response) =>
+ { results, mayCacheResults } = response
+ @completions = results
+ # Update completion list with the new suggestions.
+ @completionList.innerHTML = @completions.map((completion) -> "
#{completion.html}
").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()
+ @highlightCommonMatches response
callback?()
- populateUiWithCompletions: (completions) ->
- # Update completion list with the new suggestions.
- @completionList.innerHTML = completions.map((completion) -> "
#{completion.html}
").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: =>
@completer.cancel()
# If the user types, then don't reset any previous text, and re-enable auto-select.
@@ -209,11 +300,14 @@ class VomnibarUI
# interface is snappy).
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
# keystroke will cause a dedicated update)
- @updateTimer = Utils.setTimeout @refreshInterval, => @updateCompletions callback
+ @updateTimer = Utils.setTimeout @refreshInterval, =>
+ @updateTimer = null
+ @updateCompletions callback
@input.focus()
@@ -268,14 +362,14 @@ class BackgroundCompleter
# Cache the result -- if we have been told it's ok to do so (it could be that more results will be
# posted shortly). We cache the result even if it arrives late.
- if msg.mayCacheResult
+ if msg.mayCacheResults
console.log "cache set:", "-#{msg.cacheKey}-" if @debug
- @cache[msg.cacheKey] = msg.results
+ @cache[msg.cacheKey] = msg
else
console.log "not setting cache:", "-#{msg.cacheKey}-" if @debug
# Handle the message, but only if it hasn't arrived too late.
- @mostRecentCallback msg.results if msg.id == @messageId
+ @mostRecentCallback msg if msg.id == @messageId
filter: (query, @mostRecentCallback) ->
queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
diff --git a/pages/vomnibar.css b/pages/vomnibar.css
index 2042a6c4..4b7199e3 100644
--- a/pages/vomnibar.css
+++ b/pages/vomnibar.css
@@ -134,3 +134,7 @@
font-weight: normal;
}
+#vomnibarInput::selection {
+ /* This is the light grey color of the vomnibar border. */
+ background-color: #F1F1F1;
+}
diff --git a/pages/vomnibar.html b/pages/vomnibar.html
index 2ca463d0..87acc081 100644
--- a/pages/vomnibar.html
+++ b/pages/vomnibar.html
@@ -14,7 +14,7 @@
-
+
diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee
index 0e02bb7b..e32c050d 100644
--- a/tests/dom_tests/vomnibar_test.coffee
+++ b/tests/dom_tests/vomnibar_test.coffee
@@ -14,7 +14,7 @@ context "Keep selection within bounds",
oldGetCompleter = vomnibarFrame.Vomnibar.getCompleter.bind vomnibarFrame.Vomnibar
stub vomnibarFrame.Vomnibar, 'getCompleter', (name) =>
completer = oldGetCompleter name
- stub completer, 'filter', (query, callback) => callback(@completions)
+ stub completer, 'filter', (query, callback) => callback results: @completions
completer
# Shoulda.js doesn't support async tests, so we have to hack around.
--
cgit v1.2.3
From 311b35e416053a0d5d03eaf7eb894375f6e0f20d Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 16:47:28 +0100
Subject: Search completion; tweaks and tests.
---
background_scripts/completion.coffee | 28 ++++++++-------
pages/vomnibar.coffee | 64 ++++++++++++++++++---------------
pages/vomnibar.css | 5 ++-
tests/unit_tests/completion_test.coffee | 4 +--
4 files changed, 57 insertions(+), 44 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 850a257d..7966452d 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -375,33 +375,37 @@ class SearchEngineCompleter
custom = searchUrl? and keyword?
searchUrl ?= Settings.get "searchUrl"
haveDescription = description? and 0 < description.length
- description ||= "#{if custom then "custom " else ""}search"
+ description ||= "search#{if custom then " [#{keyword}]" else ""}"
queryTerms = queryTerms[1..] if custom
query = queryTerms.join " "
+ haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
+ # If this is a custom search engine and we have a completer, then exclude results from other completers.
+ filter =
+ if custom and haveCompletionEngine
+ (suggestion) -> suggestion.type == description
+ else
+ null
+
# For custom search engines, we add an auto-selected suggestion.
if custom
suggestions.push new Suggestion
queryTerms: queryTerms
type: description
url: Utils.createSearchUrl queryTerms, searchUrl
- title: if haveDescription then query else "#{keyword}: #{query}"
+ title: query
relevancy: 1
highlightTerms: false
- autoSelect: true
+ insertText: query
+ # NOTE (smblott) Disbaled pending consideration of how to handle text selection within the vomnibar
+ # itself.
+ # autoSelect: true
# Always reset the selection to this suggestion on query change. The UX is weird otherwise.
- forceAutoSelect: true
+ # forceAutoSelect: true
# Suppress the "w" from "w query terms" in the vomnibar input.
suppressLeadingKeyword: true
-
- haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
- # If this is a custom search engine and we have a completer, then exclude results from other completers.
- filter =
- if custom and haveCompletionEngine
- (suggestion) -> suggestion.type == description
- else
- null
+ completeSuggestions: filter?
# Post suggestions and bail if there is no prospect of adding further suggestions.
if queryTerms.length == 0 or not haveCompletionEngine
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index db380063..b53028ca 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -93,13 +93,22 @@ class VomnibarUI
# 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.
if 0 <= @selection and @completions[@selection].insertText?
- @previousInputValue ?= @getInputWithoutSelectionRange()
- @input.value = @completions[@selection].insertText + " "
+ @previousInputValue ?=
+ value: @input.value
+ selectionStart: @input.selectionStart
+ selectionEnd: @input.selectionEnd
+ @input.value = @completions[@selection].insertText + (if @selection == 0 then "" else " ")
else if @previousInputValue?
- @input.value = @previousInputValue
+ @input.value = @previousInputValue.value
+ if @previousInputValue.selectionStart? and @previousInputValue.selectionEnd? and
+ @previousInputValue.selectionStart != @previousInputValue.selectionEnd
+ @input.setSelectionRange @previousInputValue.selectionStart, @previousInputValue.selectionEnd
@previousInputValue = null
# Highlight the the selected entry, and only the selected entry.
+ @highlightTheSelectedEntry()
+
+ highlightTheSelectedEntry: ->
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
@@ -121,11 +130,10 @@ class VomnibarUI
@previousLength = currentLength
# Bail if the query didn't get longer.
- console.log previousLength < currentLength, previousLength, currentLength, @input.value
return unless previousLength < currentLength
- # Bail if these aren't completions from a custom search engine.
- return unless @suppressedLeadingKeyword?
+ # Bail if these aren't completions from a custom search engine with completion.
+ return unless @suppressedLeadingKeyword? and @completions[0]?.completeSuggestions
# Bail if there are too few suggestions.
return unless 1 < @completions.length
@@ -158,7 +166,6 @@ class VomnibarUI
# Install completion.
@input.value = suggestions[0].slice 0, length
@input.setSelectionRange query.length, length
- # @previousLength = @input.value.length
#
# Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
@@ -195,11 +202,10 @@ class VomnibarUI
else if action in [ "tab", "down" ]
if action == "tab"
if @inputContainsASelectionRange()
- # There is a selection: callapse it and update the completions.
- window.getSelection().collapseToEnd()
- @update true
+ # The first tab collapses the selection to the end.
+ window.getSelection()?.collapseToEnd()
else
- # There is no selection: treat "tab" as "down".
+ # Subsequent tabs behave the same as "down".
action = "down"
if action == "down"
@selection += 1
@@ -210,27 +216,27 @@ class VomnibarUI
@selection = @completions.length - 1 if @selection < @initialSelectionValue
@updateSelection()
else if (action == "enter")
- if @inputContainsASelectionRange()
- # There is selected completion text in the input, put there by highlightCommonMatches(). It looks to
- # the user like, if they type "enter", then that's the query which will fire. But we don't actually
- # have a URL for this query (it doesn't actually correspond to any of the current completions). So we
- # fire off a new query and immediately launch the first resulting URL.
- @update true, =>
- if @completions[0]?
- completion = @completions[0]
- @hide -> completion.performAction openInNewTab
-
- # If the user types something and hits enter without selecting a completion from the list, then try to
- # open their query as a URL directly. If it doesn't look like a URL, then use the default search
- # engine.
- else if (@selection == -1)
+ if @selection == -1
query = @input.value.trim()
# on an empty vomnibar is a no-op.
return unless 0 < query.length
- @hide ->
- chrome.runtime.sendMessage
- handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
- url: query
+ if @suppressedLeadingKeyword?
+ # This is a custom search engine completion. Because of the way we add the text common to all
+ # completions to the input (highlighted), the text in the input might not correspond to any of the
+ # completions. So we fire the query off to the background page and use the completion at the top of
+ # the list (which will be the right one).
+ @update true, =>
+ if @completions[0]?
+ completion = @completions[0]
+ @hide -> completion.performAction openInNewTab
+ else
+ # If the user types something and hits enter without selecting a completion from the list, then try
+ # to open their query as a URL directly. If it doesn't look like a URL, then use the default search
+ # engine.
+ @hide ->
+ chrome.runtime.sendMessage
+ handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
+ url: query
else
completion = @completions[@selection]
@hide -> completion.performAction openInNewTab
diff --git a/pages/vomnibar.css b/pages/vomnibar.css
index 4b7199e3..dbcaf6e4 100644
--- a/pages/vomnibar.css
+++ b/pages/vomnibar.css
@@ -136,5 +136,8 @@
#vomnibarInput::selection {
/* This is the light grey color of the vomnibar border. */
- background-color: #F1F1F1;
+ /* background-color: #F1F1F1; */
+
+ /* This is the light blue color of the vomnibar selected item. */
+ background-color: #BBCEE9;
}
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index b9a062f2..b45c99dd 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -248,8 +248,8 @@ context "search engines",
should "return search engine suggestion without description", ->
results = filterCompleter(@completer, ["foo", "hello"])
assert.arrayEqual ["bar?q=hello"], results.map (result) -> result.url
- assert.arrayEqual ["foo: hello"], results.map (result) -> result.title
- assert.arrayEqual ["custom search"], results.map (result) -> result.type
+ assert.arrayEqual ["hello"], results.map (result) -> result.title
+ assert.arrayEqual ["search [foo]"], results.map (result) -> result.type
should "return search engine suggestion with description", ->
results = filterCompleter(@completer, ["baz", "hello"])
--
cgit v1.2.3
From 0c6b6e53d60a1c4b694d1515fdb7e43080bbf0d3 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 16:53:41 +0100
Subject: Search completion; tweak default setting.
---
background_scripts/settings.coffee | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 89b26bff..e5604b78 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -93,7 +93,9 @@ root.Settings = Settings =
searchEngines: [
"w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
""
- "# More examples:"
+ "# More examples."
+ "#"
+ "# (Vimium has built-in completion for these.)"
"#"
"# g: http://www.google.com/search?q=%s Google"
"# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
--
cgit v1.2.3
From b332710f8395582809b1a1e2c436628b7f6e8c2a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 17:04:40 +0100
Subject: Search completion; tweak default setting.
---
background_scripts/settings.coffee | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index e5604b78..44ed897d 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -100,10 +100,13 @@ 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..."
"# y: http://www.youtube.com/results?search_query=%s Youtube"
- "# 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"
"# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
+ "#"
+ "# Another example (for Vimium does not have completion)."
+ "#"
+ "# m: https://www.google.com/maps/search/%s Google Maps"
].join "\n"
newTabUrl: "chrome://newtab"
grabBackFocus: false
--
cgit v1.2.3
From 275a91f203086b8f81542c174e736748dce68628 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sat, 9 May 2015 18:06:12 +0100
Subject: Search completion; tweak for engines without completers.
---
background_scripts/completion.coffee | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 7966452d..cc334d78 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -382,6 +382,8 @@ class SearchEngineCompleter
haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
# If this is a custom search engine and we have a completer, then exclude results from other completers.
+ # The truthiness of filter? also serves to distinguish two very different vomnibar behaviors, one for
+ # custom search engines with completers (filter? is true), and one for those without (filter? is false).
filter =
if custom and haveCompletionEngine
(suggestion) -> suggestion.type == description
@@ -390,22 +392,26 @@ class SearchEngineCompleter
# For custom search engines, we add an auto-selected suggestion.
if custom
+ # Note. This suggestion always appears at the top of the suggestion list. Its settings serves to serve
+ # to configure various vomnibar behaviors.
suggestions.push new Suggestion
queryTerms: queryTerms
type: description
url: Utils.createSearchUrl queryTerms, searchUrl
title: query
relevancy: 1
- highlightTerms: false
- insertText: query
- # NOTE (smblott) Disbaled pending consideration of how to handle text selection within the vomnibar
- # itself.
- # autoSelect: true
- # Always reset the selection to this suggestion on query change. The UX is weird otherwise.
- # forceAutoSelect: true
- # Suppress the "w" from "w query terms" in the vomnibar input.
+ insertText: if filter? then query else null
+ # For all custom search engines, we suppress the leading keyword, for example "w something" becomes
+ # "something" in the vomnibar.
suppressLeadingKeyword: true
+ # We complete suggestions only for custom search engines where we have an associated completion
+ # engine.
completeSuggestions: filter?
+ # We only use autoSelect and highlight query terms (on custom search engines) when we do not have a
+ # completer.
+ autoSelect: not filter?
+ forceAutoSelect: not filter?
+ highlightTerms: not filter?
# Post suggestions and bail if there is no prospect of adding further suggestions.
if queryTerms.length == 0 or not haveCompletionEngine
--
cgit v1.2.3
From 313a1f96d666f23c2bc75ef340f0f828319e127c Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 05:35:50 +0100
Subject: Search completion; refactor searchEngineCompleter.
This revamps how search-engine configuration is handled, and revises
some rather strange legacy code.
---
background_scripts/completion.coffee | 254 ++++++++++++++++----------------
background_scripts/settings.coffee | 3 -
lib/utils.coffee | 16 ++
tests/unit_tests/completion_test.coffee | 21 ---
tests/unit_tests/settings_test.coffee | 9 --
5 files changed, 139 insertions(+), 164 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index cc334d78..94109b84 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -355,143 +355,135 @@ class TabCompleter
tabRecency.recencyScore(suggestion.tabId)
class SearchEngineCompleter
- searchEngines: {}
-
- cancel: ->
- CompletionEngines.cancel()
+ searchEngineConfig: null
refresh: (port) ->
- @searchEngines = SearchEngineCompleter.getSearchEngines()
- # Let the vomnibar know the custom search engine keywords.
- port.postMessage
- handler: "customSearchEngineKeywords"
- keywords: key for own key of @searchEngines
+ # Load and parse the search-engine configuration.
+ @searchEngineConfig = new AsyncDataFetcher (callback) ->
+ engines = {}
+ for line in Settings.get("searchEngines").split "\n"
+ line = line.trim()
+ continue if /^[#"]/.test line
+ tokens = line.split /\s+/
+ continue unless 2 <= tokens.length
+ keyword = tokens[0].split(":")[0]
+ description = tokens[2..].join(" ") || "search (#{keyword})"
+ engines[keyword] =
+ keyword: keyword
+ searchUrl: tokens[1]
+ description: description
+
+ # Deliver the resulting engines lookup table.
+ callback engines
+
+ # Let the vomnibar know the custom search engine keywords.
+ port.postMessage
+ handler: "customSearchEngineKeywords"
+ keywords: key for own key of engines
filter: ({ queryTerms, query }, onComplete) ->
return onComplete [] if queryTerms.length == 0
- suggestions = []
-
- { keyword, searchUrl, description } = @getSearchEngineMatches queryTerms, query
- custom = searchUrl? and keyword?
- searchUrl ?= Settings.get "searchUrl"
- haveDescription = description? and 0 < description.length
- description ||= "search#{if custom then " [#{keyword}]" else ""}"
-
- queryTerms = queryTerms[1..] if custom
- query = queryTerms.join " "
-
- haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
- # If this is a custom search engine and we have a completer, then exclude results from other completers.
- # The truthiness of filter? also serves to distinguish two very different vomnibar behaviors, one for
- # custom search engines with completers (filter? is true), and one for those without (filter? is false).
- filter =
- if custom and haveCompletionEngine
- (suggestion) -> suggestion.type == description
- else
- null
- # For custom search engines, we add an auto-selected suggestion.
- if custom
- # Note. This suggestion always appears at the top of the suggestion list. Its settings serves to serve
- # to configure various vomnibar behaviors.
- suggestions.push new Suggestion
- queryTerms: queryTerms
- type: description
- url: Utils.createSearchUrl queryTerms, searchUrl
- title: query
- relevancy: 1
- insertText: if filter? then query else null
- # For all custom search engines, we suppress the leading keyword, for example "w something" becomes
- # "something" in the vomnibar.
- suppressLeadingKeyword: true
- # We complete suggestions only for custom search engines where we have an associated completion
- # engine.
- completeSuggestions: filter?
- # We only use autoSelect and highlight query terms (on custom search engines) when we do not have a
- # completer.
- autoSelect: not filter?
- forceAutoSelect: not filter?
- highlightTerms: not filter?
-
- # Post suggestions and bail if there is no prospect of adding further suggestions.
- if queryTerms.length == 0 or not haveCompletionEngine
- return onComplete suggestions, { filter }
-
- onComplete suggestions,
- filter: filter
- 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: 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]
-
- getSearchEngineMatches: (queryTerms, query = queryTerms.join " ") ->
- # To allow users to write queries with leading search-engine keywords, leading whitespace disables custom
- # search engines; for example, " w" is a regular query.
- return {} if /^\s/.test query
- # Trailing whitespace is significant when activating a custom search engine; for example, "w" (just a
- # regular query) is different from "w " (a custom search engine).
- length = queryTerms.length + (if /\s$/.test query then 1 else 0)
- (1 < length and @searchEngines[queryTerms[0]]) or {}
-
- # Static data and methods for parsing the configured search engines. We keep a cache of the search-engine
- # mapping in @searchEnginesMap.
- @searchEnginesMap: null
-
- # Parse the custom search engines setting and cache it in SearchEngineCompleter.searchEnginesMap.
- @parseSearchEngines: (searchEnginesText) ->
- searchEnginesMap = SearchEngineCompleter.searchEnginesMap = {}
- for line in searchEnginesText.split /\n/
- tokens = line.trim().split /\s+/
- continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#")
- keywords = tokens[0].split ":"
- continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ].
- searchEnginesMap[keywords[0]] =
- keyword: keywords[0]
- searchUrl: tokens[1]
- description: tokens[2..].join(" ")
-
- # Fetch the search-engine map, building it if necessary.
- @getSearchEngines: ->
- unless SearchEngineCompleter.searchEnginesMap?
- SearchEngineCompleter.parseSearchEngines Settings.get "searchEngines"
- SearchEngineCompleter.searchEnginesMap
+ @searchEngineConfig.use (engines) =>
+ keyword = queryTerms[0]
+
+ { custom, searchUrl, description, queryTerms } =
+ if engines[keyword]? and (1 < queryTerms.length or /\s$/.test query)
+ { searchUrl, description } = engines[keyword]
+ custom: true
+ searchUrl: searchUrl
+ description: description
+ queryTerms: queryTerms[1..]
+ else
+ custom: false
+ searchUrl: Settings.get "searchUrl"
+ description: "search"
+ queryTerms: queryTerms
+
+ query = queryTerms.join " "
+ haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
+
+ # 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
+
+ # 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
+
+ suggestions = []
+
+ # For custom search engines, we add a single, top-ranked entry for the unmodified query. This
+ # suggestion always appears at the top of the suggestion list. Its setting serve to define various
+ # vomnibar behaviors.
+ if custom
+ suggestions.push new Suggestion
+ queryTerms: queryTerms
+ type: description
+ url: Utils.createSearchUrl queryTerms, searchUrl
+ title: query
+ relevancy: 1
+ insertText: if version2 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
+ # Toggles for the legacy behaviour.
+ autoSelect: not version2
+ forceAutoSelect: not version2
+ highlightTerms: not version2
+
+ # Post suggestions and bail if there is no prospect of adding further suggestions.
+ if queryTerms.length == 0 or not haveCompletionEngine
+ return onComplete suggestions, { filter }
+
+ # Post any initial suggestion, and then deliver suggestions from completion engines as a continuation
+ # (so, asynchronously).
+ onComplete suggestions,
+ filter: filter
+ continuation: (existingSuggestions, onComplete) =>
+ suggestions = []
+ # 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: 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]
+
+ cancel: ->
+ CompletionEngines.cancel()
# 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.
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 44ed897d..11f492d7 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -32,9 +32,6 @@ root.Settings = Settings =
root.Commands.parseCustomKeyMappings value
root.refreshCompletionKeysAfterMappingSave()
- searchEngines: (value) ->
- root.SearchEngineCompleter.parseSearchEngines value
-
exclusionRules: (value) ->
root.Exclusions.postUpdateHook value
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 4c2a7a14..51b16351 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -260,6 +260,22 @@ class SimpleCache
delete @previous[key]
value
+# This is a simple class for the common case where we want to use some data value which may be immediately
+# available, or we may have to wait. It implements the use-immediately-or-wait queue, and calls the function
+# to fetch the data asynchronously.
+class AsyncDataFetcher
+ constructor: (fetch) ->
+ @data = null
+ @queue = []
+ fetch (@data) =>
+ Utils.nextTick =>
+ callback @data for callback in @queue
+ @queue = null
+
+ use: (callback) ->
+ if @data? then callback @data else @queue.push callback
+
root = exports ? window
root.Utils = Utils
root.SimpleCache = SimpleCache
+root.AsyncDataFetcher = AsyncDataFetcher
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index b45c99dd..7711dac4 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -236,27 +236,6 @@ context "tab completer",
assert.arrayEqual ["tab2.com"], results.map (tab) -> tab.url
assert.arrayEqual [2], results.map (tab) -> tab.tabId
-context "search engines",
- setup ->
- searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
- Settings.set 'searchEngines', searchEngines
- @completer = new SearchEngineCompleter()
- # note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors
- # workaround is below, would be good for someone that understands the testing system better than me to improve
- @completer.searchEngines = SearchEngineCompleter.getSearchEngines()
-
- should "return search engine suggestion without description", ->
- results = filterCompleter(@completer, ["foo", "hello"])
- assert.arrayEqual ["bar?q=hello"], results.map (result) -> result.url
- assert.arrayEqual ["hello"], results.map (result) -> result.title
- assert.arrayEqual ["search [foo]"], results.map (result) -> result.type
-
- should "return search engine suggestion with description", ->
- results = filterCompleter(@completer, ["baz", "hello"])
- # assert.arrayEqual ["qux?q=hello"], results.map (result) -> result.searchUrl
- # assert.arrayEqual ["hello"], results.map (result) -> result.title
- # assert.arrayEqual ["baz description"], results.map (result) -> result.type
-
context "suggestions",
should "escape html in page titles", ->
suggestion = new Suggestion
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 52eec30d..4cd20211 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -70,14 +70,5 @@ context "settings",
chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) }
assert.equal message, Sync.message
- should "set search engines, retrieve them correctly and check that they have been parsed correctly", ->
- searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
- Settings.set 'searchEngines', searchEngines
- result = SearchEngineCompleter.getSearchEngines()
- assert.equal "bar?q=%s", result["foo"].searchUrl
- assert.isFalse result["foo"].description
- assert.equal "qux?q=%s", result["baz"].searchUrl
- assert.equal "baz description", result["baz"].description
-
should "sync a key which is not a known setting (without crashing)", ->
chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") }
--
cgit v1.2.3
From 5fdbb8e579c068a54e9a397097d87063a3d8a146 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 05:57:47 +0100
Subject: Search completion; rework SimpleCache.
---
background_scripts/completion.coffee | 16 ++++++------
lib/utils.coffee | 49 +++++++++++++++++++-----------------
pages/vomnibar.coffee | 2 +-
3 files changed, 35 insertions(+), 32 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 94109b84..024ea54c 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -355,11 +355,14 @@ class TabCompleter
tabRecency.recencyScore(suggestion.tabId)
class SearchEngineCompleter
- searchEngineConfig: null
+ searchEngines: null
+
+ cancel: ->
+ CompletionEngines.cancel()
refresh: (port) ->
# Load and parse the search-engine configuration.
- @searchEngineConfig = new AsyncDataFetcher (callback) ->
+ @searchEngines = new AsyncDataFetcher (callback) ->
engines = {}
for line in Settings.get("searchEngines").split "\n"
line = line.trim()
@@ -373,18 +376,18 @@ class SearchEngineCompleter
searchUrl: tokens[1]
description: description
- # Deliver the resulting engines lookup table.
+ # Deliver the resulting engines AsyncDataFetcher lookup table.
callback engines
# Let the vomnibar know the custom search engine keywords.
port.postMessage
- handler: "customSearchEngineKeywords"
+ handler: "keywords"
keywords: key for own key of engines
filter: ({ queryTerms, query }, onComplete) ->
return onComplete [] if queryTerms.length == 0
- @searchEngineConfig.use (engines) =>
+ @searchEngines.use (engines) =>
keyword = queryTerms[0]
{ custom, searchUrl, description, queryTerms } =
@@ -482,9 +485,6 @@ class SearchEngineCompleter
count = Math.min 6, Math.max 3, MultiCompleter.maxResults - existingSuggestions.length
onComplete suggestions[...count]
- cancel: ->
- CompletionEngines.cancel()
-
# 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
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 51b16351..b0abfd8f 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -212,24 +212,45 @@ globalRoot.extend = (hash1, hash2) ->
# A simple cache. Entries used within two expiry periods are retained, otherwise they are discarded.
# At most 2 * @entries entries are retained.
+#
+# Note. We need to be careful with @timer. If all references to a cache are lost, then eventually its
+# contents must be garbage collected, which will not happen if there are active timers.
class SimpleCache
# expiry: expiry time in milliseconds (default, one hour)
# entries: maximum number of entries in @cache (there may be this many entries in @previous, too)
constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
@cache = {}
- @rotate() # Force start the rotation timer.
+ @previous = {}
+ @timer = null
rotate: ->
@previous = @cache
@cache = {}
# We reset the timer every time the cache is rotated (which could be because a previous timer expired, or
- # because the number of @entries was exceeded.
+ # because the number of @entries was exceeded). We only restart the timer if the cache is not empty.
clearTimeout @timer if @timer?
- @timer = Utils.setTimeout @expiry, => @rotate()
+ @timer = null
+ @checkTimer() if 0 < Object.keys(@previous).length
+
+ checkTimer: ->
+ unless @timer?
+ @timer = Utils.setTimeout @expiry, => @rotate()
has: (key) ->
(key of @cache) or key of @previous
+ # Set value, and return that value. If value is null, then delete key.
+ set: (key, value = null) ->
+ @checkTimer()
+ if value?
+ @cache[key] = value
+ delete @previous[key]
+ @rotate() if @entries < Object.keys(@cache).length
+ else
+ delete @cache[key]
+ delete @previous[key]
+ value
+
get: (key) ->
if key of @cache
@cache[key]
@@ -242,27 +263,9 @@ class SimpleCache
@rotate()
@rotate()
- # Because of the timer, we can't just let these caches go out of scope and have the garbage collector
- # harvest them. Whenever they may fall out of use, we need to remove the timer. @rotate() can be used to
- # restart the cache.
- suspend: ->
- clearTimeout @timer if @timer?
- @timer = null
-
- # Set value, and return that value. If value is null, then delete key.
- set: (key, value = null) ->
- if value?
- @cache[key] = value
- delete @previous[key]
- @rotate() if @entries < Object.keys(@cache).length
- else
- delete @cache[key]
- delete @previous[key]
- value
-
# This is a simple class for the common case where we want to use some data value which may be immediately
-# available, or we may have to wait. It implements the use-immediately-or-wait queue, and calls the function
-# to fetch the data asynchronously.
+# available, or for which we may have to wait. It implements the use-immediately-or-wait queue, and calls the
+# function to fetch the data asynchronously.
class AsyncDataFetcher
constructor: (fetch) ->
@data = null
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index b53028ca..b8ada233 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -352,7 +352,7 @@ class BackgroundCompleter
@port.onMessage.addListener (msg) =>
switch msg.handler
- when "customSearchEngineKeywords"
+ when "keywords"
@keywords = msg.keywords
@lastUI.setKeywords @keywords
when "completions"
--
cgit v1.2.3
From 198dd8fa89148f3389c289bf71c40b9ab1a5681f Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 06:45:37 +0100
Subject: Search completion; refactor job-running logic.
---
background_scripts/completion.coffee | 130 ++++++++++++++++-------------------
lib/utils.coffee | 16 +++++
2 files changed, 76 insertions(+), 70 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 024ea54c..40123337 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -499,76 +499,66 @@ class MultiCompleter
cancel: (port) ->
completer.cancel? port for completer in @completers
- filter: do ->
- defaultCallbackOptions =
- # Completers may provide a continuation function. This will be run after all completers have posted
- # their suggestions, and is used to post additional (slow) asynchronous suggestions (e.g. search-engine
- # completions fetched over HTTP).
- continuation: null
- # Completers may provide a filter function. This allows one completer to filter out suggestions from
- # other completers.
- filter: null
-
- (request, onComplete) ->
- @debug = true
- # Allow only one query to run at a time, and remember the most recent query.
- return @mostRecentQuery = arguments if @filterInProgress
-
- { queryTerms } = request
- RegexpCache.clear()
- @mostRecentQuery = null
- @filterInProgress = true
- suggestions = []
- continuations = []
- filters = []
- activeCompleters = [0...@completers.length]
-
- # Call filter() on every completer and wait for them all to finish before filtering and posting the
- # results, then calling any continuations.
- for completer, index in @completers
- do (index) =>
- completer.filter request, (newSuggestions = [], { continuation, filter } = defaultCallbackOptions) =>
-
- # Store the results.
- suggestions.push newSuggestions...
- continuations.push continuation if continuation?
- filters.push filter if filter?
-
- activeCompleters = activeCompleters.filter (i) -> i != index
- if activeCompleters.length == 0
- # All the completers have now yielded their (initial) results, we're good to go.
-
- # Apply filters.
- suggestions = suggestions.filter filter for filter in filters
-
- # Should we run continuations?
- shouldRunContinuations = 0 < continuations.length and not @mostRecentQuery?
-
- # 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
- onComplete
- results: @prepareSuggestions queryTerms, suggestions
- mayCacheResults: continuations.length == 0
-
- # Run any continuations, unless there's a pending query.
- if shouldRunContinuations
- for continuation in continuations
- console.log "launching continuation..." if @debug
- continuation suggestions, (newSuggestions) =>
- console.log "posting continuation" if @debug
- suggestions.push newSuggestions...
- onComplete
- results: @prepareSuggestions queryTerms, suggestions
- # FIXME(smblott) This currently assumes that there is at most one continuation. We
- # should really be counting pending/completed continuations.
- mayCacheResults: true
-
- # Admit subsequent queries, and launch any pending query.
- @filterInProgress = false
- if @mostRecentQuery
- console.log "running pending query:", @mostRecentQuery[0] if @debug
- @filter @mostRecentQuery...
+ filter: (request, onComplete) ->
+ @debug = true
+ # Allow only one query to run at a time.
+ return @mostRecentQuery = arguments if @filterInProgress
+
+ RegexpCache.clear()
+ { queryTerms } = request
+
+ @mostRecentQuery = null
+ @filterInProgress = true
+
+ suggestions = []
+ continuations = []
+ filters = []
+
+ # Run each of the completers (asynchronously).
+ jobs = new JobRunner @completers.map (completer) ->
+ (callback) ->
+ completer.filter request, (newSuggestions = [], { continuation, filter } = {}) ->
+ suggestions.push newSuggestions...
+ continuations.push continuation if continuation?
+ filters.push filter if filter?
+ callback()
+
+ # Once all completers have finished, process and post the results, and run any continuations or pending
+ # queries.
+ jobs.onReady =>
+ # Apply filters.
+ suggestions = suggestions.filter filter for filter in filters
+
+ # Should we run continuations?
+ shouldRunContinuations = 0 < continuations.length and not @mostRecentQuery?
+
+ # 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
+ onComplete
+ results: @prepareSuggestions queryTerms, suggestions
+ mayCacheResults: continuations.length == 0
+
+ # Run any continuations (asynchronously).
+ if shouldRunContinuations
+ continuationJobs = new JobRunner continuations.map (continuation) ->
+ (callback) ->
+ continuation suggestions, (newSuggestions) ->
+ suggestions.push newSuggestions...
+ callback()
+
+ continuationJobs.onReady =>
+ # We post these results even if a new query has started. The vomnibar will not display the
+ # completions, but will cache the results.
+ onComplete
+ results: @prepareSuggestions queryTerms, suggestions
+ mayCacheResults: true
+
+ # Admit subsequent queries, and launch any pending query.
+ @filterInProgress = false
+ if @mostRecentQuery
+ console.log "running pending query:", @mostRecentQuery[0] if @debug
+ @filter @mostRecentQuery...
prepareSuggestions: (queryTerms, suggestions) ->
suggestion.computeRelevancy queryTerms for suggestion in suggestions
diff --git a/lib/utils.coffee b/lib/utils.coffee
index b0abfd8f..16adc305 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -278,7 +278,23 @@ class AsyncDataFetcher
use: (callback) ->
if @data? then callback @data else @queue.push callback
+# This takes a list of jobs (functions) and runs them, asynchronously. Functions queued with @onReady() are
+# run once all of the jobs have completed.
+class JobRunner
+ constructor: (@jobs) ->
+ @fetcher = new AsyncDataFetcher (callback) =>
+ for job in @jobs
+ do (job) =>
+ Utils.nextTick =>
+ job =>
+ @jobs = @jobs.filter (j) -> j != job
+ callback true if @jobs.length == 0
+
+ onReady: (callback) ->
+ @fetcher.use callback
+
root = exports ? window
root.Utils = Utils
root.SimpleCache = SimpleCache
root.AsyncDataFetcher = AsyncDataFetcher
+root.JobRunner = JobRunner
--
cgit v1.2.3
From ef4d9cb7a6f5ddceb643deaeb94edc5886baad93 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 06:55:30 +0100
Subject: Search completion; minor tweaks.
---
background_scripts/completion.coffee | 4 ++--
background_scripts/main.coffee | 6 +++++-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 40123337..447c47a2 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -376,10 +376,10 @@ class SearchEngineCompleter
searchUrl: tokens[1]
description: description
- # Deliver the resulting engines AsyncDataFetcher lookup table.
+ # Deliver the resulting engines AsyncDataFetcher table/data.
callback engines
- # Let the vomnibar know the custom search engine keywords.
+ # Let the vomnibar in the front end know the custom search engine keywords.
port.postMessage
handler: "keywords"
keywords: key for own key of engines
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 612f6170..d0411f6e 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -62,7 +62,11 @@ completers =
completionHandlers =
filter: (completer, request, port) ->
completer.filter request, (response) ->
- port.postMessage extend request, extend response, handler: "completions"
+ # We use try here because this may fail if the sender has already navigated away from the original page.
+ # This can happen, for example, when posting completion suggestions from the SearchEngineCompleter
+ # (which can be slow).
+ try
+ port.postMessage extend request, extend response, handler: "completions"
refresh: (completer, _, port) -> completer.refresh port
cancel: (completer, _, port) -> completer.cancel port
--
cgit v1.2.3
From 0fa0f17190459bb37bfe8752f98cd8b37e689437 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 07:15:55 +0100
Subject: Search completion; highlight also for default search engine.
---
background_scripts/completion.coffee | 4 ++++
pages/vomnibar.coffee | 27 ++++++++++++++-------------
2 files changed, 18 insertions(+), 13 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 447c47a2..c529f376 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -437,6 +437,8 @@ class SearchEngineCompleter
autoSelect: not version2
forceAutoSelect: not version2
highlightTerms: not version2
+ # Do not use this entry for vomnibar completion.
+ highlightCommonMatches: false
# Post suggestions and bail if there is no prospect of adding further suggestions.
if queryTerms.length == 0 or not haveCompletionEngine
@@ -477,6 +479,8 @@ class SearchEngineCompleter
relevancy: relavancy *= 0.9
highlightTerms: false
insertText: suggestion
+ # Do use this entry for vomnibar completion.
+ highlightCommonMatches: true
# 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
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index b8ada233..4dd05cd4 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -121,7 +121,7 @@ class VomnibarUI
# 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 @input and the correct completion state are out of sync).
+ # 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
@@ -129,20 +129,21 @@ class VomnibarUI
currentLength = @input.value.length
@previousLength = currentLength
- # Bail if the query didn't get longer.
+ # We only highlight matches if the query gets longer (so, not on deletions).
return unless previousLength < currentLength
- # Bail if these aren't completions from a custom search engine with completion.
- return unless @suppressedLeadingKeyword? and @completions[0]?.completeSuggestions
+ # Get the completions for which we can highlight matching text.
+ completions = @completions.filter (completion) ->
+ completion.highlightCommonMatches? and completion.highlightCommonMatches
- # Bail if there are too few suggestions.
- return unless 1 < @completions.length
+ # 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[1..].map (completion) -> completion.title
+ suggestions = completions.map (completion) -> completion.title
- # Ensure that the query is a prefix of all suggestions.
+ # Ensure that the query is a prefix of all of the suggestions.
for suggestion in suggestions
return unless 0 == suggestion.toLowerCase().indexOf query
@@ -150,20 +151,20 @@ class VomnibarUI
length = suggestions[0].length
length = Math.min length, suggestion.length for suggestion in suggestions
- # Find the thenght of the longest common continuation.
+ # Find the the length of the longest common continuation.
length = do ->
for index in [query.length...length]
for suggestion in suggestions
return index if suggestions[0][index].toLowerCase() != suggestion[index].toLowerCase()
length
- # But don't complete only whitespace.
- return if /^\s+$/.test suggestions[0].slice query.length, length
-
# Bail if there's nothing to complete.
return unless query.length < length
- # Install completion.
+ # 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
+
+ # Highlight match.
@input.value = suggestions[0].slice 0, length
@input.setSelectionRange query.length, length
--
cgit v1.2.3
From 8d51ccbb01fe2a4e7b548cc14617a05048a8d68c Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 07:25:20 +0100
Subject: Search completion; more minor tweaks.
---
pages/vomnibar.coffee | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 4dd05cd4..2077c559 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -218,16 +218,17 @@ class VomnibarUI
@updateSelection()
else if (action == "enter")
if @selection == -1
+ # The user has not selected a suggestion.
query = @input.value.trim()
# 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 the text common to all
- # completions to the input (highlighted), the text in the input might not correspond to any of the
- # completions. So we fire the query off 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 and highlight the text
+ # common to all completions in the input (highlightCommonMatches), the text in the input might not
+ # correspond to any of the completions. So we fire the query off to the background page and use the
+ # completion at the top of the list (which will be the right one).
@update true, =>
- if @completions[0]?
+ if @completions[0]
completion = @completions[0]
@hide -> completion.performAction openInNewTab
else
--
cgit v1.2.3
From 1a337a261a6dd6deffa836cbd949bb036e103f36 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 08:59:14 +0100
Subject: Search completion; reuse previous query.
---
background_scripts/completion_engines.coffee | 19 ++++++++++++++++++-
lib/utils.coffee | 10 ++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 85062c49..425ff47e 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -176,7 +176,7 @@ CompletionEngines =
# to generate a key. We mix in some junk generated by pwgen. A key clash might be possible, but
# vanishingly unlikely.
junk = "//Zi?ei5;o//"
- completionCacheKey = searchUrl + junk + queryTerms.join junk
+ completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk
@completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
if @completionCache.has completionCacheKey
# We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
@@ -186,6 +186,21 @@ CompletionEngines =
callback @completionCache.get completionCacheKey
return
+ if @mostRecentQuery? and @mostRecentSuggestions?
+ # If the user appears to be typing a continuation of the characters in all of the most recent query,
+ # then we can re-use the results of the previous query.
+ reusePreviousSuggestions = do (query) =>
+ query = queryTerms.join(" ").toLowerCase()
+ return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
+ previousSuggestions = @mostRecentSuggestions.map (s) -> s.toLowerCase()
+ return false unless query.length <= Utils.longestCommonPrefix previousSuggestions
+ true
+
+ if reusePreviousSuggestions
+ console.log "reuse previous query", @mostRecentQuery if @debug
+ @mostRecentQuery = queryTerms.join " "
+ return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
+
fetchSuggestions = (engine, callback) =>
url = engine.getUrl queryTerms
query = queryTerms.join(" ").toLowerCase()
@@ -227,6 +242,8 @@ CompletionEngines =
queue = @inTransit[completionCacheKey] = []
engine = @lookupEngine searchUrl
fetchSuggestions engine, (suggestions) =>
+ @mostRecentQuery = queryTerms.join " "
+ @mostRecentSuggestions = suggestions
@completionCache.set completionCacheKey, suggestions unless engine.doNotCache
callback suggestions
delete @inTransit[completionCacheKey]
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 16adc305..a1ed23c2 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -184,6 +184,16 @@ Utils =
return true if re.test string
false
+ # Calculate the length of the longest shared prefix of a list of strings.
+ longestCommonPrefix: (strings) ->
+ return 0 unless 0 < strings.length
+ strings.sort (a,b) -> a.length - b.length
+ [ shortest, strings... ] = strings
+ for ch, index in shortest.split ""
+ for str in strings
+ return index if ch != str[index]
+ return shortest.length
+
# Convenience wrapper for setTimeout (with the arguments around the other way).
setTimeout: (ms, func) -> setTimeout func, ms
--
cgit v1.2.3
From 146957c9dae54c31ce4676b5a4b60a0000c05487 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 09:49:09 +0100
Subject: Search completion; use cached suggestions synchronously.
---
background_scripts/completion.coffee | 49 ++++++++++++++++++++--------
background_scripts/completion_engines.coffee | 30 ++++++++++++-----
2 files changed, 57 insertions(+), 22 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index c529f376..4f0401f0 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -406,6 +406,18 @@ class SearchEngineCompleter
query = queryTerms.join " "
haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
+ # 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)
+
# 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.
@@ -440,8 +452,30 @@ class SearchEngineCompleter
# Do not use this entry for vomnibar completion.
highlightCommonMatches: false
- # Post suggestions and bail if there is no prospect of adding further suggestions.
- if queryTerms.length == 0 or not haveCompletionEngine
+ mkSuggestion = do ->
+ (suggestion) ->
+ new Suggestion
+ queryTerms: queryTerms
+ type: description
+ url: Utils.createSearchUrl suggestion, searchUrl
+ title: suggestion
+ relevancy: relavancy *= 0.9
+ highlightTerms: false
+ insertText: suggestion
+ # Do use this entry for vomnibar completion.
+ highlightCommonMatches: true
+
+ # If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to do an HTTP
+ # request, which we do asynchronously). This is a synchronous call (for cached suggestions only)
+ # because no callback is provided.
+ cachedSuggestions = CompletionEngines.complete searchUrl, queryTerms
+
+ # Post suggestions and bail if we already have all of the suggestions, or if there is no prospect of
+ # adding further suggestions.
+ if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
+ if cachedSuggestions?
+ console.log "using cached suggestions"
+ suggestions.push cachedSuggestions.map(mkSuggestion)...
return onComplete suggestions, { filter }
# Post any initial suggestion, and then deliver suggestions from completion engines as a continuation
@@ -450,17 +484,6 @@ class SearchEngineCompleter
filter: filter
continuation: (existingSuggestions, onComplete) =>
suggestions = []
- # 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
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 425ff47e..51799971 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -161,10 +161,16 @@ CompletionEngines =
# - queryTerms are the query terms.
# - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
# wrong).
- complete: (searchUrl, queryTerms, callback) ->
+ complete: (searchUrl, queryTerms, callback = null) ->
@mostRecentHandler = null
query = queryTerms.join ""
+ # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie.
+ # from a cache). In this case we return the results and don't call callback. Return null if we cannot
+ # service the request synchronously.
+ returnResultsOnlyFromCache = not callback?
+ callback ?= (suggestions) -> suggestions
+
# We don't complete single characters: the results are usually useless.
return callback [] unless 1 < query.length
@@ -179,16 +185,20 @@ CompletionEngines =
completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk
@completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
if @completionCache.has completionCacheKey
- # We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
- # suggestions are posted.
- Utils.setTimeout 75, =>
- console.log "hit", completionCacheKey if @debug
- callback @completionCache.get completionCacheKey
- return
+ if returnResultsOnlyFromCache
+ return @completionCache.get completionCacheKey
+ 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, =>
+ 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 in all of the most recent query,
- # then we can re-use the results of the previous query.
+ # 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.
reusePreviousSuggestions = do (query) =>
query = queryTerms.join(" ").toLowerCase()
return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
@@ -201,6 +211,8 @@ CompletionEngines =
@mostRecentQuery = queryTerms.join " "
return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
+ return null if returnResultsOnlyFromCache
+
fetchSuggestions = (engine, callback) =>
url = engine.getUrl queryTerms
query = queryTerms.join(" ").toLowerCase()
--
cgit v1.2.3
From 5191ee986615f45bfaedb389e3b34fd0a1ce9ca9 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 08:59:14 +0100
Subject: Search completion; reuse previous query.
---
background_scripts/completion_engines.coffee | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 51799971..729c68b0 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -213,6 +213,21 @@ CompletionEngines =
return null if returnResultsOnlyFromCache
+ if @mostRecentQuery? and @mostRecentSuggestions?
+ # If the user appears to be typing a continuation of the characters in all of the most recent query,
+ # then we can re-use the results of the previous query.
+ reusePreviousSuggestions = do (query) =>
+ query = queryTerms.join(" ").toLowerCase()
+ return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
+ previousSuggestions = @mostRecentSuggestions.map (s) -> s.toLowerCase()
+ return false unless query.length <= Utils.longestCommonPrefix previousSuggestions
+ true
+
+ if reusePreviousSuggestions
+ console.log "reuse previous query", @mostRecentQuery if @debug
+ @mostRecentQuery = queryTerms.join " "
+ return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
+
fetchSuggestions = (engine, callback) =>
url = engine.getUrl queryTerms
query = queryTerms.join(" ").toLowerCase()
--
cgit v1.2.3
From e56b5fc0129dd67d7978ee6453d366cb3642a4e2 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 09:49:09 +0100
Subject: Search completion; use cached suggestions synchronously.
Conflicts:
background_scripts/completion_engines.coffee
---
background_scripts/completion_engines.coffee | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 729c68b0..78782513 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -214,8 +214,9 @@ CompletionEngines =
return null if returnResultsOnlyFromCache
if @mostRecentQuery? and @mostRecentSuggestions?
- # If the user appears to be typing a continuation of the characters in all of the most recent query,
- # then we can re-use the results of the previous query.
+ # 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.
reusePreviousSuggestions = do (query) =>
query = queryTerms.join(" ").toLowerCase()
return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
@@ -228,6 +229,8 @@ CompletionEngines =
@mostRecentQuery = queryTerms.join " "
return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
+ return null if returnResultsOnlyFromCache
+
fetchSuggestions = (engine, callback) =>
url = engine.getUrl queryTerms
query = queryTerms.join(" ").toLowerCase()
--
cgit v1.2.3
From 52fbd19e24ae5679d9a2cdb7f1fa16b83c40e308 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 11:40:48 +0100
Subject: Search completion; smooth matched-selection extension.
---
pages/vomnibar.coffee | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 2077c559..8c655b07 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -256,6 +256,16 @@ class VomnibarUI
event.preventDefault()
true
+ onKeypress: (event) =>
+ if @inputContainsASelectionRange()
+ if @input.value[@input.selectionStart][0] == String.fromCharCode event.charCode
+ console.log "extend selection:", @getInputWithoutSelectionRange()
+ @input.setSelectionRange @input.selectionStart + 1, @input.selectionEnd
+ @update()
+ event.stopImmediatePropagation()
+ event.preventDefault()
+ true
+
# Test whether the input contains selected text.
inputContainsASelectionRange: ->
@input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd
@@ -270,7 +280,7 @@ class VomnibarUI
# Return the background-page query corresponding to the current input state. In other words, reinstate any
# custom search engine keyword which is currently stripped from the input.
getInputValueAsQuery: ->
- (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @input.value
+ (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutSelectionRange()
updateCompletions: (callback = null) ->
@completer.filter @getInputValueAsQuery(), (response) =>
@@ -325,6 +335,7 @@ class VomnibarUI
@input = @box.querySelector("input")
@input.addEventListener "input", @updateOnInput
@input.addEventListener "keydown", @onKeydown
+ @input.addEventListener "keypress", @onKeypress
@completionList = @box.querySelector("ul")
@completionList.style.display = ""
--
cgit v1.2.3
From cdf87b7a870303df9baee7161b15362242f65dcb Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 11:48:38 +0100
Subject: Search completion; strip duplicated code.
---
background_scripts/completion_engines.coffee | 18 ------------------
1 file changed, 18 deletions(-)
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 78782513..51799971 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -213,24 +213,6 @@ CompletionEngines =
return null if returnResultsOnlyFromCache
- 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.
- reusePreviousSuggestions = do (query) =>
- query = queryTerms.join(" ").toLowerCase()
- return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
- previousSuggestions = @mostRecentSuggestions.map (s) -> s.toLowerCase()
- return false unless query.length <= Utils.longestCommonPrefix previousSuggestions
- true
-
- if reusePreviousSuggestions
- console.log "reuse previous query", @mostRecentQuery if @debug
- @mostRecentQuery = queryTerms.join " "
- return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
-
- return null if returnResultsOnlyFromCache
-
fetchSuggestions = (engine, callback) =>
url = engine.getUrl queryTerms
query = queryTerms.join(" ").toLowerCase()
--
cgit v1.2.3
From 9bc1c215e3857d109fca2a073fd50799e0021cc8 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 12:07:48 +0100
Subject: Search completion; fix synchronisation issue.
---
lib/utils.coffee | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/utils.coffee b/lib/utils.coffee
index a1ed23c2..1ff33300 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -274,14 +274,14 @@ class SimpleCache
@rotate()
# This is a simple class for the common case where we want to use some data value which may be immediately
-# available, or for which we may have to wait. It implements the use-immediately-or-wait queue, and calls the
-# function to fetch the data asynchronously.
+# available, or for which we may have to wait. It implements a use-immediately-or-wait queue, and calls the
+# fetch function to fetch the data asynchronously.
class AsyncDataFetcher
constructor: (fetch) ->
@data = null
@queue = []
- fetch (@data) =>
- Utils.nextTick =>
+ Utils.nextTick =>
+ fetch (@data) =>
callback @data for callback in @queue
@queue = null
--
cgit v1.2.3
From 09b2aad039c7894c6023100d03c9941649843b77 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 13:03:32 +0100
Subject: Search completion; more minor tweaks.
---
background_scripts/completion.coffee | 27 +++++++++--------
background_scripts/completion_engines.coffee | 17 ++++++-----
pages/vomnibar.coffee | 45 +++++++++++++++++++++-------
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
--
cgit v1.2.3
From 8493811a4279950194cc8b1f5941cf9730cda1f0 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 15:08:01 +0100
Subject: Search completion; even more minor tweaks.
---
background_scripts/completion.coffee | 2 +-
background_scripts/completion_engines.coffee | 2 +-
pages/vomnibar.coffee | 18 ++++++++++--------
3 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 4fa91b9d..ccb5e4e6 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -474,7 +474,7 @@ class SearchEngineCompleter
# adding further suggestions.
if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
if cachedSuggestions?
- console.log "using cached suggestions"
+ console.log "using cached suggestions:", query
suggestions.push cachedSuggestions.map(mkSuggestion)...
return onComplete suggestions, { filter }
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index ac5c86aa..638d7f60 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -210,7 +210,7 @@ CompletionEngines =
true
if reusePreviousSuggestions
- console.log "reuse previous query", @mostRecentQuery if @debug
+ console.log "reuse previous query:", @mostRecentQuery if @debug
@mostRecentQuery = queryTerms.join " "
return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 85930aff..df67d2eb 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -129,10 +129,10 @@ class VomnibarUI
currentLength = @input.value.length
@previousLength = currentLength
- # We only highlight matches if the query gets longer (so, not on deletions).
+ # We only highlight matches when the query gets longer (so, not on deletions).
return unless previousLength < currentLength
- # Get the completions for which we can highlight matching text.
+ # Get the completions from which we can select text to highlight.
completions = @completions.filter (completion) ->
completion.highlightCommonMatches? and completion.highlightCommonMatches
@@ -173,19 +173,21 @@ class VomnibarUI
length
# Bail if there's nothing to complete.
- return unless query.length < length
-
- # 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
+ return unless query.length < length
completion = suggestions[0].slice query.length, length
+ # Don't complete trailing whitespace; so, 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
- # Highlight match.
+ # Insert the completion and highlight it.
@input.value = @input.value + completion
- @input.setSelectionRange query.length, length
+ @input.setSelectionRange query.length, query.length + completion.length
#
# Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
--
cgit v1.2.3
From 6b52c9e6397ac0a040c1bd46b7e6825a0a8415d2 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 15:19:25 +0100
Subject: Search completion; move completion engines to their own file.
---
background_scripts/completion.coffee | 8 +-
background_scripts/completion_engines.coffee | 170 ++-------------------------
background_scripts/completion_search.coffee | 140 ++++++++++++++++++++++
manifest.json | 1 +
4 files changed, 154 insertions(+), 165 deletions(-)
create mode 100644 background_scripts/completion_search.coffee
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index ccb5e4e6..7d39ccae 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -358,7 +358,7 @@ class SearchEngineCompleter
searchEngines: null
cancel: ->
- CompletionEngines.cancel()
+ CompletionSearch.cancel()
refresh: (port) ->
# Load and parse the search-engine configuration.
@@ -404,7 +404,7 @@ class SearchEngineCompleter
queryTerms: queryTerms
query = queryTerms.join " "
- haveCompletionEngine = CompletionEngines.haveCompletionEngine searchUrl
+ haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl
# Relevancy:
# - Relevancy does not depend upon the actual suggestion (so, it does not depend upon word
@@ -468,7 +468,7 @@ class SearchEngineCompleter
# If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to do an HTTP
# request, which we do asynchronously). This is a synchronous call (for cached suggestions only)
# because no callback is provided.
- cachedSuggestions = CompletionEngines.complete searchUrl, queryTerms
+ cachedSuggestions = CompletionSearch.complete searchUrl, queryTerms
# Post suggestions and bail if we already have all of the suggestions, or if there is no prospect of
# adding further suggestions.
@@ -493,7 +493,7 @@ class SearchEngineCompleter
console.log "skip: cannot add completions" if @debug
return onComplete []
- CompletionEngines.complete searchUrl, queryTerms, (completionSuggestions = []) =>
+ CompletionSearch.complete searchUrl, queryTerms, (completionSuggestions = []) =>
for suggestion in completionSuggestions
suggestions.push new Suggestion
queryTerms: queryTerms
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 638d7f60..07ecfa26 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -4,18 +4,20 @@
#
# Each completion engine defines three functions:
#
-# 1. "match" - This takes a searchUrl, and returns a boolean indicating whether this completion engine can
+# 1. "match" - This takes a searchUrl and returns a boolean indicating whether this completion engine can
# perform completion for the given search engine.
#
# 2. "getUrl" - This takes a list of query terms (queryTerms) and generates a completion URL, that is, a URL
# which will provide completions for this completion engine.
#
# 3. "parse" - This takes a successful XMLHttpRequest object (the request has completed successfully), and
-# returns a list of suggestions (a list of strings).
+# returns a list of suggestions (a list of strings). This method is always executed within the context
+# of a try/catch block, so errors do not propagate.
+#
+# Each new completion engine must be add to the list "CompletionEngines" at the bottom of this file.
+#
+# The lookup logic which uses these completion engines is in "./completion_search.coffee".
#
-# The main completion entry point is CompletionEngines.complete(). This implements all lookup and caching
-# logic. It is possible to add new completion engines without changing the CompletionEngines infrastructure
-# itself.
# A base class for common regexp-based matching engines.
class RegexpEngine
@@ -106,7 +108,8 @@ class DummyCompletionEngine
getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css"
parse: -> []
-completionEngines = [
+# Note: Order matters here.
+CompletionEngines = [
Youtube
Google
DuckDuckGo
@@ -116,160 +119,5 @@ completionEngines = [
DummyCompletionEngine
]
-# A note on caching.
-# Some completion engines allow caching, and Chrome serves up cached responses to requests (e.g. Google,
-# Wikipedia, YouTube). Others do not (e.g. Bing, DuckDuckGo, Amazon). A completion engine can set
-# @doNotCache to a truthy value to disable caching in cases where it is unnecessary.
-
-CompletionEngines =
- debug: true
-
- # 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: 0
-
- get: (searchUrl, url, callback) ->
- xhr = new XMLHttpRequest()
- xhr.open "GET", url, true
- xhr.timeout = 1000
- xhr.ontimeout = xhr.onerror = -> callback null
- xhr.send()
-
- xhr.onreadystatechange = ->
- if xhr.readyState == 4
- callback(if xhr.status == 200 then xhr else null)
-
- # 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 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
- @engineCache.get searchUrl
- else
- for engine in 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.
- # - queryTerms are the query terms.
- # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
- # wrong).
- complete: (searchUrl, queryTerms, callback = null) ->
- @mostRecentHandler = null
- query = queryTerms.join ""
-
- # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie.
- # from a cache). In this case we return the results and don't call callback. Return null if we cannot
- # service the request synchronously.
- returnResultsOnlyFromCache = not callback?
- callback ?= (suggestions) -> suggestions
-
- # We don't complete single characters: the results are usually useless.
- return callback [] unless 1 < query.length
-
- # We don't complete regular URLs or Javascript URLs.
- return callback [] if 1 == queryTerms.length and Utils.isUrl query
- return callback [] if Utils.hasJavascriptPrefix query
-
- # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
- # to generate a key. We mix in some junk generated by pwgen. A key clash might be possible, but
- # vanishingly unlikely.
- junk = "//Zi?ei5;o//"
- completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk
- @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
- if @completionCache.has completionCacheKey
- if returnResultsOnlyFromCache
- return @completionCache.get completionCacheKey
- else
- # We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
- # suggestions are posted.
- 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, 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()
- # 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
- console.log "reuse previous query:", @mostRecentQuery if @debug
- @mostRecentQuery = queryTerms.join " "
- return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
-
- return null if returnResultsOnlyFromCache
-
- fetchSuggestions = (engine, callback) =>
- url = engine.getUrl queryTerms
- query = queryTerms.join(" ").toLowerCase()
- @get searchUrl, url, (xhr = null) =>
- # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
- # all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of
- # incorrect or out-of-date completion engines.
- try
- suggestions = engine.parse xhr
- # Make sure we really do have an iterable of strings.
- suggestions = (suggestion for suggestion in suggestions when "string" == typeof suggestion)
- # Filter out the query itself. It's not adding anything.
- suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
- console.log "GET", url if @debug
- catch
- suggestions = []
- # We allow failures to be cached, but remove them after just ten minutes. This (it is hoped) avoids
- # repeated unnecessary XMLHttpRequest failures over a short period of time.
- removeCompletionCacheKey = => @completionCache.set completionCacheKey, null
- setTimeout removeCompletionCacheKey, 10 * 60 * 1000 # Ten minutes.
- console.log "fail", url if @debug
-
- callback suggestions
-
- # We pause in case the user is still typing.
- Utils.setTimeout @delay, handler = @mostRecentHandler = =>
- if handler != @mostRecentHandler
- # Bail! Another completion has begun, or the user is typing.
- # NOTE: We do *not* call the callback (because we are not providing results, and we don't want allow
- # any higher-level component to cache the results -- specifically, the vomnibar itself, via
- # callerMayCacheResults).
- console.log "bail", completionCacheKey if @debug
- return
- @mostRecentHandler = null
- # Don't allow duplicate identical active requests. This can happen, for example, when the user enters or
- # removes a space, or when they enter a character and immediately delete it.
- @inTransit ?= {}
- unless @inTransit[completionCacheKey]?.push callback
- queue = @inTransit[completionCacheKey] = []
- engine = @lookupEngine searchUrl
- fetchSuggestions engine, (suggestions) =>
- @mostRecentQuery = queryTerms.join " "
- @mostRecentSuggestions = suggestions
- @completionCache.set completionCacheKey, suggestions unless engine.doNotCache
- callback suggestions
- delete @inTransit[completionCacheKey]
- console.log "callbacks", queue.length, completionCacheKey if @debug and 0 < queue.length
- callback suggestions for callback in queue
-
- # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries.
- cancel: ->
- if @mostRecentHandler?
- @mostRecentHandler = null
- console.log "cancel (user is typing)" if @debug
-
root = exports ? window
root.CompletionEngines = CompletionEngines
diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee
new file mode 100644
index 00000000..eb27c076
--- /dev/null
+++ b/background_scripts/completion_search.coffee
@@ -0,0 +1,140 @@
+
+CompletionSearch =
+ debug: true
+
+ # 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
+
+ get: (searchUrl, url, callback) ->
+ xhr = new XMLHttpRequest()
+ xhr.open "GET", url, true
+ xhr.timeout = 1000
+ xhr.ontimeout = xhr.onerror = -> callback null
+ xhr.send()
+
+ xhr.onreadystatechange = ->
+ if xhr.readyState == 4
+ callback(if xhr.status == 200 then xhr else null)
+
+ # 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 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
+ @engineCache.get searchUrl
+ else
+ for engine in 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.
+ # - queryTerms are the query terms.
+ # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
+ # wrong).
+ complete: (searchUrl, queryTerms, callback = null) ->
+ query = queryTerms.join ""
+
+ # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie.
+ # from a cache). In this case we just return the results. Return null if we cannot service the request
+ # synchronously.
+ returnResultsOnlyFromCache = not callback?
+ callback ?= (suggestions) -> suggestions
+
+ # We don't complete single characters: the results are usually useless.
+ return callback [] unless 1 < query.length
+
+ # We don't complete regular URLs or Javascript URLs.
+ return callback [] if 1 == queryTerms.length and Utils.isUrl query
+ return callback [] if Utils.hasJavascriptPrefix query
+
+ # Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
+ # to generate a key. We mix in some junk generated by pwgen. A key clash might be possible, but
+ # vanishingly unlikely.
+ junk = "//Zi?ei5;o//"
+ completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk
+ @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
+ if @completionCache.has completionCacheKey
+ if returnResultsOnlyFromCache
+ return callback @completionCache.get completionCacheKey
+ else
+ # We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
+ # suggestions are posted.
+ Utils.setTimeout 50, =>
+ console.log "hit", completionCacheKey if @debug
+ callback @completionCache.get completionCacheKey
+ return
+
+ # 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.
+ if @mostRecentQuery? and @mostRecentSuggestions?
+ 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()
+ # 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
+ console.log "reuse previous query:", @mostRecentQuery if @debug
+ @mostRecentQuery = queryTerms.join " "
+ return callback @completionCache.set completionCacheKey, @mostRecentSuggestions
+
+ # That's all of the caches we can try. Bail if the caller is looking for synchronous results.
+ return callback null if returnResultsOnlyFromCache
+
+ fetchSuggestions = (engine, callback) =>
+ url = engine.getUrl queryTerms
+ query = queryTerms.join(" ").toLowerCase()
+ @get searchUrl, url, (xhr = null) =>
+ # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
+ # all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of
+ # incorrect or out-of-date completion engines.
+ try
+ suggestions = engine.parse xhr
+ # Filter out the query itself. It's not adding anything.
+ suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
+ console.log "GET", url if @debug
+ catch
+ suggestions = []
+ # We cache failures too, but remove them after just thirty minutes.
+ Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null
+ console.log "fail", url if @debug
+
+ callback suggestions
+
+ # We pause in case the user is still typing.
+ Utils.setTimeout @delay, handler = @mostRecentHandler = =>
+ if handler == @mostRecentHandler
+ @mostRecentHandler = null
+
+ # Share duplicate requests. First fetch the suggestions...
+ @inTransit ?= {}
+ @inTransit[completionCacheKey] ?= new AsyncDataFetcher (callback) =>
+ fetchSuggestions @lookupEngine(searchUrl), callback
+
+ # ... then use the suggestions.
+ @inTransit[completionCacheKey].use (suggestions) =>
+ @mostRecentQuery = queryTerms.join " "
+ @mostRecentSuggestions = suggestions
+ callback @completionCache.set completionCacheKey, suggestions
+ delete @inTransit[completionCacheKey]
+
+ # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries.
+ cancel: ->
+ if @mostRecentHandler?
+ @mostRecentHandler = null
+ console.log "cancel (user is typing)" if @debug
+
+root = exports ? window
+root.CompletionSearch = CompletionSearch
diff --git a/manifest.json b/manifest.json
index 195d16c8..7abd7738 100644
--- a/manifest.json
+++ b/manifest.json
@@ -15,6 +15,7 @@
"background_scripts/settings.js",
"background_scripts/exclusions.js",
"background_scripts/completion_engines.js",
+ "background_scripts/completion_search.js",
"background_scripts/completion.js",
"background_scripts/marks.js",
"background_scripts/main.js"
--
cgit v1.2.3
From 91da4285f4d89f9ec43f5f8a05eaa741899d24ee Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 17:16:08 +0100
Subject: Search completion; even more minor tweaks.
---
background_scripts/completion.coffee | 123 +++++++++-------------------
background_scripts/completion_search.coffee | 97 ++++++++++------------
pages/vomnibar.coffee | 8 +-
3 files changed, 89 insertions(+), 139 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 7d39ccae..4663c091 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -361,7 +361,7 @@ class SearchEngineCompleter
CompletionSearch.cancel()
refresh: (port) ->
- # Load and parse the search-engine configuration.
+ # Parse the search-engine configuration.
@searchEngines = new AsyncDataFetcher (callback) ->
engines = {}
for line in Settings.get("searchEngines").split "\n"
@@ -370,16 +370,17 @@ class SearchEngineCompleter
tokens = line.split /\s+/
continue unless 2 <= tokens.length
keyword = tokens[0].split(":")[0]
+ url = tokens[1]
description = tokens[2..].join(" ") || "search (#{keyword})"
+ continue unless Utils.hasFullUrlPrefix url
engines[keyword] =
keyword: keyword
- searchUrl: tokens[1]
+ searchUrl: url
description: description
- # Deliver the resulting engines AsyncDataFetcher table/data.
callback engines
- # Let the vomnibar in the front end know the custom search engine keywords.
+ # Let the front-end vomnibar know the search-engine keywords.
port.postMessage
handler: "keywords"
keywords: key for own key of engines
@@ -388,6 +389,7 @@ class SearchEngineCompleter
return onComplete [] if queryTerms.length == 0
@searchEngines.use (engines) =>
+ suggestions = []
keyword = queryTerms[0]
{ custom, searchUrl, description, queryTerms } =
@@ -408,30 +410,27 @@ class SearchEngineCompleter
# 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.
+ # relevancy, say). 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.
+ # - Scores are weighted such that they retain the order provided by the completion engine.
# - 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)
# 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.
+ # engines for which we do not have a completion engine. By "exclusive vomnibar", we mean suggestions
+ # from other completers are suppressed (so the vomnibar "exclusively" uses suggestions from this search
+ # engine).
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 useExclusiveVomnibar then (suggestion) -> suggestion.type == description else null
- suggestions = []
-
# For custom search engines, we add a single, top-ranked entry for the unmodified query. This
- # suggestion always appears at the top of the suggestion list. Its setting serve to define various
- # vomnibar behaviors.
+ # suggestion always appears at the top of the list.
if custom
suggestions.push new Suggestion
queryTerms: queryTerms
@@ -442,15 +441,11 @@ class SearchEngineCompleter
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: useExclusiveVomnibar
+ selectCommonMatches: false
# Toggles for the legacy behaviour.
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 ->
(suggestion) ->
@@ -460,14 +455,12 @@ class SearchEngineCompleter
url: Utils.createSearchUrl suggestion, searchUrl
title: suggestion
relevancy: relavancy *= 0.9
- highlightTerms: false
insertText: suggestion
- # Do use this entry for vomnibar completion.
- highlightCommonMatches: true
+ highlightTerms: false
+ selectCommonMatches: true
- # If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to do an HTTP
- # request, which we do asynchronously). This is a synchronous call (for cached suggestions only)
- # because no callback is provided.
+ # If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to fetch them
+ # asynchronously).
cachedSuggestions = CompletionSearch.complete searchUrl, queryTerms
# Post suggestions and bail if we already have all of the suggestions, or if there is no prospect of
@@ -476,42 +469,15 @@ class SearchEngineCompleter
if cachedSuggestions?
console.log "using cached suggestions:", query
suggestions.push cachedSuggestions.map(mkSuggestion)...
- return onComplete suggestions, { filter }
+ return onComplete suggestions, { filter, continuation: null }
- # Post any initial suggestion, and then deliver suggestions from completion engines as a continuation
- # (so, asynchronously).
+ # Post any initial suggestion, and then deliver the rest of the suggestions as a continuation (so,
+ # asynchronously).
onComplete suggestions,
filter: filter
- continuation: (existingSuggestions, onComplete) =>
- suggestions = []
-
- if 0 < existingSuggestions.length
- existingSuggestionsMinScore = existingSuggestions[existingSuggestions.length-1].relevancy
- 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 []
-
- CompletionSearch.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: suggestion
- # Do use this entry for vomnibar completion.
- highlightCommonMatches: true
-
- # 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, maxResults - existingSuggestions.length
- onComplete suggestions[...count]
+ continuation: (onComplete) =>
+ CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) =>
+ onComplete suggestions.map mkSuggestion
# 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.
@@ -520,11 +486,8 @@ class MultiCompleter
constructor: (@completers) ->
- refresh: (port) ->
- completer.refresh? port for completer in @completers
-
- cancel: (port) ->
- completer.cancel? port for completer in @completers
+ refresh: (port) -> completer.refresh? port for completer in @completers
+ cancel: (port) -> completer.cancel? port for completer in @completers
filter: (request, onComplete) ->
@debug = true
@@ -533,14 +496,9 @@ class MultiCompleter
RegexpCache.clear()
{ queryTerms } = request
- request.maxResults = @maxResults
-
- @mostRecentQuery = null
- @filterInProgress = true
- suggestions = []
- continuations = []
- filters = []
+ [ @mostRecentQuery, @filterInProgress ] = [ null, true ]
+ [ suggestions, continuations, filters ] = [ [], [], [] ]
# Run each of the completers (asynchronously).
jobs = new JobRunner @completers.map (completer) ->
@@ -554,38 +512,37 @@ class MultiCompleter
# Once all completers have finished, process and post the results, and run any continuations or pending
# queries.
jobs.onReady =>
- # Apply filters.
suggestions = suggestions.filter filter for filter in filters
-
- # Should we run continuations?
shouldRunContinuations = 0 < continuations.length and not @mostRecentQuery?
- # Post results, unless there are none AND we will be running a continuation. This avoids
+ # 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
onComplete
- results: @prepareSuggestions queryTerms, suggestions
+ results: suggestions
mayCacheResults: continuations.length == 0
# Run any continuations (asynchronously).
if shouldRunContinuations
- continuationJobs = new JobRunner continuations.map (continuation) ->
+ jobs = new JobRunner continuations.map (continuation) ->
(callback) ->
- continuation suggestions, (newSuggestions) ->
+ continuation (newSuggestions) ->
suggestions.push newSuggestions...
callback()
- continuationJobs.onReady =>
+ jobs.onReady =>
+ suggestions = @prepareSuggestions queryTerms, suggestions
# We post these results even if a new query has started. The vomnibar will not display the
- # completions, but will cache the results.
+ # completions (they're arriving too late), but it will cache them.
onComplete
- results: @prepareSuggestions queryTerms, suggestions
+ results: suggestions
mayCacheResults: true
# Admit subsequent queries, and launch any pending query.
@filterInProgress = false
if @mostRecentQuery
- console.log "running pending query:", @mostRecentQuery[0] if @debug
+ console.log "running pending query:", @mostRecentQuery[0].query if @debug
@filter @mostRecentQuery...
prepareSuggestions: (queryTerms, suggestions) ->
diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee
index eb27c076..46533833 100644
--- a/background_scripts/completion_search.coffee
+++ b/background_scripts/completion_search.coffee
@@ -1,9 +1,12 @@
CompletionSearch =
debug: true
+ inTransit: {}
+ completionCache: new SimpleCache 2 * 60 * 60 * 1000, 5000 # Two hour, 5000 entries.
+ engineCache:new SimpleCache 1000 * 60 * 60 * 1000 # 1000 hours.
- # 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.
+ # The amount of time to wait for new requests before launching the current request (for example, if the user
+ # is still typing).
delay: 100
get: (searchUrl, url, callback) ->
@@ -15,13 +18,11 @@ CompletionSearch =
xhr.onreadystatechange = ->
if xhr.readyState == 4
- callback(if xhr.status == 200 then xhr else null)
+ callback if xhr.status == 200 then xhr else null
- # 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 query, we cache the result.
+ # Look up the completion engine for this searchUrl. Because of DummyCompletionEngine, we know there will
+ # always be a match.
lookupEngine: (searchUrl) ->
- @engineCache ?= new SimpleCache 30 * 60 * 60 * 1000 # 30 hours (these are small, we can keep them longer).
if @engineCache.has searchUrl
@engineCache.get searchUrl
else
@@ -29,7 +30,7 @@ CompletionSearch =
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.
+ # True if we have a completion engine for this search URL, false otherwise.
haveCompletionEngine: (searchUrl) ->
not @lookupEngine(searchUrl).dummy
@@ -39,17 +40,19 @@ CompletionSearch =
# - queryTerms are the query terms.
# - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes
# wrong).
+ #
+ # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie.
+ # from a cache). In this case we just return the results. Returns null if we cannot service the request
+ # synchronously.
+ #
complete: (searchUrl, queryTerms, callback = null) ->
- query = queryTerms.join ""
+ query = queryTerms.join(" ").toLowerCase()
- # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie.
- # from a cache). In this case we just return the results. Return null if we cannot service the request
- # synchronously.
returnResultsOnlyFromCache = not callback?
callback ?= (suggestions) -> suggestions
- # We don't complete single characters: the results are usually useless.
- return callback [] unless 1 < query.length
+ # We don't complete queries which are too short: the results are usually useless.
+ return callback [] unless 3 < query.length
# We don't complete regular URLs or Javascript URLs.
return callback [] if 1 == queryTerms.length and Utils.isUrl query
@@ -57,26 +60,18 @@ CompletionSearch =
# Cache completions. However, completions depend upon both the searchUrl and the query terms. So we need
# to generate a key. We mix in some junk generated by pwgen. A key clash might be possible, but
- # vanishingly unlikely.
+ # is vanishingly unlikely.
junk = "//Zi?ei5;o//"
completionCacheKey = searchUrl + junk + queryTerms.map((s) -> s.toLowerCase()).join junk
- @completionCache ?= new SimpleCache 60 * 60 * 1000, 2000 # One hour, 2000 entries.
+
if @completionCache.has completionCacheKey
- if returnResultsOnlyFromCache
- return callback @completionCache.get completionCacheKey
- else
- # We add a short delay, even for a cache hit. This avoids an ugly flicker when the additional
- # suggestions are posted.
- Utils.setTimeout 50, =>
- console.log "hit", completionCacheKey if @debug
- callback @completionCache.get completionCacheKey
- return
+ console.log "hit", completionCacheKey if @debug
+ return callback @completionCache.get completionCacheKey
# 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.
if @mostRecentQuery? and @mostRecentSuggestions?
- reusePreviousSuggestions = do (query) =>
- query = queryTerms.join(" ").toLowerCase()
+ reusePreviousSuggestions = do =>
# Verify that the previous query is a prefix of the current query.
return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
# Ensure that every previous suggestion contains the text of the new query.
@@ -93,44 +88,42 @@ CompletionSearch =
# That's all of the caches we can try. Bail if the caller is looking for synchronous results.
return callback null if returnResultsOnlyFromCache
- fetchSuggestions = (engine, callback) =>
- url = engine.getUrl queryTerms
- query = queryTerms.join(" ").toLowerCase()
- @get searchUrl, url, (xhr = null) =>
- # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. In
- # all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of
- # incorrect or out-of-date completion engines.
- try
- suggestions = engine.parse xhr
- # Filter out the query itself. It's not adding anything.
- suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
- console.log "GET", url if @debug
- catch
- suggestions = []
- # We cache failures too, but remove them after just thirty minutes.
- Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null
- console.log "fail", url if @debug
-
- callback suggestions
-
# We pause in case the user is still typing.
Utils.setTimeout @delay, handler = @mostRecentHandler = =>
if handler == @mostRecentHandler
@mostRecentHandler = null
- # Share duplicate requests. First fetch the suggestions...
- @inTransit ?= {}
+ # Elide duplicate requests. First fetch the suggestions...
@inTransit[completionCacheKey] ?= new AsyncDataFetcher (callback) =>
- fetchSuggestions @lookupEngine(searchUrl), callback
+ engine = @lookupEngine searchUrl
+ url = engine.getUrl queryTerms
+
+ @get searchUrl, url, (xhr = null) =>
+ # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response.
+ # In all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of
+ # incorrect or out-of-date completion engines.
+ try
+ suggestions = engine.parse xhr
+ # Filter out the query itself. It's not adding anything.
+ suggestions = (suggestion for suggestion in suggestions when suggestion.toLowerCase() != query)
+ console.log "GET", url if @debug
+ catch
+ suggestions = []
+ # We cache failures too, but remove them after just thirty minutes.
+ Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null
+ console.log "fail", url if @debug
+
+ callback suggestions
# ... then use the suggestions.
@inTransit[completionCacheKey].use (suggestions) =>
- @mostRecentQuery = queryTerms.join " "
+ @mostRecentQuery = query
@mostRecentSuggestions = suggestions
callback @completionCache.set completionCacheKey, suggestions
delete @inTransit[completionCacheKey]
- # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries.
+ # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries. This is called
+ # whenever the user is typing.
cancel: ->
if @mostRecentHandler?
@mostRecentHandler = null
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index df67d2eb..74963bfe 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -112,7 +112,7 @@ class VomnibarUI
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
- highlightCommonMatches: (response) ->
+ selectCommonMatches: (response) ->
# For custom search engines, add characters to the input which are:
# - not in the query/input
# - in all completions
@@ -134,7 +134,7 @@ class VomnibarUI
# Get the completions from which we can select text to highlight.
completions = @completions.filter (completion) ->
- completion.highlightCommonMatches? and completion.highlightCommonMatches
+ completion.selectCommonMatches? and completion.selectCommonMatches
# Fetch the query and the suggestion texts.
query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
@@ -246,7 +246,7 @@ class VomnibarUI
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 (highlightCommonMatches), the text in the input might not
+ # common to all completions in the input (selectCommonMatches), the text in the input might not
# correspond to any of the completions. So we fire the query off to the background page and use the
# completion at the top of the list (which will be the right one).
@update true, =>
@@ -317,7 +317,7 @@ class VomnibarUI
@selection = Math.min @completions.length - 1, Math.max @initialSelectionValue, @selection
@previousAutoSelect = null if @completions[0]?.autoSelect and @completions[0]?.forceAutoSelect
@updateSelection()
- @highlightCommonMatches response
+ @selectCommonMatches response
callback?()
updateOnInput: =>
--
cgit v1.2.3
From e65f30aeebab3a6646d9918db36af325e83e0e34 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 17:34:58 +0100
Subject: Search completion; fix for multiple spaces in input.
---
pages/vomnibar.coffee | 34 ++++++++++++++++------------------
1 file changed, 16 insertions(+), 18 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 74963bfe..e5b66574 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -99,25 +99,23 @@ class VomnibarUI
selectionEnd: @input.selectionEnd
@input.value = @completions[@selection].insertText + (if @selection == 0 then "" else " ")
else if @previousInputValue?
- @input.value = @previousInputValue.value
- if @previousInputValue.selectionStart? and @previousInputValue.selectionEnd? and
- @previousInputValue.selectionStart != @previousInputValue.selectionEnd
- @input.setSelectionRange @previousInputValue.selectionStart, @previousInputValue.selectionEnd
- @previousInputValue = null
-
- # Highlight the the selected entry, and only the selected entry.
- @highlightTheSelectedEntry()
+ # Restore the text.
+ @input.value = @previousInputValue.value
+ # Restore the selection.
+ if @previousInputValue.selectionStart? and @previousInputValue.selectionEnd? and
+ @previousInputValue.selectionStart != @previousInputValue.selectionEnd
+ @input.setSelectionRange @previousInputValue.selectionStart, @previousInputValue.selectionEnd
+ @previousInputValue = null
- highlightTheSelectedEntry: ->
+ # Highlight the selected entry, and only the selected entry.
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) ->
- # For custom search engines, add characters to the input which are:
- # - not in the query/input
- # - in all completions
- # and select the added text.
-
+ #
# Bail if we don't yet have the background completer's final word on the current query.
return unless response.mayCacheResults
@@ -142,8 +140,8 @@ class VomnibarUI
# 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.
+ # "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
@@ -177,7 +175,7 @@ class VomnibarUI
completion = suggestions[0].slice query.length, length
- # Don't complete trailing whitespace; so, strip it. Then , verify that the completion is still long
+ # Don't complete trailing whitespace; so, strip it. Then, verify that the completion is still long
# enough.
completion = completion.replace /\s+$/, ""
return unless 0 < completion.length
@@ -186,7 +184,7 @@ class VomnibarUI
completion = completion.toLowerCase() unless /[A-Z]/.test @input.value
# Insert the completion and highlight it.
- @input.value = @input.value + completion
+ @input.value = query + completion
@input.setSelectionRange query.length, query.length + completion.length
#
--
cgit v1.2.3
From 3503f1c8622c37ce96e621c20d91aff35da1300c Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 17:54:20 +0100
Subject: Search completion; and even more minor tweaks.
---
pages/vomnibar.coffee | 46 +++++++++++++++++++---------------------------
1 file changed, 19 insertions(+), 27 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index e5b66574..8154c209 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -175,8 +175,7 @@ class VomnibarUI
completion = suggestions[0].slice query.length, length
- # Don't complete trailing whitespace; so, strip it. Then, verify that the completion is still long
- # enough.
+ # 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
@@ -187,11 +186,9 @@ class VomnibarUI
@input.value = query + completion
@input.setSelectionRange query.length, query.length + completion.length
- #
# Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
- # keypress.
- # We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
- #
+ # keypress. We support the arrow keys and various other shortcuts for moving. This method hides that
+ # complexity.
actionFromKeyEvent: (event) ->
key = KeyboardUtils.getKeyChar(event)
if (KeyboardUtils.isEscape(event))
@@ -245,7 +242,7 @@ class VomnibarUI
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 the query off to the background page and use the
+ # 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).
@update true, =>
if @completions[0]
@@ -279,8 +276,9 @@ class VomnibarUI
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. We
- # do this so that the selection doesn't flicker as the user types.
+ # 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
@@ -293,7 +291,7 @@ class VomnibarUI
inputContainsASelectionRange: ->
@input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd
- # Return the text of the input, with any selected text renage removed.
+ # Return the text of the input, with any selected text removed.
getInputWithoutSelectionRange: ->
if @inputContainsASelectionRange()
@input.value[0...@input.selectionStart] + @input.value[@input.selectionEnd..]
@@ -301,7 +299,7 @@ class VomnibarUI
@input.value
# Return the background-page query corresponding to the current input state. In other words, reinstate any
- # custom search engine keyword which is currently stripped from the input.
+ # search engine keyword which is currently stripped from the input, and strip any selection.
getInputValueAsQuery: ->
(if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutSelectionRange()
@@ -337,15 +335,15 @@ class VomnibarUI
1 < queryTerms.length and queryTerms[0] in @keywords
update: (updateSynchronously = false, callback = null) =>
- # If the query text becomes a custom search, then we need to force a synchronous update (so that the
- # interface is snappy).
+ # 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).
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
- # keystroke will cause a dedicated update)
+ # keystroke will cause a dedicated update).
@updateTimer = Utils.setTimeout @refreshInterval, =>
@updateTimer = null
@updateCompletions callback
@@ -376,14 +374,10 @@ class VomnibarUI
class BackgroundCompleter
debug: true
- # name is background-page completer to connect to: "omni", "tabs", or "bookmarks".
+ # The "name" is the background-page completer to connect to: "omni", "tabs", or "bookmarks".
constructor: (@name) ->
@port = chrome.runtime.connect name: "completions"
@messageId = null
- # @keywords and @cache are both reset in @reset().
- # We only cache for the duration of a single vomnibar activation.
- @keywords = []
- @cache = {}
@reset()
@port.onMessage.addListener (msg) =>
@@ -393,7 +387,7 @@ class BackgroundCompleter
@lastUI.setKeywords @keywords
when "completions"
# The result objects coming from the background page will be of the form:
- # { html: "", type: "", url: "" }
+ # { html: "", type: "", url: "", ... }
# Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
for result in msg.results
result.performAction =
@@ -402,8 +396,8 @@ class BackgroundCompleter
else
@completionActions.navigateToUrl.curry result.url
- # Cache the result -- if we have been told it's ok to do so (it could be that more results will be
- # posted shortly). We cache the result even if it arrives late.
+ # Cache the results, but only if we have been told it's ok to do so (it could be that more results
+ # will be posted shortly). We cache the results even if they arrive late.
if msg.mayCacheResults
console.log "cache set:", "-#{msg.cacheKey}-" if @debug
@cache[msg.cacheKey] = msg
@@ -416,7 +410,7 @@ class BackgroundCompleter
filter: (query, @mostRecentCallback) ->
queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
cacheKey = queryTerms.join " "
- cacheKey += " " if 0 < queryTerms.length and queryTerms[0] in @keywords and /\s$/.test query
+ cacheKey += " " if queryTerms.length == 1 and queryTerms[0] in @keywords and /\s$/.test query
if cacheKey of @cache
console.log "cache hit:", "-#{cacheKey}-" if @debug
@@ -432,17 +426,15 @@ class BackgroundCompleter
cacheKey: cacheKey
reset: ->
- @keywords = []
- @cache = {}
+ [ @keywords, @cache ] = [ [], {} ]
refresh: (@lastUI) ->
@reset()
- # Inform the background completer that we have a new vomnibar activation.
@port.postMessage name: @name, handler: "refresh"
cancel: ->
# Inform the background completer that it may (should it choose to do so) abandon any pending query
- # (because the user is typing, and there'll be another query along soon).
+ # (because the user is typing, and there will be another query along soon).
@port.postMessage name: @name, handler: "cancel"
# These are the actions we can perform when the user selects a result.
--
cgit v1.2.3
From 3bfb6cab1948ebce7421d82cb5fb5f759b6a0838 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 17:54:20 +0100
Subject: Search completion; and even more minor tweaks.
---
pages/vomnibar.coffee | 64 +++++++++++++++++++++++----------------------------
1 file changed, 29 insertions(+), 35 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index e5b66574..8ae926f3 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -56,6 +56,7 @@ class VomnibarUI
hide: (@postHideCallback = null) ->
UIComponentServer.postMessage "hide"
@reset()
+ @completer?.reset()
onHidden: ->
@postHideCallback?()
@@ -175,8 +176,7 @@ class VomnibarUI
completion = suggestions[0].slice query.length, length
- # Don't complete trailing whitespace; so, strip it. Then, verify that the completion is still long
- # enough.
+ # 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
@@ -187,11 +187,9 @@ class VomnibarUI
@input.value = query + completion
@input.setSelectionRange query.length, query.length + completion.length
- #
# Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
- # keypress.
- # We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
- #
+ # keypress. We support the arrow keys and various other shortcuts for moving. This method hides that
+ # complexity.
actionFromKeyEvent: (event) ->
key = KeyboardUtils.getKeyChar(event)
if (KeyboardUtils.isEscape(event))
@@ -245,7 +243,7 @@ class VomnibarUI
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 the query off to the background page and use the
+ # 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).
@update true, =>
if @completions[0]
@@ -279,8 +277,9 @@ class VomnibarUI
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. We
- # do this so that the selection doesn't flicker as the user types.
+ # 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
@@ -293,7 +292,7 @@ class VomnibarUI
inputContainsASelectionRange: ->
@input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd
- # Return the text of the input, with any selected text renage removed.
+ # Return the text of the input, with any selected text removed.
getInputWithoutSelectionRange: ->
if @inputContainsASelectionRange()
@input.value[0...@input.selectionStart] + @input.value[@input.selectionEnd..]
@@ -301,7 +300,7 @@ class VomnibarUI
@input.value
# Return the background-page query corresponding to the current input state. In other words, reinstate any
- # custom search engine keyword which is currently stripped from the input.
+ # search engine keyword which is currently stripped from the input, and strip any selection.
getInputValueAsQuery: ->
(if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutSelectionRange()
@@ -337,15 +336,15 @@ class VomnibarUI
1 < queryTerms.length and queryTerms[0] in @keywords
update: (updateSynchronously = false, callback = null) =>
- # If the query text becomes a custom search, then we need to force a synchronous update (so that the
- # interface is snappy).
+ # 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).
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
- # keystroke will cause a dedicated update)
+ # keystroke will cause a dedicated update).
@updateTimer = Utils.setTimeout @refreshInterval, =>
@updateTimer = null
@updateCompletions callback
@@ -376,14 +375,10 @@ class VomnibarUI
class BackgroundCompleter
debug: true
- # name is background-page completer to connect to: "omni", "tabs", or "bookmarks".
+ # The "name" is the background-page completer to connect to: "omni", "tabs", or "bookmarks".
constructor: (@name) ->
@port = chrome.runtime.connect name: "completions"
@messageId = null
- # @keywords and @cache are both reset in @reset().
- # We only cache for the duration of a single vomnibar activation.
- @keywords = []
- @cache = {}
@reset()
@port.onMessage.addListener (msg) =>
@@ -393,17 +388,18 @@ class BackgroundCompleter
@lastUI.setKeywords @keywords
when "completions"
# The result objects coming from the background page will be of the form:
- # { html: "", type: "", url: "" }
+ # { html: "", type: "", url: "", ... }
# Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
for result in msg.results
- result.performAction =
- if result.type == "tab"
- @completionActions.switchToTab.curry result.tabId
- else
- @completionActions.navigateToUrl.curry result.url
-
- # Cache the result -- if we have been told it's ok to do so (it could be that more results will be
- # posted shortly). We cache the result even if it arrives late.
+ extend result,
+ performAction:
+ if result.type == "tab"
+ @completionActions.switchToTab result.tabId
+ else
+ @completionActions.navigateToUrl result.url
+
+ # Cache the results, but only if we have been told it's ok to do so (it could be that more results
+ # will be posted shortly). We cache the results even if they arrive late.
if msg.mayCacheResults
console.log "cache set:", "-#{msg.cacheKey}-" if @debug
@cache[msg.cacheKey] = msg
@@ -416,7 +412,7 @@ class BackgroundCompleter
filter: (query, @mostRecentCallback) ->
queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
cacheKey = queryTerms.join " "
- cacheKey += " " if 0 < queryTerms.length and queryTerms[0] in @keywords and /\s$/.test query
+ cacheKey += " " if queryTerms.length == 1 and queryTerms[0] in @keywords and /\s$/.test query
if cacheKey of @cache
console.log "cache hit:", "-#{cacheKey}-" if @debug
@@ -432,22 +428,20 @@ class BackgroundCompleter
cacheKey: cacheKey
reset: ->
- @keywords = []
- @cache = {}
+ [ @keywords, @cache ] = [ [], {} ]
refresh: (@lastUI) ->
@reset()
- # Inform the background completer that we have a new vomnibar activation.
@port.postMessage name: @name, handler: "refresh"
cancel: ->
# Inform the background completer that it may (should it choose to do so) abandon any pending query
- # (because the user is typing, and there'll be another query along soon).
+ # (because the user is typing, and there will be another query along soon).
@port.postMessage name: @name, handler: "cancel"
# These are the actions we can perform when the user selects a result.
completionActions:
- navigateToUrl: (url, openInNewTab) ->
+ navigateToUrl: (url) -> (openInNewTab) ->
# If the URL is a bookmarklet (so, prefixed with "javascript:"), then we always open it in the current
# tab.
openInNewTab &&= not Utils.hasJavascriptPrefix url
@@ -456,7 +450,7 @@ class BackgroundCompleter
url: url
selected: openInNewTab
- switchToTab: (tabId) ->
+ switchToTab: (tabId) -> ->
chrome.runtime.sendMessage handler: "selectSpecificTab", id: tabId
UIComponentServer.registerHandler (event) ->
--
cgit v1.2.3
From b733790d6b1b0f4110f3910e317e0be32b962fa4 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 19:07:04 +0100
Subject: Search completion; refactor search-engine detection.
---
background_scripts/completion.coffee | 191 ++++++++++++++++++-----------------
pages/vomnibar.coffee | 3 +-
2 files changed, 102 insertions(+), 92 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 4663c091..3ddb21d2 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -360,6 +360,16 @@ class SearchEngineCompleter
cancel: ->
CompletionSearch.cancel()
+ triageRequest: (request) ->
+ @searchEngines.use (engines) =>
+ { queryTerms, query } = request
+ keyword = queryTerms[0]
+ if keyword and engines[keyword] and (1 < queryTerms.length or /\s$/.test query)
+ extend request,
+ queryTerms: queryTerms[1..]
+ keyword: keyword
+ engine: engines[keyword]
+
refresh: (port) ->
# Parse the search-engine configuration.
@searchEngines = new AsyncDataFetcher (callback) ->
@@ -385,99 +395,95 @@ class SearchEngineCompleter
handler: "keywords"
keywords: key for own key of engines
- filter: ({ queryTerms, query, maxResults }, onComplete) ->
- return onComplete [] if queryTerms.length == 0
+ filter: ({ queryTerms, query, engine }, onComplete) ->
+ suggestions = []
- @searchEngines.use (engines) =>
- suggestions = []
- keyword = queryTerms[0]
-
- { custom, searchUrl, description, queryTerms } =
- if engines[keyword]? and (1 < queryTerms.length or /\s$/.test query)
- { searchUrl, description } = engines[keyword]
- custom: true
- searchUrl: searchUrl
- description: description
- queryTerms: queryTerms[1..]
- else
- custom: false
- searchUrl: Settings.get "searchUrl"
- description: "search"
- queryTerms: queryTerms
-
- query = queryTerms.join " "
- haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl
-
- # Relevancy:
- # - Relevancy does not depend upon the actual suggestion (so, it does not depend upon word
- # relevancy, say). 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.
- # - Scores are weighted such that they retain the order provided by the completion engine.
- # - 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.
- #
- characterCount = query.length - queryTerms.length + 1
- relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
-
- # 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. By "exclusive vomnibar", we mean suggestions
- # from other completers are suppressed (so the vomnibar "exclusively" uses suggestions from this search
- # engine).
- useExclusiveVomnibar = custom and haveCompletionEngine
- filter = if useExclusiveVomnibar then (suggestion) -> suggestion.type == description else null
-
- # For custom search engines, we add a single, top-ranked entry for the unmodified query. This
- # suggestion always appears at the top of the list.
- if custom
- suggestions.push new Suggestion
+ { custom, searchUrl, description } =
+ if engine
+ { keyword, searchUrl, description } = engine
+ custom: true
+ searchUrl: searchUrl
+ description: description
+ else
+ custom: false
+ searchUrl: Settings.get "searchUrl"
+ description: "search"
+
+ return onComplete [] unless custom or 0 < queryTerms.length
+
+ query = queryTerms.join " "
+ haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl
+
+ # Relevancy:
+ # - Relevancy does not depend upon the actual suggestion (so, it does not depend upon word
+ # relevancy, say). 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.
+ # - Scores are weighted such that they retain the order provided by the completion engine.
+ # - 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.
+ #
+ characterCount = query.length - queryTerms.length + 1
+ relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
+
+ # 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. By "exclusive vomnibar", we mean suggestions
+ # from other completers are suppressed (so the vomnibar "exclusively" uses suggestions from this search
+ # engine).
+ useExclusiveVomnibar = custom and haveCompletionEngine
+ filter = if useExclusiveVomnibar then (suggestion) -> suggestion.type == description else null
+
+ # For custom search engines, we add a single, top-ranked entry for the unmodified query. This
+ # suggestion always appears at the top of the list.
+ if custom
+ suggestions.push new Suggestion
+ queryTerms: queryTerms
+ type: description
+ url: Utils.createSearchUrl queryTerms, searchUrl
+ title: query
+ relevancy: 1
+ insertText: if useExclusiveVomnibar then query else null
+ # We suppress the leading keyword, for example "w something" becomes "something" in the vomnibar.
+ suppressLeadingKeyword: true
+ selectCommonMatches: false
+ # Toggles for the legacy behaviour.
+ autoSelect: not useExclusiveVomnibar
+ forceAutoSelect: not useExclusiveVomnibar
+ highlightTerms: not useExclusiveVomnibar
+
+ mkSuggestion = do ->
+ (suggestion) ->
+ new Suggestion
queryTerms: queryTerms
type: description
- url: Utils.createSearchUrl queryTerms, searchUrl
- title: query
- relevancy: 1
- insertText: if useExclusiveVomnibar then query else null
- # We suppress the leading keyword, for example "w something" becomes "something" in the vomnibar.
- suppressLeadingKeyword: true
- selectCommonMatches: false
- # Toggles for the legacy behaviour.
- autoSelect: not useExclusiveVomnibar
- forceAutoSelect: not useExclusiveVomnibar
- highlightTerms: not useExclusiveVomnibar
-
- mkSuggestion = do ->
- (suggestion) ->
- new Suggestion
- queryTerms: queryTerms
- type: description
- url: Utils.createSearchUrl suggestion, searchUrl
- title: suggestion
- relevancy: relavancy *= 0.9
- insertText: suggestion
- highlightTerms: false
- selectCommonMatches: true
-
- # If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to fetch them
- # asynchronously).
- cachedSuggestions = CompletionSearch.complete searchUrl, queryTerms
-
- # Post suggestions and bail if we already have all of the suggestions, or if there is no prospect of
- # adding further suggestions.
- if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
- if cachedSuggestions?
- console.log "using cached suggestions:", query
- suggestions.push cachedSuggestions.map(mkSuggestion)...
- return onComplete suggestions, { filter, continuation: null }
-
- # Post any initial suggestion, and then deliver the rest of the suggestions as a continuation (so,
- # asynchronously).
- onComplete suggestions,
- filter: filter
- continuation: (onComplete) =>
- CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) =>
- onComplete suggestions.map mkSuggestion
+ url: Utils.createSearchUrl suggestion, searchUrl
+ title: suggestion
+ relevancy: relavancy *= 0.9
+ insertText: suggestion
+ highlightTerms: false
+ selectCommonMatches: true
+
+ # If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to fetch them
+ # asynchronously).
+ cachedSuggestions = CompletionSearch.complete searchUrl, queryTerms
+
+ # Post suggestions and bail if we already have all of the suggestions, or if there is no prospect of
+ # adding further suggestions.
+ if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
+ if cachedSuggestions?
+ console.log "using cached suggestions:", query
+ suggestions.push cachedSuggestions.map(mkSuggestion)...
+ return onComplete suggestions, { filter, continuation: null }
+
+ # Post any initial suggestion, and then deliver the rest of the suggestions as a continuation (so,
+ # asynchronously).
+ onComplete suggestions,
+ filter: filter
+ continuation: (onComplete) =>
+ CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) =>
+ onComplete suggestions.map mkSuggestion
# 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.
@@ -494,6 +500,11 @@ class MultiCompleter
# Allow only one query to run at a time.
return @mostRecentQuery = arguments if @filterInProgress
+ # Provide each completer with an opportunity to see (and possibly alter) the request before it is
+ # launched.
+ for completer in @completers
+ completer.triageRequest? request
+
RegexpCache.clear()
{ queryTerms } = request
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 8ae926f3..196ad766 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -411,8 +411,7 @@ class BackgroundCompleter
filter: (query, @mostRecentCallback) ->
queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
- cacheKey = queryTerms.join " "
- cacheKey += " " if queryTerms.length == 1 and queryTerms[0] in @keywords and /\s$/.test query
+ cacheKey = query.ltrim().split(/\s+/).join " "
if cacheKey of @cache
console.log "cache hit:", "-#{cacheKey}-" if @debug
--
cgit v1.2.3
From 21895866c43c09caff096ba2ef5503ee220bab73 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 20:15:39 +0100
Subject: Search completion; minor changes to comments.
---
background_scripts/completion.coffee | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 3ddb21d2..d0058538 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -360,6 +360,7 @@ class SearchEngineCompleter
cancel: ->
CompletionSearch.cancel()
+ # Look up the search engine and, if one is found, then note it and remove its keyword from the query terms.
triageRequest: (request) ->
@searchEngines.use (engines) =>
{ queryTerms, query } = request
@@ -501,9 +502,10 @@ class MultiCompleter
return @mostRecentQuery = arguments if @filterInProgress
# Provide each completer with an opportunity to see (and possibly alter) the request before it is
- # launched.
- for completer in @completers
- completer.triageRequest? request
+ # launched. This is primarily for SearchEngineCompleter, which notes a search query and removes any
+ # keyword from the queryTerms. Then, other completers don't include keywords in their matching and
+ # relevancy scores.
+ completer.triageRequest? request for completer in @completers
RegexpCache.clear()
{ queryTerms } = request
--
cgit v1.2.3
From 447115fdc5eccf0edebd3c24eec9dd7f94a2eae2 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 20:49:10 +0100
Subject: Search completion; alternative vomnibar completion.
---
background_scripts/completion.coffee | 2 ++
pages/vomnibar.coffee | 47 ++++++++++++++++++++++++++++++++++--
2 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index d0058538..e09d01ef 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -449,6 +449,7 @@ class SearchEngineCompleter
# We suppress the leading keyword, for example "w something" becomes "something" in the vomnibar.
suppressLeadingKeyword: true
selectCommonMatches: false
+ custonSearchEnginePrimarySuggestion: true
# Toggles for the legacy behaviour.
autoSelect: not useExclusiveVomnibar
forceAutoSelect: not useExclusiveVomnibar
@@ -465,6 +466,7 @@ class SearchEngineCompleter
insertText: suggestion
highlightTerms: false
selectCommonMatches: true
+ custonSearchEngineCompletionSuggestion: true
# If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to fetch them
# asynchronously).
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 196ad766..a52b14b3 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -116,7 +116,6 @@ class VomnibarUI
# 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) ->
- #
# Bail if we don't yet have the background completer's final word on the current query.
return unless response.mayCacheResults
@@ -187,6 +186,50 @@ class VomnibarUI
@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?
+
+ @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
+
+ completion = do =>
+ for completion in @completions
+ continue if completion.custonSearchEnginePrimarySuggestion
+ return completion if completion.custonSearchEngineCompletionSuggestion
+ return null
+
+ console.log 1
+ return unless completion
+
+ # Fetch the query and suggestion.
+ query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
+ suggestion = completion.title
+
+ index = suggestion.toLowerCase().indexOf query
+ console.log 2
+ return unless index <= 1
+
+ suggestion = suggestion[index..]
+ console.log 3, suggestion
+ return unless query.length < suggestion.length
+ console.log 4
+
+ # 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[query.length..]
+ @input.value = query + suggestion
+ @input.setSelectionRange query.length, query.length + suggestion.length
+
# Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
# keypress. We support the arrow keys and various other shortcuts for moving. This method hides that
# complexity.
@@ -314,7 +357,7 @@ class VomnibarUI
@selection = Math.min @completions.length - 1, Math.max @initialSelectionValue, @selection
@previousAutoSelect = null if @completions[0]?.autoSelect and @completions[0]?.forceAutoSelect
@updateSelection()
- @selectCommonMatches response
+ @selectFirstSuggestion response
callback?()
updateOnInput: =>
--
cgit v1.2.3
From 6d0e3a0f019e85fd76a92cb015ffcfb0d44a4e07 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 22:01:39 +0100
Subject: Search completion; alternative vomnibar completion.
---
background_scripts/completion.coffee | 2 +-
pages/vomnibar.coffee | 22 ++++++++++------------
2 files changed, 11 insertions(+), 13 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index e09d01ef..018740c0 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -466,7 +466,7 @@ class SearchEngineCompleter
insertText: suggestion
highlightTerms: false
selectCommonMatches: true
- custonSearchEngineCompletionSuggestion: true
+ customSearchEngineCompletionSuggestion: true
# If we have cached suggestions, then we can bundle them immediately (otherwise we'll have to fetch them
# asynchronously).
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index a52b14b3..5cd2e2f1 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -201,13 +201,12 @@ class VomnibarUI
# We only highlight matches when the query gets longer (so, not on deletions).
return unless previousLength < currentLength
- completion = do =>
+ completion = do (completion) =>
for completion in @completions
continue if completion.custonSearchEnginePrimarySuggestion
- return completion if completion.custonSearchEngineCompletionSuggestion
- return null
+ return completion if completion.customSearchEngineCompletionSuggestion
+ null
- console.log 1
return unless completion
# Fetch the query and suggestion.
@@ -215,13 +214,10 @@ class VomnibarUI
suggestion = completion.title
index = suggestion.toLowerCase().indexOf query
- console.log 2
- return unless index <= 1
+ return unless 0 <= index
suggestion = suggestion[index..]
- console.log 3, suggestion
return unless query.length < suggestion.length
- console.log 4
# If the typed text is all lower case, then make the completion lower case too.
suggestion = suggestion.toLowerCase() unless /[A-Z]/.test @getInputWithoutSelectionRange()
@@ -263,11 +259,13 @@ class VomnibarUI
else if action in [ "tab", "down" ]
if action == "tab"
if @inputContainsASelectionRange()
- # The first tab collapses the selection to the end.
- window.getSelection()?.collapseToEnd()
- @updateOnInput()
+ # 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
- # Subsequent tabs behave the same as "down".
+ # Other tabs behave the same as "down".
action = "down"
if action == "down"
@selection += 1
--
cgit v1.2.3
From d9d2e1ed9286523081a49705e4827425f565c202 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Sun, 10 May 2015 22:40:47 +0100
Subject: Search completion; tweak selection color.
---
background_scripts/completion_search.coffee | 2 +-
pages/vomnibar.css | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee
index 46533833..841990c9 100644
--- a/background_scripts/completion_search.coffee
+++ b/background_scripts/completion_search.coffee
@@ -74,7 +74,7 @@ CompletionSearch =
reusePreviousSuggestions = do =>
# Verify that the previous query is a prefix of the current query.
return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase()
- # Ensure that every previous suggestion contains the text of the new query.
+ # Verify 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.
diff --git a/pages/vomnibar.css b/pages/vomnibar.css
index dbcaf6e4..9fdc43ba 100644
--- a/pages/vomnibar.css
+++ b/pages/vomnibar.css
@@ -139,5 +139,9 @@
/* background-color: #F1F1F1; */
/* This is the light blue color of the vomnibar selected item. */
- background-color: #BBCEE9;
+ /* background-color: #BBCEE9; */
+
+ /* This is a considerably lighter blue than Vimium blue, which seems softer
+ * on the eye for this purpose. */
+ background-color: #E6EEFB;
}
--
cgit v1.2.3
From c628508ce66f9f32ae88f36ae57a6b44f900ef88 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 08:47:57 +0100
Subject: Domain completer should not complete with trailing whitespace.
---
background_scripts/completion.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 3ddb21d2..a12f1bcf 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -231,8 +231,8 @@ class DomainCompleter
# If `referenceCount` goes to zero, the domain entry can and should be deleted.
domains: null
- filter: ({ queryTerms }, onComplete) ->
- return onComplete([]) unless queryTerms.length == 1
+ filter: ({ queryTerms, query }, onComplete) ->
+ return onComplete [] unless queryTerms.length == 1 and not /\s$/.test query
if @domains
@performSearch(queryTerms, onComplete)
else
--
cgit v1.2.3
From ee418ed5a03eaeaf591ebcdf748dd07534a9bc85 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 08:55:49 +0100
Subject: Search completion; disable on redundant whitespace.
---
pages/vomnibar.coffee | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 196ad766..855218ea 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -135,8 +135,13 @@ class VomnibarUI
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 = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
+ 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"
--
cgit v1.2.3
From cee14fd4be36649aa96bb400ca3c2ed1937b4b5b Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 08:57:50 +0100
Subject: Search completion; disable on redundant whitespace.
---
pages/vomnibar.coffee | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index a2e5c56a..6268afdd 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -214,8 +214,13 @@ class VomnibarUI
return unless completion
- # Fetch the query and suggestion.
- query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
+ # 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()
suggestion = completion.title
index = suggestion.toLowerCase().indexOf query
--
cgit v1.2.3
From 9ea1dc3a69180ffa7778c7f2cf1879f22fe60a92 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 09:17:45 +0100
Subject: Search completion; more efficient filtering.
---
background_scripts/completion.coffee | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index a12f1bcf..175bc27d 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -365,6 +365,7 @@ class SearchEngineCompleter
{ queryTerms, query } = request
keyword = queryTerms[0]
if keyword and engines[keyword] and (1 < queryTerms.length or /\s$/.test query)
+ request.completers = [ this ]
extend request,
queryTerms: queryTerms[1..]
keyword: keyword
@@ -501,9 +502,12 @@ class MultiCompleter
return @mostRecentQuery = arguments if @filterInProgress
# Provide each completer with an opportunity to see (and possibly alter) the request before it is
- # launched.
- for completer in @completers
- completer.triageRequest? request
+ # launched. The completer is provided with a list of the completers we're using (request.completers), and
+ # may change that list to override the default.
+ request.completers = @completers
+ completer.triageRequest? request for completer in @completers
+ completers = request.completers
+ delete request.completers
RegexpCache.clear()
{ queryTerms } = request
@@ -512,7 +516,7 @@ class MultiCompleter
[ suggestions, continuations, filters ] = [ [], [], [] ]
# Run each of the completers (asynchronously).
- jobs = new JobRunner @completers.map (completer) ->
+ jobs = new JobRunner completers.map (completer) ->
(callback) ->
completer.filter request, (newSuggestions = [], { continuation, filter } = {}) ->
suggestions.push newSuggestions...
--
cgit v1.2.3
From 34ced40ea60669110dc9a21d74360f751c87aff0 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 11:23:54 +0100
Subject: Search completion; fix synchronization issue.
---
pages/vomnibar.coffee | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 6268afdd..fed8680b 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -198,14 +198,19 @@ class VomnibarUI
# 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
+ value = @getInputWithoutSelectionRange()
+ @previousLength ?= value.length
previousLength = @previousLength
- currentLength = @input.value.length
+ 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
@@ -214,11 +219,6 @@ class VomnibarUI
return unless completion
- # 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()
suggestion = completion.title
--
cgit v1.2.3
From c3134f6496f9b0136f1fa454a2c5f81683713a3a Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 12:48:08 +0100
Subject: Search completion; pre-merge tweaks.
---
background_scripts/completion.coffee | 29 +++--
background_scripts/completion_engines.coffee | 5 +-
background_scripts/completion_search.coffee | 4 +-
pages/vomnibar.coffee | 186 +++++++--------------------
4 files changed, 64 insertions(+), 160 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 68cd52fc..25fdf44e 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -355,12 +355,14 @@ class TabCompleter
tabRecency.recencyScore(suggestion.tabId)
class SearchEngineCompleter
+ @debug: false
searchEngines: null
cancel: ->
CompletionSearch.cancel()
- # Look up the search engine and, if one is found, then note it and remove its keyword from the query terms.
+ # This looks up the custom search engine and, if one is found, then notes it and removes its keyword from
+ # the query terms. It also sets request.completers to indicate that only this completer should run.
triageRequest: (request) ->
@searchEngines.use (engines) =>
{ queryTerms, query } = request
@@ -450,7 +452,7 @@ class SearchEngineCompleter
# We suppress the leading keyword, for example "w something" becomes "something" in the vomnibar.
suppressLeadingKeyword: true
selectCommonMatches: false
- custonSearchEnginePrimarySuggestion: true
+ customSearchEnginePrimarySuggestion: true
# Toggles for the legacy behaviour.
autoSelect: not useExclusiveVomnibar
forceAutoSelect: not useExclusiveVomnibar
@@ -477,7 +479,7 @@ class SearchEngineCompleter
# adding further suggestions.
if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
if cachedSuggestions?
- console.log "using cached suggestions:", query
+ console.log "cached suggestions:", cachedSuggestions.length, query if SearchEngineCompleter.debug
suggestions.push cachedSuggestions.map(mkSuggestion)...
return onComplete suggestions, { filter, continuation: null }
@@ -487,6 +489,7 @@ class SearchEngineCompleter
filter: filter
continuation: (onComplete) =>
CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) =>
+ console.log "fetched suggestions:", suggestions.length, query if SearchEngineCompleter.debug
onComplete suggestions.map mkSuggestion
# A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top
@@ -495,18 +498,17 @@ class MultiCompleter
maxResults: 10
constructor: (@completers) ->
-
refresh: (port) -> completer.refresh? port for completer in @completers
cancel: (port) -> completer.cancel? port for completer in @completers
filter: (request, onComplete) ->
- @debug = true
# Allow only one query to run at a time.
return @mostRecentQuery = arguments if @filterInProgress
# Provide each completer with an opportunity to see (and possibly alter) the request before it is
- # launched. Each completer is provided with a list of all of the completers we're using
- # (request.completers), and may change that list to override the default.
+ # launched. Each completer is also provided with a list of all of the completers we're using
+ # (request.completers), and may change that list to override the default (for example, the
+ # search-engine completer does this if it wants to be the *only* completer).
request.completers = @completers
completer.triageRequest? request for completer in @completers
completers = request.completers
@@ -527,8 +529,8 @@ class MultiCompleter
filters.push filter if filter?
callback()
- # Once all completers have finished, process and post the results, and run any continuations or pending
- # queries.
+ # Once all completers have finished, process the results and post them, and run any continuations or
+ # pending queries.
jobs.onReady =>
suggestions = suggestions.filter filter for filter in filters
shouldRunContinuations = 0 < continuations.length and not @mostRecentQuery?
@@ -541,7 +543,9 @@ class MultiCompleter
results: suggestions
mayCacheResults: continuations.length == 0
- # Run any continuations (asynchronously).
+ # Run any continuations (asynchronously); for example, the search-engine completer
+ # (SearchEngineCompleter) uses a continuation to fetch suggestions from completion engines
+ # asynchronously.
if shouldRunContinuations
jobs = new JobRunner continuations.map (continuation) ->
(callback) ->
@@ -551,8 +555,8 @@ class MultiCompleter
jobs.onReady =>
suggestions = @prepareSuggestions queryTerms, suggestions
- # We post these results even if a new query has started. The vomnibar will not display the
- # completions (they're arriving too late), but it will cache them.
+ # 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
results: suggestions
mayCacheResults: true
@@ -560,7 +564,6 @@ class MultiCompleter
# Admit subsequent queries, and launch any pending query.
@filterInProgress = false
if @mostRecentQuery
- console.log "running pending query:", @mostRecentQuery[0].query if @debug
@filter @mostRecentQuery...
prepareSuggestions: (queryTerms, suggestions) ->
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 07ecfa26..14e65692 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -64,20 +64,17 @@ class Wikipedia extends RegexpEngine
parse: (xhr) ->
JSON.parse(xhr.responseText)[1]
+## Does not work...
## class GoogleMaps extends RegexpEngine
## # Example search URL: https://www.google.com/maps/search/%s
## constructor: ->
## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
##
## getUrl: (queryTerms) ->
-## console.log "xxxxxxxxxxxxxxxxxxxxx"
## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
##
## parse: (xhr) ->
-## console.log "yyy", xhr.responseText
## data = JSON.parse xhr.responseText
-## console.log "zzz"
-## console.log data
## []
class Bing extends RegexpEngine
diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee
index 841990c9..2d2ee439 100644
--- a/background_scripts/completion_search.coffee
+++ b/background_scripts/completion_search.coffee
@@ -1,6 +1,6 @@
CompletionSearch =
- debug: true
+ debug: false
inTransit: {}
completionCache: new SimpleCache 2 * 60 * 60 * 1000, 5000 # Two hour, 5000 entries.
engineCache:new SimpleCache 1000 * 60 * 60 * 1000 # 1000 hours.
@@ -109,7 +109,7 @@ CompletionSearch =
console.log "GET", url if @debug
catch
suggestions = []
- # We cache failures too, but remove them after just thirty minutes.
+ # We allow failures to be cached too, but remove them after just thirty minutes.
Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null
console.log "fail", url if @debug
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 .
+ 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
# 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
--
cgit v1.2.3
From 212a47dca607983f424a59da3b5ab915f3aff403 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 13:48:26 +0100
Subject: Search completion; add bindings for Ctrl-Left/Right.
---
pages/vomnibar.coffee | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index d0abe9da..56159dab 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -149,9 +149,8 @@ class VomnibarUI
@input.value = query + suggestion
@input.setSelectionRange query.length, query.length + suggestion.length
- # Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
- # keypress. We support the arrow keys and various other shortcuts for moving. This method hides that
- # complexity.
+ # Returns the user's action ("up", "down", "tab", etc, or null) based on their keypress. We support the
+ # arrow keys and various other shortcuts, and this function hides the event-decoding complexity.
actionFromKeyEvent: (event) ->
key = KeyboardUtils.getKeyChar(event)
if (KeyboardUtils.isEscape(event))
@@ -169,6 +168,10 @@ class VomnibarUI
return "enter"
else if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
return "delete"
+ else if key in [ "left", "right" ] and event.ctrlKey and
+ not (event.altKey or event.metaKey or event.shiftKey)
+ return "control-#{key}"
+
null
onKeydown: (event) =>
@@ -224,6 +227,19 @@ class VomnibarUI
else
# Don't suppress the Delete. We want it to happen.
return true
+ else if action == "control-right" and @inputContainsASelectionRange()
+ # "Control-Right" advances the start of the selection by a word.
+ [ start, end ] = [ @input.selectionStart, @input.selectionEnd ]
+ text = @input.value[start...end]
+ newText = text.replace /^\s*\S+\s*/, ""
+ @input.setSelectionRange start + (text.length - newText.length), end
+
+ else if action == "control-left"
+ # "Control-Left" extends the start of the selection to the start of the current word.
+ [ start, end ] = [ @input.selectionStart, @input.selectionEnd ]
+ text = @input.value[0...start]
+ newText = text.replace /\S+\s*$/, ""
+ @input.setSelectionRange start + (newText.length - text.length), end
# It seems like we have to manually suppress the event here and still return true.
event.stopImmediatePropagation()
@@ -231,6 +247,7 @@ class VomnibarUI
true
onKeypress: (event) =>
+ # Handle typing with prompted text.
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
--
cgit v1.2.3
From 3975c13fe040639beb56582e50d951ad4839afbb Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 14:37:57 +0100
Subject: Search completion; add weigthing option.
---
background_scripts/completion.coffee | 6 ++++--
background_scripts/settings.coffee | 1 +
pages/options.coffee | 1 +
pages/options.css | 5 +++--
pages/options.html | 27 +++++++++++++++++++++++++++
5 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 25fdf44e..23526f85 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -416,7 +416,9 @@ class SearchEngineCompleter
return onComplete [] unless custom or 0 < queryTerms.length
query = queryTerms.join " "
+ factor = Settings.get "omniSearchWeight"
haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl
+ haveCompletionEngine = false unless 0.0 < factor
# Relevancy:
# - Relevancy does not depend upon the actual suggestion (so, it does not depend upon word
@@ -429,7 +431,7 @@ class SearchEngineCompleter
# a useful suggestion from another completer.
#
characterCount = query.length - queryTerms.length + 1
- relavancy = 0.6 * (Math.min(characterCount, 10.0)/10.0)
+ relavancy = factor * (Math.min(characterCount, 10.0)/10.0)
# 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
@@ -478,7 +480,7 @@ class SearchEngineCompleter
# Post suggestions and bail if we already have all of the suggestions, or if there is no prospect of
# adding further suggestions.
if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
- if cachedSuggestions?
+ if cachedSuggestions? and 0 < factor
console.log "cached suggestions:", cachedSuggestions.length, query if SearchEngineCompleter.debug
suggestions.push cachedSuggestions.map(mkSuggestion)...
return onComplete suggestions, { filter, continuation: null }
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 11f492d7..e042eded 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -43,6 +43,7 @@ root.Settings = Settings =
# or strings
defaults:
scrollStepSize: 60
+ omniSearchWeight: 0.6
smoothScroll: true
keyMappings: "# Insert your preferred key mappings here."
linkHintCharacters: "sadfjklewcmpgh"
diff --git a/pages/options.coffee b/pages/options.coffee
index b3ecf69a..18ff226d 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -261,6 +261,7 @@ initOptionsPage = ->
searchEngines: TextOption
searchUrl: NonEmptyTextOption
userDefinedLinkHintCss: TextOption
+ omniSearchWeight: NumberOption
# Populate options. The constructor adds each new object to "Option.all".
for name, type of options
diff --git a/pages/options.css b/pages/options.css
index 5b098c8f..1a3ff757 100644
--- a/pages/options.css
+++ b/pages/options.css
@@ -107,9 +107,10 @@ input#linkHintNumbers {
input#linkHintCharacters {
width: 100%;
}
-input#scrollStepSize {
- width: 40px;
+input#scrollStepSize, input#omniSearchWeight {
+ width: 50px;
margin-right: 3px;
+ padding-left: 3px;
}
textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines {
width: 100%;;
diff --git a/pages/options.html b/pages/options.html
index f89ddcbb..75089d75 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -233,6 +233,33 @@ b: http://b.com/?q=%s description
+
+
+
+
Vimium Labs
+
+
+
+
+
+
+
+
+ These features are experimental and may be changed or removed in future releases.
+
+
+
+
Search weighting
+
+
+
+ How prominent should suggestions be in the vomnibar?
+ 0 disables suggestions altogether.
+
+
+ (0 to 1)
+
+
--
cgit v1.2.3
From 4ba12991a277d193969e87706facdba12fdee4d0 Mon Sep 17 00:00:00 2001
From: Stephen Blott
Date: Mon, 11 May 2015 14:52:48 +0100
Subject: Search completion; better left/right controls.
---
pages/vomnibar.coffee | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 56159dab..acf45648 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -227,16 +227,18 @@ class VomnibarUI
else
# Don't suppress the Delete. We want it to happen.
return true
- else if action == "control-right" and @inputContainsASelectionRange()
- # "Control-Right" advances the start of the selection by a word.
+ else if action == "control-right"
[ start, end ] = [ @input.selectionStart, @input.selectionEnd ]
+ return true unless @inputContainsASelectionRange() and end == @input.value.length
+ # "Control-Right" advances the start of the selection by a word.
text = @input.value[start...end]
newText = text.replace /^\s*\S+\s*/, ""
@input.setSelectionRange start + (text.length - newText.length), end
else if action == "control-left"
- # "Control-Left" extends the start of the selection to the start of the current word.
[ start, end ] = [ @input.selectionStart, @input.selectionEnd ]
+ return true unless @inputContainsASelectionRange() and end == @input.value.length
+ # "Control-Left" extends the start of the selection to the start of the current word.
text = @input.value[0...start]
newText = text.replace /\S+\s*$/, ""
@input.setSelectionRange start + (newText.length - text.length), end
--
cgit v1.2.3