root = exports ? window # 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() frameIdsForTab = {} portsForTab = {} root.urlForTab = {} # This is exported for use by "marks.coffee". root.tabLoadedHandlers = {} # tabId -> function() # A secret, available only within the current instantiation of Vimium. The secret is big, likely unguessable # in practice, but less than 2^31. chrome.storage.local.set vimiumSecret: Math.floor Math.random() * 2000000000 completionSources = bookmarks: new BookmarkCompleter history: new HistoryCompleter domains: new DomainCompleter tabs: new TabCompleter searchEngines: new SearchEngineCompleter completers = 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 = (sender) -> (request, port) -> completionHandlers[request.handler] completers[request.name], request, port chrome.runtime.onConnect.addListener (port) -> if (portHandlers[port.name]) port.onMessage.addListener portHandlers[port.name] port.sender, port chrome.runtime.onMessage.addListener((request, sender, sendResponse) -> request = extend {count: 1, frameId: sender.frameId}, extend request, tab: sender.tab, tabId: sender.tab.id if (sendRequestHandlers[request.handler]) sendResponse(sendRequestHandlers[request.handler](request, sender)) # Ensure the sendResponse callback is freed. return false) 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. getHelpDialogHtml = ({showUnboundCommands, showCommandNames, customTitle}) -> commandsToKey = {} for own key of Commands.keyToCommandRegistry command = Commands.keyToCommandRegistry[key].command commandsToKey[command] = (commandsToKey[command] || []).concat(key) replacementStrings = version: currentVersion title: customTitle || "Help" tip: if showCommandNames then "Tip: click command names to yank them to the clipboard." else " " for own group of Commands.commandGroups replacementStrings[group] = helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands, showUnboundCommands, showCommandNames) replacementStrings # # Generates HTML for a given set of commands. commandGroups are defined in commands.js # helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands, showUnboundCommands, showCommandNames) -> html = [] for command in Commands.commandGroups[group] keys = commandsToKey[command] || [] bindings = ("#{Utils.escapeHtml key}" for key in keys).join ", " if (showUnboundCommands || commandsToKey[command]) isAdvanced = Commands.advancedCommands.indexOf(command) >= 0 description = availableCommands[command].description if keys.join(", ").length < 12 helpDialogHtmlForCommand html, isAdvanced, bindings, description, showCommandNames, command else # If the length of the bindings is too long, then we display the bindings on a separate row from the # description. This prevents the column alignment from becoming out of whack. helpDialogHtmlForCommand html, isAdvanced, bindings, "", false, "" helpDialogHtmlForCommand html, isAdvanced, "", description, showCommandNames, command html.join("\n") helpDialogHtmlForCommand = (html, isAdvanced, bindings, description, showCommandNames, command) -> html.push "" if description html.push "#{bindings}" html.push "", description html.push("(#{command})") if showCommandNames else html.push "", bindings html.push("") # Cache "content_scripts/vimium.css" in chrome.storage.local for UI components. do -> req = new XMLHttpRequest() req.open "GET", chrome.runtime.getURL("content_scripts/vimium.css"), true # true -> asynchronous. req.onload = -> {status, responseText} = req chrome.storage.local.set vimiumCSSInChromeStorage: responseText if status == 200 req.send() TabOperations = # Opens the url in the current tab. openUrlInCurrentTab: (request) -> chrome.tabs.update request.tabId, url: Utils.convertToUrl request.url # Opens request.url in new tab and switches to it. openUrlInNewTab: (request, callback = (->)) -> tabConfig = url: Utils.convertToUrl request.url index: request.tab.index + 1 selected: true windowId: request.tab.windowId openerTabId: request.tab.id chrome.tabs.create tabConfig, callback # # Selects the tab with the ID specified in request.id # selectSpecificTab = (request) -> chrome.tabs.get(request.id, (tab) -> chrome.windows.update(tab.windowId, { focused: true }) chrome.tabs.update(request.id, { selected: true })) moveTab = ({count, tab, registryEntry}) -> count = -count if registryEntry.command == "moveTabLeft" chrome.tabs.getAllInWindow null, (tabs) -> pinnedCount = (tabs.filter (tab) -> tab.pinned).length minIndex = if tab.pinned then 0 else pinnedCount maxIndex = (if tab.pinned then pinnedCount else tabs.length) - 1 chrome.tabs.move tab.id, index: Math.max minIndex, Math.min maxIndex, tab.index + count mkRepeatCommand = (command) -> (request) -> if 0 < request.count-- command request, (request) -> (mkRepeatCommand command) request # These are commands which are bound to keystrokes which must be handled by the background page. They are # mapped in commands.coffee. BackgroundCommands = createTab: mkRepeatCommand (request, callback) -> request.url ?= do -> 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. if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl else url TabOperations.openUrlInNewTab request, (tab) -> callback extend request, {tab, tabId: tab.id} duplicateTab: mkRepeatCommand (request, callback) -> chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id} moveTabToNewWindow: ({count, tab}) -> chrome.tabs.query {currentWindow: true}, (tabs) -> activeTabIndex = tab.index startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count [ tab, tabs... ] = tabs[startTabIndex...startTabIndex + count] chrome.windows.create {tabId: tab.id, incognito: tab.incognito}, (window) -> chrome.tabs.move (tab.id for tab in tabs), {windowId: window.id, index: -1} nextTab: (request) -> selectTab "next", request previousTab: (request) -> selectTab "previous", request firstTab: (request) -> selectTab "first", request lastTab: (request) -> selectTab "last", request removeTab: ({count, tab}) -> chrome.tabs.query {currentWindow: true}, (tabs) -> activeTabIndex = tab.index startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count]) restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste() openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste() togglePinTab: ({tab}) -> chrome.tabs.update tab.id, {pinned: !tab.pinned} toggleMuteTab: ({tab}) -> chrome.tabs.update tab.id, {muted: !tab.mutedInfo.muted} moveTabLeft: moveTab moveTabRight: moveTab nextFrame: ({count, frameId, tabId}) -> frameIdsForTab[tabId] = cycleToFrame frameIdsForTab[tabId], frameId, count chrome.tabs.sendMessage tabId, name: "focusFrame", frameId: frameIdsForTab[tabId][0], highlight: true closeTabsOnLeft: (request) -> removeTabsRelative "before", request closeTabsOnRight: (request) -> removeTabsRelative "after", request closeOtherTabs: (request) -> removeTabsRelative "both", request visitPreviousTab: ({count, tab}) -> tabIds = BgUtils.tabRecency.getTabsByRecency().filter (tabId) -> tabId != tab.id if 0 < tabIds.length selectSpecificTab id: tabIds[(count-1) % tabIds.length] # Remove tabs before, after, or either side of the currently active tab removeTabsRelative = (direction, {tab: activeTab}) -> chrome.tabs.query {currentWindow: true}, (tabs) -> shouldDelete = switch direction when "before" (index) -> index < activeTab.index when "after" (index) -> index > activeTab.index when "both" (index) -> index != activeTab.index chrome.tabs.remove (tab.id for tab in tabs when not tab.pinned and shouldDelete tab.index) # Selects a tab before or after the currently selected tab. # - direction: "next", "previous", "first" or "last". selectTab = (direction, {count, tab}) -> chrome.tabs.getAllInWindow null, (tabs) -> if 1 < tabs.length toSelect = switch direction when "next" (tab.index + count) % tabs.length when "previous" (tab.index - count + count * tabs.length) % tabs.length when "first" Math.min tabs.length - 1, count - 1 when "last" Math.max 0, tabs.length - count chrome.tabs.update tabs[toSelect].id, selected: true chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) -> return unless changeInfo.status == "loading" # Only do this once per URL change. cssConf = allFrames: true code: Settings.get("userDefinedLinkHintCss") runAt: "document_start" chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError # Symbolic names for the three browser-action icons. ENABLED_ICON = "icons/browser_action_enabled.png" DISABLED_ICON = "icons/browser_action_disabled.png" PARTIAL_ICON = "icons/browser_action_partial.png" # Convert the three icon PNGs to image data. iconImageData = {} for icon in [ENABLED_ICON, DISABLED_ICON, PARTIAL_ICON] iconImageData[icon] = {} for scale in [19, 38] do (icon, scale) -> canvas = document.createElement "canvas" canvas.width = canvas.height = scale # We cannot do the rest of this in the tests. unless chrome.areRunningVimiumTests? and chrome.areRunningVimiumTests context = canvas.getContext "2d" image = new Image image.src = icon image.onload = -> context.drawImage image, 0, 0, scale, scale iconImageData[icon][scale] = context.getImageData 0, 0, scale, scale document.body.removeChild canvas document.body.appendChild canvas Frames = onConnect: (sender, port) -> [tabId, frameId] = [sender.tab.id, sender.frameId] port.onDisconnect.addListener -> Frames.unregisterFrame {tabId, frameId} port.postMessage handler: "registerFrameId", chromeFrameId: frameId # Return our onMessage handler for this port. (request, port) => this[request.handler] {request, tabId, frameId, port} registerFrame: ({tabId, frameId, port}) -> frameIdsForTab[tabId].push frameId unless frameId in frameIdsForTab[tabId] ?= [] (portsForTab[tabId] ?= {})[frameId] = port unregisterFrame: ({tabId, frameId}) -> # FrameId 0 is the top/main frame. We never unregister that frame. If the tab is closing, then we tidy # up elsewhere. If the tab is navigating to a new page, then a new top frame will be along soon. # This mitigates against the unregister and register messages arriving out of order. See #2125. if 0 < frameId if tabId of frameIdsForTab frameIdsForTab[tabId] = (fId for fId in frameIdsForTab[tabId] when fId != frameId) if tabId of portsForTab delete portsForTab[tabId][frameId] HintCoordinator.unregisterFrame tabId, frameId isEnabledForUrl: ({request, tabId, port}) -> urlForTab[tabId] = request.url if request.frameIsFocused enabledState = Exclusions.isEnabledForUrl request.url if request.frameIsFocused chrome.browserAction.setIcon tabId: tabId, imageData: do -> enabledStateIcon = if not enabledState.isEnabledForUrl DISABLED_ICON else if 0 < enabledState.passKeys.length PARTIAL_ICON else ENABLED_ICON iconImageData[enabledStateIcon] port.postMessage extend request, enabledState domReady: ({tabId, frameId}) -> if frameId == 0 tabLoadedHandlers[tabId]?() delete tabLoadedHandlers[tabId] linkHintsMessage: ({request, tabId, frameId}) -> HintCoordinator.onMessage tabId, frameId, request handleFrameFocused = ({tabId, frameId}) -> frameIdsForTab[tabId] ?= [] frameIdsForTab[tabId] = cycleToFrame frameIdsForTab[tabId], frameId # Inform all frames that a frame has received the focus. chrome.tabs.sendMessage tabId, name: "frameFocused", focusFrameId: frameId # Rotate through frames to the frame count places after frameId. cycleToFrame = (frames, frameId, count = 0) -> # 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]...] HintCoordinator = tabState: {} onMessage: (tabId, frameId, request) -> if request.messageType of this this[request.messageType] tabId, frameId, request else # If there's no handler here, then the message is forwarded to all frames in the sender's tab. @sendMessage request.messageType, tabId, request # Post a link-hints message to a particular frame's port. We catch errors in case the frame has gone away. postMessage: (tabId, frameId, messageType, port, request = {}) -> try port.postMessage extend request, {handler: "linkHintsMessage", messageType} catch @unregisterFrame tabId, frameId # Post a link-hints message to all participating frames. sendMessage: (messageType, tabId, request = {}) -> for own frameId, port of @tabState[tabId].ports @postMessage tabId, parseInt(frameId), messageType, port, request prepareToActivateMode: (tabId, originatingFrameId, {modeIndex, isVimiumHelpDialog}) -> @tabState[tabId] = {frameIds: frameIdsForTab[tabId][..], hintDescriptors: {}, originatingFrameId, modeIndex} @tabState[tabId].ports = extend {}, portsForTab[tabId] @sendMessage "getHintDescriptors", tabId, {modeIndex, isVimiumHelpDialog} # Receive hint descriptors from all frames and activate link-hints mode when we have them all. postHintDescriptors: (tabId, frameId, {hintDescriptors}) -> if frameId in @tabState[tabId].frameIds @tabState[tabId].hintDescriptors[frameId] = hintDescriptors @tabState[tabId].frameIds = @tabState[tabId].frameIds.filter (fId) -> fId != frameId if @tabState[tabId].frameIds.length == 0 for own frameId, port of @tabState[tabId].ports if frameId of @tabState[tabId].hintDescriptors hintDescriptors = extend {}, @tabState[tabId].hintDescriptors # We do not send back the frame's own hint descriptors. This is faster (approx. speedup 3/2) for # link-busy sites like reddit. delete hintDescriptors[frameId] @postMessage tabId, parseInt(frameId), "activateMode", port, originatingFrameId: @tabState[tabId].originatingFrameId hintDescriptors: hintDescriptors modeIndex: @tabState[tabId].modeIndex # If an unregistering frame is participating in link-hints mode, then we need to tidy up after it. unregisterFrame: (tabId, frameId) -> if @tabState[tabId]? if @tabState[tabId].ports?[frameId]? delete @tabState[tabId].ports[frameId] if @tabState[tabId].frameIds? and frameId in @tabState[tabId].frameIds # We fake an empty "postHintDescriptors" because the frame has gone away. @postHintDescriptors tabId, frameId, hintDescriptors: [] # Port handler mapping portHandlers = completions: handleCompletions frames: Frames.onConnect.bind Frames sendRequestHandlers = runBackgroundCommand: (request) -> BackgroundCommands[request.registryEntry.command] request getHelpDialogHtml: getHelpDialogHtml # getCurrentTabUrl is used by the content scripts to get their full URL, because window.location cannot help # with Chrome-specific URLs like "view-source:http:..". getCurrentTabUrl: ({tab}) -> tab.url openUrlInNewTab: (request) -> TabOperations.openUrlInNewTab request openUrlInIncognito: (request) -> chrome.windows.create incognito: true, url: Utils.convertToUrl request.url openUrlInCurrentTab: TabOperations.openUrlInCurrentTab openOptionsPageInNewTab: (request) -> chrome.tabs.create url: chrome.runtime.getURL("pages/options.html"), index: request.tab.index + 1 frameFocused: handleFrameFocused nextFrame: BackgroundCommands.nextFrame copyToClipboard: Clipboard.copy.bind Clipboard pasteFromClipboard: Clipboard.paste.bind Clipboard selectSpecificTab: selectSpecificTab createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) # 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 extension's logging page. log: ({frameId, message}, sender) -> BgUtils.log "#{frameId} #{message}", sender # We always remove chrome.storage.local/findModeRawQueryListIncognito on startup. chrome.storage.local.remove "findModeRawQueryListIncognito" # Tidy up tab caches when tabs are removed. Also remove chrome.storage.local/findModeRawQueryListIncognito if # there are no remaining incognito-mode windows. Since the common case is that there are none to begin with, # we first check whether the key is set at all. chrome.tabs.onRemoved.addListener (tabId) -> delete cache[tabId] for cache in [frameIdsForTab, urlForTab, portsForTab, HintCoordinator.tabState] chrome.storage.local.get "findModeRawQueryListIncognito", (items) -> if items.findModeRawQueryListIncognito chrome.windows.getAll null, (windows) -> for window in windows return if window.incognito # There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set. chrome.storage.local.remove "findModeRawQueryListIncognito" # Convenience function for development use. window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html')) # # Begin initialization. # # Show notification on upgrade. do 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 chrome.tabs.getSelected null, (tab) -> TabOperations.openUrlInNewTab {tab, tabId: tab.id, 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 # The install date is shown on the logging page. chrome.runtime.onInstalled.addListener ({reason}) -> unless reason in ["chrome_update", "shared_module_update"] chrome.storage.local.set installDate: new Date().toString() extend root, {TabOperations, Frames}