| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
 | 
# 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 XML responses in this way.
class GoogleXMLRegexpEngine extends RegexpEngine
  doNotCache: false # true (disbaled, experimental)
  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
  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/" ]
  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 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"
  parse: -> []
completionEngines = [
  Youtube
  Google
  DuckDuckGo
  Wikipedia
  Bing
  Amazon
  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
  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) ->
    @mostRecentHandler = null
    query = queryTerms.join ""
    # 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.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
    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) =>
          @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
 |