diff options
| -rw-r--r-- | background_scripts/completion.coffee | 59 | ||||
| -rw-r--r-- | background_scripts/search_engines.coffee | 98 | ||||
| -rw-r--r-- | lib/utils.coffee | 38 | ||||
| -rw-r--r-- | manifest.json | 1 |
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" |
