aboutsummaryrefslogtreecommitdiffstats
path: root/background_scripts/completion.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'background_scripts/completion.coffee')
-rw-r--r--background_scripts/completion.coffee148
1 files changed, 148 insertions, 0 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
new file mode 100644
index 00000000..15725b62
--- /dev/null
+++ b/background_scripts/completion.coffee
@@ -0,0 +1,148 @@
+class Suggestion
+ # - type: one of [bookmark, history, tab].
+ # - action: one of [navigateToUrl, switchToTab].
+ # TODO(philc): remove action. I don't think we need it here.
+ constructor: (@queryTerms, @type, @url, @title, @action) ->
+
+ generateHtml: ->
+ @html ||=
+ "<div class='topHalf'>
+ <span class='source'>#{@type}</span>
+ <span class='title'>#{@highlightTerms(utils.escapeHtml(@title))}</span>
+ </div>
+ <div class='bottomHalf'><span class='url'>#{@shortenUrl(@highlightTerms(@url))}</span></div>"
+
+ shortenUrl: (url) ->
+ @stripTrailingSlash(url).replace(/^http:\/\//, "")
+
+ stripTrailingSlash: (url) ->
+ url = url.substring(url, url.length - 1) if url[url.length - 1] == "/"
+ url
+
+ # Wraps each occurence of the query terms in the given string in a <span>.
+ highlightTerms: (string) ->
+ for term in @queryTerms
+ regexp = @escapeRegexp(term)
+ string = string.replace(regexp, "<span class='match'>$&</span>")
+ string
+
+ # Creates a Regexp from the given string, with all special Regexp characters escaped.
+ escapeRegexp: (string) ->
+ # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
+ Suggestion.escapeRegExp ||= /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
+ new RegExp(string.replace(Suggestion.escapeRegExp, "\\$&"), "i")
+
+ computeRelevancy: ->
+ # TODO(philc):
+ 1
+
+class BookmarkCompleter
+ currentSearch: null
+ # These bookmarks are loaded asynchronously when refresh() is called.
+ bookmarks: null
+
+ filter: (@queryTerms, @onComplete) ->
+ @currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
+ @performSearch() if @bookmarks
+
+ onBookmarksLoaded: -> @performSearch() if @currentSearch
+
+ performSearch: ->
+ results = @bookmarks.filter (bookmark) =>
+ RankingUtils.matches(@currentSearch.queryTerms, bookmark.url, bookmark.title)
+ suggestions = results.map (bookmark) =>
+ new Suggestion(@currentSearch.queryTerms, "bookmark", bookmark.url, bookmark.title, "navigateToUrl")
+ onComplete = @currentSearch.onComplete
+ @currentSearch = null
+ onComplete(suggestions)
+
+ refresh: ->
+ @bookmarks = null
+ chrome.bookmarks.getTree (bookmarks) =>
+ @bookmarks = @traverseBookmarks(bookmarks).filter((bookmark) -> bookmark.url?)
+ @onBookmarksLoaded()
+
+ # Traverses the bookmark hierarchy, and retuns a flattened list of all bookmarks in the tree.
+ traverseBookmarks: (bookmarks) ->
+ results = []
+ toVisit = bookmarks
+ while toVisit.length > 0
+ bookmark = toVisit.shift()
+ results.push(bookmark)
+ toVisit.push.apply(toVisit, bookmark.children) if (bookmark.children)
+
+ results
+
+class HistoryCompleter
+ filter: (queryTerms, onComplete) ->
+ @currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete }
+ results = []
+ HistoryCache.use (history) ->
+ results = history.filter (entry) -> RankingUtils.matches(queryTerms, entry.url, entry.title)
+ suggestions = results.map (entry) =>
+ new Suggestion(queryTerms, "history", entry.url, entry.title, "navigateToUrl")
+ onComplete(suggestions)
+
+ refresh: ->
+
+class MultiCompleter
+ constructor: (@completers) ->
+ @maxResults = 10 # TODO(philc): Should this be configurable?
+
+ refresh: -> completer.refresh() for completer in @completers
+
+ filter: (queryTerms, onComplete) ->
+ suggestions = []
+ completersFinished = 0
+ 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)
+
+ sortSuggestions: (suggestions) ->
+ for suggestion in suggestions
+ suggestion.computeRelevancy(@queryTerms)
+ suggestions.sort (a, b) -> a.relevancy - b.relevancy
+ suggestions
+
+RankingUtils =
+ # Whether the given URL or title match any one of the query terms. This is used to prune out irrelevant
+ # suggestions before we try to rank them.
+ matches: (queryTerms, url, title) ->
+ return false if queryTerms.length == 0
+ for term in queryTerms
+ return false unless title.indexOf(term) >= 0 || url.indexOf(term) >= 0
+ true
+
+# Provides cached access to Chrome's history.
+HistoryCache =
+ size: 20000
+ history: null # An array of History items returned from Chrome.
+
+ use: (callback) ->
+ return @fetchHistory(callback) unless @history?
+ callback(@history)
+
+ fetchHistory: (callback) ->
+ return @callbacks.push(callback) if @callbacks
+ @callbacks = [callback]
+ chrome.history.search { text: "", maxResults: @size, startTime: 0 }, (history) =>
+ # sorting in ascending order. We will push new items on to the end as the user navigates to new pages.
+ history.sort((a, b) -> (a.lastVisitTime || 0) - (b.lastVisitTime || 0))
+ @history = history
+ chrome.history.onVisited.addListener (newSite) =>
+ firstTimeVisit = (newSite.visitedCount == 1)
+ @history.push(newSite) if firstTimeVisit
+ callback(@history) for callback in @callbacks
+ @callbacks = null
+
+root = exports ? window
+root.Suggestion = Suggestion
+root.BookmarkCompleter = BookmarkCompleter
+root.MultiCompleter = MultiCompleter
+root.HistoryCompleter = HistoryCompleter \ No newline at end of file