From fe29253fa5654ff5e4e1ece0287803a914bc56b6 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Mon, 29 Jun 2015 00:27:28 +0200 Subject: [PATCH] Support multi-process Firefox See #378. --- extension/bootstrap.coffee | 34 +- extension/install.rdf.tmpl | 1 + extension/lib/api.coffee | 22 +- extension/lib/button.coffee | 37 +- extension/lib/commands-frame.coffee | 316 +++++++++++++++++ extension/lib/commands.coffee | 509 ++++++++------------------- extension/lib/defaults.coffee | 5 + extension/lib/events-frame.coffee | 122 +++++++ extension/lib/events.coffee | 368 +++++++------------ extension/lib/help.coffee | 16 +- extension/lib/hints.coffee | 152 ++++---- extension/lib/l10n.coffee | 3 + extension/lib/legacy.coffee | 3 + extension/lib/main-frame.coffee | 19 +- extension/lib/main.coffee | 51 +-- extension/lib/marker.coffee | 25 +- extension/lib/message-manager.coffee | 67 ++-- extension/lib/migrations.coffee | 3 + extension/lib/modes.coffee | 95 +++-- extension/lib/options.coffee | 21 +- extension/lib/parse-prefs.coffee | 2 + extension/lib/prefs.coffee | 8 +- extension/lib/public.coffee | 3 + extension/lib/utils.coffee | 128 +++---- extension/lib/vim-frame.coffee | 67 ++++ extension/lib/vim.coffee | 109 ++++-- extension/lib/vimfx.coffee | 67 ++-- extension/test/test-api.coffee | 81 +++-- extension/test/utils.coffee | 27 ++ 29 files changed, 1348 insertions(+), 1013 deletions(-) create mode 100644 extension/lib/commands-frame.coffee create mode 100644 extension/lib/events-frame.coffee create mode 100644 extension/lib/vim-frame.coffee create mode 100644 extension/test/utils.coffee diff --git a/extension/bootstrap.coffee b/extension/bootstrap.coffee index 3bffec0..cbb5de9 100644 --- a/extension/bootstrap.coffee +++ b/extension/bootstrap.coffee @@ -18,19 +18,26 @@ # along with VimFx. If not, see . ### -# Expose `Components` shortcuts a well as `Services` and `console` in all -# modules. -{ classes: Cc, interfaces: Ci, utils: Cu } = Components +# This file boots the main VimFx process, as well as each frame script. It tries +# to do the minimum amount of things to run main.coffee, or main-frame.coffee +# for frame scripts. It defines a few global variables, and sets up a +# Node.js-style `require` module loader. + +# Expose `Components` shortcuts a well as `Services`, `console` and +# `IS_FRAME_SCRIPT` in all modules. The `@`s are needed for frame scripts. +{ classes: @Cc, interfaces: @Ci, utils: @Cu } = Components Cu.import('resource://gre/modules/Services.jsm') Cu.import('resource://gre/modules/devtools/Console.jsm') +@IS_FRAME_SCRIPT = (typeof content != 'undefined') + do (global = this) -> - isFrameScript = (typeof content != 'undefined') - if isFrameScript + if IS_FRAME_SCRIPT [ global.__SCRIPT_URI_SPEC__ ] = sendSyncMessage('VimFx:tabCreated') return if __SCRIPT_URI_SPEC__ == false + global.FRAME_SCRIPT_ENVIRONMENT = global shutdownHandlers = [] @@ -67,7 +74,7 @@ do (global = this) -> require.scopes = {} require.data = require('./require-data') - unless isFrameScript + unless IS_FRAME_SCRIPT # Set default prefs and apply migrations as early as possible. { applyMigrations } = require('./lib/legacy') migrations = require('./lib/migrations') @@ -76,17 +83,18 @@ do (global = this) -> prefs.default._init() applyMigrations(migrations) - main = if isFrameScript then './lib/main-frame' else './lib/main' + main = if IS_FRAME_SCRIPT then './lib/main-frame' else './lib/main' global.startup = require(main) - global.shutdown = (data, reason) -> + global.shutdown = -> + require('./lib/message-manager').send('shutdown') unless IS_FRAME_SCRIPT + for shutdownHandler in shutdownHandlers try shutdownHandler() catch error Cu.reportError(error) shutdownHandlers = null - removeEventListener('unload', shutdown, true) if isFrameScript # Release everything in `require`d modules. This must be done _after_ all # shutdownHandlers, since they use variables in these scopes. @@ -95,10 +103,10 @@ do (global = this) -> scope[name] = null require.scopes = null - global.install = (data, reason) -> + global.install = -> - global.uninstall = (data, reason) -> + global.uninstall = -> - if isFrameScript - addEventListener('unload', shutdown, true) + if IS_FRAME_SCRIPT + require('./lib/message-manager').listenOnce('shutdown', shutdown) startup() diff --git a/extension/install.rdf.tmpl b/extension/install.rdf.tmpl index 6522ef9..798da3f 100644 --- a/extension/install.rdf.tmpl +++ b/extension/install.rdf.tmpl @@ -9,6 +9,7 @@ https://github.com/akhodakivskiy/VimFx {{version}} true + true 2 diff --git a/extension/lib/api.coffee b/extension/lib/api.coffee index 8cc77d8..a48158f 100644 --- a/extension/lib/api.coffee +++ b/extension/lib/api.coffee @@ -17,6 +17,8 @@ # along with VimFx. If not, see . ### +# This file defines VimFx’s public API. + defaults = require('./defaults') prefs = require('./prefs') utils = require('./utils') @@ -79,11 +81,8 @@ createAPI = (vimfx) -> vimfx.optionOverrides = [] vimfx.options = new Proxy(vimfx.options, { get: (options, pref) -> - location = vimfx.getCurrentLocation() - # If there is no current location available yet, simply ignore the - # overrides. - if location - overrides = getOverrides(vimfx.optionOverrides, location) + location = utils.getCurrentLocation() + overrides = getOverrides(vimfx.optionOverrides, location) return overrides?[pref] ? options[pref] }) vimfx.optionOverrides.push(rules...) @@ -92,17 +91,14 @@ createAPI = (vimfx) -> unless vimfx.keyOverrides vimfx.keyOverrides = [] vimfx.options.keyValidator = (keyStr, mode) -> - location = vimfx.getCurrentLocation() - # If there is no current location available yet, simply ignore the - # overrides. - if location - overrides = getOverrides(vimfx.keyOverrides, location, mode) + location = utils.getCurrentLocation() + overrides = getOverrides(vimfx.keyOverrides, location, mode) return keyStr not in (overrides ? []) vimfx.keyOverrides.push(rules...) - on: vimfx.on.bind(vimfx) - refresh: vimfx.createKeyTrees.bind(vimfx) - modes: vimfx.modes + on: vimfx.on.bind(vimfx) + refresh: vimfx.createKeyTrees.bind(vimfx) + modes: vimfx.modes getOverrides = (rules, args...) -> for [match, overrides] in rules diff --git a/extension/lib/button.coffee b/extension/lib/button.coffee index c468e7f..e132be4 100644 --- a/extension/lib/button.coffee +++ b/extension/lib/button.coffee @@ -17,8 +17,11 @@ # along with VimFx. If not, see . ### +# This file creates VimFx’s toolbar button. + help = require('./help') translate = require('./l10n') +utils = require('./utils') BUTTON_ID = 'VimFxButton' @@ -35,25 +38,33 @@ injectButton = (vimfx, window) -> if mode == 'normal' help.injectHelp(window, vimfx) else - vimfx.currentVim.enterMode('normal') + vimfx.getCurrentVim(window).enterMode('normal') onCreated: (node) -> button = node button.setAttribute('vimfx-mode', 'normal') - vimfx.on('modeChange', updateButton.bind(null, button)) - vimfx.on('currentVimChange', updateButton.bind(null, button)) + + updateButton = -> + # - The 'modeChange' event provides the `vim` object that changed mode, + # but it might not be the current `vim` anymore, so always get the + # current instance. + # - A 'TabSelect' event fires for the current tab when Firefox starts. + # By then a `vim` object for that tab might not have been constructed + # yet. If so, simply do nothing. + return unless vim = vimfx.getCurrentVim(window) + button.setAttribute('vimfx-mode', vim.mode) + tooltip = + if vim.mode == 'normal' + translate('button.tooltip.normal') + else + translate('button.tooltip.other_mode', + translate("mode.#{ vim.mode }"), translate('mode.normal')) + button.setAttribute('tooltiptext', tooltip) + + vimfx.on('modeChange', updateButton) + utils.listen(window, 'TabSelect', updateButton) }) module.onShutdown(cui.destroyWidget.bind(cui, BUTTON_ID)) -updateButton = (button, { mode }) -> - button.setAttribute('vimfx-mode', mode) - tooltip = - if mode == 'normal' - translate('button.tooltip.normal') - else - translate('button.tooltip.other_mode', translate("mode.#{ mode }"), - translate('mode.normal')) - button.setAttribute('tooltiptext', tooltip) - module.exports = { injectButton BUTTON_ID diff --git a/extension/lib/commands-frame.coffee b/extension/lib/commands-frame.coffee new file mode 100644 index 0000000..a2d88e7 --- /dev/null +++ b/extension/lib/commands-frame.coffee @@ -0,0 +1,316 @@ +### +# Copyright Simon Lydell 2015. +# +# This file is part of VimFx. +# +# VimFx is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# VimFx is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with VimFx. If not, see . +### + +# This file is the equivalent to commands.coffee, but for frame scripts, +# allowing interaction with web page content. Most “commands” here have the +# same name as the command in commands.coffee that calls it. There are also a +# few more generalized “commands” used in more than one place. + +hints = require('./hints') +utils = require('./utils') + +{ isProperLink, isTextInputElement, isContentEditable } = utils + +XULDocument = Ci.nsIDOMXULDocument + +commands = {} + +commands.go_up_path = ({ vim, count = 1 }) -> + vim.content.location.pathname = vim.content.location.pathname.replace( + /// (?: /[^/]+ ){1,#{ count }} /?$ ///, '' + ) + +commands.go_to_root = ({ vim }) -> + vim.content.location.href = vim.content.location.origin + +commands.scroll = (args) -> + { vim, method, type, direction, amount, property, smooth } = args + activeElement = utils.getActiveElement(vim.content) + document = activeElement.ownerDocument + element = + if vim.state.scrollableElements.has(activeElement) + activeElement + else + document.documentElement + + options = {} + options[direction] = switch type + when 'lines' then amount + when 'pages' then amount * element[property] + when 'other' then Math.min(amount, element[property]) + options.behavior = 'smooth' if smooth + + element[method](options) + # When scrolling the whole page, the body sometimes needs to be scrolled + # too. + if element == document.documentElement + document.body?[method](options) + +# Combine links with the same href. +combine = (hrefs, element, wrapper) -> + if wrapper.type == 'link' + { href } = element + wrapper.href = href + if href of hrefs + parent = hrefs[href] + wrapper.parentIndex = parent.elementIndex + parent.shape.area += wrapper.shape.area + parent.numChildren++ + else + wrapper.numChildren = 0 + hrefs[href] = wrapper + return wrapper + +commands.follow = ({ vim, storage }) -> + hrefs = {} + storage.markerElements = [] + filter = (element, getElementShape) -> + document = element.ownerDocument + isXUL = (document instanceof XULDocument) + semantic = true + switch + when isProperLink(element) + type = 'link' + when isTextInputElement(element) or isContentEditable(element) + type = 'text' + when element.tabIndex > -1 and + not (isXUL and element.nodeName.endsWith('box')) + type = 'clickable' + unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON'] + semantic = false + when element != document.documentElement and + vim.state.scrollableElements.has(element) + type = 'scrollable' + when element.hasAttribute('onclick') or + element.hasAttribute('onmousedown') or + element.hasAttribute('onmouseup') or + element.hasAttribute('oncommand') or + element.getAttribute('role') in ['link', 'button'] or + # Twitter special-case. + element.classList.contains('js-new-tweets-bar') or + # Feedly special-case. + element.hasAttribute('data-app-action') or + element.hasAttribute('data-uri') or + element.hasAttribute('data-page-action') + type = 'clickable' + semantic = false + # Putting markers on `