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
|
root = exports ? window
# 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
timestamp: 1
current: -1
cache: {}
lastVisited: null
lastVisitedTime: null
timeDelta: 500 # Milliseconds.
constructor: ->
chrome.tabs.onActivated.addListener (activeInfo) => @register activeInfo.tabId
chrome.tabs.onRemoved.addListener (tabId) => @deregister tabId
chrome.tabs.onReplaced.addListener (addedTabId, removedTabId) =>
@deregister removedTabId
@register addedTabId
chrome.windows?.onFocusChanged.addListener (wnd) =>
if wnd != chrome.windows.WINDOW_ID_NONE
chrome.tabs.query {windowId: wnd, active: true}, (tabs) =>
@register tabs[0].id if tabs[0]
register: (tabId) ->
currentTime = new Date()
# Register tabId if it has been visited for at least @timeDelta ms. Tabs which are visited only for a
# very-short time (e.g. those passed through with `5J`) aren't registered as visited at all.
if @lastVisitedTime? and @timeDelta <= currentTime - @lastVisitedTime
@cache[@lastVisited] = ++@timestamp
@current = @lastVisited = tabId
@lastVisitedTime = currentTime
deregister: (tabId) ->
if tabId == @lastVisited
# Ensure we don't register this tab, since it's going away.
@lastVisited = @lastVisitedTime = null
delete @cache[tabId]
# Recently-visited tabs get a higher score (except the current tab, which gets a low score).
recencyScore: (tabId) ->
@cache[tabId] ||= 1
if tabId == @current then 0.0 else @cache[tabId] / @timestamp
# Returns a list of tab Ids sorted by recency, most recent tab first.
getTabsByRecency: ->
tabIds = (tId for own tId of @cache)
tabIds.sort (a,b) => @cache[b] - @cache[a]
tabIds.map (tId) -> parseInt tId
BgUtils =
tabRecency: new TabRecency()
# Log messages to the extension's logging page, but only if that page is open.
log: do ->
loggingPageUrl = chrome.runtime.getURL "pages/logging.html"
console.log "Vimium logging URL:\n #{loggingPageUrl}" if loggingPageUrl? # Do not output URL for tests.
# For development, it's sometimes useful to automatically launch the logging page on reload.
chrome.windows.create url: loggingPageUrl, focused: false if localStorage.autoLaunchLoggingPage
(message, sender = null) ->
for viewWindow in chrome.extension.getViews {type: "tab"}
if viewWindow.location.pathname == "/pages/logging.html"
# Don't log messages from the logging page itself. We do this check late because most of the time
# it's not needed.
if sender?.url != loggingPageUrl
date = new Date
[hours, minutes, seconds, milliseconds] =
[date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()]
minutes = "0" + minutes if minutes < 10
seconds = "0" + seconds if seconds < 10
milliseconds = "00" + milliseconds if milliseconds < 10
milliseconds = "0" + milliseconds if milliseconds < 100
dateString = "#{hours}:#{minutes}:#{seconds}.#{milliseconds}"
logElement = viewWindow.document.getElementById "log-text"
logElement.value += "#{dateString}: #{message}\n"
logElement.scrollTop = 2000000000
# Remove comments and leading/trailing whitespace from a list of lines, and merge lines where the last
# character on the preceding line is "\".
parseLines: (text) ->
for line in text.replace(/\\\n/g, "").split("\n").map((line) -> line.trim())
continue if line.length == 0
continue if line[0] in '#"'
line
# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the
# search-engine configuration is unchanged.
SearchEngines =
previousSearchEngines: null
searchEngines: null
refresh: (searchEngines) ->
unless @previousSearchEngines? and searchEngines == @previousSearchEngines
@previousSearchEngines = searchEngines
@searchEngines = new AsyncDataFetcher (callback) ->
engines = {}
for line in BgUtils.parseLines searchEngines
tokens = line.split /\s+/
if 2 <= tokens.length
keyword = tokens[0].split(":")[0]
searchUrl = tokens[1]
description = tokens[2..].join(" ") || "search (#{keyword})"
engines[keyword] = {keyword, searchUrl, description} if Utils.hasFullUrlPrefix searchUrl
callback engines
# Use the parsed search-engine configuration, possibly asynchronously.
use: (callback) ->
@searchEngines.use callback
# Both set (refresh) the search-engine configuration and use it at the same time.
refreshAndUse: (searchEngines, callback) ->
@refresh searchEngines
@use callback
root.SearchEngines = SearchEngines
root.BgUtils = BgUtils
|