diff options
| author | Phil Crosby | 2012-09-01 15:10:02 -0700 | 
|---|---|---|
| committer | Phil Crosby | 2012-09-01 23:36:17 -0700 | 
| commit | 81dbdcf7e7f7db32af1fc6aafcf870b2b8377a54 (patch) | |
| tree | f065d7bfa745d97e2edb20293594e6e8ae70f85e /background_scripts | |
| parent | 8bcc3c11b1989ee9ebd89b057a953e2dc8364e52 (diff) | |
| download | vimium-81dbdcf7e7f7db32af1fc6aafcf870b2b8377a54.tar.bz2 | |
port main.js to main.coffee. All coffeescript all the time.
Diffstat (limited to 'background_scripts')
| -rw-r--r-- | background_scripts/main.coffee | 627 | ||||
| -rw-r--r-- | background_scripts/main.js | 786 | 
2 files changed, 627 insertions, 786 deletions
| diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee new file mode 100644 index 00000000..49a1cee5 --- /dev/null +++ b/background_scripts/main.coffee @@ -0,0 +1,627 @@ +root = exports ? window + +currentVersion = Utils.getCurrentVersion() + +tabQueue = {} # windowId -> Array +openTabs = {} # tabId -> object with various tab properties +keyQueue = "" # Queue of keys typed +validFirstKeys = {} +singleKeyCommands = [] +focusedFrame = null +framesForTab = {} + +# 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 +# the string. +namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/ + +# Event handlers +selectionChangedHandlers = [] +tabLoadedHandlers = {} # tabId -> function() + +completionSources = +  bookmarks: new BookmarkCompleter() +  history: new HistoryCompleter() +  domains: new DomainCompleter() +  tabs: new TabCompleter() + +completers = +  omni: new MultiCompleter([ +    completionSources.bookmarks, +    completionSources.history, +    completionSources.domains]) +  bookmarks: new MultiCompleter([completionSources.bookmarks]) +  tabs: new MultiCompleter([completionSources.tabs]) + +chrome.extension.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 +  # the scroll position will not be possible. +  if (port.name == "domReady" && senderTabId != null) +    if (tabLoadedHandlers[senderTabId]) +      toCall = tabLoadedHandlers[senderTabId] +      # Delete first to be sure there's no circular events. +      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.sendRequest(senderTabId, { name: "showUpgradeNotification", version: currentVersion }) + +  if (portHandlers[port.name]) +    port.onMessage.addListener(portHandlers[port.name]) +) + +chrome.extension.onRequest.addListener((request, sender, sendResponse) -> +  if (sendRequestHandlers[request.handler]) +    sendResponse(sendRequestHandlers[request.handler](request, sender)) +  # Ensure the sendResponse callback is freed. +  return false) + +# +# Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:# .." +# because window.location doesn't know anything about the Chrome-specific "view-source:". +# +getCurrentTabUrl = (request, sender) -> sender.tab.url + +# +# Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL. +# +isEnabledForUrl = (request) -> +  # excludedUrls are stored as a series of URL expressions separated by newlines. +  excludedUrls = Settings.get("excludedUrls").split("\n") +  isEnabled = true +  for url in excludedUrls +    # The user can add "*" to the URL which means ".*" +    regexp = new RegExp("^" + url.replace(/\*/g, ".*") + "$") +    isEnabled = false if request.url.match(regexp) +  { isEnabledForUrl: isEnabled } + +# +# Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings. +# +addExcludedUrl = (url) -> +  return unless url = url.trim() + +  excludedUrls = Settings.get("excludedUrls") +  excludedUrls += "\n" + url +  Settings.set("excludedUrls", excludedUrls) + +  chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, +    (tabs) -> updateActiveState(tabs[0].id)) + +getShowAdvancedCommands = (request) -> Settings.get("helpDialog_showAdvancedCommands") + +saveHelpDialogSettings = (request) -> +  Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands) + +# +# Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings. +# +helpDialogHtml = (showUnboundCommands, showCommandNames, customTitle) -> +  commandsToKey = {} +  for key of Commands.keyToCommandRegistry +    command = Commands.keyToCommandRegistry[key].command +    commandsToKey[command] = (commandsToKey[command] || []).concat(key) + +  dialogHtml = fetchFileContents("help_dialog.html") +  for group of Commands.commandGroups +    dialogHtml = dialogHtml.replace("{{#{group}}}", +        helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands, +                                      showUnboundCommands, showCommandNames)) +  dialogHtml = dialogHtml.replace("{{version}}", currentVersion) +  dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help") +  dialogHtml = dialogHtml.replace("{{showAdvancedCommands}}", +    Settings.get("helpDialog_showAdvancedCommands")) +  dialogHtml + +# +# 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] +    bindings = (commandsToKey[command] || [""]).join(", ") +    if (showUnboundCommands || commandsToKey[command]) +      isAdvanced = Commands.advancedCommands.indexOf(command) >= 0 +      html.push( +        "<tr class='vimiumReset #{"advanced" if isAdvanced}'>", +        "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>", +        "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description) + +      if (showCommandNames) +        html.push("<span class='vimiumReset commandName'>(#{command})</span>") + +      html.push("</td></tr>") +  html.join("\n") + +# +# Fetches the contents of a file bundled with this extension. +# +fetchFileContents = (extensionFileName) -> +  req = new XMLHttpRequest() +  req.open("GET", chrome.extension.getURL(extensionFileName), false) # false => synchronous +  req.send() +  req.responseText + +# +# Returns the keys that can complete a valid command given the current key queue. +# +getCompletionKeysRequest = (request) -> +  name: "refreshCompletionKeys" +  completionKeys: generateCompletionKeys() +  validFirstKeys: validFirstKeys + +# +# Opens the url in the current tab. +# +openUrlInCurrentTab = (request) -> +  chrome.tabs.getSelected(null, +    (tab) -> chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) })) + +# +# 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 })) + +# +# 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 some data (request.data) to the clipboard. +# +copyToClipboard = (request) -> Clipboard.copy(request.data) + +# +# Selects the tab with the ID specified in request.id +# +selectSpecificTab = (request) -> chrome.tabs.update(request.id, { selected: true }) + +# +# 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() + +filterCompleter = (args, port) -> +  queryTerms = if (args.query == "") then [] else args.query.split(" ") +  completers[args.name].filter(queryTerms, (results) -> port.postMessage({ id: args.id, results: results })) + +# +# Used by everyone to get settings from local storage. +# +getSettingFromLocalStorage = (setting) -> +  if (localStorage[setting] != "" && !localStorage[setting]) +    defaultSettings[setting] +  else +    localStorage[setting] + +getCurrentTimeInSeconds = -> Math.floor((new Date()).getTime() / 1000) + +chrome.tabs.onSelectionChanged.addListener((tabId, selectionInfo) -> +  if (selectionChangedHandlers.length > 0) +    selectionChangedHandlers.pop().call()) + +repeatFunction = (func, totalCount, currentCount, frameId) -> +  if (currentCount < totalCount) +    func( +      -> repeatFunction(func, totalCount, currentCount + 1, frameId), +      frameId) + +# Start action functions + +# 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({}, (tab) -> callback()) +  nextTab: (callback) -> selectTab(callback, "next") +  previousTab: (callback) -> selectTab(callback, "previous") +  firstTab: (callback) -> selectTab(callback, "first") +  lastTab: (callback) -> selectTab(callback, "last") +  removeTab: (callback) -> +    chrome.tabs.getSelected(null, (tab) -> +      chrome.tabs.remove(tab.id) +      # We can't just call the callback here because we need to wait +      # for the selection to change to consider this action done. +      selectionChangedHandlers.push(callback)) +  restoreTab: (callback) -> +    # TODO(ilya): Should this be getLastFocused instead? +    chrome.windows.getCurrent((window) -> +      return unless (tabQueue[window.id] && tabQueue[window.id].length > 0) +      tabQueueEntry = tabQueue[window.id].pop() +      # Clean out the tabQueue so we don't have unused windows laying about. +      delete tabQueue[window.id] if (tabQueue[window.id].length == 0) + +      # We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the +      # tab is created because the content script is not available during the "loading" state. We need to +      # wait until that's over before we can call setScrollPosition. +      chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, (tab) -> +        tabLoadedHandlers[tab.id] = -> +          scrollPort = chrome.tabs.sendRequest(tab.id, +            name: "setScrollPosition", +            scrollX: tabQueueEntry.scrollX, +            scrollY: tabQueueEntry.scrollY) +        callback())) +  openCopiedUrlInCurrentTab: (request) -> openUrlInCurrentTab({ url: Clipboard.paste() }) +  openCopiedUrlInNewTab: (request) -> openUrlInNewTab({ url: Clipboard.paste() }) +  showHelp: (callback, frameId) -> +    chrome.tabs.getSelected(null, (tab) -> +      chrome.tabs.sendRequest(tab.id, +        { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId })) + +# Selects a tab before or after the currently selected tab. +# - direction: "next", "previous", "first" or "last". +selectTab = (callback, direction) -> +  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 }))) + +updateOpenTabs = (tab) -> +  openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId } +  # Frames are recreated on refresh +  delete framesForTab[tab.id] + +# Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page. +# Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist. +# This lets you disable Vimium on a page without needing to reload. +# +# Three situations are considered: +# 1. Active tab is disabled -> disable icon +# 2. Active tab is enabled and should be enabled -> enable icon +# 3. Active tab is enabled but should be disabled -> disable icon and disable vimium +updateActiveState = (tabId) -> +  enabledIcon = "icons/browser_action_enabled.png" +  disabledIcon = "icons/browser_action_disabled.png" +  chrome.tabs.get(tabId, (tab) -> +    # Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page. +    chrome.browserAction.setIcon({ path: disabledIcon }) +    chrome.tabs.sendRequest(tabId, { name: "getActiveState" }, (response) -> +      isCurrentlyEnabled = (response? && response.enabled) +      shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl + +      if (isCurrentlyEnabled) +        if (shouldBeEnabled) +          chrome.browserAction.setIcon({ path: enabledIcon }) +        else +          chrome.browserAction.setIcon({ path: disabledIcon }) +          chrome.tabs.sendRequest(tabId, { name: "disableVimium" }) +      else +        chrome.browserAction.setIcon({ path: disabledIcon }))) + +handleUpdateScrollPosition = (request, sender) -> +  updateScrollPosition(sender.tab, request.scrollX, request.scrollY) + +updateScrollPosition = (tab, scrollX, scrollY) -> +  openTabs[tab.id].scrollX = scrollX +  openTabs[tab.id].scrollY = scrollY + +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) -> +  return unless changeInfo.status == "loading" # only do this once per URL change +  updateOpenTabs(tab) +  updateActiveState(tabId)) + +chrome.tabs.onAttached.addListener((tabId, attachedInfo) -> +  # We should update all the tabs in the old window and the new window. +  if openTabs[tabId] +    updatePositionsAndWindowsForAllTabsInWindow(openTabs[tabId].windowId) +  updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId)) + +chrome.tabs.onMoved.addListener((tabId, moveInfo) -> +  updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId)) + +chrome.tabs.onRemoved.addListener((tabId) -> +  openTabInfo = openTabs[tabId] +  updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId) + +  # If we restore chrome:# pages, they'll ignore Vimium keystrokes when they reappear. +  # Pretend they never existed and adjust tab indices accordingly. +  # Could possibly expand this into a blacklist in the future +  if (/^chrome[^:]*:\/\/.*/.test(openTabInfo.url)) +    for i of tabQueue[openTabInfo.windowId] +      if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex) +        tabQueue[openTabInfo.windowId][i].positionIndex-- +    return + +  if (tabQueue[openTabInfo.windowId]) +    tabQueue[openTabInfo.windowId].push(openTabInfo) +  else +    tabQueue[openTabInfo.windowId] = [openTabInfo] + +  delete openTabs[tabId] +  delete framesForTab[tabId]) + +chrome.tabs.onActiveChanged.addListener((tabId, selectInfo) -> updateActiveState(tabId)) + +chrome.windows.onRemoved.addListener((windowId) -> delete tabQueue[windowId]) + +# End action functions + +updatePositionsAndWindowsForAllTabsInWindow = (windowId) -> +  chrome.tabs.getAllInWindow(windowId, (tabs) -> +    for tab in tabs +      openTabInfo = openTabs[tab.id] +      if (openTabInfo) +        openTabInfo.positionIndex = tab.index +        openTabInfo.windowId = tab.windowId) + +splitKeyIntoFirstAndSecond = (key) -> +  if (key.search(namedKeyRegex) == 0) +      { first: RegExp.$1, second: RegExp.$2 } +  else +    { first: key[0], second: key.slice(1) } + +getActualKeyStrokeLength = (key) -> +  if (key.search(namedKeyRegex) == 0) +    1 + getActualKeyStrokeLength(RegExp.$2) +  else +    key.length + +populateValidFirstKeys = -> +  for key of Commands.keyToCommandRegistry +    if (getActualKeyStrokeLength(key) == 2) +      validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true + +populateSingleKeyCommands = -> +  for key of Commands.keyToCommandRegistry +    if (getActualKeyStrokeLength(key) == 1) +      singleKeyCommands.push(key) + +refreshCompletionKeysAfterMappingSave = -> +  validFirstKeys = {} +  singleKeyCommands = [] + +  populateValidFirstKeys() +  populateSingleKeyCommands() + +  sendRequestToAllTabs(getCompletionKeysRequest()) + +# Generates a list of keys that can complete a valid command given the current key queue or the one passed in +generateCompletionKeys = (keysToCheck) -> +  splitHash = splitKeyQueue(keysToCheck || keyQueue) +  command = splitHash.command +  count = splitHash.count + +  completionKeys = singleKeyCommands.slice(0) + +  if (getActualKeyStrokeLength(command) == 1) +    for key of Commands.keyToCommandRegistry +      splitKey = splitKeyIntoFirstAndSecond(key) +      if (splitKey.first == command) +       completionKeys.push(splitKey.second) + +  completionKeys + +splitKeyQueue = (queue) -> +  match = /([1-9][0-9]*)?(.*)/.exec(queue) +  count = parseInt(match[1], 10) +  command = match[2] + +  { count: count, command: command } + +handleKeyDown = (request, port) -> +  key = request.keyChar +  if (key == "<ESC>") +    console.log("clearing keyQueue") +    keyQueue = "" +  else +    console.log("checking keyQueue: [", keyQueue + key, "]") +    keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId) +    console.log("new KeyQueue: " + keyQueue) + +checkKeyQueue = (keysToCheck, tabId, frameId) -> +  refreshedCompletionKeys = false +  splitHash = splitKeyQueue(keysToCheck) +  command = splitHash.command +  count = splitHash.count + +  return keysToCheck if command.length == 0 +  count = 1 if isNaN(count) + +  if (Commands.keyToCommandRegistry[command]) +    registryEntry = Commands.keyToCommandRegistry[command] + +    if !registryEntry.isBackgroundCommand +      chrome.tabs.sendRequest(tabId, +        name: "executePageCommand", +        command: registryEntry.command, +        frameId: frameId, +        count: count, +        passCountToFunction: registryEntry.passCountToFunction, +        completionKeys: generateCompletionKeys("")) +      refreshedCompletionKeys = true +    else +      if registryEntry.passCountToFunction +        BackgroundCommands[registryEntry.command](count) +      else +        repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId) + +    newKeyQueue = "" +  else if (getActualKeyStrokeLength(command) > 1) +    splitKey = splitKeyIntoFirstAndSecond(command) + +    # The second key might be a valid command by its self. +    if (Commands.keyToCommandRegistry[splitKey.second]) +      newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId) +    else +      newKeyQueue = (if validFirstKeys[splitKey.second] then splitKey.second else "") +  else +    newKeyQueue = (if validFirstKeys[command] then count.toString() + command else "") + +  # If we haven't sent the completion keys piggybacked on executePageCommand, +  # send them by themselves. +  unless refreshedCompletionKeys +    chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null) + +  newKeyQueue + +# +# Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API. +# +sendRequestToAllTabs = (args) -> +  chrome.windows.getAll({ populate: true }, (windows) -> +    for window in windows +      for tab in window.tabs +        chrome.tabs.sendRequest(tab.id, args, null)) + +# Compares two version strings (e.g. "1.1" and "1.5") and returns +# -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB. +compareVersions = (versionA, versionB) -> +  versionA = versionA.split(".") +  versionB = versionB.split(".") +  for i in [0...(Math.max(versionA.length, versionB.length))] +    a = parseInt(versionA[i] || 0, 10) +    b = parseInt(versionB[i] || 0, 10) +    if (a < b) +      return -1 +    else if (a > b) +      return 1 +  0 + +# +# 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") +  compareVersions(currentVersion, Settings.get("previousVersion")) == 1 + +openOptionsPageInNewTab = -> +  chrome.tabs.getSelected(null, (tab) -> +    chrome.tabs.create({ url: chrome.extension.getURL("options/options.html"), index: tab.index + 1 })) + +registerFrame = (request, sender) -> +  unless framesForTab[sender.tab.id] +    framesForTab[sender.tab.id] = { frames: [] } + +  if (request.is_top) +    focusedFrame = request.frameId +    framesForTab[sender.tab.id].total = request.total + +  framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area }) + +  # We've seen all the frames. Time to focus the largest one. +  # NOTE: Disabled because it's buggy with iframes. +  # if (framesForTab[sender.tab.id].frames.length >= framesForTab[sender.tab.id].total) +  #  focusLargestFrame(sender.tab.id) + +focusLargestFrame = (tabId) -> +  mainFrameId = null +  mainFrameArea = 0 + +  for frame in framesForTab[tabId] +    if (frame.area > mainFrameArea) +      mainFrameId = frame.id +      mainFrameArea = frame.area + +  chrome.tabs.sendRequest(tabId, { name: "focusFrame", frameId: mainFrameId, highlight: false }) + +handleFrameFocused = (request, sender) -> focusedFrame = request.frameId + +nextFrame = (count) -> +  chrome.tabs.getSelected(null, (tab) -> +    frames = framesForTab[tab.id].frames +    curr_index = getCurrFrameIndex(frames) + +    # TODO: Skip the "top" frame (which doesn't actually have a <frame> tag), +    # since it exists only to contain the other frames. +    new_index = (curr_index + count) % frames.length + +    chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true })) + +getCurrFrameIndex = (frames) -> +  for i in [0...frames.length] +    return i if frames[i].id == focusedFrame +  frames.length + 1 + +# Port handler mapping +portHandlers = +  keyDown: handleKeyDown, +  settings: handleSettings, +  filterCompleter: filterCompleter + +sendRequestHandlers = +  getCompletionKeys: getCompletionKeysRequest, +  getCurrentTabUrl: getCurrentTabUrl, +  getShowAdvancedCommands: getShowAdvancedCommands, +  openUrlInNewTab: openUrlInNewTab, +  openUrlInCurrentTab: openUrlInCurrentTab, +  openOptionsPageInNewTab: openOptionsPageInNewTab, +  registerFrame: registerFrame, +  frameFocused: handleFrameFocused, +  upgradeNotificationClosed: upgradeNotificationClosed, +  updateScrollPosition: handleUpdateScrollPosition, +  copyToClipboard: copyToClipboard, +  isEnabledForUrl: isEnabledForUrl, +  saveHelpDialogSettings: saveHelpDialogSettings, +  selectSpecificTab: selectSpecificTab, +  refreshCompleter: refreshCompleter + +init = -> +  Commands.clearKeyMappingsAndSetDefaults() + +  if (Settings.has("keyMappings")) +    Commands.parseCustomKeyMappings(Settings.get("keyMappings")) + +  # In version 1.22, we changed the mapping for "d" and "u" to be scroll page down/up instead of close +  # and restore tab. For existing users, we want to preserve existing behavior for them by adding some +  # custom key mappings on their behalf. +  if (Settings.get("previousVersion") == "1.21") +    customKeyMappings = Settings.get("keyMappings") || "" +    if ((Commands.keyToCommandRegistry["d"] || {}).command == "scrollPageDown") +      customKeyMappings += "\nmap d removeTab" +    if ((Commands.keyToCommandRegistry["u"] || {}).command == "scrollPageUp") +      customKeyMappings += "\nmap u restoreTab" +    if (customKeyMappings != "") +      Settings.set("keyMappings", customKeyMappings) +      Commands.parseCustomKeyMappings(customKeyMappings) + +  populateValidFirstKeys() +  populateSingleKeyCommands() +  if (shouldShowUpgradeMessage()) +    sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion }) + +  # Ensure that openTabs is populated when Vimium is installed. +  chrome.windows.getAll({ populate: true }, (windows) -> +    for window in windows +      for tab in window.tabs +        updateOpenTabs(tab) +        createScrollPositionHandler = -> +          (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response? +        chrome.tabs.sendRequest(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())) + + +init() + +# +# Convenience function for development use. +# +runTests = -> open(chrome.extension.getURL('test_harnesses/automated.html')) diff --git a/background_scripts/main.js b/background_scripts/main.js deleted file mode 100644 index 8e75314e..00000000 --- a/background_scripts/main.js +++ /dev/null @@ -1,786 +0,0 @@ -var currentVersion = Utils.getCurrentVersion(); - -var tabQueue = {}; // windowId -> Array -var openTabs = {}; // tabId -> object with various tab properties -var keyQueue = ""; // Queue of keys typed -var validFirstKeys = {}; -var singleKeyCommands = []; -var focusedFrame = null; -var framesForTab = {}; - -// 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 -// the string. -var namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/; - -// Port handler mapping -var portHandlers = { -  keyDown:              handleKeyDown, -  settings:             handleSettings, -  filterCompleter:      filterCompleter -}; - -var sendRequestHandlers = { -  getCompletionKeys: getCompletionKeysRequest, -  getCurrentTabUrl: getCurrentTabUrl, -  getShowAdvancedCommands: getShowAdvancedCommands, -  openUrlInNewTab: openUrlInNewTab, -  openUrlInCurrentTab: openUrlInCurrentTab, -  openOptionsPageInNewTab: openOptionsPageInNewTab, -  registerFrame: registerFrame, -  frameFocused: handleFrameFocused, -  upgradeNotificationClosed: upgradeNotificationClosed, -  updateScrollPosition: handleUpdateScrollPosition, -  copyToClipboard: copyToClipboard, -  isEnabledForUrl: isEnabledForUrl, -  saveHelpDialogSettings: saveHelpDialogSettings, -  selectSpecificTab: selectSpecificTab, -  refreshCompleter: refreshCompleter -}; - -// Event handlers -var selectionChangedHandlers = []; -var tabLoadedHandlers = {}; // tabId -> function() - -var completionSources = { -  bookmarks: new BookmarkCompleter(), -  history: new HistoryCompleter(), -  domains: new DomainCompleter(), -  tabs: new TabCompleter() -}; - -var completers = { -  omni: new MultiCompleter([ -    completionSources.bookmarks, -    completionSources.history, -    completionSources.domains]), -  bookmarks: new MultiCompleter([completionSources.bookmarks]), -  tabs: new MultiCompleter([completionSources.tabs]) -}; - -chrome.extension.onConnect.addListener(function(port, name) { -  var senderTabId = port.sender.tab ? port.sender.tab.id : 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 -  // the scroll position will not be possible. -  if (port.name === "domReady" && senderTabId !== null) { -    if (tabLoadedHandlers[senderTabId]) { -      var toCall = tabLoadedHandlers[senderTabId]; -      // Delete first to be sure there's no circular events. -      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.sendRequest(senderTabId, { name: "showUpgradeNotification", version: currentVersion }); -  } - -  if (portHandlers[port.name]) -    port.onMessage.addListener(portHandlers[port.name]); -}); - -chrome.extension.onRequest.addListener(function (request, sender, sendResponse) { -  var senderTabId = sender.tab ? sender.tab.id : null; -  if (sendRequestHandlers[request.handler]) -    sendResponse(sendRequestHandlers[request.handler](request, sender)); -  // Ensure the sendResponse callback is freed. -  return false; -}); - -/* - * Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:// .." - * because window.location doesn't know anything about the Chrome-specific "view-source:". - */ -function getCurrentTabUrl(request, sender) { -  return sender.tab.url; -} - -/* - * Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL. - */ -function isEnabledForUrl(request) { -  // excludedUrls are stored as a series of URL expressions separated by newlines. -  var excludedUrls = Settings.get("excludedUrls").split("\n"); -  var isEnabled = true; -  for (var i = 0; i < excludedUrls.length; i++) { -    // The user can add "*" to the URL which means ".*" -    var regexp = new RegExp("^" + excludedUrls[i].replace(/\*/g, ".*") + "$"); -    if (request.url.match(regexp)) -      isEnabled = false; -  } -  return { isEnabledForUrl: isEnabled }; -} - -/* - * Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings. - */ -function addExcludedUrl(url) { -  url = trim(url); -  if (url === "") { return; } - -  var excludedUrls = Settings.get("excludedUrls"); -  excludedUrls += "\n" + url; -  Settings.set("excludedUrls", excludedUrls); - -  chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, function(tabs) { -    updateActiveState(tabs[0].id); -  }); -} - -function getShowAdvancedCommands(request){ -  return Settings.get("helpDialog_showAdvancedCommands"); -} - -function saveHelpDialogSettings(request) { -  Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands); -} - -function showHelp(callback, frameId) { -  chrome.tabs.getSelected(null, function(tab) { -    chrome.tabs.sendRequest(tab.id, -      { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }); -  }); -} - -/* - * Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings. - */ -function helpDialogHtml(showUnboundCommands, showCommandNames, customTitle) { -  var commandsToKey = {}; -  for (var key in Commands.keyToCommandRegistry) { -    var command = Commands.keyToCommandRegistry[key].command; -    commandsToKey[command] = (commandsToKey[command] || []).concat(key); -  } -  var dialogHtml = fetchFileContents("help_dialog.html"); -  for (var group in Commands.commandGroups) -    dialogHtml = dialogHtml.replace("{{" + group + "}}", -        helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands, -                                      showUnboundCommands, showCommandNames)); -  dialogHtml = dialogHtml.replace("{{version}}", currentVersion); -  dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help"); -  return dialogHtml; -} - -/* - * Generates HTML for a given set of commands. commandGroups are defined in commands.js - */ -function helpDialogHtmlForCommandGroup(group, commandsToKey, availableCommands, -                                       showUnboundCommands, showCommandNames) { -  var html = []; -  for (var i = 0; i < Commands.commandGroups[group].length; i++) { -    var command = Commands.commandGroups[group][i]; -    bindings = (commandsToKey[command] || [""]).join(", "); -    if (showUnboundCommands || commandsToKey[command]) { -      html.push( -        "<tr class='vimiumReset " + -            (Commands.advancedCommands.indexOf(command) >= 0 ? "advanced" : "") + "'>", -        "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>", -        "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description); - -      if (showCommandNames) -        html.push("<span class='vimiumReset commandName'>(" + command + ")</span>"); - -      html.push("</td></tr>"); -    } -  } -  return html.join("\n"); -} - -/* - * Fetches the contents of a file bundled with this extension. - */ -function fetchFileContents(extensionFileName) { -  var req = new XMLHttpRequest(); -  req.open("GET", chrome.extension.getURL(extensionFileName), false); // false => synchronous -  req.send(); -  return req.responseText; -} - -/** - * Returns the keys that can complete a valid command given the current key queue. - */ -function getCompletionKeysRequest(request) { -  return { name: "refreshCompletionKeys", -           completionKeys: generateCompletionKeys(), -           validFirstKeys: validFirstKeys -         }; -} - -/* -  * Opens the url in the current tab. -  */ - function openUrlInCurrentTab(request) { -   chrome.tabs.getSelected(null, function(tab) { -     chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) }); -   }); - } - -/* - * Opens request.url in new tab and switches to it if request.selected is true. - */ -function openUrlInNewTab(request) { -  chrome.tabs.getSelected(null, function(tab) { -    chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true }); -  }); -} - -function openCopiedUrlInCurrentTab(request) { openUrlInCurrentTab({ url: Clipboard.paste() }); } - -function openCopiedUrlInNewTab(request) { openUrlInNewTab({ url: Clipboard.paste() }); } - -/* - * 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. - */ -function upgradeNotificationClosed(request) { -  Settings.set("previousVersion", currentVersion); -  sendRequestToAllTabs({ name: "hideUpgradeNotification" }); -} - -/* - * Copies some data (request.data) to the clipboard. - */ -function copyToClipboard(request) { -  Clipboard.copy(request.data); -} - -/** -  * Selects the tab with the ID specified in request.id -  */ -function selectSpecificTab(request) { -  chrome.tabs.update(request.id, { selected: true }); -} - -/* - * Used by the content scripts to get settings from the local storage. - */ -function handleSettings(args, port) { -  if (args.operation == "get") { -    var value = Settings.get(args.key); -    port.postMessage({ key: args.key, value: value }); -  } -  else { // operation == "set" -    Settings.set(args.key, args.value); -  } -} - -function refreshCompleter(request) { -  completers[request.name].refresh(); -} - -function filterCompleter(args, port) { -  var queryTerms = args.query === "" ? [] : args.query.split(" "); -  completers[args.name].filter(queryTerms, function(results) { -    port.postMessage({ id: args.id, results: results }); -  }); -} - -/* - * Used by everyone to get settings from local storage. - */ -function getSettingFromLocalStorage(setting) { -  if (localStorage[setting] !== "" && !localStorage[setting]) { -    return defaultSettings[setting]; -  } else { -    return localStorage[setting]; -  } -} - -function getCurrentTimeInSeconds() { Math.floor((new Date()).getTime() / 1000); } - -chrome.tabs.onSelectionChanged.addListener(function(tabId, selectionInfo) { -  if (selectionChangedHandlers.length > 0) { selectionChangedHandlers.pop().call(); } -}); - -function repeatFunction(func, totalCount, currentCount, frameId) { -  if (currentCount < totalCount) -    func(function() { repeatFunction(func, totalCount, currentCount + 1, frameId); }, frameId); -} - -// Start action functions -function createTab(callback) { -  chrome.tabs.create({}, function(tab) { callback(); }); -} - -function nextTab(callback) { selectTab(callback, "next"); } -function previousTab(callback) { selectTab(callback, "previous"); } -function firstTab(callback) { selectTab(callback, "first"); } -function lastTab(callback) { selectTab(callback, "last"); } - -/* - * Selects a tab before or after the currently selected tab. Direction is either "next", "previous", "first" or "last". - */ -function selectTab(callback, direction) { -  chrome.tabs.getAllInWindow(null, function(tabs) { -    if (tabs.length <= 1) -      return; -    chrome.tabs.getSelected(null, function(currentTab) { -        switch (direction) { -          case "next": -            toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length]; -            break; -          case "previous": -            toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length]; -            break; -          case "first": -            toSelect = tabs[0]; -            break; -          case "last": -            toSelect = tabs[tabs.length - 1]; -            break; -        } -        selectionChangedHandlers.push(callback); -        chrome.tabs.update(toSelect.id, { selected: true }); -    }); -  }); -} - -function removeTab(callback) { -  chrome.tabs.getSelected(null, function(tab) { -    chrome.tabs.remove(tab.id); -    // We can't just call the callback here because we actually need to wait -    // for the selection to change to consider this action done. -    selectionChangedHandlers.push(callback); -  }); -} - -function updateOpenTabs(tab) { -  openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId }; -  // Frames are recreated on refresh -  delete framesForTab[tab.id]; -} - -/* Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page. - * Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist. - * This lets you disable Vimium on a page without needing to reload. - * - * Three situations are considered: - * 1. Active tab is disabled -> disable icon - * 2. Active tab is enabled and should be enabled -> enable icon - * 3. Active tab is enabled but should be disabled -> disable icon and disable vimium - */ -function updateActiveState(tabId) { -  var enabledIcon = "icons/browser_action_enabled.png"; -  var disabledIcon = "icons/browser_action_disabled.png"; -  chrome.tabs.get(tabId, function(tab) { -    // Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page. -    chrome.browserAction.setIcon({ path: disabledIcon }); -    chrome.tabs.sendRequest(tabId, { name: "getActiveState" }, function(response) { -      var isCurrentlyEnabled = response !== undefined && response.enabled; -      var shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl; - -      if (isCurrentlyEnabled) { -        if (shouldBeEnabled) { -          chrome.browserAction.setIcon({ path: enabledIcon }); -        } else { -          chrome.browserAction.setIcon({ path: disabledIcon }); -          chrome.tabs.sendRequest(tabId, { name: "disableVimium" }); -        } -      } else { -        chrome.browserAction.setIcon({ path: disabledIcon }); -      } -    }); -  }); -} - -function handleUpdateScrollPosition(request, sender) { -  updateScrollPosition(sender.tab, request.scrollX, request.scrollY); -} - -function updateScrollPosition(tab, scrollX, scrollY) { -  openTabs[tab.id].scrollX = scrollX; -  openTabs[tab.id].scrollY = scrollY; -} - - -chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { -  if (changeInfo.status != "loading") { return; } // only do this once per URL change -  updateOpenTabs(tab); -  updateActiveState(tabId); -}); - -chrome.tabs.onAttached.addListener(function(tabId, attachedInfo) { -  // We should update all the tabs in the old window and the new window. -  if (openTabs[tabId]) { -    updatePositionsAndWindowsForAllTabsInWindow(openTabs[tabId].windowId); -  } -  updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId); -}); - -chrome.tabs.onMoved.addListener(function(tabId, moveInfo) { -  updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId); -}); - -chrome.tabs.onRemoved.addListener(function(tabId) { -  var openTabInfo = openTabs[tabId]; -  updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId); - -  // If we restore chrome:// pages, they'll ignore Vimium keystrokes when they reappear. -  // Pretend they never existed and adjust tab indices accordingly. -  // Could possibly expand this into a blacklist in the future -  if (/^chrome[^:]*:\/\/.*/.test(openTabInfo.url)) { -    for (var i in tabQueue[openTabInfo.windowId]) { -      if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex) -        tabQueue[openTabInfo.windowId][i].positionIndex--; -    } -    return; -  } - -  if (tabQueue[openTabInfo.windowId]) -    tabQueue[openTabInfo.windowId].push(openTabInfo); -  else -    tabQueue[openTabInfo.windowId] = [openTabInfo]; - -  delete openTabs[tabId]; -  delete framesForTab[tabId]; -}); - -chrome.tabs.onActiveChanged.addListener(function(tabId, selectInfo) { -  updateActiveState(tabId); -}); - -chrome.windows.onRemoved.addListener(function(windowId) { -  delete tabQueue[windowId]; -}); - -function restoreTab(callback) { -  // TODO(ilya): Should this be getLastFocused instead? -  chrome.windows.getCurrent(function(window) { -    if (tabQueue[window.id] && tabQueue[window.id].length > 0) -    { -      var tabQueueEntry = tabQueue[window.id].pop(); - -      // Clean out the tabQueue so we don't have unused windows laying about. -      if (tabQueue[window.id].length === 0) -        delete tabQueue[window.id]; - -      // We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the -      // tab is created because the content script is not available during the "loading" state. We need to -      // wait until that's over before we can call setScrollPosition. -      chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, function(tab) { -        tabLoadedHandlers[tab.id] = function() { -          var scrollPort = chrome.tabs.sendRequest(tab.id, { -            name: "setScrollPosition", -            scrollX: tabQueueEntry.scrollX, -            scrollY: tabQueueEntry.scrollY -          }); -        }; -        callback(); -      }); -    } -  }); -} -// End action functions - -function updatePositionsAndWindowsForAllTabsInWindow(windowId) { -  chrome.tabs.getAllInWindow(windowId, function (tabs) { -    for (var i = 0; i < tabs.length; i++) { -      var tab = tabs[i]; -      var openTabInfo = openTabs[tab.id]; -      if (openTabInfo) { -        openTabInfo.positionIndex = tab.index; -        openTabInfo.windowId = tab.windowId; -      } -    } -  }); -} - -function splitKeyIntoFirstAndSecond(key) { -  if (key.search(namedKeyRegex) === 0) -      return { first: RegExp.$1, second: RegExp.$2 }; -  else -    return { first: key[0], second: key.slice(1) }; -} - -function getActualKeyStrokeLength(key) { -  if (key.search(namedKeyRegex) === 0) -    return 1 + getActualKeyStrokeLength(RegExp.$2); -  else -    return key.length; -} - -function populateValidFirstKeys() { -  for (var key in Commands.keyToCommandRegistry) -  { -    if (getActualKeyStrokeLength(key) == 2) -      validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true; -  } -} - -function populateSingleKeyCommands() { -  for (var key in Commands.keyToCommandRegistry) -  { -    if (getActualKeyStrokeLength(key) == 1) -      singleKeyCommands.push(key); -  } -} - -function refreshCompletionKeysAfterMappingSave() { -  validFirstKeys = {}; -  singleKeyCommands = []; - -  populateValidFirstKeys(); -  populateSingleKeyCommands(); - -  sendRequestToAllTabs(getCompletionKeysRequest()); -} - -/* - * Generates a list of keys that can complete a valid command given the current key queue or the one passed - * in. - */ -function generateCompletionKeys(keysToCheck) { -  var splitHash = splitKeyQueue(keysToCheck || keyQueue); -  command = splitHash.command; -  count = splitHash.count; - -  var completionKeys = singleKeyCommands.slice(0); - -  if (getActualKeyStrokeLength(command) == 1) -  { -    for (var key in Commands.keyToCommandRegistry) -    { -      var splitKey = splitKeyIntoFirstAndSecond(key); -      if (splitKey.first == command) -       completionKeys.push(splitKey.second); -    } -  } - -  return completionKeys; -} - -function splitKeyQueue(queue) { -  var match = /([1-9][0-9]*)?(.*)/.exec(queue); -  var count = parseInt(match[1], 10); -  var command = match[2]; - -  return {count: count, command: command}; -} - -function handleKeyDown(request, port) { -  var key = request.keyChar; -  if (key == "<ESC>") { -    console.log("clearing keyQueue"); -    keyQueue = ""; -  } -  else { -    console.log("checking keyQueue: [", keyQueue + key, "]"); -    keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId); -    console.log("new KeyQueue: " + keyQueue); -  } -} - -function checkKeyQueue(keysToCheck, tabId, frameId) { -  var refreshedCompletionKeys = false; -  var splitHash = splitKeyQueue(keysToCheck); -  command = splitHash.command; -  count = splitHash.count; - -  if (command.length === 0) { return keysToCheck; } -  if (isNaN(count)) { count = 1; } - -  if (Commands.keyToCommandRegistry[command]) { -    registryEntry = Commands.keyToCommandRegistry[command]; - -    if (!registryEntry.isBackgroundCommand) { -      chrome.tabs.sendRequest(tabId, { -        name: "executePageCommand", -        command: registryEntry.command, -        frameId: frameId, -        count: count, -        passCountToFunction: registryEntry.passCountToFunction, -        completionKeys: generateCompletionKeys("") -      }); -      refreshedCompletionKeys = true; -    } else { -      if(registryEntry.passCountToFunction){ -        this[registryEntry.command](count); -      } else { -        repeatFunction(this[registryEntry.command], count, 0, frameId); -      } -    } - -    newKeyQueue = ""; -  } else if (getActualKeyStrokeLength(command) > 1) { -    var splitKey = splitKeyIntoFirstAndSecond(command); - -    // The second key might be a valid command by its self. -    if (Commands.keyToCommandRegistry[splitKey.second]) -      newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId); -    else -      newKeyQueue = (validFirstKeys[splitKey.second] ? splitKey.second : ""); -  } else { -    newKeyQueue = (validFirstKeys[command] ? count.toString() + command : ""); -  } - -  // If we haven't sent the completion keys piggybacked on executePageCommand, -  // send them by themselves. -  if (!refreshedCompletionKeys) { -    chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null); -  } - -  return newKeyQueue; -} - -/* - * Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API. - */ -function sendRequestToAllTabs(args) { -  chrome.windows.getAll({ populate: true }, function(windows) { -    for (var i = 0; i < windows.length; i++) -      for (var j = 0; j < windows[i].tabs.length; j++) -        chrome.tabs.sendRequest(windows[i].tabs[j].id, args, null); -  }); -} - -// Compares two version strings (e.g. "1.1" and "1.5") and returns -// -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB. -function compareVersions(versionA, versionB) { -  versionA = versionA.split("."); -  versionB = versionB.split("."); -  for (var i = 0; i < Math.max(versionA.length, versionB.length); i++) { -    var a = parseInt(versionA[i] || 0, 10); -    var b = parseInt(versionB[i] || 0, 10); -    if (a < b) return -1; -    else if (a > b) return 1; -  } -  return 0; -} - -/* - * Returns true if the current extension version is greater than the previously recorded version in - * localStorage, and false otherwise. - */ -function shouldShowUpgradeMessage() { -  // Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new -  // installs. -  if (!Settings.get("previousVersion")) -    Settings.set("previousVersion", currentVersion); -  return compareVersions(currentVersion, Settings.get("previousVersion")) == 1; -} - -function openOptionsPageInNewTab() { -  chrome.tabs.getSelected(null, function(tab) { -    chrome.tabs.create({ url: chrome.extension.getURL("options/options.html"), index: tab.index + 1 }); -  }); -} - -function registerFrame(request, sender) { -  if (!framesForTab[sender.tab.id]) -    framesForTab[sender.tab.id] = { frames: [] }; - -  if (request.is_top) { -    focusedFrame = request.frameId; -    framesForTab[sender.tab.id].total = request.total; -  } - -  framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area }); - -  // We've seen all the frames. Time to focus the largest one. -  // NOTE: Disabled because it's buggy with iframes. -  // if (framesForTab[sender.tab.id].frames.length >= framesForTab[sender.tab.id].total) -  //  focusLargestFrame(sender.tab.id); -} - -function focusLargestFrame(tabId) { -  var mainFrameId = null; -  var mainFrameArea = 0; - -  for (var i = 0; i < framesForTab[tabId].frames.length; i++) { -    var currentFrame = framesForTab[tabId].frames[i]; - -    if (currentFrame.area > mainFrameArea) { -      mainFrameId = currentFrame.id; -      mainFrameArea = currentFrame.area; -    } -  } - -  chrome.tabs.sendRequest(tabId, { name: "focusFrame", frameId: mainFrameId, highlight: false }); -} - -function handleFrameFocused(request, sender) { -  focusedFrame = request.frameId; -} - -function nextFrame(count) { -  chrome.tabs.getSelected(null, function(tab) { -    var frames = framesForTab[tab.id].frames; -    var curr_index = getCurrFrameIndex(frames); - -    // TODO: Skip the "top" frame (which doesn't actually have a <frame> tag), -    // since it exists only to contain the other frames. -    var new_index = (curr_index + count) % frames.length; - -    chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true }); -  }); -} - -function getCurrFrameIndex(frames) { -  var index; -  for (index=0; index < frames.length; index++) { -    if (frames[index].id == focusedFrame) -        break; -  } -  return index; -} - -/* - * Convenience function for trimming leading and trailing whitespace. - */ -function trim(str) { -  return str.replace(/^\s*/, "").replace(/\s*$/, ""); -} - -function init() { -  Commands.clearKeyMappingsAndSetDefaults(); - -  if (Settings.has("keyMappings")) -    Commands.parseCustomKeyMappings(Settings.get("keyMappings")); - -  // In version 1.22, we changed the mapping for "d" and "u" to be scroll page down/up instead of close -  // and restore tab. For existing users, we want to preserve existing behavior for them by adding some -  // custom key mappings on their behalf. -  if (Settings.get("previousVersion") == "1.21") { -    var customKeyMappings = Settings.get("keyMappings") || ""; -    if ((Commands.keyToCommandRegistry["d"] || {}).command == "scrollPageDown") -      customKeyMappings += "\nmap d removeTab"; -    if ((Commands.keyToCommandRegistry["u"] || {}).command == "scrollPageUp") -      customKeyMappings += "\nmap u restoreTab"; -    if (customKeyMappings !== "") { -      Settings.set("keyMappings", customKeyMappings); -      Commands.parseCustomKeyMappings(customKeyMappings); -    } -  } - -  populateValidFirstKeys(); -  populateSingleKeyCommands(); -  if (shouldShowUpgradeMessage()) -    sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion }); - -  // Ensure that openTabs is populated when Vimium is installed. -  chrome.windows.getAll({ populate: true }, function(windows) { -    for (var i in windows) { -      for (var j in windows[i].tabs) { -        var tab = windows[i].tabs[j]; -        updateOpenTabs(tab); -        chrome.tabs.sendRequest(tab.id, { name: "getScrollPosition" }, function() { -            return function(response) { -                if (response === undefined) -                    return; -                updateScrollPosition(tab, response.scrollX, response.scrollY); -            }; -        }()); -      } -    } -  }); -} -init(); - -/** - * Convenience function for development use. - */ -function runTests() { -  open(chrome.extension.getURL('test_harnesses/automated.html')); -} | 
