diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..7c68c4195 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": ["env"], + "parserOpts": { + "allowReturnOutsideFunction": true + }, + "ignore": [ + "shoulda.js" + ] +} diff --git a/.gitignore b/.gitignore index 4dbfa889d..ba770922d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist jscoverage.json tags .cake_task_cache +!src/**/*.js diff --git a/Cakefile b/Cakefile deleted file mode 100644 index 5c8887370..000000000 --- a/Cakefile +++ /dev/null @@ -1,176 +0,0 @@ -util = require "util" -fs = require "fs" -path = require "path" -child_process = require "child_process" - -spawn = (procName, optArray, silent = false, sync = false) -> - if process.platform is "win32" - # if win32, prefix arguments with "/c {original command}" - # e.g. "coffee -c c:\git\vimium" becomes "cmd.exe /c coffee -c c:\git\vimium" - optArray.unshift "/c", procName - procName = "cmd.exe" - if sync - proc = child_process.spawnSync procName, optArray, { - stdio: [undefined, process.stdout, process.stderr] - } - else - proc = child_process.spawn procName, optArray - unless silent - proc.stdout.on 'data', (data) -> process.stdout.write data - proc.stderr.on 'data', (data) -> process.stderr.write data - proc - -optArrayFromDict = (opts) -> - result = [] - for own key, value of opts - if value instanceof Array - result.push "--#{key}=#{v}" for v in value - else - result.push "--#{key}=#{value}" - result - -# visitor will get passed the file path as a parameter -visitDirectory = (directory, visitor) -> - fs.readdirSync(directory).forEach (filename) -> - filepath = path.join directory, filename - if (fs.statSync filepath).isDirectory() - return visitDirectory filepath, visitor - - return unless (fs.statSync filepath).isFile() - visitor(filepath) - -task "build", "compile all coffeescript files to javascript", -> - coffee = spawn "coffee", ["-c", __dirname] - coffee.on 'exit', (returnCode) -> process.exit returnCode - -task "clean", "removes any js files which were compiled from coffeescript", -> - visitDirectory __dirname, (filepath) -> - return unless (path.extname filepath) == ".js" - - directory = path.dirname filepath - - # Check if there exists a corresponding .coffee file - try - coffeeFile = fs.statSync path.join directory, "#{path.basename filepath, ".js"}.coffee" - catch _ - return - - fs.unlinkSync filepath if coffeeFile.isFile() - -task "autobuild", "continually rebuild coffeescript files using coffee --watch", -> - coffee = spawn "coffee", ["-cw", __dirname] - -task "package", "Builds a zip file for submission to the Chrome store. The output is in dist/", -> - vimium_version = JSON.parse(fs.readFileSync("manifest.json").toString())["version"] - - invoke "build" - - spawn "rm", ["-rf", "dist/vimium"], false, true - spawn "mkdir", ["-p", "dist/vimium"], false, true - - blacklist = [".*", "*.coffee", "*.md", "reference", "test_harnesses", "tests", "dist", "git_hooks", - "CREDITS", "node_modules", "MIT-LICENSE.txt", "Cakefile"] - rsyncOptions = [].concat.apply( - ["-r", ".", "dist/vimium"], - blacklist.map((item) -> ["--exclude", "#{item}"])) - - spawn "rsync", rsyncOptions, false, true - - distManifest = "dist/vimium/manifest.json" - manifest = JSON.parse fs.readFileSync(distManifest).toString() - - # Build the Chrome Store package; this does not require the clipboardWrite permission. - manifest.permissions = (permission for permission in manifest.permissions when permission != "clipboardWrite") - fs.writeFileSync distManifest, JSON.stringify manifest, null, 2 - spawn "zip", ["-r", "dist/vimium-chrome-store-#{vimium_version}.zip", "dist/vimium"], false, true - - # Build the Chrome Store dev package. - manifest.name = "Vimium Canary" - manifest.description = "This is the development branch of Vimium (it is beta software)." - fs.writeFileSync distManifest, JSON.stringify manifest, null, 2 - spawn "zip", ["-r", "dist/vimium-canary-#{vimium_version}.zip", "dist/vimium"], false, true - - # Build Firefox release. - spawn "zip", "-r -FS dist/vimium-ff-#{vimium_version}.zip background_scripts Cakefile content_scripts CONTRIBUTING.md CREDITS icons lib - manifest.json MIT-LICENSE.txt pages README.md -x *.coffee -x Cakefile -x CREDITS -x *.md".split(/\s+/), false, true - -# This builds a CRX that's distributable outside of the Chrome web store. Is this used by folks who fork -# Vimium and want to distribute their fork? -task "package-custom-crx", "build .crx file", -> - # To get crxmake, use `sudo gem install crxmake`. - invoke "build" - - # ugly hack to modify our manifest file on-the-fly - origManifestText = fs.readFileSync "manifest.json" - manifest = JSON.parse origManifestText - # Update manifest fields that you would like to override here. If - # distributing your CRX outside the Chrome webstore in a fork, please follow - # the instructions available at - # https://developer.chrome.com/extensions/autoupdate. - # manifest.update_url = "http://philc.github.com/vimium/updates.xml" - fs.writeFileSync "manifest.json", JSON.stringify manifest - - pem = process.env.VIMIUM_CRX_PEM ? "vimium.pem" - target = "vimium-latest.crx" - - console.log "Building crx file..." - console.log " using pem-file: #{pem}" - console.log " target: #{target}" - - crxmake = spawn "crxmake", optArrayFromDict - "pack-extension": "." - "pack-extension-key": pem - "extension-output": target - "ignore-file": "(^\\.|\\.(coffee|crx|pem|un~)$)" - "ignore-dir": "^(\\.|test)" - - crxmake.on "exit", -> fs.writeFileSync "manifest.json", origManifestText - -runUnitTests = (projectDir=".", testNameFilter) -> - console.log "Running unit tests..." - basedir = path.join projectDir, "/tests/unit_tests/" - test_files = fs.readdirSync(basedir).filter((filename) -> filename.indexOf("_test.js") > 0) - test_files = test_files.map((filename) -> basedir + filename) - test_files.forEach (file) -> require (if file[0] == '/' then '' else './') + file - Tests.run(testNameFilter) - return Tests.testsFailed - -option '', '--filter-tests [string]', 'filter tests by matching string' -task "test", "run all tests", (options) -> - unitTestsFailed = runUnitTests('.', options['filter-tests']) - - console.log "Running DOM tests..." - phantom = spawn "phantomjs", ["./tests/dom_tests/phantom_runner.js"] - phantom.on 'exit', (returnCode) -> - if returnCode > 0 or unitTestsFailed > 0 - process.exit 1 - else - process.exit 0 - -task "coverage", "generate coverage report", -> - {Utils} = require './lib/utils' - temp = require 'temp' - tmpDir = temp.mkdirSync null - jscoverage = spawn "jscoverage", [".", tmpDir].concat optArrayFromDict - "exclude": [".git", "node_modules"] - "no-instrument": "tests" - - jscoverage.on 'exit', (returnCode) -> - process.exit 1 unless returnCode == 0 - - console.log "Running DOM tests..." - phantom = spawn "phantomjs", [path.join(tmpDir, "tests/dom_tests/phantom_runner.js"), "--coverage"] - phantom.on 'exit', -> - # merge the coverage counts from the DOM tests with those from the unit tests - global._$jscoverage = JSON.parse fs.readFileSync path.join(tmpDir, - 'tests/dom_tests/dom_tests_coverage.json') - runUnitTests(tmpDir) - - # marshal the counts into a form that the JSCoverage front-end expects - result = {} - for own fname, coverage of _$jscoverage - result[fname] = - coverage: coverage - source: (Utils.escapeHtml fs.readFileSync fname, 'utf-8').split '\n' - - fs.writeFileSync 'jscoverage.json', JSON.stringify(result) diff --git a/Cakefile.js b/Cakefile.js new file mode 100644 index 000000000..4ebcf2c3e --- /dev/null +++ b/Cakefile.js @@ -0,0 +1,235 @@ +/* eslint-disable + camelcase, + consistent-return, + func-names, + global-require, + import/no-dynamic-require, + import/no-unresolved, + max-len, + no-console, + no-param-reassign, + no-restricted-syntax, + no-return-assign, + no-undef, + no-underscore-dangle, + no-unused-vars, + no-var, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS203: Remove `|| {}` from converted for-own loops + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const util = require('util'); +const fs = require('fs'); +const path = require('path'); +const child_process = require('child_process'); + +const spawn = function (procName, optArray, silent, sync) { + let proc; + if (silent == null) { silent = false; } + if (sync == null) { sync = false; } + if (process.platform === 'win32') { + // if win32, prefix arguments with "/c {original command}" + // e.g. "coffee -c c:\git\vimium" becomes "cmd.exe /c coffee -c c:\git\vimium" + optArray.unshift('/c', procName); + procName = 'cmd.exe'; + } + if (sync) { + proc = child_process.spawnSync(procName, optArray, { + stdio: [undefined, process.stdout, process.stderr], + }); + } else { + proc = child_process.spawn(procName, optArray); + if (!silent) { + proc.stdout.on('data', (data) => process.stdout.write(data)); + proc.stderr.on('data', (data) => process.stderr.write(data)); + } + } + return proc; +}; + +const optArrayFromDict = function (opts) { + const result = []; + for (const key of Object.keys(opts || {})) { + const value = opts[key]; + if (value instanceof Array) { + for (const v of Array.from(value)) { result.push(`--${key}=${v}`); } + } else { + result.push(`--${key}=${value}`); + } + } + return result; +}; + +// visitor will get passed the file path as a parameter +var visitDirectory = (directory, visitor) => fs.readdirSync(directory).forEach((filename) => { + const filepath = path.join(directory, filename); + if ((fs.statSync(filepath)).isDirectory()) { + return visitDirectory(filepath, visitor); + } + + if (!(fs.statSync(filepath)).isFile()) { return; } + return visitor(filepath); +}); + +task('build', 'compile all coffeescript files to javascript', () => { + const coffee = spawn('coffee', ['-c', __dirname]); + return coffee.on('exit', (returnCode) => process.exit(returnCode)); +}); + +task('clean', 'removes any js files which were compiled from coffeescript', () => visitDirectory(__dirname, (filepath) => { + let coffeeFile; + if ((path.extname(filepath)) !== '.js') { return; } + + const directory = path.dirname(filepath); + + // Check if there exists a corresponding .coffee file + try { + coffeeFile = fs.statSync(path.join(directory, `${path.basename(filepath, '.js')}.coffee`)); + } catch (_) { + return; + } + + if (coffeeFile.isFile()) { return fs.unlinkSync(filepath); } +})); + +task('autobuild', 'continually rebuild coffeescript files using coffee --watch', () => { + let coffee; + return coffee = spawn('coffee', ['-cw', __dirname]); +}); + +task('package', 'Builds a zip file for submission to the Chrome store. The output is in dist/', () => { + const vimium_version = JSON.parse(fs.readFileSync('manifest.json').toString()).version; + + invoke('build'); + + spawn('rm', ['-rf', 'dist/vimium'], false, true); + spawn('mkdir', ['-p', 'dist/vimium'], false, true); + + const blacklist = ['.*', '*.coffee', '*.md', 'reference', 'test_harnesses', 'tests', 'dist', 'git_hooks', + 'CREDITS', 'node_modules', 'MIT-LICENSE.txt', 'Cakefile']; + const rsyncOptions = [].concat.apply( + ['-r', '.', 'dist/vimium'], + blacklist.map((item) => ['--exclude', `${item}`]), + ); + + spawn('rsync', rsyncOptions, false, true); + + const distManifest = 'dist/vimium/manifest.json'; + const manifest = JSON.parse(fs.readFileSync(distManifest).toString()); + + // Build the Chrome Store package; this does not require the clipboardWrite permission. + manifest.permissions = (Array.from(manifest.permissions).filter((permission) => permission !== 'clipboardWrite')); + fs.writeFileSync(distManifest, JSON.stringify(manifest, null, 2)); + spawn('zip', ['-r', `dist/vimium-chrome-store-${vimium_version}.zip`, 'dist/vimium'], false, true); + + // Build the Chrome Store dev package. + manifest.name = 'Vimium Canary'; + manifest.description = 'This is the development branch of Vimium (it is beta software).'; + fs.writeFileSync(distManifest, JSON.stringify(manifest, null, 2)); + spawn('zip', ['-r', `dist/vimium-canary-${vimium_version}.zip`, 'dist/vimium'], false, true); + + // Build Firefox release. + return spawn('zip', `-r -FS dist/vimium-ff-${vimium_version}.zip background_scripts Cakefile content_scripts CONTRIBUTING.md CREDITS icons lib \ +manifest.json MIT-LICENSE.txt pages README.md -x *.coffee -x Cakefile -x CREDITS -x *.md`.split(/\s+/), false, true); +}); + +// This builds a CRX that's distributable outside of the Chrome web store. Is this used by folks who fork +// Vimium and want to distribute their fork? +task('package-custom-crx', 'build .crx file', () => { + // To get crxmake, use `sudo gem install crxmake`. + invoke('build'); + + // ugly hack to modify our manifest file on-the-fly + const origManifestText = fs.readFileSync('manifest.json'); + const manifest = JSON.parse(origManifestText); + // Update manifest fields that you would like to override here. If + // distributing your CRX outside the Chrome webstore in a fork, please follow + // the instructions available at + // https://developer.chrome.com/extensions/autoupdate. + // manifest.update_url = "http://philc.github.com/vimium/updates.xml" + fs.writeFileSync('manifest.json', JSON.stringify(manifest)); + + const pem = process.env.VIMIUM_CRX_PEM != null ? process.env.VIMIUM_CRX_PEM : 'vimium.pem'; + const target = 'vimium-latest.crx'; + + console.log('Building crx file...'); + console.log(` using pem-file: ${pem}`); + console.log(` target: ${target}`); + + const crxmake = spawn('crxmake', optArrayFromDict({ + 'pack-extension': '.', + 'pack-extension-key': pem, + 'extension-output': target, + 'ignore-file': '(^\\.|\\.(coffee|crx|pem|un~)$)', + 'ignore-dir': '^(\\.|test)', + })); + + return crxmake.on('exit', () => fs.writeFileSync('manifest.json', origManifestText)); +}); + +const runUnitTests = function (projectDir, testNameFilter) { + if (projectDir == null) { projectDir = '.'; } + console.log('Running unit tests...'); + const basedir = path.join(projectDir, '/tests/unit_tests/'); + let test_files = fs.readdirSync(basedir).filter((filename) => filename.indexOf('_test.js') > 0); + test_files = test_files.map((filename) => basedir + filename); + test_files.forEach((file) => require((file[0] === '/' ? '' : './') + file)); + Tests.run(testNameFilter); + return Tests.testsFailed; +}; + +option('', '--filter-tests [string]', 'filter tests by matching string'); +task('test', 'run all tests', (options) => { + const unitTestsFailed = runUnitTests('.', options['filter-tests']); + + console.log('Running DOM tests...'); + const phantom = spawn('phantomjs', ['./tests/dom_tests/phantom_runner.js']); + return phantom.on('exit', (returnCode) => { + if ((returnCode > 0) || (unitTestsFailed > 0)) { + return process.exit(1); + } + return process.exit(0); + }); +}); + +task('coverage', 'generate coverage report', () => { + const { Utils } = require('./lib/utils'); + const temp = require('temp'); + const tmpDir = temp.mkdirSync(null); + const jscoverage = spawn('jscoverage', ['.', tmpDir].concat(optArrayFromDict({ + exclude: ['.git', 'node_modules'], + 'no-instrument': 'tests', + }))); + + return jscoverage.on('exit', (returnCode) => { + if (returnCode !== 0) { process.exit(1); } + + console.log('Running DOM tests...'); + const phantom = spawn('phantomjs', [path.join(tmpDir, 'tests/dom_tests/phantom_runner.js'), '--coverage']); + return phantom.on('exit', () => { + // merge the coverage counts from the DOM tests with those from the unit tests + global._$jscoverage = JSON.parse(fs.readFileSync(path.join(tmpDir, + 'tests/dom_tests/dom_tests_coverage.json'))); + runUnitTests(tmpDir); + + // marshal the counts into a form that the JSCoverage front-end expects + const result = {}; + for (const fname of Object.keys(_$jscoverage || {})) { + const coverage = _$jscoverage[fname]; + result[fname] = { + coverage, + source: (Utils.escapeHtml(fs.readFileSync(fname, 'utf-8'))).split('\n'), + }; + } + + return fs.writeFileSync('jscoverage.json', JSON.stringify(result)); + }); + }); +}); diff --git a/README.md b/README.md index 30abd68f8..b9bddad09 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,22 @@ -Vimium - The Hacker's Browser -============================= +# decaffeinate fork of vimium -[![Build Status](https://travis-ci.org/philc/vimium.svg?branch=master)](https://travis-ci.org/philc/vimium) +![Conversion Status](https://decaffeinate-examples.github.io/vimium/conversion-status.svg) +![Test Status](https://decaffeinate-examples.github.io/vimium/test-status.svg) -Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of -the Vim editor. +[Travis logs](https://travis-ci.org/decaffeinate/decaffeinate-example-builder/jobs/647897729) -__Installation instructions:__ +## Conversion results -You can install the stable version of Vimium from the -[Chrome Extensions Gallery](https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb). +All files were successfully converted to JavaScript. -Please see -[CONTRIBUTING.md](CONTRIBUTING.md#installing-from-source) -for instructions on how you can install Vimium from source. +## Test results -The Options page can be reached via a link on the help dialog (type `?`) or via the button next to Vimium on -the Chrome Extensions page (`chrome://extensions`). +The patch to set up tests did not apply cleanly. -Keyboard Bindings ------------------ +## About this repository -Modifier keys are specified as ``, ``, and `` for ctrl+x, meta+x, and alt+x -respectively. For shift+x and ctrl-shift-x, just type `X` and ``. See the next section for how to -customize these bindings. +This repository was generated automatically by the [decaffeinate-examples] +project using the [decaffeinate] tool. -Once you have Vimium installed, you can see this list of key bindings at any time by typing `?`. - -Navigating the current page: - - ? show the help dialog for a list of all available keys - h scroll left - j scroll down - k scroll up - l scroll right - gg scroll to top of the page - G scroll to bottom of the page - d scroll down half a page - u scroll up half a page - f open a link in the current tab - F open a link in a new tab - r reload - gs view source - i enter insert mode -- all commands will be ignored until you hit Esc to exit - yy copy the current url to the clipboard - yf copy a link url to the clipboard - gf cycle forward to the next frame - gF focus the main/top frame - -Navigating to new pages: - - o Open URL, bookmark, or history entry - O Open URL, bookmark, history entry in a new tab - b Open bookmark - B Open bookmark in a new tab - -Using find: - - / enter find mode - -- type your search query and hit enter to search, or Esc to cancel - n cycle forward to the next find match - N cycle backward to the previous find match - -For advanced usage, see [regular expressions](https://github.com/philc/vimium/wiki/Find-Mode) on the wiki. - -Navigating your history: - - H go back in history - L go forward in history - -Manipulating tabs: - - J, gT go one tab left - K, gt go one tab right - g0 go to the first tab - g$ go to the last tab - ^ visit the previously-visited tab - t create tab - yt duplicate current tab - x close current tab - X restore closed tab (i.e. unwind the 'x' command) - T search through your open tabs - W move current tab to new window - pin/unpin current tab - -Using marks: - - ma, mA set local mark "a" (global mark "A") - `a, `A jump to local mark "a" (global mark "A") - `` jump back to the position before the previous jump - -- that is, before the previous gg, G, n, N, / or `a - -Additional advanced browsing commands: - - ]], [[ Follow the link labeled 'next' or '>' ('previous' or '<') - - helpful for browsing paginated sites - open multiple links in a new tab - gi focus the first (or n-th) text input box on the page - gu go up one level in the URL hierarchy - gU go up to root of the URL hierarchy - ge edit the current URL - gE edit the current URL and open in a new tab - zH scroll all the way left - zL scroll all the way right - v enter visual mode; use p/P to paste-and-go, use y to yank - V enter visual line mode - -Vimium supports command repetition so, for example, hitting `5t` will open 5 tabs in rapid succession. `` (or -``) will clear any partial commands in the queue and will also exit insert and find modes. - -There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full -list. - -Custom Key Mappings -------------------- - -You may remap or unmap any of the default key bindings in the "Custom key mappings" on the options page. - -Enter one of the following key mapping commands per line: - -- `map key command`: Maps a key to a Vimium command. Overrides Chrome's default behavior (if any). -- `unmap key`: Unmaps a key and restores Chrome's default behavior (if any). -- `unmapAll`: Unmaps all bindings. This is useful if you want to completely wipe Vimium's defaults and start - from scratch with your own setup. - -Examples: - -- `map scrollPageDown` maps ctrl+d to scrolling the page down. Chrome's default behavior of bringing up - a bookmark dialog is suppressed. -- `map r reload` maps the r key to reloading the page. -- `unmap ` removes any mapping for ctrl+d and restores Chrome's default behavior. -- `unmap r` removes any mapping for the r key. - -Available Vimium commands can be found via the "Show available commands" link -near the key mapping box on the options page. The command name appears to the -right of the description in parenthesis. - -You can add comments to key mappings by starting a line with `"` or `#`. - -The following special keys are available for mapping: - -- ``, ``, `` for ctrl, alt, and meta (command on Mac) respectively with any key. Replace `*` - with the key of choice. -- ``, ``, ``, `` for the arrow keys. -- `` through `` for the function keys. -- `` for the space key. -- ``, ``, ``, ``, ``, `` and `` for the corresponding non-printable keys (version 1.62 onwards). - -Shifts are automatically detected so, for example, `` corresponds to ctrl+shift+7 on an English keyboard. - -More documentation ------------------- -Many of the more advanced or involved features are documented on -[Vimium's GitHub wiki](https://github.com/philc/vimium/wiki). Also -see the [FAQ](https://github.com/philc/vimium/wiki/FAQ). - -Contributing ------------- -Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. - -Firefox Support ---------------- - -There is an *experimental* port of Vimium on Firefox [here](https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/). -This is very much experimental: most features work, although some bugs and issues remain. - -PRs are welcome. - -Release Notes -------------- - -Read about the major changes in each release [here](CHANGELOG.md). - -License -------- -Copyright (c) Phil Crosby, Ilya Sukhar. See [MIT-LICENSE.txt](MIT-LICENSE.txt) for details. +[decaffeinate-examples]: https://github.com/decaffeinate/decaffeinate-examples +[decaffeinate]: https://github.com/decaffeinate/decaffeinate diff --git a/background_scripts/.gitkeep b/background_scripts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/background_scripts/bg_utils.coffee b/background_scripts/bg_utils.coffee deleted file mode 100644 index 88a8c8141..000000000 --- a/background_scripts/bg_utils.coffee +++ /dev/null @@ -1,123 +0,0 @@ -root = exports ? window - -# TabRecency associates a logical timestamp with each tab id. These are used to provide an initial -# recency-based ordering in the tabs vomnibar (which allows jumping quickly between recently-visited tabs). -class TabRecency - timestamp: 1 - current: -1 - cache: {} - lastVisited: null - lastVisitedTime: null - timeDelta: 500 # Milliseconds. - - constructor: -> - chrome.tabs.onActivated.addListener (activeInfo) => @register activeInfo.tabId - chrome.tabs.onRemoved.addListener (tabId) => @deregister tabId - - chrome.tabs.onReplaced.addListener (addedTabId, removedTabId) => - @deregister removedTabId - @register addedTabId - - chrome.windows?.onFocusChanged.addListener (wnd) => - if wnd != chrome.windows.WINDOW_ID_NONE - chrome.tabs.query {windowId: wnd, active: true}, (tabs) => - @register tabs[0].id if tabs[0] - - register: (tabId) -> - currentTime = new Date() - # Register tabId if it has been visited for at least @timeDelta ms. Tabs which are visited only for a - # very-short time (e.g. those passed through with `5J`) aren't registered as visited at all. - if @lastVisitedTime? and @timeDelta <= currentTime - @lastVisitedTime - @cache[@lastVisited] = ++@timestamp - - @current = @lastVisited = tabId - @lastVisitedTime = currentTime - - deregister: (tabId) -> - if tabId == @lastVisited - # Ensure we don't register this tab, since it's going away. - @lastVisited = @lastVisitedTime = null - delete @cache[tabId] - - # Recently-visited tabs get a higher score (except the current tab, which gets a low score). - recencyScore: (tabId) -> - @cache[tabId] ||= 1 - if tabId == @current then 0.0 else @cache[tabId] / @timestamp - - # Returns a list of tab Ids sorted by recency, most recent tab first. - getTabsByRecency: -> - tabIds = (tId for own tId of @cache) - tabIds.sort (a,b) => @cache[b] - @cache[a] - tabIds.map (tId) -> parseInt tId - -BgUtils = - tabRecency: new TabRecency() - - # Log messages to the extension's logging page, but only if that page is open. - log: do -> - loggingPageUrl = chrome.runtime.getURL "pages/logging.html" - console.log "Vimium logging URL:\n #{loggingPageUrl}" if loggingPageUrl? # Do not output URL for tests. - # For development, it's sometimes useful to automatically launch the logging page on reload. - chrome.windows.create url: loggingPageUrl, focused: false if localStorage.autoLaunchLoggingPage - (message, sender = null) -> - for viewWindow in chrome.extension.getViews {type: "tab"} - if viewWindow.location.pathname == "/pages/logging.html" - # Don't log messages from the logging page itself. We do this check late because most of the time - # it's not needed. - if sender?.url != loggingPageUrl - date = new Date - [hours, minutes, seconds, milliseconds] = - [date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()] - minutes = "0" + minutes if minutes < 10 - seconds = "0" + seconds if seconds < 10 - milliseconds = "00" + milliseconds if milliseconds < 10 - milliseconds = "0" + milliseconds if milliseconds < 100 - dateString = "#{hours}:#{minutes}:#{seconds}.#{milliseconds}" - logElement = viewWindow.document.getElementById "log-text" - logElement.value += "#{dateString}: #{message}\n" - logElement.scrollTop = 2000000000 - - # Remove comments and leading/trailing whitespace from a list of lines, and merge lines where the last - # character on the preceding line is "\". - parseLines: (text) -> - for line in text.replace(/\\\n/g, "").split("\n").map((line) -> line.trim()) - continue if line.length == 0 - continue if line[0] in '#"' - line - - escapedEntities: '"': ""s;", '&': "&", "'": "'", "<": "<", ">": ">" - escapeAttribute: (string) -> string.replace /["&'<>]/g, (char) -> BgUtils.escapedEntities[char] - -# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the -# search-engine configuration is unchanged. -SearchEngines = - previousSearchEngines: null - searchEngines: null - - refresh: (searchEngines) -> - unless @previousSearchEngines? and searchEngines == @previousSearchEngines - @previousSearchEngines = searchEngines - @searchEngines = new AsyncDataFetcher (callback) -> - engines = {} - for line in BgUtils.parseLines searchEngines - tokens = line.split /\s+/ - if 2 <= tokens.length - keyword = tokens[0].split(":")[0] - searchUrl = tokens[1] - description = tokens[2..].join(" ") || "search (#{keyword})" - if Utils.hasFullUrlPrefix(searchUrl) or Utils.hasJavascriptPrefix searchUrl - engines[keyword] = {keyword, searchUrl, description} - - callback engines - - # Use the parsed search-engine configuration, possibly asynchronously. - use: (callback) -> - @searchEngines.use callback - - # Both set (refresh) the search-engine configuration and use it at the same time. - refreshAndUse: (searchEngines, callback) -> - @refresh searchEngines - @use callback - -root.SearchEngines = SearchEngines -root.BgUtils = BgUtils diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee deleted file mode 100644 index 218753b26..000000000 --- a/background_scripts/commands.coffee +++ /dev/null @@ -1,416 +0,0 @@ -Commands = - availableCommands: {} - keyToCommandRegistry: null - mapKeyRegistry: null - - init: -> - for own command, [description, options] of commandDescriptions - @availableCommands[command] = extend (options ? {}), description: description - - Settings.postUpdateHooks["keyMappings"] = @loadKeyMappings.bind this - @loadKeyMappings Settings.get "keyMappings" - - loadKeyMappings: (customKeyMappings) -> - @keyToCommandRegistry = {} - @mapKeyRegistry = {} - - configLines = ("map #{key} #{command}" for own key, command of defaultKeyMappings) - configLines.push BgUtils.parseLines(customKeyMappings)... - seen = {} - unmapAll = false - for line in configLines.reverse() - tokens = line.split /\s+/ - switch tokens[0].toLowerCase() - when "map" - if 3 <= tokens.length and not unmapAll - [_, key, command, optionList...] = tokens - if not seen[key] and registryEntry = @availableCommands[command] - seen[key] = true - keySequence = @parseKeySequence key - options = @parseCommandOptions command, optionList - @keyToCommandRegistry[key] = extend {keySequence, command, options, optionList}, @availableCommands[command] - when "unmap" - if tokens.length == 2 - seen[tokens[1]] = true - when "unmapall" - unmapAll = true - when "mapkey" - if tokens.length == 3 - fromChar = @parseKeySequence tokens[1] - toChar = @parseKeySequence tokens[2] - @mapKeyRegistry[fromChar[0]] ?= toChar[0] if fromChar.length == toChar.length == 1 - - chrome.storage.local.set mapKeyRegistry: @mapKeyRegistry - @installKeyStateMapping() - @prepareHelpPageData() - - # Push the key mapping for passNextKey into Settings so that it's available in the front end for insert - # mode. We exclude single-key mappings (that is, printable keys) because when users press printable keys - # in insert mode they expect the character to be input, not to be droppped into some special Vimium - # mode. - Settings.set "passNextKeyKeys", - (key for own key of @keyToCommandRegistry when @keyToCommandRegistry[key].command == "passNextKey" and 1 < key.length) - - # Lower-case the appropriate portions of named keys. - # - # A key name is one of three forms exemplified by or - # (prefixed normal key, named key, or prefixed named key). Internally, for - # simplicity, we would like prefixes and key names to be lowercase, though - # humans may prefer other forms or . - # On the other hand, and are different named keys - for one of - # them you have to press "shift" as well. - # We sort modifiers here to match the order used in keyboard_utils.coffee. - # The return value is a sequence of keys: e.g. "b" -> ["", "", "b"]. - parseKeySequence: do -> - modifier = "(?:[acm]-)" # E.g. "a-", "c-", "m-". - namedKey = "(?:[a-z][a-z0-9]+)" # E.g. "left" or "f12" (always two characters or more). - modifiedKey = "(?:#{modifier}+(?:.|#{namedKey}))" # E.g. "c-*" or "c-left". - specialKeyRegexp = new RegExp "^<(#{namedKey}|#{modifiedKey})>(.*)", "i" - (key) -> - if key.length == 0 - [] - # Parse "bcd" as "" and "bcd". - else if 0 == key.search specialKeyRegexp - [modifiers..., keyChar] = RegExp.$1.split "-" - keyChar = keyChar.toLowerCase() unless keyChar.length == 1 - modifiers = (modifier.toLowerCase() for modifier in modifiers) - modifiers.sort() - ["<#{[modifiers..., keyChar].join '-'}>", @parseKeySequence(RegExp.$2)...] - else - [key[0], @parseKeySequence(key[1..])...] - - # Command options follow command mappings, and are of one of two forms: - # key=value - a value - # key - a flag - parseCommandOptions: (command, optionList) -> - options = {} - for option in optionList - parse = option.split "=", 2 - options[parse[0]] = if parse.length == 1 then true else parse[1] - - # We parse any `count` option immediately (to avoid having to parse it repeatedly later). - if "count" of options - options.count = parseInt options.count - delete options.count if isNaN(options.count) or @availableCommands[command].noRepeat - - options - - # This generates and installs a nested key-to-command mapping structure. There is an example in - # mode_key_handler.coffee. - installKeyStateMapping: -> - keyStateMapping = {} - for own keys, registryEntry of @keyToCommandRegistry - currentMapping = keyStateMapping - for key, index in registryEntry.keySequence - if currentMapping[key]?.command - # Do not overwrite existing command bindings, they take priority. NOTE(smblott) This is the legacy - # behaviour. - break - else if index < registryEntry.keySequence.length - 1 - currentMapping = currentMapping[key] ?= {} - else - currentMapping[key] = extend {}, registryEntry - # We don't need these properties in the content scripts. - delete currentMapping[key][prop] for prop in ["keySequence", "description"] - chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping - # Inform `KeyboardUtils.isEscape()` whether `` should be interpreted as `Escape` (which it is by - # default). - chrome.storage.local.set useVimLikeEscape: "" not of keyStateMapping - - # Build the "helpPageData" data structure which the help page needs and place it in Chrome storage. - prepareHelpPageData: -> - commandToKey = {} - for own key, registryEntry of @keyToCommandRegistry - (commandToKey[registryEntry.command] ?= []).push key - commandGroups = {} - for own group, commands of @commandGroups - commandGroups[group] = [] - for command in commands - commandGroups[group].push - command: command - description: @availableCommands[command].description - keys: commandToKey[command] ? [] - advanced: command in @advancedCommands - chrome.storage.local.set helpPageData: commandGroups - - # An ordered listing of all available commands, grouped by type. This is the order they will - # be shown in the help page. - commandGroups: - pageNavigation: - ["scrollDown", - "scrollUp", - "scrollToTop", - "scrollToBottom", - "scrollPageDown", - "scrollPageUp", - "scrollFullPageDown", - "scrollFullPageUp", - "scrollLeft", - "scrollRight", - "scrollToLeft", - "scrollToRight", - "reload", - "copyCurrentUrl", - "openCopiedUrlInCurrentTab", - "openCopiedUrlInNewTab", - "goUp", - "goToRoot", - "enterInsertMode", - "enterVisualMode", - "enterVisualLineMode", - "passNextKey", - "focusInput", - "LinkHints.activateMode", - "LinkHints.activateModeToOpenInNewTab", - "LinkHints.activateModeToOpenInNewForegroundTab", - "LinkHints.activateModeWithQueue", - "LinkHints.activateModeToDownloadLink", - "LinkHints.activateModeToOpenIncognito", - "LinkHints.activateModeToCopyLinkUrl", - "goPrevious", - "goNext", - "nextFrame", - "mainFrame", - "Marks.activateCreateMode", - "Marks.activateGotoMode"] - vomnibarCommands: - ["Vomnibar.activate", - "Vomnibar.activateInNewTab", - "Vomnibar.activateBookmarks", - "Vomnibar.activateBookmarksInNewTab", - "Vomnibar.activateTabSelection", - "Vomnibar.activateEditUrl", - "Vomnibar.activateEditUrlInNewTab"] - findCommands: ["enterFindMode", "performFind", "performBackwardsFind"] - historyNavigation: - ["goBack", "goForward"] - tabManipulation: - ["createTab", - "previousTab", - "nextTab", - "visitPreviousTab", - "firstTab", - "lastTab", - "duplicateTab", - "togglePinTab", - "toggleMuteTab", - "removeTab", - "restoreTab", - "moveTabToNewWindow", - "closeTabsOnLeft","closeTabsOnRight", - "closeOtherTabs", - "moveTabLeft", - "moveTabRight"] - misc: - ["showHelp", - "toggleViewSource"] - - # Rarely used commands are not shown by default in the help dialog or in the README. The goal is to present - # a focused, high-signal set of commands to the new and casual user. Only those truly hungry for more power - # from Vimium will uncover these gems. - advancedCommands: [ - "scrollToLeft", - "scrollToRight", - "moveTabToNewWindow", - "goUp", - "goToRoot", - "LinkHints.activateModeWithQueue", - "LinkHints.activateModeToDownloadLink", - "Vomnibar.activateEditUrl", - "Vomnibar.activateEditUrlInNewTab", - "LinkHints.activateModeToOpenIncognito", - "LinkHints.activateModeToCopyLinkUrl", - "goNext", - "goPrevious", - "Marks.activateCreateMode", - "Marks.activateGotoMode", - "moveTabLeft", - "moveTabRight", - "closeTabsOnLeft", - "closeTabsOnRight", - "closeOtherTabs", - "enterVisualLineMode", - "toggleViewSource", - "passNextKey"] - -defaultKeyMappings = - "?": "showHelp" - "j": "scrollDown" - "k": "scrollUp" - "h": "scrollLeft" - "l": "scrollRight" - "gg": "scrollToTop" - "G": "scrollToBottom" - "zH": "scrollToLeft" - "zL": "scrollToRight" - "": "scrollDown" - "": "scrollUp" - - "d": "scrollPageDown" - "u": "scrollPageUp" - "r": "reload" - "gs": "toggleViewSource" - - "i": "enterInsertMode" - "v": "enterVisualMode" - "V": "enterVisualLineMode" - - "H": "goBack" - "L": "goForward" - "gu": "goUp" - "gU": "goToRoot" - - "gi": "focusInput" - - "f": "LinkHints.activateMode" - "F": "LinkHints.activateModeToOpenInNewTab" - "": "LinkHints.activateModeWithQueue" - "yf": "LinkHints.activateModeToCopyLinkUrl" - - "/": "enterFindMode" - "n": "performFind" - "N": "performBackwardsFind" - - "[[": "goPrevious" - "]]": "goNext" - - "yy": "copyCurrentUrl" - - "p": "openCopiedUrlInCurrentTab" - "P": "openCopiedUrlInNewTab" - - "K": "nextTab" - "J": "previousTab" - "gt": "nextTab" - "gT": "previousTab" - "^": "visitPreviousTab" - "<<": "moveTabLeft" - ">>": "moveTabRight" - "g0": "firstTab" - "g$": "lastTab" - - "W": "moveTabToNewWindow" - "t": "createTab" - "yt": "duplicateTab" - "x": "removeTab" - "X": "restoreTab" - - "": "togglePinTab" - "": "toggleMuteTab" - - "o": "Vomnibar.activate" - "O": "Vomnibar.activateInNewTab" - - "T": "Vomnibar.activateTabSelection" - - "b": "Vomnibar.activateBookmarks" - "B": "Vomnibar.activateBookmarksInNewTab" - - "ge": "Vomnibar.activateEditUrl" - "gE": "Vomnibar.activateEditUrlInNewTab" - - "gf": "nextFrame" - "gF": "mainFrame" - - "m": "Marks.activateCreateMode" - "`": "Marks.activateGotoMode" - - -# This is a mapping of: commandIdentifier => [description, options]. -# If the noRepeat and repeatLimit options are both specified, then noRepeat takes precedence. -commandDescriptions = - # Navigating the current page - showHelp: ["Show help", { topFrame: true, noRepeat: true }] - scrollDown: ["Scroll down"] - scrollUp: ["Scroll up"] - scrollLeft: ["Scroll left"] - scrollRight: ["Scroll right"] - - scrollToTop: ["Scroll to the top of the page"] - scrollToBottom: ["Scroll to the bottom of the page", { noRepeat: true }] - scrollToLeft: ["Scroll all the way to the left", { noRepeat: true }] - scrollToRight: ["Scroll all the way to the right", { noRepeat: true }] - - scrollPageDown: ["Scroll a half page down"] - scrollPageUp: ["Scroll a half page up"] - scrollFullPageDown: ["Scroll a full page down"] - scrollFullPageUp: ["Scroll a full page up"] - - reload: ["Reload the page", { background: true }] - toggleViewSource: ["View page source", { noRepeat: true }] - - copyCurrentUrl: ["Copy the current URL to the clipboard", { noRepeat: true }] - openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { noRepeat: true }] - openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { repeatLimit: 20 }] - - enterInsertMode: ["Enter insert mode", { noRepeat: true }] - passNextKey: ["Pass the next key to the page"] - enterVisualMode: ["Enter visual mode", { noRepeat: true }] - enterVisualLineMode: ["Enter visual line mode", { noRepeat: true }] - - focusInput: ["Focus the first text input on the page"] - - "LinkHints.activateMode": ["Open a link in the current tab"] - "LinkHints.activateModeToOpenInNewTab": ["Open a link in a new tab"] - "LinkHints.activateModeToOpenInNewForegroundTab": ["Open a link in a new tab & switch to it"] - "LinkHints.activateModeWithQueue": ["Open multiple links in a new tab", { noRepeat: true }] - "LinkHints.activateModeToOpenIncognito": ["Open a link in incognito window"] - "LinkHints.activateModeToDownloadLink": ["Download link url"] - "LinkHints.activateModeToCopyLinkUrl": ["Copy a link URL to the clipboard"] - - enterFindMode: ["Enter find mode", { noRepeat: true }] - performFind: ["Cycle forward to the next find match"] - performBackwardsFind: ["Cycle backward to the previous find match"] - - goPrevious: ["Follow the link labeled previous or <", { noRepeat: true }] - goNext: ["Follow the link labeled next or >", { noRepeat: true }] - - # Navigating your history - goBack: ["Go back in history"] - goForward: ["Go forward in history"] - - # Navigating the URL hierarchy - goUp: ["Go up the URL hierarchy"] - goToRoot: ["Go to root of current URL hierarchy"] - - # Manipulating tabs - nextTab: ["Go one tab right", { background: true }] - previousTab: ["Go one tab left", { background: true }] - visitPreviousTab: ["Go to previously-visited tab", { background: true }] - firstTab: ["Go to the first tab", { background: true }] - lastTab: ["Go to the last tab", { background: true }] - - createTab: ["Create new tab", { background: true, repeatLimit: 20 }] - duplicateTab: ["Duplicate current tab", { background: true, repeatLimit: 20 }] - removeTab: ["Close current tab", { background: true, repeatLimit: chrome.session?.MAX_SESSION_RESULTS ? 25 }] - restoreTab: ["Restore closed tab", { background: true, repeatLimit: 20 }] - - moveTabToNewWindow: ["Move tab to new window", { background: true }] - togglePinTab: ["Pin or unpin current tab", { background: true }] - toggleMuteTab: ["Mute or unmute current tab", { background: true, noRepeat: true }] - - closeTabsOnLeft: ["Close tabs on the left", {background: true, noRepeat: true}] - closeTabsOnRight: ["Close tabs on the right", {background: true, noRepeat: true}] - closeOtherTabs: ["Close all other tabs", {background: true, noRepeat: true}] - - moveTabLeft: ["Move tab to the left", { background: true }] - moveTabRight: ["Move tab to the right", { background: true }] - - "Vomnibar.activate": ["Open URL, bookmark or history entry", { topFrame: true }] - "Vomnibar.activateInNewTab": ["Open URL, bookmark or history entry in a new tab", { topFrame: true }] - "Vomnibar.activateTabSelection": ["Search through your open tabs", { topFrame: true }] - "Vomnibar.activateBookmarks": ["Open a bookmark", { topFrame: true }] - "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { topFrame: true }] - "Vomnibar.activateEditUrl": ["Edit the current URL", { topFrame: true }] - "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { topFrame: true }] - - nextFrame: ["Select the next frame on the page", { background: true }] - mainFrame: ["Select the page's main/top frame", { topFrame: true, noRepeat: true }] - - "Marks.activateCreateMode": ["Create a new mark", { noRepeat: true }] - "Marks.activateGotoMode": ["Go to a mark", { noRepeat: true }] - -Commands.init() - -root = exports ? window -root.Commands = Commands diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee deleted file mode 100644 index 36baacc85..000000000 --- a/background_scripts/completion.coffee +++ /dev/null @@ -1,837 +0,0 @@ -# This file contains the definition of the completers used for the Vomnibox's suggestion UI. A completer will -# take a query (whatever the user typed into the Vomnibox) and return a list of Suggestions, e.g. bookmarks, -# domains, URLs from history. -# -# The Vomnibox frontend script makes a "filterCompleter" request to the background page, which in turn calls -# filter() on each these completers. -# -# A completer is a class which has three functions: -# - filter(query, onComplete): "query" will be whatever the user typed into the Vomnibox. -# - refresh(): (optional) refreshes the completer's data source (e.g. refetches the list of bookmarks). -# - cancel(): (optional) cancels any pending, cancelable action. -class Suggestion - showRelevancy: false # Set this to true to render relevancy when debugging the ranking scores. - - constructor: (@options) -> - # Required options. - @queryTerms = null - @type = null - @url = null - @relevancyFunction = null - # Other options. - @title = "" - # Extra data which will be available to the relevancy function. - @relevancyData = null - # If @autoSelect is truthy, then this suggestion is automatically pre-selected in the vomnibar. This only - # affects the suggestion in slot 0 in the vomnibar. - @autoSelect = false - # If @highlightTerms is true, then we highlight matched terms in the title and URL. Otherwise we don't. - @highlightTerms = true - # @insertText is text to insert into the vomnibar input when the suggestion is selected. - @insertText = null - # @deDuplicate controls whether this suggestion is a candidate for deduplication. - @deDuplicate = true - - # Other options set by individual completers include: - # - tabId (TabCompleter) - # - isSearchSuggestion, customSearchMode (SearchEngineCompleter) - - extend this, @options - - computeRelevancy: -> - # We assume that, once the relevancy has been set, it won't change. Completers must set either @relevancy - # or @relevancyFunction. - @relevancy ?= @relevancyFunction this - - generateHtml: (request) -> - return @html if @html - relevancyHtml = if @showRelevancy then "#{@computeRelevancy()}" else "" - insertTextClass = if @insertText then "vomnibarInsertText" else "vomnibarNoInsertText" - insertTextIndicator = "↪" # A right hooked arrow. - @title = @insertText if @insertText and request.isCustomSearch - # NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS. - favIcon = - if @type == "tab" and not Utils.isFirefox() - """""" - else - "" - @html = - if request.isCustomSearch - """ -
- #{insertTextIndicator}#{@type} - #{@highlightQueryTerms Utils.escapeHtml @title} - #{relevancyHtml} -
- """ - else - """ -
- #{insertTextIndicator}#{@type} - #{@highlightQueryTerms Utils.escapeHtml @title} -
-
- #{insertTextIndicator}#{favIcon}#{@highlightUrlTerms Utils.escapeHtml @shortenUrl()} - #{relevancyHtml} -
- """ - - # Use neat trick to snatch a domain (http://stackoverflow.com/a/8498668). - getUrlRoot: (url) -> - a = document.createElement 'a' - a.href = url - a.protocol + "//" + a.hostname - - getHostname: (url) -> - a = document.createElement 'a' - a.href = url - a.hostname - - stripTrailingSlash: (url) -> - url = url.substring(url, url.length - 1) if url[url.length - 1] == "/" - url - - # Push the ranges within `string` which match `term` onto `ranges`. - pushMatchingRanges: (string,term,ranges) -> - textPosition = 0 - # Split `string` into a (flat) list of pairs: - # - for i=0,2,4,6,... - # - splits[i] is unmatched text - # - splits[i+1] is the following matched text (matching `term`) - # (except for the final element, for which there is no following matched text). - # Example: - # - string = "Abacab" - # - term = "a" - # - splits = [ "", "A", "b", "a", "c", "a", b" ] - # UM M UM M UM M UM (M=Matched, UM=Unmatched) - splits = string.split(RegexpCache.get(term, "(", ")")) - for index in [0..splits.length-2] by 2 - unmatchedText = splits[index] - matchedText = splits[index+1] - # Add the indices spanning `matchedText` to `ranges`. - textPosition += unmatchedText.length - ranges.push([textPosition, textPosition + matchedText.length]) - textPosition += matchedText.length - - # Wraps each occurence of the query terms in the given string in a . - highlightQueryTerms: (string) -> - return string unless @highlightTerms - ranges = [] - escapedTerms = @queryTerms.map (term) -> Utils.escapeHtml(term) - for term in escapedTerms - @pushMatchingRanges string, term, ranges - - return string if ranges.length == 0 - - ranges = @mergeRanges(ranges.sort (a, b) -> a[0] - b[0]) - # Replace portions of the string from right to left. - ranges = ranges.sort (a, b) -> b[0] - a[0] - for [start, end] in ranges - string = - string.substring(0, start) + - "#{string.substring(start, end)}" + - string.substring(end) - string - - highlightUrlTerms: (string) -> - if @highlightTermsExcludeUrl then string else @highlightQueryTerms string - - # Merges the given list of ranges such that any overlapping regions are combined. E.g. - # mergeRanges([0, 4], [3, 6]) => [0, 6]. A range is [startIndex, endIndex]. - mergeRanges: (ranges) -> - previous = ranges.shift() - mergedRanges = [previous] - ranges.forEach (range) -> - if previous[1] >= range[0] - previous[1] = Math.max(range[1], previous[1]) - else - mergedRanges.push(range) - previous = range - mergedRanges - - # Simplify a suggestion's URL (by removing those parts which aren't useful for display or comparison). - shortenUrl: () -> - return @shortUrl if @shortUrl? - # We get easier-to-read shortened URLs if we URI-decode them. - url = (Utils.decodeURIByParts(@url) || @url).toLowerCase() - for [ filter, replacements ] in @stripPatterns - if new RegExp(filter).test url - for replace in replacements - url = url.replace replace, "" - @shortUrl = url - - # Patterns to strip from URLs; of the form [ [ filter, replacements ], [ filter, replacements ], ... ] - # - filter is a regexp string; a URL must match this regexp first. - # - replacements (itself a list) is a list of regexp objects, each of which is removed from URLs matching - # the filter. - # - # Note. This includes site-specific patterns for very-popular sites with URLs which don't work well in the - # vomnibar. - # - stripPatterns: [ - # Google search specific replacements; this replaces query parameters which are known to not be helpful. - # There's some additional information here: http://www.teknoids.net/content/google-search-parameters-2012 - [ "^https?://www\\.google\\.(com|ca|com\\.au|co\\.uk|ie)/.*[&?]q=" - "ei gws_rd url ved usg sa usg sig2 bih biw cd aqs ie sourceid es_sm" - .split(/\s+/).map (param) -> new RegExp "\&#{param}=[^&]+" ] - - # On Google maps, we get a new history entry for every pan and zoom event. - [ "^https?://www\\.google\\.(com|ca|com\\.au|co\\.uk|ie)/maps/place/.*/@" - [ new RegExp "/@.*" ] ] - - # General replacements; replaces leading and trailing fluff. - [ '.', [ "^https?://", "\\W+$" ].map (re) -> new RegExp re ] - ] - - # Boost a relevancy score by a factor (in the range (0,1.0)), while keeping the score in the range [0,1]. - # This makes greater adjustments to scores near the middle of the range (so, very poor relevancy scores - # remain very poor). - @boostRelevancyScore: (factor, score) -> - score + if score < 0.5 then score * factor else (1.0 - score) * factor - -class BookmarkCompleter - folderSeparator: "/" - currentSearch: null - # These bookmarks are loaded asynchronously when refresh() is called. - bookmarks: null - - filter: ({ @queryTerms }, @onComplete) -> - @currentSearch = { queryTerms: @queryTerms, onComplete: @onComplete } - @performSearch() if @bookmarks - - onBookmarksLoaded: -> @performSearch() if @currentSearch - - performSearch: -> - # If the folder separator character the first character in any query term, then we'll use the bookmark's full path as its title. - # Otherwise, we'll just use the its regular title. - usePathAndTitle = @currentSearch.queryTerms.reduce ((prev,term) => prev || term.indexOf(@folderSeparator) == 0), false - results = - if @currentSearch.queryTerms.length > 0 - @bookmarks.filter (bookmark) => - suggestionTitle = if usePathAndTitle then bookmark.pathAndTitle else bookmark.title - bookmark.hasJavascriptPrefix ?= Utils.hasJavascriptPrefix bookmark.url - bookmark.shortUrl ?= "javascript:..." if bookmark.hasJavascriptPrefix - suggestionUrl = bookmark.shortUrl ? bookmark.url - RankingUtils.matches(@currentSearch.queryTerms, suggestionUrl, suggestionTitle) - else - [] - suggestions = results.map (bookmark) => - new Suggestion - queryTerms: @currentSearch.queryTerms - type: "bookmark" - url: bookmark.url - title: if usePathAndTitle then bookmark.pathAndTitle else bookmark.title - relevancyFunction: @computeRelevancy - shortUrl: bookmark.shortUrl - deDuplicate: not bookmark.shortUrl? - onComplete = @currentSearch.onComplete - @currentSearch = null - onComplete suggestions - - refresh: -> - @bookmarks = null - chrome.bookmarks.getTree (bookmarks) => - @bookmarks = @traverseBookmarks(bookmarks).filter((bookmark) -> bookmark.url?) - @onBookmarksLoaded() - - # If these names occur as top-level bookmark names, then they are not included in the names of bookmark folders. - ignoreTopLevel: - 'Other Bookmarks': true - 'Mobile Bookmarks': true - 'Bookmarks Bar': true - - # Traverses the bookmark hierarchy, and returns a flattened list of all bookmarks. - traverseBookmarks: (bookmarks) -> - results = [] - bookmarks.forEach (folder) => - @traverseBookmarksRecursive folder, results - results - - # Recursive helper for `traverseBookmarks`. - traverseBookmarksRecursive: (bookmark, results, parent={pathAndTitle:""}) -> - bookmark.pathAndTitle = - if bookmark.title and not (parent.pathAndTitle == "" and @ignoreTopLevel[bookmark.title]) - parent.pathAndTitle + @folderSeparator + bookmark.title - else - parent.pathAndTitle - results.push bookmark - bookmark.children.forEach((child) => @traverseBookmarksRecursive child, results, bookmark) if bookmark.children - - computeRelevancy: (suggestion) -> - RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.shortUrl ? suggestion.url, suggestion.title) - -class HistoryCompleter - filter: ({ queryTerms, seenTabToOpenCompletionList }, onComplete) -> - if queryTerms.length == 0 and not seenTabToOpenCompletionList - onComplete [] - # Prime the history cache so that it will (hopefully) be available on the user's next keystroke. - Utils.nextTick -> HistoryCache.use -> - else - HistoryCache.use (history) => - results = - if 0 < queryTerms.length - history.filter (entry) -> RankingUtils.matches queryTerms, entry.url, entry.title - else - # The user has typed to open the entire history (sorted by recency). - history - onComplete results.map (entry) => - new Suggestion - queryTerms: queryTerms - type: "history" - url: entry.url - title: entry.title - relevancyFunction: @computeRelevancy - relevancyData: entry - - computeRelevancy: (suggestion) -> - historyEntry = suggestion.relevancyData - recencyScore = RankingUtils.recencyScore(historyEntry.lastVisitTime) - # If there are no query terms, then relevancy is based on recency alone. - return recencyScore if suggestion.queryTerms.length == 0 - wordRelevancy = RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title) - # Average out the word score and the recency. Recency has the ability to pull the score up, but not down. - (wordRelevancy + Math.max recencyScore, wordRelevancy) / 2 - -# The domain completer is designed to match a single-word query which looks like it is a domain. This supports -# the user experience where they quickly type a partial domain, hit tab -> enter, and expect to arrive there. -class DomainCompleter - # A map of domain -> { entry: , referenceCount: } - # - `entry` is the most recently accessed page in the History within this domain. - # - `referenceCount` is a count of the number of History entries within this domain. - # If `referenceCount` goes to zero, the domain entry can and should be deleted. - domains: null - - filter: ({ queryTerms, query }, onComplete) -> - # Do not offer completions if the query is empty, or if the user has finished typing the first word. - return onComplete [] if queryTerms.length == 0 or /\S\s/.test query - if @domains - @performSearch(queryTerms, onComplete) - else - @populateDomains => @performSearch(queryTerms, onComplete) - - performSearch: (queryTerms, onComplete) -> - query = queryTerms[0] - domains = (domain for own domain of @domains when 0 <= domain.indexOf query) - domains = @sortDomainsByRelevancy queryTerms, domains - onComplete [ - new Suggestion - queryTerms: queryTerms - type: "domain" - url: domains[0]?[0] ? "" # This is the URL or an empty string, but not null. - relevancy: 2.0 - ].filter (s) -> 0 < s.url.length - - # Returns a list of domains of the form: [ [domain, relevancy], ... ] - sortDomainsByRelevancy: (queryTerms, domainCandidates) -> - results = - for domain in domainCandidates - recencyScore = RankingUtils.recencyScore(@domains[domain].entry.lastVisitTime || 0) - wordRelevancy = RankingUtils.wordRelevancy queryTerms, domain, null - score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2 - [domain, score] - results.sort (a, b) -> b[1] - a[1] - results - - populateDomains: (onComplete) -> - HistoryCache.use (history) => - @domains = {} - history.forEach (entry) => @onPageVisited entry - chrome.history.onVisited.addListener(@onPageVisited.bind(this)) - chrome.history.onVisitRemoved.addListener(@onVisitRemoved.bind(this)) - onComplete() - - onPageVisited: (newPage) -> - domain = @parseDomainAndScheme newPage.url - if domain - slot = @domains[domain] ||= { entry: newPage, referenceCount: 0 } - # We want each entry in our domains hash to point to the most recent History entry for that domain. - slot.entry = newPage if slot.entry.lastVisitTime < newPage.lastVisitTime - slot.referenceCount += 1 - - onVisitRemoved: (toRemove) -> - if toRemove.allHistory - @domains = {} - else - toRemove.urls.forEach (url) => - domain = @parseDomainAndScheme url - if domain and @domains[domain] and ( @domains[domain].referenceCount -= 1 ) == 0 - delete @domains[domain] - - # Return something like "http://www.example.com" or false. - parseDomainAndScheme: (url) -> - Utils.hasFullUrlPrefix(url) and not Utils.hasChromePrefix(url) and url.split("/",3).join "/" - -# Searches through all open tabs, matching on title and URL. -class TabCompleter - filter: ({ queryTerms }, onComplete) -> - # NOTE(philc): We search all tabs, not just those in the current window. I'm not sure if this is the - # correct UX. - chrome.tabs.query {}, (tabs) => - results = tabs.filter (tab) -> RankingUtils.matches(queryTerms, tab.url, tab.title) - suggestions = results.map (tab) => - new Suggestion - queryTerms: queryTerms - type: "tab" - url: tab.url - title: tab.title - relevancyFunction: @computeRelevancy - tabId: tab.id - deDuplicate: false - onComplete suggestions - - computeRelevancy: (suggestion) -> - if suggestion.queryTerms.length - RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title) - else - BgUtils.tabRecency.recencyScore(suggestion.tabId) - -class SearchEngineCompleter - @debug: false - previousSuggestions: null - - cancel: -> - CompletionSearch.cancel() - - # This looks up the custom search engine and, if one is found, notes it and removes its keyword from the - # query terms. - preprocessRequest: (request) -> - SearchEngines.use (engines) => - { queryTerms, query } = request - extend request, searchEngines: engines, keywords: key for own key of engines - keyword = queryTerms[0] - # Note. For a keyword "w", we match "w search terms" and "w ", but not "w" on its own. - if keyword and engines[keyword] and (1 < queryTerms.length or /\S\s/.test query) - extend request, - queryTerms: queryTerms[1..] - keyword: keyword - engine: engines[keyword] - isCustomSearch: true - - refresh: (port) -> - @previousSuggestions = {} - SearchEngines.refreshAndUse Settings.get("searchEngines"), (engines) -> - # Let the front-end vomnibar know the search-engine keywords. It needs to know them so that, when the - # query goes from "w" to "w ", the vomnibar can synchronously launch the next filter() request (which - # avoids an ugly delay/flicker). - port.postMessage - handler: "keywords" - keywords: key for own key of engines - - filter: (request, onComplete) -> - { queryTerms, query, engine } = request - return onComplete [] unless engine - - { keyword, searchUrl, description } = engine - extend request, searchUrl, customSearchMode: true - - @previousSuggestions[searchUrl] ?= [] - haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl - - # This filter is applied to all of the suggestions from all of the completers, after they have been - # aggregated by the MultiCompleter. - filter = (suggestions) -> - # We only keep suggestions which either *were* generated by this search engine, or *could have - # been* generated by this search engine (and match the current query). - for suggestion in suggestions - if suggestion.isSearchSuggestion or suggestion.isCustomSearch - suggestion - else - terms = Utils.extractQuery searchUrl, suggestion.url - continue unless terms and RankingUtils.matches queryTerms, terms - suggestion.url = Utils.createSearchUrl terms, searchUrl - suggestion - - # If a previous suggestion still matches the query, then we keep it (even if the completion engine may not - # return it for the current query). This allows the user to pick suggestions that they've previously seen - # by typing fragments of their text, without regard to whether the completion engine can continue to - # complete the actual text of the query. - previousSuggestions = - if queryTerms.length == 0 - [] - else - for own _, suggestion of @previousSuggestions[searchUrl] - continue unless RankingUtils.matches queryTerms, suggestion.title - # Reset various fields, they may not be correct wrt. the current query. - extend suggestion, relevancy: null, html: null, queryTerms: queryTerms - suggestion.relevancy = null - suggestion - - primarySuggestion = new Suggestion - queryTerms: queryTerms - type: description - url: Utils.createSearchUrl queryTerms, searchUrl - title: queryTerms.join " " - searchUrl: searchUrl - relevancy: 2.0 - autoSelect: true - highlightTerms: false - isSearchSuggestion: true - isPrimarySuggestion: true - - return onComplete [ primarySuggestion ], { filter } if queryTerms.length == 0 - - mkSuggestion = do => - count = 0 - (suggestion) => - url = Utils.createSearchUrl suggestion, searchUrl - @previousSuggestions[searchUrl][url] = new Suggestion - queryTerms: queryTerms - type: description - url: url - title: suggestion - searchUrl: searchUrl - insertText: suggestion - highlightTerms: false - highlightTermsExcludeUrl: true - isCustomSearch: true - relevancy: if ++count == 1 then 1.0 else null - relevancyFunction: @computeRelevancy - - cachedSuggestions = - if haveCompletionEngine then CompletionSearch.complete searchUrl, queryTerms else null - - suggestions = previousSuggestions - suggestions.push primarySuggestion - - if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine - # There is no prospect of adding further completions, so we're done. - suggestions.push cachedSuggestions.map(mkSuggestion)... if cachedSuggestions? - onComplete suggestions, { filter, continuation: null } - else - # Post the initial suggestions, but then deliver any further completions asynchronously, as a - # continuation. - onComplete suggestions, - filter: filter - continuation: (onComplete) => - CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) => - console.log "fetched suggestions:", suggestions.length, query if SearchEngineCompleter.debug - onComplete suggestions.map mkSuggestion - - computeRelevancy: ({ relevancyData, queryTerms, title }) -> - # Tweaks: - # - Calibration: we boost relevancy scores to try to achieve an appropriate balance between relevancy - # scores here, and those provided by other completers. - # - Relevancy depends only on the title (which is the search terms), and not on the URL. - Suggestion.boostRelevancyScore 0.5, - 0.7 * RankingUtils.wordRelevancy queryTerms, title, title - - postProcessSuggestions: (request, suggestions) -> - return unless request.searchEngines - engines = (engine for own _, engine of request.searchEngines) - engines.sort (a,b) -> b.searchUrl.length - a.searchUrl.length - engines.push keyword: null, description: "search history", searchUrl: Settings.get "searchUrl" - for suggestion in suggestions - unless suggestion.isSearchSuggestion or suggestion.insertText - for engine in engines - if suggestion.insertText = Utils.extractQuery engine.searchUrl, suggestion.url - # suggestion.customSearchMode informs the vomnibar that, if the users edits the text from this - # suggestion, then custom search-engine mode should be activated. - suggestion.customSearchMode = engine.keyword - suggestion.title ||= suggestion.insertText - break - -# A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top -# 10. All queries from the vomnibar come through a multi completer. -class MultiCompleter - maxResults: 10 - filterInProgress: false - mostRecentQuery: null - - constructor: (@completers) -> - refresh: (port) -> completer.refresh? port for completer in @completers - cancel: (port) -> completer.cancel? port for completer in @completers - - filter: (request, onComplete) -> - # Allow only one query to run at a time. - return @mostRecentQuery = arguments if @filterInProgress - - # Provide each completer with an opportunity to see (and possibly alter) the request before it is - # launched. - completer.preprocessRequest? request for completer in @completers - - RegexpCache.clear() - { queryTerms } = request - - [ @mostRecentQuery, @filterInProgress ] = [ null, true ] - [ suggestions, continuations, filters ] = [ [], [], [] ] - - # Run each of the completers (asynchronously). - jobs = new JobRunner @completers.map (completer) -> - (callback) -> - completer.filter request, (newSuggestions = [], { continuation, filter } = {}) -> - suggestions.push newSuggestions... - continuations.push continuation if continuation? - filters.push filter if filter? - callback() - - # Once all completers have finished, process the results and post them, and run any continuations or a - # pending query. - jobs.onReady => - suggestions = filter suggestions for filter in filters - shouldRunContinuations = 0 < continuations.length and not @mostRecentQuery? - - # Post results, unless there are none and we will be running a continuation. This avoids - # collapsing the vomnibar briefly before expanding it again, which looks ugly. - unless suggestions.length == 0 and shouldRunContinuations - suggestions = @prepareSuggestions request, queryTerms, suggestions - onComplete results: suggestions - - # Run any continuations (asynchronously); for example, the search-engine completer - # (SearchEngineCompleter) uses a continuation to fetch suggestions from completion engines - # asynchronously. - if shouldRunContinuations - jobs = new JobRunner continuations.map (continuation) -> - (callback) -> - continuation (newSuggestions) -> - suggestions.push newSuggestions... - callback() - - jobs.onReady => - suggestions = filter suggestions for filter in filters - suggestions = @prepareSuggestions request, queryTerms, suggestions - onComplete results: suggestions - - # Admit subsequent queries and launch any pending query. - @filterInProgress = false - if @mostRecentQuery - @filter @mostRecentQuery... - - prepareSuggestions: (request, queryTerms, suggestions) -> - # Compute suggestion relevancies and sort. - suggestion.computeRelevancy queryTerms for suggestion in suggestions - suggestions.sort (a, b) -> b.relevancy - a.relevancy - - # Simplify URLs and remove duplicates (duplicate simplified URLs, that is). - count = 0 - seenUrls = {} - suggestions = - for suggestion in suggestions - url = suggestion.shortenUrl() - continue if suggestion.deDuplicate and seenUrls[url] - break if count++ == @maxResults - seenUrls[url] = suggestion - - # Give each completer the opportunity to tweak the suggestions. - completer.postProcessSuggestions? request, suggestions for completer in @completers - - # Generate HTML for the remaining suggestions and return them. - suggestion.generateHtml request for suggestion in suggestions - suggestions - -# Utilities which help us compute a relevancy score for a given item. -RankingUtils = - # Whether the given things (usually URLs or titles) match any one of the query terms. - # This is used to prune out irrelevant suggestions before we try to rank them, and for calculating word relevancy. - # Every term must match at least one thing. - matches: (queryTerms, things...) -> - for term in queryTerms - regexp = RegexpCache.get(term) - matchedTerm = false - for thing in things - matchedTerm ||= thing.match regexp - return false unless matchedTerm - true - - # Weights used for scoring matches. - matchWeights: - matchAnywhere: 1 - matchStartOfWord: 1 - matchWholeWord: 1 - # The following must be the sum of the three weights above; it is used for normalization. - maximumScore: 3 - # - # Calibration factor for balancing word relevancy and recency. - recencyCalibrator: 2.0/3.0 - # The current value of 2.0/3.0 has the effect of: - # - favoring the contribution of recency when matches are not on word boundaries ( because 2.0/3.0 > (1)/3 ) - # - favoring the contribution of word relevance when matches are on whole words ( because 2.0/3.0 < (1+1+1)/3 ) - - # Calculate a score for matching term against string. - # The score is in the range [0, matchWeights.maximumScore], see above. - # Returns: [ score, count ], where count is the number of matched characters in string. - scoreTerm: (term, string) -> - score = 0 - count = 0 - nonMatching = string.split(RegexpCache.get term) - if nonMatching.length > 1 - # Have match. - score = RankingUtils.matchWeights.matchAnywhere - count = nonMatching.reduce(((p,c) -> p - c.length), string.length) - if RegexpCache.get(term, "\\b").test string - # Have match at start of word. - score += RankingUtils.matchWeights.matchStartOfWord - if RegexpCache.get(term, "\\b", "\\b").test string - # Have match of whole word. - score += RankingUtils.matchWeights.matchWholeWord - [ score, if count < string.length then count else string.length ] - - # Returns a number between [0, 1] indicating how often the query terms appear in the url and title. - wordRelevancy: (queryTerms, url, title) -> - urlScore = titleScore = 0.0 - urlCount = titleCount = 0 - # Calculate initial scores. - for term in queryTerms - [ s, c ] = RankingUtils.scoreTerm term, url - urlScore += s - urlCount += c - if title - [ s, c ] = RankingUtils.scoreTerm term, title - titleScore += s - titleCount += c - - maximumPossibleScore = RankingUtils.matchWeights.maximumScore * queryTerms.length - - # Normalize scores. - urlScore /= maximumPossibleScore - urlScore *= RankingUtils.normalizeDifference urlCount, url.length - - if title - titleScore /= maximumPossibleScore - titleScore *= RankingUtils.normalizeDifference titleCount, title.length - else - titleScore = urlScore - - # Prefer matches in the title over matches in the URL. - # In other words, don't let a poor urlScore pull down the titleScore. - # For example, urlScore can be unreasonably poor if the URL is very long. - urlScore = titleScore if urlScore < titleScore - - # Return the average. - (urlScore + titleScore) / 2 - - # Untested alternative to the above: - # - Don't let a poor urlScore pull down a good titleScore, and don't let a poor titleScore pull down a - # good urlScore. - # - # return Math.max(urlScore, titleScore) - - # Returns a score between [0, 1] which indicates how recent the given timestamp is. Items which are over - # a month old are counted as 0. This range is quadratic, so an item from one day ago has a much stronger - # score than an item from two days ago. - recencyScore: (lastAccessedTime) -> - @oneMonthAgo ||= 1000 * 60 * 60 * 24 * 30 - recency = Date.now() - lastAccessedTime - recencyDifference = Math.max(0, @oneMonthAgo - recency) / @oneMonthAgo - - # recencyScore is between [0, 1]. It is 1 when recenyDifference is 0. This quadratic equation will - # incresingly discount older history entries. - recencyScore = recencyDifference * recencyDifference * recencyDifference - - # Calibrate recencyScore vis-a-vis word-relevancy scores. - recencyScore *= RankingUtils.matchWeights.recencyCalibrator - - # Takes the difference of two numbers and returns a number between [0, 1] (the percentage difference). - normalizeDifference: (a, b) -> - max = Math.max(a, b) - (max - Math.abs(a - b)) / max - -# We cache regexps because we use them frequently when comparing a query to history entries and bookmarks, -# and we don't want to create fresh objects for every comparison. -RegexpCache = - init: -> - @initialized = true - @clear() - - clear: -> @cache = {} - - # Get rexexp for `string` from cache, creating it if necessary. - # Regexp meta-characters in `string` are escaped. - # Regexp is wrapped in `prefix`/`suffix`, which may contain meta-characters (these are not escaped). - # With their default values, `prefix` and `suffix` have no effect. - # Example: - # - string="go", prefix="\b", suffix="" - # - this returns regexp matching "google", but not "agog" (the "go" must occur at the start of a word) - # TODO: `prefix` and `suffix` might be useful in richer word-relevancy scoring. - get: (string, prefix="", suffix="") -> - @init() unless @initialized - regexpString = Utils.escapeRegexSpecialCharacters string - # Avoid cost of constructing new strings if prefix/suffix are empty (which is expected to be a common case). - regexpString = prefix + regexpString if prefix - regexpString = regexpString + suffix if suffix - # Smartcase: Regexp is case insensitive, unless `string` contains a capital letter (testing `string`, not `regexpString`). - @cache[regexpString] ||= new RegExp regexpString, (if Utils.hasUpperCase(string) then "" else "i") - -# Provides cached access to Chrome's history. As the user browses to new pages, we add those pages to this -# history cache. -HistoryCache = - size: 20000 - history: null # An array of History items returned from Chrome. - - reset: -> - @history = null - @callbacks = null - - use: (callback) -> - if @history? then callback @history else @fetchHistory callback - - fetchHistory: (callback) -> - return @callbacks.push(callback) if @callbacks - @callbacks = [callback] - chrome.history.search { text: "", maxResults: @size, startTime: 0 }, (history) => - # On Firefox, some history entries do not have titles. - history.map (entry) -> entry.title ?= "" - history.sort @compareHistoryByUrl - @history = history - chrome.history.onVisited.addListener(@onPageVisited.bind(this)) - chrome.history.onVisitRemoved.addListener(@onVisitRemoved.bind(this)) - callback(@history) for callback in @callbacks - @callbacks = null - - compareHistoryByUrl: (a, b) -> - return 0 if a.url == b.url - return 1 if a.url > b.url - -1 - - # When a page we've seen before has been visited again, be sure to replace our History item so it has the - # correct "lastVisitTime". That's crucial for ranking Vomnibar suggestions. - onPageVisited: (newPage) -> - # On Firefox, some history entries do not have titles. - newPage.title ?= "" - i = HistoryCache.binarySearch(newPage, @history, @compareHistoryByUrl) - pageWasFound = (@history[i]?.url == newPage.url) - if pageWasFound - @history[i] = newPage - else - @history.splice(i, 0, newPage) - - # When a page is removed from the chrome history, remove it from the vimium history too. - onVisitRemoved: (toRemove) -> - if toRemove.allHistory - @history = [] - else - toRemove.urls.forEach (url) => - i = HistoryCache.binarySearch({url:url}, @history, @compareHistoryByUrl) - if i < @history.length and @history[i].url == url - @history.splice(i, 1) - -# Returns the matching index or the closest matching index if the element is not found. That means you -# must check the element at the returned index to know whether the element was actually found. -# This method is used for quickly searching through our history cache. -HistoryCache.binarySearch = (targetElement, array, compareFunction) -> - high = array.length - 1 - low = 0 - - while (low <= high) - middle = Math.floor((low + high) / 2) - element = array[middle] - compareResult = compareFunction(element, targetElement) - if (compareResult > 0) - high = middle - 1 - else if (compareResult < 0) - low = middle + 1 - else - return middle - # We didn't find the element. Return the position where it should be in this array. - return if compareFunction(element, targetElement) < 0 then middle + 1 else middle - -root = exports ? window -root.Suggestion = Suggestion -root.BookmarkCompleter = BookmarkCompleter -root.MultiCompleter = MultiCompleter -root.HistoryCompleter = HistoryCompleter -root.DomainCompleter = DomainCompleter -root.TabCompleter = TabCompleter -root.SearchEngineCompleter = SearchEngineCompleter -root.HistoryCache = HistoryCache -root.RankingUtils = RankingUtils -root.RegexpCache = RegexpCache diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee deleted file mode 100644 index 4f7db7505..000000000 --- a/background_scripts/completion_engines.coffee +++ /dev/null @@ -1,203 +0,0 @@ - -# A completion engine provides search suggestions for a custom search engine. A custom search engine is -# identified by a "searchUrl". An "engineUrl" is used for fetching suggestions, whereas a "searchUrl" is used -# for the actual search itself. -# -# Each completion engine defines: -# -# 1. An "engineUrl". This is the URL to use for search completions and is passed as the option "engineUrl" -# to the "BaseEngine" constructor. -# -# 2. One or more regular expressions which define the custom search engine URLs for which the completion -# engine will be used. This is passed as the "regexps" option to the "BaseEngine" constructor. -# -# 3. A "parse" function. This takes a successful XMLHttpRequest object (the request has completed -# successfully), and returns a list of suggestions (a list of strings). This method is always executed -# within the context of a try/catch block, so errors do not propagate. -# -# 4. Each completion engine *must* include an example custom search engine. The example must include an -# example "keyword" and an example "searchUrl", and may include an example "description" and an -# "explanation". -# -# Each new completion engine must be added to the list "CompletionEngines" at the bottom of this file. -# -# The lookup logic which uses these completion engines is in "./completion_search.coffee". -# - -# A base class for common regexp-based matching engines. "options" must define: -# options.engineUrl: the URL to use for the completion engine. This must be a string. -# options.regexps: one or regular expressions. This may either a single string or a list of strings. -# options.example: an example object containing at least "keyword" and "searchUrl", and optional "description". -class BaseEngine - constructor: (options) -> - extend this, options - @regexps = [ @regexps ] if "string" == typeof @regexps - @regexps = @regexps.map (regexp) -> new RegExp regexp - - match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl - getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, @engineUrl - -# Several Google completion engines package responses as XML. This parses such XML. -class GoogleXMLBaseEngine extends BaseEngine - parse: (xhr) -> - for suggestion in xhr.responseXML.getElementsByTagName "suggestion" - continue unless suggestion = suggestion.getAttribute "data" - suggestion - -class Google extends GoogleXMLBaseEngine - constructor: () -> - super - engineUrl: "https://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s" - regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.(uk|jp)|ca|com\\.au)/" - example: - searchUrl: "https://www.google.com/search?q=%s" - keyword: "g" - -class GoogleMaps extends GoogleXMLBaseEngine - prefix: "map of " - constructor: () -> - super - engineUrl: "https://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{@prefix.split(' ').join '+'}%s" - regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.(uk|jp)|ca|com\\.au)/maps" - example: - searchUrl: "https://www.google.com/maps?q=%s" - keyword: "m" - explanation: - """ - This uses regular Google completion, but prepends the text "map of" to the query. It works - well for places, countries, states, geographical regions and the like, but will not perform address - search. - """ - - parse: (xhr) -> - for suggestion in super xhr - continue unless suggestion.startsWith @prefix - suggestion[@prefix.length..] - -class Youtube extends GoogleXMLBaseEngine - constructor: -> - super - engineUrl: "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s" - regexps: "^https?://[a-z]+\\.youtube\\.com/results" - example: - searchUrl: "https://www.youtube.com/results?search_query=%s" - keyword: "y" - -class Wikipedia extends BaseEngine - constructor: -> - super - engineUrl: "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=%s" - regexps: "^https?://[a-z]+\\.wikipedia\\.org/" - example: - searchUrl: "https://www.wikipedia.org/w/index.php?title=Special:Search&search=%s" - keyword: "w" - - parse: (xhr) -> JSON.parse(xhr.responseText)[1] - -class Bing extends BaseEngine - constructor: -> - super - engineUrl: "https://api.bing.com/osjson.aspx?query=%s" - regexps: "^https?://www\\.bing\\.com/search" - example: - searchUrl: "https://www.bing.com/search?q=%s" - keyword: "b" - - parse: (xhr) -> JSON.parse(xhr.responseText)[1] - -class Amazon extends BaseEngine - constructor: -> - super - engineUrl: "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=%s" - regexps: "^https?://www\\.amazon\\.(com|co\\.uk|ca|de|com\\.au)/s/" - example: - searchUrl: "https://www.amazon.com/s/?field-keywords=%s" - keyword: "a" - - parse: (xhr) -> JSON.parse(xhr.responseText)[1] - -class AmazonJapan extends BaseEngine - constructor: -> - super - engineUrl: "https://completion.amazon.co.jp/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=6&q=%s" - regexps: "^https?://www\\.amazon\\.co\\.jp/(s/|gp/search)" - example: - searchUrl: "https://www.amazon.co.jp/s/?field-keywords=%s" - keyword: "aj" - - parse: (xhr) -> JSON.parse(xhr.responseText)[1] - -class DuckDuckGo extends BaseEngine - constructor: -> - super - engineUrl: "https://duckduckgo.com/ac/?q=%s" - regexps: "^https?://([a-z]+\\.)?duckduckgo\\.com/" - example: - searchUrl: "https://duckduckgo.com/?q=%s" - keyword: "d" - - parse: (xhr) -> - suggestion.phrase for suggestion in JSON.parse xhr.responseText - -class Webster extends BaseEngine - constructor: -> - super - engineUrl: "https://www.merriam-webster.com/lapi/v1/mwol-search/autocomplete?search=%s" - regexps: "^https?://www.merriam-webster.com/dictionary/" - example: - searchUrl: "https://www.merriam-webster.com/dictionary/%s" - keyword: "dw" - description: "Dictionary" - - parse: (xhr) -> - suggestion.word for suggestion in JSON.parse(xhr.responseText).docs - -class Qwant extends BaseEngine - constructor: -> - super - engineUrl: "https://api.qwant.com/api/suggest?q=%s" - regexps: "^https?://www\\.qwant\\.com/" - example: - searchUrl: "https://www.qwant.com/?q=%s" - keyword: "qw" - - parse: (xhr) -> - suggestion.value for suggestion in JSON.parse(xhr.responseText).data.items - -class UpToDate extends BaseEngine - constructor: -> - super - engineUrl: "https://www.uptodate.com/services/app/contents/search/autocomplete/json?term=%s&limit=10" - regexps: "^https?://www\\.uptodate\\.com/" - example: - searchUrl: "https://www.uptodate.com/contents/search?search=%s&searchType=PLAIN_TEXT&source=USER_INPUT&searchControl=TOP_PULLDOWN&autoComplete=false" - keyword: "upto" - - parse: (xhr) -> JSON.parse(xhr.responseText).data.searchTerms - -# A dummy search engine which is guaranteed to match any search URL, but never produces completions. This -# allows the rest of the logic to be written knowing that there will always be a completion engine match. -class DummyCompletionEngine extends BaseEngine - constructor: -> - super - regexps: "." - dummy: true - -# Note: Order matters here. -CompletionEngines = [ - Youtube - GoogleMaps - Google - DuckDuckGo - Wikipedia - Bing - Amazon - AmazonJapan - Webster - Qwant - UpToDate - DummyCompletionEngine -] - -root = exports ? window -root.CompletionEngines = CompletionEngines diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee deleted file mode 100644 index bd6741ddc..000000000 --- a/background_scripts/completion_search.coffee +++ /dev/null @@ -1,172 +0,0 @@ - -# This is a wrapper class for completion engines. It handles the case where a custom search engine includes a -# prefix query term (or terms). For example: -# -# https://www.google.com/search?q=javascript+%s -# -# In this case, we get better suggestions if we include the term "javascript" in queries sent to the -# completion engine. This wrapper handles adding such prefixes to completion-engine queries and removing them -# from the resulting suggestions. -class EnginePrefixWrapper - constructor: (@searchUrl, @engine) -> - - getUrl: (queryTerms) -> - # This tests whether @searchUrl contains something of the form "...=abc+def+%s...", from which we extract - # a prefix of the form "abc def ". - if /\=.+\+%s/.test @searchUrl - terms = @searchUrl.replace /\+%s.*/, "" - terms = terms.replace /.*=/, "" - terms = terms.replace /\+/g, " " - - queryTerms = [ terms.split(" ")..., queryTerms... ] - prefix = "#{terms} " - - @postprocessSuggestions = - (suggestions) -> - for suggestion in suggestions - continue unless suggestion.startsWith prefix - suggestion[prefix.length..] - - @engine.getUrl queryTerms - - parse: (xhr) -> - @postprocessSuggestions @engine.parse xhr - - postprocessSuggestions: (suggestions) -> suggestions - -CompletionSearch = - debug: false - inTransit: {} - completionCache: new SimpleCache 2 * 60 * 60 * 1000, 5000 # Two hours, 5000 entries. - engineCache:new SimpleCache 1000 * 60 * 60 * 1000 # 1000 hours. - - # The amount of time to wait for new requests before launching the current request (for example, if the user - # is still typing). - delay: 100 - - get: (searchUrl, url, callback) -> - xhr = new XMLHttpRequest() - xhr.open "GET", url, true - xhr.timeout = 2500 - # According to https://xhr.spec.whatwg.org/#request-error-steps, - # readystatechange always gets called whether a request succeeds or not, - # and the `readyState == 4` means an associated `state` is "done", which is true even if any error happens - xhr.onreadystatechange = -> - if xhr.readyState == 4 - callback if xhr.status == 200 then xhr else null - xhr.send() - - # Look up the completion engine for this searchUrl. Because of DummyCompletionEngine, we know there will - # always be a match. - lookupEngine: (searchUrl) -> - if @engineCache.has searchUrl - @engineCache.get searchUrl - else - for engine in CompletionEngines - engine = new engine() - return @engineCache.set searchUrl, engine if engine.match searchUrl - - # True if we have a completion engine for this search URL, false otherwise. - haveCompletionEngine: (searchUrl) -> - not @lookupEngine(searchUrl).dummy - - # This is the main entry point. - # - searchUrl is the search engine's URL, e.g. Settings.get("searchUrl"), or a custom search engine's URL. - # This is only used as a key for determining the relevant completion engine. - # - queryTerms are the query terms. - # - callback will be applied to a list of suggestion strings (which may be an empty list, if anything goes - # wrong). - # - # If no callback is provided, then we're to provide suggestions only if we can do so synchronously (ie. - # from a cache). In this case we just return the results. Returns null if we cannot service the request - # synchronously. - # - complete: (searchUrl, queryTerms, callback = null) -> - query = queryTerms.join(" ").toLowerCase() - - returnResultsOnlyFromCache = not callback? - callback ?= (suggestions) -> suggestions - - # We don't complete queries which are too short: the results are usually useless. - return callback [] unless 3 < query.length - - # We don't complete regular URLs or Javascript URLs. - return callback [] if 1 == queryTerms.length and Utils.isUrl query - return callback [] if Utils.hasJavascriptPrefix query - - completionCacheKey = JSON.stringify [ searchUrl, queryTerms ] - if @completionCache.has completionCacheKey - console.log "hit", completionCacheKey if @debug - return callback @completionCache.get completionCacheKey - - # If the user appears to be typing a continuation of the characters of the most recent query, then we can - # sometimes re-use the previous suggestions. - if @mostRecentQuery? and @mostRecentSuggestions? and @mostRecentSearchUrl? - if searchUrl == @mostRecentSearchUrl - reusePreviousSuggestions = do => - # Verify that the previous query is a prefix of the current query. - return false unless 0 == query.indexOf @mostRecentQuery.toLowerCase() - # Verify that every previous suggestion contains the text of the new query. - # Note: @mostRecentSuggestions may also be empty, in which case we drop though. The effect is that - # previous queries with no suggestions suppress subsequent no-hope HTTP requests as the user continues - # to type. - for suggestion in @mostRecentSuggestions - return false unless 0 <= suggestion.indexOf query - # Ok. Re-use the suggestion. - true - - if reusePreviousSuggestions - console.log "reuse previous query:", @mostRecentQuery, @mostRecentSuggestions.length if @debug - return callback @completionCache.set completionCacheKey, @mostRecentSuggestions - - # That's all of the caches we can try. Bail if the caller is only requesting synchronous results. We - # signal that we haven't found a match by returning null. - return callback null if returnResultsOnlyFromCache - - # We pause in case the user is still typing. - Utils.setTimeout @delay, handler = @mostRecentHandler = => - if handler == @mostRecentHandler - @mostRecentHandler = null - - # Elide duplicate requests. First fetch the suggestions... - @inTransit[completionCacheKey] ?= new AsyncDataFetcher (callback) => - engine = new EnginePrefixWrapper searchUrl, @lookupEngine searchUrl - url = engine.getUrl queryTerms - - @get searchUrl, url, (xhr = null) => - # Parsing the response may fail if we receive an unexpected or an unexpectedly-formatted response. - # In all cases, we fall back to the catch clause, below. Therefore, we "fail safe" in the case of - # incorrect or out-of-date completion engines. - try - suggestions = engine.parse xhr - # Make all suggestions lower case. It looks odd when suggestions from one completion engine are - # upper case, and those from another are lower case. - suggestions = (suggestion.toLowerCase() for suggestion in suggestions) - # Filter out the query itself. It's not adding anything. - suggestions = (suggestion for suggestion in suggestions when suggestion != query) - console.log "GET", url if @debug - catch - suggestions = [] - # We allow failures to be cached too, but remove them after just thirty seconds. - Utils.setTimeout 30 * 1000, => @completionCache.set completionCacheKey, null - console.log "fail", url if @debug - - callback suggestions - delete @inTransit[completionCacheKey] - - # ... then use the suggestions. - @inTransit[completionCacheKey].use (suggestions) => - @mostRecentSearchUrl = searchUrl - @mostRecentQuery = query - @mostRecentSuggestions = suggestions - callback @completionCache.set completionCacheKey, suggestions - - # Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries. This is called - # whenever the user is typing. - cancel: -> - if @mostRecentHandler? - @mostRecentHandler = null - console.log "cancel (user is typing)" if @debug - -root = exports ? window -root.CompletionSearch = CompletionSearch diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee deleted file mode 100644 index fec66f4cc..000000000 --- a/background_scripts/exclusions.coffee +++ /dev/null @@ -1,60 +0,0 @@ -RegexpCache = - cache: {} - clear: (@cache = {}) -> - get: (pattern) -> - if pattern of @cache - @cache[pattern] - else - @cache[pattern] = - # We use try/catch to ensure that a broken regexp doesn't wholly cripple Vimium. - try - new RegExp("^" + pattern.replace(/\*/g, ".*") + "$") - catch - BgUtils.log "bad regexp in exclusion rule: #{pattern}" - /^$/ # Match the empty string. - -# The Exclusions class manages the exclusion rule setting. An exclusion is an object with two attributes: -# pattern and passKeys. The exclusion rules are an array of such objects. - -Exclusions = - # Make RegexpCache, which is required on the page popup, accessible via the Exclusions object. - RegexpCache: RegexpCache - - rules: Settings.get "exclusionRules" - - # Merge the matching rules for URL, or null. In the normal case, we use the configured @rules; hence, this - # is the default. However, when called from the page popup, we are testing what effect candidate new rules - # would have on the current tab. In this case, the candidate rules are provided by the caller. - getRule: (url, rules = @rules) -> - matchingRules = (rule for rule in rules when rule.pattern and 0 <= url.search RegexpCache.get rule.pattern) - # An absolute exclusion rule (one with no passKeys) takes priority. - for rule in matchingRules - return rule unless rule.passKeys - # Strip whitespace from all matching passKeys strings, and join them together. - passKeys = (rule.passKeys.split(/\s+/).join "" for rule in matchingRules).join "" - if 0 < matchingRules.length - passKeys: Utils.distinctCharacters passKeys - else - null - - isEnabledForUrl: (url) -> - rule = Exclusions.getRule url - isEnabledForUrl: not rule or 0 < rule.passKeys.length - passKeys: rule?.passKeys ? "" - - setRules: (rules) -> - # Callers map a rule to null to have it deleted, and rules without a pattern are useless. - @rules = rules.filter (rule) -> rule and rule.pattern - Settings.set "exclusionRules", @rules - - postUpdateHook: (rules) -> - # NOTE(mrmr1993): In FF, the |rules| argument will be garbage collected when the exclusions popup is - # closed. Do NOT store it/use it asynchronously. - @rules = Settings.get "exclusionRules" - RegexpCache.clear() - -# Register postUpdateHook for exclusionRules setting. -Settings.postUpdateHooks["exclusionRules"] = Exclusions.postUpdateHook.bind Exclusions - -root = exports ? window -extend root, {Exclusions} diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee deleted file mode 100644 index b19fb3c51..000000000 --- a/background_scripts/main.coffee +++ /dev/null @@ -1,544 +0,0 @@ -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" ] - return if Utils.isFirefox() - 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 - -frameIdsForTab = {} -root.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) -> - # NOTE(smblott): response contains `relevancyFunction` (function) properties which cause postMessage, - # below, to fail in Firefox. See #2576. We cannot simply delete these methods, as they're needed - # elsewhere. Converting the response to JSON and back is a quick and easy way to sanitize the object. - response = JSON.parse JSON.stringify 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 that the sendResponse callback is freed. - 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. - -# 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) -> - if Utils.hasJavascriptPrefix request.url - {tabId, frameId} = request - chrome.tabs.sendMessage tabId, {frameId, name: "executeScript", script: request.url} - else - 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 - active: true - windowId: request.tab.windowId - { position } = request - - tabIndex = null - # TODO(philc): Convert to a switch statement ES6. - switch position - when "start" then tabIndex = 0 - when "before" then tabIndex = request.tab.index - # if on Chrome or on Firefox but without openerTabId, `tabs.create` opens a tab at the end. - # but on Firefox and with openerTabId, it opens a new tab next to the opener tab - when "end" then tabIndex = (if Utils.isFirefox() then 9999 else null) - # "after" is the default case when there are no options. - else tabIndex = request.tab.index + 1 - tabConfig.index = tabIndex - - tabConfig.active = request.active if request.active? - # Firefox does not support "about:newtab" in chrome.tabs.create. - delete tabConfig["url"] if tabConfig["url"] == Settings.defaults.newTabUrl - - # Firefox <57 throws an error when openerTabId is used (issue 1238314). - canUseOpenerTabId = not (Utils.isFirefox() and Utils.compareVersions(Utils.firefoxVersion(), "57") < 0) - tabConfig.openerTabId = request.tab.id if canUseOpenerTabId - - chrome.tabs.create tabConfig, (tab) -> - # clean position and active, so following `openUrlInNewTab(request)` will create a tab just next to this new tab - callback extend request, {tab, tabId: tab.id, position: "", active: false} - - # Opens request.url in new window and switches to it. - openUrlInNewWindow: (request, callback = (->)) -> - winConfig = - url: Utils.convertToUrl request.url - active: true - winConfig.active = request.active if request.active? - # Firefox does not support "about:newtab" in chrome.tabs.create. - delete winConfig["url"] if winConfig["url"] == Settings.defaults.newTabUrl - chrome.windows.create winConfig, callback - -toggleMuteTab = do -> - muteTab = (tab) -> chrome.tabs.update tab.id, {muted: !tab.mutedInfo.muted} - - ({tab: currentTab, registryEntry, tabId, frameId}) -> - if registryEntry.options.all? or registryEntry.options.other? - # If there are any audible, unmuted tabs, then we mute them; otherwise we unmute any muted tabs. - chrome.tabs.query {audible: true}, (tabs) -> - if registryEntry.options.other? - tabs = (tab for tab in tabs when tab.id != currentTab.id) - audibleUnmutedTabs = (tab for tab in tabs when tab.audible and not tab.mutedInfo.muted) - if 0 < audibleUnmutedTabs.length - chrome.tabs.sendMessage tabId, {frameId, name: "showMessage", message: "Muting #{audibleUnmutedTabs.length} tab(s)."} - muteTab tab for tab in audibleUnmutedTabs - else - chrome.tabs.sendMessage tabId, {frameId, name: "showMessage", message: "Unmuting all muted tabs."} - muteTab tab for tab in tabs when tab.mutedInfo.muted - else - if currentTab.mutedInfo.muted - chrome.tabs.sendMessage tabId, {frameId, name: "showMessage", message: "Unmuted tab."} - else - chrome.tabs.sendMessage tabId, {frameId, name: "showMessage", message: "Muted tab."} - muteTab currentTab - -# -# 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, { active: true })) - -moveTab = ({count, tab, registryEntry}) -> - count = -count if registryEntry.command == "moveTabLeft" - chrome.tabs.query { currentWindow: true }, (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 = - # Create a new tab. Also, with: - # map X createTab http://www.bbc.com/news - # create a new tab with the given URL. - createTab: mkRepeatCommand (request, callback) -> - request.urls ?= - if request.url - # If the request contains a URL, then use it. - [request.url] - else - # Otherwise, if we have a registryEntry containing URLs, then use them. - urlList = (opt for opt in request.registryEntry.optionList when Utils.isUrl opt) - if 0 < urlList.length - urlList - else - # Otherwise, just create a new tab. - newTabUrl = Settings.get "newTabUrl" - if newTabUrl == "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 - [newTabUrl] - if request.registryEntry.options.incognito or request.registryEntry.options.window - windowConfig = - url: request.urls - incognito: request.registryEntry.options.incognito ? false - chrome.windows.create windowConfig, -> callback request - else - urls = request.urls[..].reverse() - request.position ?= request.registryEntry.options.position - do openNextUrl = (request) -> - if 0 < urls.length - TabOperations.openUrlInNewTab (extend request, {url: urls.pop()}), openNextUrl - else - callback request - 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}) -> forCountTabs count, tab, (tab) -> chrome.tabs.remove tab.id - restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request - togglePinTab: ({count, tab}) -> forCountTabs count, tab, (tab) -> chrome.tabs.update tab.id, {pinned: !tab.pinned} - toggleMuteTab: toggleMuteTab - 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] - reload: ({count, tabId, registryEntry, tab: {windowId}})-> - bypassCache = registryEntry.options.hard ? false - chrome.tabs.query {windowId}, (tabs) -> - position = do -> - for tab, index in tabs - return index if tab.id == tabId - tabs = [tabs[position...]..., tabs[...position]...] - count = Math.min count, tabs.length - chrome.tabs.reload tab.id, {bypassCache} for tab in tabs[...count] - -forCountTabs = (count, currentTab, callback) -> - chrome.tabs.query {currentWindow: true}, (tabs) -> - activeTabIndex = currentTab.index - startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count - callback tab for tab in tabs[startTabIndex...startTabIndex + count] - -# 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.query { currentWindow: true }, (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, active: true - -chrome.webNavigation.onCommitted.addListener ({tabId, frameId}) -> - cssConf = - frameId: frameId - 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} - port.postMessage handler: "registerFrameId", chromeFrameId: frameId - (portsForTab[tabId] ?= {})[frameId] = port - - # Return our onMessage handler for this port. - (request, port) => - this[request.handler] {request, tabId, frameId, port, sender} - - registerFrame: ({tabId, frameId, port}) -> - frameIdsForTab[tabId].push frameId unless frameId in frameIdsForTab[tabId] ?= [] - (portsForTab[tabId] ?= {})[frameId] = port - - unregisterFrame: ({tabId, frameId, port}) -> - # Check that the port trying to unregister the frame hasn't already been replaced by a new frame - # registering. See #2125. - registeredPort = portsForTab[tabId]?[frameId] - if registeredPort == port or not registeredPort - 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 - request.isFirefox = Utils.isFirefox() # Update the value for Utils.isFirefox in the frontend. - 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 - - # For debugging only. This allows content scripts to log messages to the extension's logging page. - log: ({frameId, sender, request: {message}}) -> BgUtils.log "#{frameId} #{message}", sender - -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 focused, 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 = {} - frameIdsForTab[tabId].map (frameId) => @tabState[tabId].ports[frameId] = portsForTab[tabId][frameId] - @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 - # 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: mkRepeatCommand (request, callback) -> TabOperations.openUrlInNewTab request, callback - openUrlInNewWindow: (request) -> TabOperations.openUrlInNewWindow 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 - 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 - -# 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 = -> - currentVersion = Utils.getCurrentVersion() - # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new - # installs. - Settings.set "previousVersion", currentVersion unless Settings.has "previousVersion" - previousVersion = Settings.get "previousVersion" - if Utils.compareVersions(currentVersion, previousVersion ) == 1 - currentVersionNumbers = currentVersion.split "." - previousVersionNumbers = previousVersion.split "." - if currentVersionNumbers[...2].join(".") == previousVersionNumbers[...2].join(".") - # We do not show an upgrade message for patch/silent releases. Such releases have the same major and - # minor version numbers. We do, however, update the recorded previous version. - Settings.set "previousVersion", currentVersion - else - 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.query { active: true, currentWindow: true }, ([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} diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee deleted file mode 100644 index 77b07b413..000000000 --- a/background_scripts/marks.coffee +++ /dev/null @@ -1,101 +0,0 @@ - -Marks = - # This returns the key which is used for storing mark locations in chrome.storage.sync. - getLocationKey: (markName) -> "vimiumGlobalMark|#{markName}" - - # Get the part of a URL we use for matching here (that is, everything up to the first anchor). - getBaseUrl: (url) -> url.split("#")[0] - - # Create a global mark. We record vimiumSecret with the mark so that we can tell later, when the mark is - # used, whether this is the original Vimium session or a subsequent session. This affects whether or not - # tabId can be considered valid. - create: (req, sender) -> - chrome.storage.local.get "vimiumSecret", (items) => - markInfo = - vimiumSecret: items.vimiumSecret - markName: req.markName - url: @getBaseUrl sender.tab.url - tabId: sender.tab.id - scrollX: req.scrollX - scrollY: req.scrollY - - if markInfo.scrollX? and markInfo.scrollY? - @saveMark markInfo - else - # The front-end frame hasn't provided the scroll position (because it's not the top frame within its - # tab). We need to ask the top frame what its scroll position is. - chrome.tabs.sendMessage sender.tab.id, name: "getScrollPosition", (response) => - @saveMark extend markInfo, scrollX: response.scrollX, scrollY: response.scrollY - - saveMark: (markInfo) -> - item = {} - item[@getLocationKey markInfo.markName] = markInfo - Settings.storage.set item - - # Goto a global mark. We try to find the original tab. If we can't find that, then we try to find another - # tab with the original URL, and use that. And if we can't find such an existing tab, then we create a new - # one. Whichever of those we do, we then set the scroll position to the original scroll position. - goto: (req, sender) -> - chrome.storage.local.get "vimiumSecret", (items) => - vimiumSecret = items.vimiumSecret - key = @getLocationKey req.markName - Settings.storage.get key, (items) => - markInfo = items[key] - if markInfo.vimiumSecret != vimiumSecret - # This is a different Vimium instantiation, so markInfo.tabId is definitely out of date. - @focusOrLaunch markInfo, req - else - # Check whether markInfo.tabId still exists. According to here (https://developer.chrome.com/extensions/tabs), - # tab Ids are unqiue within a Chrome session. So, if we find a match, we can use it. - chrome.tabs.get markInfo.tabId, (tab) => - if not chrome.runtime.lastError and tab?.url and markInfo.url == @getBaseUrl tab.url - # The original tab still exists. - @gotoPositionInTab markInfo - else - # The original tab no longer exists. - @focusOrLaunch markInfo, req - - # Focus an existing tab and scroll to the given position within it. - gotoPositionInTab: ({ tabId, scrollX, scrollY, markName }) -> - chrome.tabs.update tabId, { active: true }, -> - chrome.tabs.sendMessage tabId, {name: "setScrollPosition", scrollX, scrollY} - - # The tab we're trying to find no longer exists. We either find another tab with a matching URL and use it, - # or we create a new tab. - focusOrLaunch: (markInfo, req) -> - # If we're not going to be scrolling to a particular position in the tab, then we choose all tabs with a - # matching URL prefix. Otherwise, we require an exact match (because it doesn't make sense to scroll - # unless there's an exact URL match). - query = if markInfo.scrollX == markInfo.scrollY == 0 then "#{markInfo.url}*" else markInfo.url - chrome.tabs.query { url: query }, (tabs) => - if 0 < tabs.length - # We have at least one matching tab. Pick one and go to it. - @pickTab tabs, (tab) => - @gotoPositionInTab extend markInfo, tabId: tab.id - else - # There is no existing matching tab, we'll have to create one. - TabOperations.openUrlInNewTab (extend req, url: @getBaseUrl markInfo.url), (tab) => - # Note. tabLoadedHandlers is defined in "main.coffee". The handler below will be called when the tab - # is loaded, its DOM is ready and it registers with the background page. - tabLoadedHandlers[tab.id] = => @gotoPositionInTab extend markInfo, tabId: tab.id - - # Given a list of tabs candidate tabs, pick one. Prefer tabs in the current window and tabs with shorter - # (matching) URLs. - pickTab: (tabs, callback) -> - tabPicker = ({ id }) -> - # Prefer tabs in the current window, if there are any. - tabsInWindow = tabs.filter (tab) -> tab.windowId == id - tabs = tabsInWindow if 0 < tabsInWindow.length - # If more than one tab remains and the current tab is still a candidate, then don't pick the current - # tab (because jumping to it does nothing). - tabs = (tab for tab in tabs when not tab.active) if 1 < tabs.length - # Prefer shorter URLs. - tabs.sort (a,b) -> a.url.length - b.url.length - callback tabs[0] - if chrome.windows? - chrome.windows.getCurrent tabPicker - else - tabPicker({id: undefined}) - -root = exports ? window -root.Marks = Marks diff --git a/content_scripts/.gitkeep b/content_scripts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee deleted file mode 100644 index 16572d339..000000000 --- a/content_scripts/hud.coffee +++ /dev/null @@ -1,167 +0,0 @@ -# -# A heads-up-display (HUD) for showing Vimium page operations. -# Note: you cannot interact with the HUD until document.body is available. -# -HUD = - tween: null - hudUI: null - findMode: null - abandon: -> @hudUI?.hide false - - pasteListener: null # Set by @pasteFromClipboard to handle the value returned by pasteResponse - - # This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html" - # test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that - # it doesn't sit on top of horizontal scrollbars like Chrome's HUD does. - - init: -> - @hudUI ?= new UIComponent "pages/hud.html", "vimiumHUDFrame", ({data}) => this[data.name]? data - @tween ?= new Tween "iframe.vimiumHUDFrame.vimiumUIComponentVisible", @hudUI.shadowDOM - - showForDuration: (text, duration) -> - @show(text) - @_showForDurationTimerId = setTimeout((=> @hide()), duration) - - show: (text) -> - DomUtils.documentComplete => - @init() - clearTimeout(@_showForDurationTimerId) - @hudUI.activate {name: "show", text} - @tween.fade 1.0, 150 - - showFindMode: (@findMode = null) -> - DomUtils.documentComplete => - @init() - @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible" - @hudUI.activate name: "showFindMode" - @tween.fade 1.0, 150 - - search: (data) -> - # NOTE(mrmr1993): On Firefox, window.find moves the window focus away from the HUD. We use postFindFocus - # to put it back, so the user can continue typing. - @findMode.findInPlace data.query, {"postFindFocus": @hudUI.iframeElement.contentWindow} - - # Show the number of matches in the HUD UI. - matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0 - showMatchText = FindMode.query.rawQuery.length > 0 - @hudUI.postMessage {name: "updateMatchesCount", matchCount, showMatchText} - - # Hide the HUD. - # If :immediate is falsy, then the HUD is faded out smoothly (otherwise it is hidden immediately). - # If :updateIndicator is truthy, then we also refresh the mode indicator. The only time we don't update the - # mode indicator, is when hide() is called for the mode indicator itself. - hide: (immediate = false, updateIndicator = true) -> - if @hudUI? and @tween? - clearTimeout @_showForDurationTimerId - @tween.stop() - if immediate - if updateIndicator then Mode.setIndicator() else @hudUI.hide() - else - @tween.fade 0, 150, => @hide true, updateIndicator - - # These parameters describe the reason find mode is exiting, and come from the HUD UI component. - hideFindMode: ({exitEventIsEnter, exitEventIsEscape}) -> - @findMode.checkReturnToViewPort() - - # An element won't receive a focus event if the search landed on it while we were in the HUD iframe. To - # end up with the correct modes active, we create a focus/blur event manually after refocusing this - # window. - window.focus() - - focusNode = DomUtils.getSelectionFocusElement() - document.activeElement?.blur() - focusNode?.focus?() - - if exitEventIsEnter - FindMode.handleEnter() - if FindMode.query.hasResults - postExit = -> new PostFindMode - else if exitEventIsEscape - # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we - # wait until the mode is closed before running it. - postExit = FindMode.handleEscape - - @findMode.exit() - postExit?() - - # These commands manage copying and pasting from the clipboard in the HUD frame. - # NOTE(mrmr1993): We need this to copy and paste on Firefox: - # * an element can't be focused in the background page, so copying/pasting doesn't work - # * we don't want to disrupt the focus in the page, in case the page is listening for focus/blur events. - # * the HUD shouldn't be active for this frame while any of the copy/paste commands are running. - copyToClipboard: (text) -> - DomUtils.documentComplete => - @init() - # Chrome 74 only acknowledges text selection when a frame has been visible. See more in #3277 . - @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible" - @hudUI.postMessage {name: "copyToClipboard", data: text} - - pasteFromClipboard: (@pasteListener) -> - DomUtils.documentComplete => - @init() - # Show the HUD frame, so Firefox will actually perform the paste. - @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible" - @tween.fade 0, 0 - @hudUI.postMessage {name: "pasteFromClipboard"} - - pasteResponse: ({data}) -> - # Hide the HUD frame again. - @hudUI.toggleIframeElementClasses "vimiumUIComponentVisible", "vimiumUIComponentHidden" - @unfocusIfFocused() - @pasteListener data - - unfocusIfFocused: -> - # On Firefox, if an