aboutsummaryrefslogtreecommitdiffstats
path: root/background_scripts/bg_utils.coffee
blob: 698f5352d0b8253f808469fab605d2172b369cb7 (plain)
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