diff options
Diffstat (limited to 'background_scripts/main.coffee')
| -rw-r--r-- | background_scripts/main.coffee | 363 |
1 files changed, 207 insertions, 156 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index d034ffb0..99a5672b 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -1,7 +1,24 @@ root = exports ? window -currentVersion = Utils.getCurrentVersion() +# The browser may have tabs already open. We inject the content scripts immediately so that they work straight +# away. +chrome.runtime.onInstalled.addListener ({ reason }) -> + # See https://developer.chrome.com/extensions/runtime#event-onInstalled + return if reason in [ "chrome_update", "shared_module_update" ] + manifest = chrome.runtime.getManifest() + # Content scripts loaded on every page should be in the same group. We assume it is the first. + contentScripts = manifest.content_scripts[0] + jobs = [ [ chrome.tabs.executeScript, contentScripts.js ], [ chrome.tabs.insertCSS, contentScripts.css ] ] + # Chrome complains if we don't evaluate chrome.runtime.lastError on errors (and we get errors for tabs on + # which Vimium cannot run). + checkLastRuntimeError = -> chrome.runtime.lastError + chrome.tabs.query { status: "complete" }, (tabs) -> + for tab in tabs + for [ func, files ] in jobs + for file in files + func tab.id, { file: file, allFrames: contentScripts.all_frames }, checkLastRuntimeError +currentVersion = Utils.getCurrentVersion() tabQueue = {} # windowId -> Array tabInfoMap = {} # tabId -> object with various tab properties keyQueue = "" # Queue of keys typed @@ -9,6 +26,7 @@ validFirstKeys = {} singleKeyCommands = [] focusedFrame = null frameIdsForTab = {} +root.urlForTab = {} # Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12> # This regular expression captures two groups: the first is a named key, the second is the remainder of @@ -25,22 +43,38 @@ chrome.storage.local.set vimiumSecret: Math.floor Math.random() * 2000000000 completionSources = - bookmarks: new BookmarkCompleter() - history: new HistoryCompleter() - domains: new DomainCompleter() - tabs: new TabCompleter() - seachEngines: new SearchEngineCompleter() + bookmarks: new BookmarkCompleter + history: new HistoryCompleter + domains: new DomainCompleter + tabs: new TabCompleter + searchEngines: new SearchEngineCompleter completers = - omni: new MultiCompleter([ - completionSources.seachEngines, - completionSources.bookmarks, - completionSources.history, - completionSources.domains]) - bookmarks: new MultiCompleter([completionSources.bookmarks]) - tabs: new MultiCompleter([completionSources.tabs]) - -chrome.runtime.onConnect.addListener((port, name) -> + omni: new MultiCompleter [ + completionSources.bookmarks + completionSources.history + completionSources.domains + completionSources.searchEngines + ] + bookmarks: new MultiCompleter [completionSources.bookmarks] + tabs: new MultiCompleter [completionSources.tabs] + +completionHandlers = + filter: (completer, request, port) -> + completer.filter request, (response) -> + # We use try here because this may fail if the sender has already navigated away from the original page. + # This can happen, for example, when posting completion suggestions from the SearchEngineCompleter + # (which is done asynchronously). + try + port.postMessage extend request, extend response, handler: "completions" + + refresh: (completer, _, port) -> completer.refresh port + cancel: (completer, _, port) -> completer.cancel port + +handleCompletions = (request, port) -> + completionHandlers[request.handler] completers[request.name], request, port + +chrome.runtime.onConnect.addListener (port, name) -> senderTabId = if port.sender.tab then port.sender.tab.id else null # If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore # the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring @@ -52,14 +86,8 @@ chrome.runtime.onConnect.addListener((port, name) -> delete tabLoadedHandlers[senderTabId] toCall.call() - # domReady is the appropriate time to show the "vimium has been upgraded" message. - # TODO: This might be broken on pages with frames. - if (shouldShowUpgradeMessage()) - chrome.tabs.sendMessage(senderTabId, { name: "showUpgradeNotification", version: currentVersion }) - if (portHandlers[port.name]) port.onMessage.addListener(portHandlers[port.name]) -) chrome.runtime.onMessage.addListener((request, sender, sendResponse) -> if (sendRequestHandlers[request.handler]) @@ -76,14 +104,24 @@ getCurrentTabUrl = (request, sender) -> sender.tab.url # # Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL, and # whether any keys should be passed through to the underlying page. +# The source frame also informs us whether or not it has the focus, which allows us to track the URL of the +# active frame. # -root.isEnabledForUrl = isEnabledForUrl = (request) -> +root.isEnabledForUrl = isEnabledForUrl = (request, sender) -> + urlForTab[sender.tab.id] = request.url if request.frameIsFocused rule = Exclusions.getRule(request.url) { isEnabledForUrl: not rule or rule.passKeys passKeys: rule?.passKeys or "" } +onURLChange = (details) -> + chrome.tabs.sendMessage details.tabId, name: "checkEnabledAfterURLChange" + +# Re-check whether Vimium is enabled for a frame when the url changes without a reload. +chrome.webNavigation.onHistoryStateUpdated.addListener onURLChange # history.pushState. +chrome.webNavigation.onReferenceFragmentUpdated.addListener onURLChange # Hash changed. + # Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings. # This is called by options.coffee. root.helpDialogHtml = (showUnboundCommands, showCommandNames, customTitle) -> @@ -149,22 +187,22 @@ openUrlInCurrentTab = (request) -> # # Opens request.url in new tab and switches to it if request.selected is true. # -openUrlInNewTab = (request) -> - chrome.tabs.getSelected(null, (tab) -> - chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true })) +openUrlInNewTab = (request, callback) -> + chrome.tabs.getSelected null, (tab) -> + tabConfig = + url: Utils.convertToUrl request.url + index: tab.index + 1 + selected: true + windowId: tab.windowId + # FIXME(smblott). openUrlInNewTab is being called in two different ways with different arguments. We + # should refactor it such that this check on callback isn't necessary. + callback = (->) unless typeof callback == "function" + chrome.tabs.create tabConfig, callback openUrlInIncognito = (request) -> chrome.windows.create({ url: Utils.convertToUrl(request.url), incognito: true}) # -# Called when the user has clicked the close icon on the "Vimium has been updated" message. -# We should now dismiss that message in all tabs. -# -upgradeNotificationClosed = (request) -> - Settings.set("previousVersion", currentVersion) - sendRequestToAllTabs({ name: "hideUpgradeNotification" }) - -# # Copies or pastes some data (request.data) to/from the clipboard. # We return null to avoid the return value from the copy operations being passed to sendResponse. # @@ -182,21 +220,16 @@ selectSpecificTab = (request) -> # # Used by the content scripts to get settings from the local storage. # -handleSettings = (args, port) -> - if (args.operation == "get") - value = Settings.get(args.key) - port.postMessage({ key: args.key, value: value }) - else # operation == "set" - Settings.set(args.key, args.value) - -refreshCompleter = (request) -> completers[request.name].refresh() - -whitespaceRegexp = /\s+/ -filterCompleter = (args, port) -> - queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp) - completers[args.name].filter(queryTerms, (results) -> port.postMessage({ id: args.id, results: results })) - -getCurrentTimeInSeconds = -> Math.floor((new Date()).getTime() / 1000) +handleSettings = (request, port) -> + switch request.operation + when "get" # Get a single settings value. + port.postMessage key: request.key, value: Settings.get request.key + when "set" # Set a single settings value. + Settings.set request.key, request.value + when "fetch" # Fetch multiple settings values. + values = request.values + values[key] = Settings.get key for own key of values + port.postMessage { values } chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) -> if (selectionChangedHandlers.length > 0) @@ -219,7 +252,14 @@ moveTab = (callback, direction) -> # These are commands which are bound to keystroke which must be handled by the background page. They are # mapped in commands.coffee. BackgroundCommands = - createTab: (callback) -> chrome.tabs.create({url: Settings.get("newTabUrl")}, (tab) -> callback()) + createTab: (callback) -> + chrome.tabs.query { active: true, currentWindow: true }, (tabs) -> + tab = tabs[0] + url = Settings.get "newTabUrl" + if url == "pages/blank.html" + # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead. + url = if tab.incognito then "chrome://newtab" else chrome.runtime.getURL url + openUrlInNewTab { url }, callback duplicateTab: (callback) -> chrome.tabs.getSelected(null, (tab) -> chrome.tabs.duplicate(tab.id) @@ -228,10 +268,10 @@ BackgroundCommands = chrome.tabs.query {active: true, currentWindow: true}, (tabs) -> tab = tabs[0] chrome.windows.create {tabId: tab.id, incognito: tab.incognito} - nextTab: (callback) -> selectTab(callback, "next") - previousTab: (callback) -> selectTab(callback, "previous") - firstTab: (callback) -> selectTab(callback, "first") - lastTab: (callback) -> selectTab(callback, "last") + nextTab: (count) -> selectTab "next", count + previousTab: (count) -> selectTab "previous", count + firstTab: -> selectTab "first" + lastTab: -> selectTab "last" removeTab: (callback) -> chrome.tabs.getSelected(null, (tab) -> chrome.tabs.remove(tab.id) @@ -271,13 +311,13 @@ BackgroundCommands = moveTabLeft: (count) -> moveTab(null, -count) moveTabRight: (count) -> moveTab(null, count) nextFrame: (count,frameId) -> - chrome.tabs.getSelected(null, (tab) -> - frames = frameIdsForTab[tab.id] - # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an - # additional offset such that we do indeed start from frameId. - count = (count + Math.max 0, frameIdsForTab[tab.id].indexOf frameId) % frames.length - frames = frameIdsForTab[tab.id] = [frames[count..]..., frames[0...count]...] - chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true })) + chrome.tabs.getSelected null, (tab) -> + frameIdsForTab[tab.id] = cycleToFrame frameIdsForTab[tab.id], frameId, count + chrome.tabs.sendMessage tab.id, name: "focusFrame", frameId: frameIdsForTab[tab.id][0], highlight: true + mainFrame: -> + chrome.tabs.getSelected null, (tab) -> + # The front end interprets a frameId of 0 to mean the main/top from. + chrome.tabs.sendMessage tab.id, name: "focusFrame", frameId: 0, highlight: true closeTabsOnLeft: -> removeTabsRelative "before" closeTabsOnRight: -> removeTabsRelative "after" @@ -305,21 +345,23 @@ removeTabsRelative = (direction) -> # Selects a tab before or after the currently selected tab. # - direction: "next", "previous", "first" or "last". -selectTab = (callback, direction) -> - chrome.tabs.getAllInWindow(null, (tabs) -> +selectTab = (direction, count = 1) -> + chrome.tabs.getAllInWindow null, (tabs) -> return unless tabs.length > 1 - chrome.tabs.getSelected(null, (currentTab) -> - switch direction - when "next" - toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length] - when "previous" - toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length] - when "first" - toSelect = tabs[0] - when "last" - toSelect = tabs[tabs.length - 1] - selectionChangedHandlers.push(callback) - chrome.tabs.update(toSelect.id, { selected: true }))) + chrome.tabs.getSelected null, (currentTab) -> + toSelect = + switch direction + when "next" + currentTab.index + count + when "previous" + currentTab.index - count + when "first" + 0 + when "last" + tabs.length - 1 + # Bring toSelect into the range [0,tabs.length). + toSelect = (toSelect + tabs.length * Math.abs count) % tabs.length + chrome.tabs.update tabs[toSelect].id, selected: true updateOpenTabs = (tab, deleteFrames = false) -> # Chrome might reuse the tab ID of a recently removed tab. @@ -335,60 +377,27 @@ updateOpenTabs = (tab, deleteFrames = false) -> # Frames are recreated on refresh delete frameIdsForTab[tab.id] if deleteFrames -setBrowserActionIcon = (tabId,path) -> - chrome.browserAction.setIcon({ tabId: tabId, path: path }) - -chrome.browserAction.setBadgeBackgroundColor - # This is Vimium blue (from the icon). - # color: [102, 176, 226, 255] - # This is a slightly darker blue. It makes the badge more striking in the corner of the eye, and the symbol - # easier to read. - color: [82, 156, 206, 255] - -setBadge = do -> - current = null - timer = null - updateBadge = (badge) -> -> chrome.browserAction.setBadgeText text: badge - (request) -> - badge = request.badge - if badge? and badge != current - current = badge - clearTimeout timer if timer - # We wait a few moments. This avoids badge flicker when there are rapid changes. - timer = setTimeout updateBadge(badge), 50 - -# Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page. -# Also propagates new enabled/disabled/passkeys state to active window, if necessary. -# This lets you disable Vimium on a page without needing to reload. -# Exported via root because it's called from the page popup. -root.updateActiveState = updateActiveState = (tabId) -> - enabledIcon = "icons/browser_action_enabled.png" - disabledIcon = "icons/browser_action_disabled.png" - partialIcon = "icons/browser_action_partial.png" - chrome.tabs.get tabId, (tab) -> - # Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page. - setBrowserActionIcon(tabId,disabledIcon) - setBadge badge: "" - chrome.tabs.sendMessage tabId, { name: "getActiveState" }, (response) -> - if response - isCurrentlyEnabled = response.enabled - currentPasskeys = response.passKeys - config = isEnabledForUrl({url: tab.url}) - enabled = config.isEnabledForUrl - passKeys = config.passKeys - if (enabled and passKeys) - setBrowserActionIcon(tabId,partialIcon) - else if (enabled) - setBrowserActionIcon(tabId,enabledIcon) - else - setBrowserActionIcon(tabId,disabledIcon) - # Propagate the new state only if it has changed. - if (isCurrentlyEnabled != enabled || currentPasskeys != passKeys) - chrome.tabs.sendMessage(tabId, { name: "setState", enabled: enabled, passKeys: passKeys, incognito: tab.incognito }) - +# Here's how we set the page icon. The default is "disabled", so if we do nothing else, then we get the +# grey-out disabled icon. Thereafter, we only set tab-specific icons, so there's no need to update the icon +# when we visit a tab on which Vimium isn't running. +# +# For active tabs, when a frame starts, it requests its active state via isEnabledForUrl. We also check the +# state every time a frame gets the focus. In both cases, the frame then updates the tab's icon accordingly. +# +# Exclusion rule changes (from either the options page or the page popup) propagate via the subsequent focus +# change. In particular, whenever a frame next gets the focus, it requests its new state and sets the icon +# accordingly. +# +setIcon = (request, sender) -> + path = switch request.icon + when "enabled" then "icons/browser_action_enabled.png" + when "partial" then "icons/browser_action_partial.png" + when "disabled" then "icons/browser_action_disabled.png" + chrome.browserAction.setIcon tabId: sender.tab.id, path: path handleUpdateScrollPosition = (request, sender) -> - updateScrollPosition(sender.tab, request.scrollX, request.scrollY) + # See note regarding sender.tab at unregisterFrame. + updateScrollPosition sender.tab, request.scrollX, request.scrollY if sender.tab? updateScrollPosition = (tab, scrollX, scrollY) -> tabInfoMap[tab.id].scrollX = scrollX @@ -402,7 +411,6 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) -> runAt: "document_start" chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError updateOpenTabs(tab) if changeInfo.url? - updateActiveState(tabId) chrome.tabs.onAttached.addListener (tabId, attachedInfo) -> # We should update all the tabs in the old window and the new window. @@ -437,8 +445,7 @@ chrome.tabs.onRemoved.addListener (tabId) -> tabInfoMap.deletor = -> delete tabInfoMap[tabId] setTimeout tabInfoMap.deletor, 1000 delete frameIdsForTab[tabId] - -chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId) + delete urlForTab[tabId] unless chrome.sessions chrome.windows.onRemoved.addListener (windowId) -> delete tabQueue[windowId] @@ -551,13 +558,13 @@ checkKeyQueue = (keysToCheck, tabId, frameId) -> if runCommand if not registryEntry.isBackgroundCommand - chrome.tabs.sendMessage(tabId, - name: "executePageCommand", - command: registryEntry.command, - frameId: frameId, - count: count, - passCountToFunction: registryEntry.passCountToFunction, - completionKeys: generateCompletionKeys("")) + chrome.tabs.sendMessage tabId, + name: "executePageCommand" + command: registryEntry.command + frameId: frameId + count: count + completionKeys: generateCompletionKeys "" + registryEntry: registryEntry refreshedCompletionKeys = true else if registryEntry.passCountToFunction @@ -595,16 +602,6 @@ sendRequestToAllTabs = (args) -> for tab in window.tabs chrome.tabs.sendMessage(tab.id, args, null)) -# -# Returns true if the current extension version is greater than the previously recorded version in -# localStorage, and false otherwise. -# -shouldShowUpgradeMessage = -> - # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new - # installs. - Settings.set("previousVersion", currentVersion) unless Settings.get("previousVersion") - Utils.compareVersions(currentVersion, Settings.get("previousVersion")) == 1 - openOptionsPageInNewTab = -> chrome.tabs.getSelected(null, (tab) -> chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 })) @@ -613,7 +610,10 @@ registerFrame = (request, sender) -> (frameIdsForTab[sender.tab.id] ?= []).push request.frameId unregisterFrame = (request, sender) -> - tabId = sender.tab.id + # When a tab is closing, Chrome sometimes passes messages without sender.tab. Therefore, we guard against + # this. + tabId = sender.tab?.id + return unless tabId? if frameIdsForTab[tabId]? if request.tab_is_closing updateOpenTabs sender.tab, true @@ -622,15 +622,36 @@ unregisterFrame = (request, sender) -> handleFrameFocused = (request, sender) -> tabId = sender.tab.id + # Cycle frameIdsForTab to the focused frame. However, also ensure that we don't inadvertently register a + # frame which wasn't previously registered (such as a frameset). if frameIdsForTab[tabId]? - frameIdsForTab[tabId] = - [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...] + frameIdsForTab[tabId] = cycleToFrame frameIdsForTab[tabId], request.frameId + # Inform all frames that a frame has received the focus. + chrome.tabs.sendMessage sender.tab.id, + name: "frameFocused" + focusFrameId: request.frameId + +# Rotate through frames to the frame count places after frameId. +cycleToFrame = (frames, frameId, count = 0) -> + frames ||= [] + # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an + # additional offset such that we do indeed start from frameId. + count = (count + Math.max 0, frames.indexOf frameId) % frames.length + [frames[count..]..., frames[0...count]...] + +# Send a message to all frames in the current tab. +sendMessageToFrames = (request, sender) -> + chrome.tabs.sendMessage sender.tab.id, request.message + +# For debugging only. This allows content scripts to log messages to the background page's console. +bgLog = (request, sender) -> + console.log "#{sender.tab.id}/#{request.frameId}", request.message # Port handler mapping portHandlers = keyDown: handleKeyDown, settings: handleSettings, - filterCompleter: filterCompleter + completions: handleCompletions sendRequestHandlers = getCompletionKeys: getCompletionKeysRequest @@ -643,16 +664,16 @@ sendRequestHandlers = unregisterFrame: unregisterFrame frameFocused: handleFrameFocused nextFrame: (request) -> BackgroundCommands.nextFrame 1, request.frameId - upgradeNotificationClosed: upgradeNotificationClosed updateScrollPosition: handleUpdateScrollPosition copyToClipboard: copyToClipboard pasteFromClipboard: pasteFromClipboard isEnabledForUrl: isEnabledForUrl selectSpecificTab: selectSpecificTab - refreshCompleter: refreshCompleter createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) - setBadge: setBadge + setIcon: setIcon + sendMessageToFrames: sendMessageToFrames + log: bgLog # We always remove chrome.storage.local/findModeRawQueryListIncognito on startup. chrome.storage.local.remove "findModeRawQueryListIncognito" @@ -668,6 +689,13 @@ chrome.tabs.onRemoved.addListener (tabId) -> # There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set. chrome.storage.local.remove "findModeRawQueryListIncognito" +# Tidy up tab caches when tabs are removed. We cannot rely on unregisterFrame because Chrome does not always +# provide sender.tab there. +# NOTE(smblott) (2015-05-05) This may break restoreTab on legacy Chrome versions, but we'll be moving to +# chrome.sessions support only soon anyway. +chrome.tabs.onRemoved.addListener (tabId) -> + delete cache[tabId] for cache in [ frameIdsForTab, urlForTab, tabInfoMap ] + # Convenience function for development use. window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html')) @@ -681,8 +709,30 @@ if Settings.has("keyMappings") populateValidFirstKeys() populateSingleKeyCommands() -if shouldShowUpgradeMessage() - sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion }) + +# Show notification on upgrade. +showUpgradeMessage = -> + # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new + # installs. + Settings.set "previousVersion", currentVersion unless Settings.get "previousVersion" + if Utils.compareVersions(currentVersion, Settings.get "previousVersion" ) == 1 + notificationId = "VimiumUpgradeNotification" + notification = + type: "basic" + iconUrl: chrome.runtime.getURL "icons/vimium.png" + title: "Vimium Upgrade" + message: "Vimium has been upgraded to version #{currentVersion}. Click here for more information." + isClickable: true + if chrome.notifications?.create? + chrome.notifications.create notificationId, notification, -> + unless chrome.runtime.lastError + Settings.set "previousVersion", currentVersion + chrome.notifications.onClicked.addListener (id) -> + if id == notificationId + openUrlInNewTab url: "https://github.com/philc/vimium#release-notes" + else + # We need to wait for the user to accept the "notifications" permission. + chrome.permissions.onAdded.addListener showUpgradeMessage # Ensure that tabInfoMap is populated when Vimium is installed. chrome.windows.getAll { populate: true }, (windows) -> @@ -694,4 +744,5 @@ chrome.windows.getAll { populate: true }, (windows) -> chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()) # Start pulling changes from synchronized storage. -Sync.init() +Settings.init() +showUpgradeMessage() |
