aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/completion.coffee59
-rw-r--r--background_scripts/search_engines.coffee98
-rw-r--r--lib/utils.coffee38
-rw-r--r--manifest.json1
4 files changed, 148 insertions, 48 deletions
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"