pax_global_header00006660000000000000000000000064146033700640014515gustar00rootroot0000000000000052 comment=665d2679cdd3ef9ee7313e33d6e034287fa0df98 vimium-2.1.2/000077500000000000000000000000001460337006400130255ustar00rootroot00000000000000vimium-2.1.2/.github/000077500000000000000000000000001460337006400143655ustar00rootroot00000000000000vimium-2.1.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001460337006400165505ustar00rootroot00000000000000vimium-2.1.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011061460337006400212400ustar00rootroot00000000000000--- name: Bug report about: File a bug title: '' labels: '' assignees: '' --- **Describe the bug** Include a clear bug description. **To Reproduce** Steps to reproduce the behavior: 1. Go to URL '...' 2. Click on '....' Include a screenshot if applicable. **Browser and Vimium version** If you're using Chrome, include the Chrome and OS version found at chrome://version. Also include the Vimium version found at chrome://extensions. If you're using Firefox, report the Firefox and OS version found at about:support. Also include the Vimium version found at about:addons. vimium-2.1.2/.github/pull_request_template.md000066400000000000000000000003011460337006400213200ustar00rootroot00000000000000## Description Provide a rationale for this PR, and a reference to the corresponding issue, if there is one. Please review the "Which pull requests get merged?" section in `CONTRIBUTING.md`. vimium-2.1.2/.gitignore000066400000000000000000000001521460337006400150130ustar00rootroot00000000000000*.un~ *.swo *.swp *.crx *.sublime* options/*.js node_modules/* dist jscoverage.json tags .cake_task_cache vimium-2.1.2/CHANGELOG.md000066400000000000000000000646271460337006400146550ustar00rootroot000000000000002.1.2 (2024-04-03) - Better fix for Vomnibar doesn't always list tabs by recency. ([#4368](https://github.com/philc/vimium/issues/4368)) - Add a workaround to make link hints work on Github Enterprise. ([#4446](https://github.com/philc/vimium/issues/4446)) - Fix position=end is ignored in createTab command ([#4450](https://github.com/philc/vimium/issues/4450)) 2.1.1 (2024-03-29) - Fix exclusion rule popup not working. ([#4447](https://github.com/philc/vimium/issues/4447)) 2.1.0 (2024-03-27) - Fix Vomnibar doesn't always list tabs by recency. ([#4368](https://github.com/philc/vimium/issues/4368)) - Better domain detection in the Vomnibar ([#3268](https://github.com/philc/vimium/issues/3268)) - Exclude keys based on the top frame URL, not a subframe's URL. This fixes many cases where the excluded keys feature didn't seem to work. ([#4402](https://github.com/philc/vimium/issues/4402)) - After selecting a link, if ESC is pressed, mouse out of the link. With this, Wikipedia's and Github's link preview popups can be dismissed after following a link. ([#3073](https://github.com/philc/vimium/issues/3073)) - Fix link hints do not appear for links inside of github's popups. This fix is available on Chrome 114+, and soon Firefox. ([#4408](https://github.com/philc/vimium/issues/4408)) 2.0.5, 2.0.6 (2023-11-06) - Fix bug where "esc" wouldn't unfocus a textarea like it should. ([#4336](https://github.com/philc/vimium/issues/4336)) - Fix passNextKey command. 2.0.4 (2023-10-19) - Bug fixes: ([#4340](https://github.com/philc/vimium/issues/4340)), ([#4341](https://github.com/philc/vimium/issues/4341)), ([#4342](https://github.com/philc/vimium/issues/4342)). 2.0.2, 2.0.3 (2023-10-11) - Fix Vomnibar tab search doesn't get pre-populated with recently visited tabs. ([#4326](https://github.com/philc/vimium/issues/4326)) - Fix bookmarklets not working when opened from the Vomnibar. This is a partial fix; a full fix is waiting on a new extensions API. See [#4329](https://github.com/philc/vimium/issues/4329) for discussion. 2.0.1 (2023-10-04) - Fix exception when migrating some pre-v2.0 settings. ([#4323](https://github.com/philc/vimium/issues/4323)) 2.0.0 (2023-09-28 -- partially rolled out to users on the Chrome store) - Support manifest v3, as now required by Chrome. This involved a partial rewrite and many changes. Please report any new issues [here](https://github.com/philc/vimium/issues). - The storage format for Vimium's options has changed in v2.x. That means an options backup from Vimium v2.x cannot be loaded on Vimium v1.x installations. - Revamp the action bar UI, which configures which keys Vimium ignores on a particular site. - Improve Vimium's options UI. - Show link hints for image maps. ([#3493](https://github.com/philc/vimium/issues/3493)) - Remove the use of window.unload handlers, in preparation for Chrome's bfcache. ([#4265](https://github.com/philc/vimium/issues/4265)) - Allow find mode to work when using only private windows. ([#3614](https://github.com/philc/vimium/issues/3614)) - Add a count option to closeTabsOnLeft and closeTabsOnRight commands, to allow binding a key to "close just 1 tab on the left/right" rather than closing all tabs, as is the default. E.g. `map cl closeTabsOnLeft count=1`. ([#4296](https://github.com/philc/vimium/pull/4296)) - Add search completions for Brave Search. ([#3851](https://github.com/philc/vimium/pull/3851)) - Make regular expressions in find mode work again; other find mode improvements. ([#4261](https://github.com/philc/vimium/issues/4261)) - Bug fixes. ([#3944](https://github.com/philc/vimium/pull/3944), [#3752](https://github.com/philc/vimium/pull/3752), [#3675](https://github.com/philc/vimium/pull/3675)) 1.67.7 (2023-07-12) - Fix an issue where focusing the google search box puts the cursor at the start, rather than end, of the search box. ([#4247](https://github.com/philc/vimium/issues/4247)) 1.67.6 (2022-12-19) - Fix a spurious issue preventing approval on the Mozilla addons site ([#4195](https://github.com/philc/vimium/issues/4195)) 1.67.5 (2022-12-17) - For Firefox only, add back the clipboard read and write permissions. This fixes the Vimium commands which use the clipboard in Firefox ([#4186](https://github.com/philc/vimium/pull/4186)) 1.67.4 (2022-12-01) - Remove clipboard read/write permissions. We no longer need them since 1.67.2 (see #4120). - Fix Vimium's dark mode styling, take 2 (see [#4156](https://github.com/philc/vimium/issues/4156), [#4159](https://github.com/philc/vimium/pull/4159)) 1.67.3 (2022-10-29) - Fix copy-to-clipboard issue ([#4147](https://github.com/philc/vimium/issues/4147)) in visual mode. - Fix Vimium's dark mode styling in latest Firefox. ([#4148](https://github.com/philc/vimium/issues/4148)) 1.67.2 (2022-10-17) - In Firefox, remove use of deprecated InstallTrigger, which was issuing a console warning ([#4033](https://github.com/philc/vimium/issues/4033)) - Fix the Vimium toolbar icon to accurately reflect whether keys are excluded ([#4118](https://github.com/philc/vimium/pull/4118)) - Fix usage of deprecated clipboard APIs, which affected commands using copy and paste ([#4120](https://github.com/philc/vimium/issues/4120)) - Fix bug preventing going into caret mode ([#3877](https://github.com/philc/vimium/pull/3877)) 1.67.1 (2022-01-19) - In Firefox 96+, make link hints open one tab, not two ([#3985](https://github.com/philc/vimium/pull/3985)) 1.67 (2021-07-09) - Dark mode: Vimium's UI (URL bar, help dialog, option page, etc.) are dark if the browser is configured for dark mode. Vimium's dark mode is also compatible when using the popular [DarkReader extension](https://github.com/darkreader/darkreader). - Convert the code base from Coffeescript to Javascript, to simplify the dev experience and allow more developers to work on Vimium. - Make search mode work in newer versions of Firefox (#3801) - Make buttons on the Vimium options page work again in newer versions of Firefox (#3624) - Allow Vimium to work in LibreWolf (a Firefox fork) - Fixes to visual mode (#3568, #3779) 1.66 (2020-03-02) - Show tabs in the Vomnibar bar search results ('o') ([#2656](https://github.com/philc/vimium/pull/2656)) - Add commands to hover or focus a link ([#3097](https://github.com/philc/vimium/pull/3097)) (see [wiki)](https://github.com/philc/vimium/wiki/Tips-and-Tricks#hovering-over-links-using-linkhints) - Allow shift as a modifier for keybindings (e.g. ``) ([#2388](https://github.com/philc/vimium/pull/2388)) - Fix some issues with link hints [(#3499](https://github.com/philc/vimium/pull/3499), [#3505](https://github.com/philc/vimium/pull/3505), [#3509](https://github.com/philc/vimium/pull/3509)) - Other fixes. 1.65.2 (2020-02-10) - No code changes; trying to debug a permissions issue as shown in the chrome store ([#3489](https://github.com/philc/vimium/issues/3489)). 1.65.1 (2020-02-09) - Fix an issue with the HUD preventing some link hints from being shown ([#3486](https://github.com/philc/vimium/issues/3486)). 1.65 (2020-02-08) - Many fixes for Firefox ([#3483](https://github.com/philc/vimium/pull/3483), [#2893](https://github.com/philc/vimium/issues/2893), [#3106](https://github.com/philc/vimium/issues/3106), [#3409](https://github.com/philc/vimium/pull/3409), [#3288](https://github.com/philc/vimium/pull/3288)) - Fix javascript bookmarks, broken by Chrome 71+ [(#3473)](https://github.com/philc/vimium/pull/3437) - Improved link hints: show hints on sites with shadow DOM [(#3406)](https://github.com/philc/vimium/pull/3406), don't show hints for obstructed/invisible links ([#2251](https://github.com/philc/vimium/pull/2251)) - Fix scrolling on Reddit.com ([#3327](https://github.com/philc/vimium/pull/3327)) - Show favicons when using the tab switcher ([#2878](https://github.com/philc/vimium/pull/2878)) - The createTab command can now take arguments (start, end, before, after) ([#2895](https://github.com/philc/vimium/pull/2895)) - When using the Vomnibar, you can manually edit the suggested URL by typing ctrl-enter [(#2464)](https://github.com/philc/vimium/pull/2914) - Other fixes 1.64.6 (2019-05-12) - Fix the find mode, and copying the page's URL to the clipboard, which were broken by Chrome 74+. ([#3260](https://github.com/philc/vimium/issues/3260)) 1.64.5 (2019-02-16) - Fix error in Chrome Store distribution. 1.64.4 (2019-02-16) - Fix [Vomnibar focus issue](https://github.com/philc/vimium/issues/3242). 1.64.3 (2018-12-26) - When yanking email addresses with `yf`, Vimium now strips the leading `mailto:`. - For custom search engines, if you use `%S` (instead of `%s`), then your search terms are not URI encoded. - Bug fixes (including horizontal scrolling broken). 1.64.2 (2018-12-16) - Better scrolling on new Reddit ~~and GMail~~. 1.64 (2018-08-30) - Custom search engines can now be `javascript:` URLs (eg., search the current [site](https://github.com/philc/vimium/issues/2956#issuecomment-366509915)). - You can now using local marks to mark a hash/anchor. This is particularly useful for marking labels on GMail. - For filtered hints, you can now start typing the link text before the hints have been generated. - On Twitter, expanded tweets are now scrollable. - Fix bug whereby `` wasn't recognised in the Vomnibar in some circumstances. - Various minor bug fixes. 1.63 (2018-02-16) - The `reload` command now accepts a count prefix; so `999r` reloads all tabs (in the current window). - Better detection of click listeners for link hints. - Display version number in page popup. - The Vomnibar is now loaded on demand (not preloaded). This should fix some issues with the dev console. - The `\I` control (case sensitivity) for find mode has been removed. Find mode uses smartcase. - Various bug fixes. - 1.63.1 (Firefox only): - Fix [#2958](https://github.com/philc/vimium/issues/2958#issuecomment-366488659), link hints broken for `target="_blank"` links. - 1.63.2 (Firefox only): - Fix [#2962](https://github.com/philc/vimium/issues/2962), find mode broken on Firefox Quantum. - 1.63.3: - Fix [#2997](https://github.com/philc/vimium/issues/2997), Vimium's DOM injection breaks Google Pay site. 1.62 (2017-12-09) - Backup and restore Vimium options (see the very bottom of the options page, below _Advanced Options_). - It is now possible to map ``, ``, ``, ``, `` and ``. - New command options for `createTab` to create new normal and incognito windows ([examples](https://github.com/philc/vimium/wiki/Tips-and-Tricks#creating-tabs-with-urls-and-windows)). - Firefox only: - Fix copy and paste commands. - When upgrading, you will be asked to re-validate permissions. The only new permission is "copy and paste to/from clipboard" (the `clipboardWrite` permission). This is necessary to support copy/paste on Firefox. - Various bug fixes. - 1.62.1: Swap global and local marks (1.62.1). In a browser, some people find global marks more useful than local marks. Example: ``` map X Marks.activateCreateMode swap map Y Marks.activateGotoMode swap ``` - Other minor versions: - 1.62.2: Fixes [#2868](https://github.com/philc/vimium/issues/2868) (`createTab` with multiple URLs). - 1.62.4: Fixes bug affecting the enabled state, and really fix `createTab`. 1.61 (2017-10-27) - For _filtered hints_, you can now use alphabetical hint characters instead of digits; use `` for hint characters. - With `map R reload hard`, the reload command now asks Chrome to bypass its cache. - You can now map `` to a command (in which case it will not be treated as `Escape`). - Various bug fixes, particularly for Firefox. - Minor versions: - 1.61.1: Fix `map R reload hard`. 1.60 (2017-09-14) - Features: - There's a new (advanced) option to ignore the keyboard layout; this can be helpful for users of non-Latin keyboards. - Firefox support. This is a work in progress; please report any issues [here](https://github.com/philc/vimium/issues?q=is%3Aopen+sort%3Aupdated-desc); see the [add on](https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/). - Bug fixes: - Fixed issue affecting hint placement when the display is zoomed. - Fixed search completion for Firefox (released as 1.59.1, Firefox only). - Minor versions: - 1.60.1: fix [#2642](https://github.com/philc/vimium/issues/2642). - 1.60.2: revert previous fix for HiDPI screens. This was breaking link-hint positioning for some users. - 1.60.3: [fix](https://github.com/philc/vimium/pull/2649) link-hint positioning. - 1.60.4: [fix](https://github.com/philc/vimium/pull/2602) hints opening in new tab (Firefox only). 1.59 (2017-04-07) - Features: - Some commands now work on PDF tabs (`J`, `K`, `o`, `b`, etc.). Scrolling and other content-related commands still do not work. 1.58 (2017-03-08) - Features: - The `createTab` command can now open specific URLs (e.g, `map X createTab http://www.bbc.com/news`). - With pass keys defined for a site (such as GMail), you can now use Vimium's bindings again with, for example, `map \ passNextKey normal`; this reactivates normal mode temporarily, but _without any pass keys_. - You can now map multi-modifier keys, for example: ``. - Vimium can now do simple key mapping in some modes; see [here](https://github.com/philc/vimium/wiki/Tips-and-Tricks#key-mapping). This can be helpful with some non-English keyboards (and can also be used to remap `Escape`). - For _Custom key mappings_ on the options page, lines which end with `\` are now continued on the following line. - Process: - In order to provide faster bug fixes, we may in future push new releases without the noisy notification. - Post-release minor fixes: - 1.58.1 (2017-03-09) fix bug in `LinkHints.activateModeWithQueue` (#2445). - 1.58.2 (2017-03-19) fix key handling bug (#2453). 1.57 (2016-10-01) - New commands: - `toggleMuteTab` - mute or unmute the current tab (default binding ``), see also [advanced usage](https://github.com/philc/vimium/wiki/Tips-and-Tricks#muting-tabs). - Other new features: - You can now map `` to a Vimium command (e.g. `map goBack`). - For link hints, when one hint marker is covered by another, `` now rotates the stacking order. If you use filtered hints, you'll need to use a modifier (e.g. ``). - Changes: - Global marks now search for an existing matching tab by prefix (rather than exact match). This allows global marks to be used as quick bookmarks on sites (like Facebook, Gmail, etc) where the URL changes as you navigate around. - Bug fixes: - `/i` can no longer hang Vimium while the page is loading. - `` is no longer handled (incorrectly) as `Escape`. This also affects ``. - If `goX` is mapped, then `go` no longer launches the vomnibar. This only affects three-key (or longer) bindings. 1.56 (2016-06-11) - Vimium now works around a Chromium bug affecting users with non-standard keyboard layouts (see #2147). - Fixed a bug preventing visual line mode (`V`) from working. 1.55 (2016-05-26) - New commands: - `visitPreviousTab` - visit the previous tab (by recency) with `^`, or the tab before that with `2^`. - `passNextKey` - pass the next key to the page. For example, using `map passNextKey`, you can close Facebook's messenger popups with ``. - Link hints: - Now work across all frames in the tab. - Now select frames and scrollable elements. - Now accept a count prefix; `3F` opens three new background tabs, `999F` opens many tabs. - For filtered link hints, a new option on the settings page requires you to press `Enter` to activate a link; this prevents unintentionally triggering Vimium commands with trailing keystrokes. - Miscellaneous: - `gg` now accepts a `count` prefix. - `W` now accepts a count prefix; `3W` moves three tabs to a new window. - With smooth scrolling, `2j`-and-hold now gives a faster scroll than `j`-and-hold. - You can now bind keys to a command with a defined count prefix; for example, `map d scrollDown count=4`. - You can now bind three-key (or longer) sequences; for example, `map abc enterInsertMode`. - `c-y` and `c-e` now scroll in visual mode. - The Vimium help dialog has been re-styled. - Bug fixes: - `` is no longer treated as escape. - Fix icon display and memory leak due to a regression in recent Chrome versions (49+). - For web-devs only: - When disabled on a tab, Vimium no longer pollutes the dev console with network requests. 1.54 (2016-01-30) - Fix occasional endless scrolling (#1911). 1.53 (2015-09-25) - Vimium now works on the new-tab page for Chrome 47. - `g0` and `g$` now accept count prefixes; so `2g0` selects the second tab, and so on. - Bug fixes: - Fix `moveTabLeft` and `moveTabRight` for pinned tabs (#1814 and #1815). 1.52 (2015-09-09) - Search completion for selected custom search engines (details on the [wiki](https://github.com/philc/vimium/wiki/Search-Completion)). - Use `Tab` on an empty Vomnibar to repeat or edit recent queries (details on the [wiki](https://github.com/philc/vimium/wiki/Tips-and-Tricks#repeat-recent-vomnibar-queries)). - Marks: - Use \`\` to jump back to the previous position after jump-like movements:
(`gg`, `G`, `n`, `N`, `/` and local mark movements). - Global marks are now persistent and synced. - For numeric link hints, you can now use `Tab` and `Enter` to select hints, and hints are ordered by the best match. - The Find Mode text entry box now supports editing, pasting, and better handles non-latin characters. - Vimium now works on XML pages. - Bug fixes. 1.51 (2015-05-02) - Bug [fixes](https://github.com/philc/vimium/pulls?utf8=%E2%9C%93&q=is%3Apr+sort%3Aupdated-desc+is%3Aclosed+merged%3A%3E%3D2015-04-26+merged%3A%3C2015-05-02+state%3Amerged). 1.50 (2015-04-26) - Visual mode (in beta): use `v` and then vim-like keystrokes to select text on the page. Use `y` to yank or `p` and `P` to search with your default search engine.. Please provide feedback on Github. - Added the option to prevent pages from stealing focus from Vimium when loaded. - Many bugfixes for custom search engines, and search engines can now have a description. - Better support for frames: key exclusion rules are much improved and work within frames; the Vomnibar is always activated in the main frame; and a new command (`gF`) focuses the main frame. - Find mode now has history. Use the up arrow to select previous searches. - Ctrl and Shift when using link hints changes the tab in which links are opened in (reinstated feature). - Focus input (`gi`) remembers previously-visited inputs. - Bug fixes. 1.49 (2014-12-16) - An option to toggle smooth scrolling. - Make Vimium work on older versions of Chrome. 1.46, 1.47, 1.48 (2014-12-15) - Site-specific excluded keys: you can disable some Vimium key bindings on sites like gmail.com, so you can use the key bindings provided by the site itself. - Smooth scrolling. - The Vomnibar now orders tabs by recency. Use this to quickly switch between your most recently-used tabs. - New commands: "close tabs to the left", "close tabs to the right", "close all other tabs". - Usability improvements. - Bug fixes. 1.45 (2014-07-20) - Vimium's settings are now synced across computers. - New commands: "open link in new tab and focus", "move tab left", "move tab right", "pin/unpin tab". - Vomnibar can now use [search engine shortcuts](https://github.com/philc/vimium/wiki/Search-Engines), similar to Chrome's Omnibar. - Due to significant ranking improvements, Vomnibar's search results are now even more helpful. - When reopening a closed tab, its history is now preserved. - Bug fixes. 1.44 (2013-11-06) - Add support for recent versions of Chromium. - Bug fixes. 1.43 (2013-05-18) - Relevancy improvements to the Vomnibar's domain & history search. - Added `gU`, which goes to the root of the current URL. - Added `yt`, which duplicates the current tab. - Added `W`, which moves the current tab to a new window. - Added marks for saving and jumping to sections of a page. `mX` to set a mark and `` `X `` to return to it. - Added "LinkHints.activateModeToOpenIncognito", currently an advanced, unbound command. - Disallowed repeat tab closings, since this causes trouble for many people. - Update our Chrome APIs so Vimium works on Chrome 28+. - Bug fixes. 1.42 (2012-11-03) - Bug fixes. 1.41 (2012-10-27) - Bug fixes. 1.40 (2012-10-27) - Bug fixes. - Added options for search engines and regex find. - Pressing unmapped keys in hints mode now deactivates the mode. 1.39 (2012-09-09) - Bug fixes. 1.38 (2012-09-08) - `O` now opens Vomnibar results in a new tab. `B` does the same for bookmarks only. - Add a browser icon to quickly add sites to Vimium's exclude list. - Restyle options page. - `gi` now launches a new mode that allows the user to tab through the input elements on the page. - Bug fixes. 1.37 (2012-07-07) - Select the first result by default in Vomnibar tab and bookmark modes. 1.36 (2012-07-07) - `b` brings up a bookmark-only Vomnibar. - Better support for some bookmarklets. 1.35 (2012-07-05) - Bug fixes. 1.34 (2012-07-03) - A bug fix for bookmarklets in Vomnibar. 1.33 (2012-07-02) - A Vomnibar, which allows you to open sites from history, bookmarks, and tabs using Vimium's UI. Type `o` to try it. 1.32 (2012-03-05) - More tweaks to the next / previous link-detection algorithm. - Minor bug fixes. 1.31 (2012-02-28) - Improve style of link hints, and use fewer characters for hints. - Add an option to hide the heads up display (HUD). Notably, the HUD obscures Facebook Chat's textbox. - Detection and following of next / previous links has been improved. - Addition of `g0` and `g$` commands, for switching tabs. - Addition of `p`/`P` commands for URL pasting. - A new find mode which optionally supports case sensitivity and regular expressions. - Bug fixes. 1.30 (2011-12-04) - Support for image maps in link hints. - Counts now work with forward & backward navigation. - `Tab` & `shift-tab` to navigate bookmarks dialog. - An alternate link hints mode: type the title of a link to select it. You can enable it in Vimium's Advanced Preferences. - Bug fixes. 1.29 (2012-07-30) - `yf` to copy a link hint url to the clipboard. - Scatter link hints to prevent clustering on dense sites. - Don't show insert mode notification unless you specifically hit `i`. - Remove zooming functionality now that Chrome does it all natively. 1.28 (2011-06-29) - Support for opening bookmarks (`b` and `B`). - Support for contenteditable text boxes. - Speed improvements and bug fixes. 1.27 (2011-03-24) - Improvements and bug fixes. 1.26 (2011-02-17) - ``, `` and related are no longer bound by default. You can rebind them on the options page. - Faster link hinting. 1.22, 1.23, 1.24, 1.25 (2011-02-10) - Some sites are now excluded by default. - View source (`gs`) now opens in a new tab. - Support for browsing paginated sites using `]]` and `[[` to go forward and backward respectively. - Many of the less-used commands are now marked as "advanced" and hidden in the help dialog by default, so that the core command set is more focused and approachable. - Improvements to link hinting. - Bug fixes. 1.21 (2010-10-24) - Critical bug fix for an excluded URLs regression due to frame support. 1.20 (2010-10-24) - In link hints mode, holding down the shift key will now toggle between opening in the current tab and opening in a new tab. - Two new commands (`zH` and `zL`) to scroll to the left and right edges of the page. - A new command (`gi`) to focus the first (or n-th) visible text input. - A new command (``) to open up multiple links at a time in new tabs. - Frame support. - More robust support for non-US keyboard layouts. - Numerous bug fixes. 1.19 (2010-06-29) - A critical bug fix for development channel Chromium. - Vimium icons for the Chrome extensions panel and other places. 1.18 (2010-06-22) - Vimium now runs on pages with file:/// and ftp:/// - The Options page is now linked from the Help dialog. - Arrow keys and function keys can now be mapped using <left>, <right>, <up>, <down>, <f1>, <f2>, etc. in the mappings interface. - There is a new command `goUp` (mapped to `gu` by default) that will go up one level in the URL hierarchy. For example: from https://vimium.github.io/foo/bar to https://vimium.github.io/foo. At the moment, `goUp` does not support command repetition. - Bug fixes and optimizations. 1.17 (2010-04-18) - `u` now restores tabs that were closed by the mouse or with native shortcuts. Tabs are also restored in their prior position. - New `unmapAll` command in the key mappings interface to remove all default mappings. - Link hints are now faster and more reliable. - Bug fixes. 1.16 (2010-03-09) - Add support for configurable key mappings under Advanced Options. - A help dialog which shows all currently bound keyboard shortcuts. Type `?` to see it. - Bug fixes related to key stroke handling. 1.15 (2010-01-31) - Make the CSS used by the link hints configurable. It's under Advanced Options. - Add a notification linking to the changelog when Vimium is updated in the background. - Link-hinting performance improvements and bug fixes. - `Ctrl+D` and `Ctrl+U` now scroll by 1/2 page instead of a fixed amount, to mirror Vim's behavior. 1.14 (2010-01-21) - Fixed a bug introduced in 1.13 that prevented excluded URLs from being saved. 1.13 (2010-01-21) - `` and `` are now mapped to scroll a full page up or down respectively. - Bug fixes related to entering insert mode when the page first loads, and when focusing Flash embeds. - Added command listing to the Options page for easy reference. - `J` & `K` have reversed for tab switching: `J` goes left and `K` goes right. - `` is now equivalent to `Esc`, to match the behavior of VIM. - `` and `` are now mapped to scroll down and up respectively. - The characters used for link hints are now configurable under Advanced Options. 1.11, 1.12 (2010-01-08) - Commands `gt` & `gT` to move to the next & previous tab. - Command `yy` to yank (copy) the current tab's url to the clipboard. - Better Linux support. - Fix for `Shift+F` link hints. - `Esc` now clears the keyQueue. So, for example, hitting `g`, `Esc`, `g` will no longer scroll the page. 1.1 (2010-01-03) - A nicer looking settings page. - An exclusion list that allows you to define URL patterns for which Vimium will be disabled (e.g. http\*://mail.google.com/\*). - Vimium-interpreted keystrokes are no longer sent to the page. - Better Windows support. - Various miscellaneous bug fixes and UI improvements. vimium-2.1.2/CONTRIBUTING.md000066400000000000000000000127601460337006400152640ustar00rootroot00000000000000# Contributing to Vimium ## Reporting a bug File the issue [here](https://github.com/philc/vimium/issues). ## Contributing code You'd like to fix a bug or implement a feature? Great! Before getting started, understand Vimium's design principles and the goals of the maintainers. ### Vimium design principles When people first start using Vimium, it provides an incredibly powerful workflow improvement and it makes them feel awesome. Surprisingly, Vimium is applicable to a huge, broad population of people, not just users of Vim. In addition to power, a secondary goal of Vimium is approachability: minimizing the barriers which prevent a new user from feeling awesome. Many of Vimium's users haven't used Vim before -- about 1 in 5 Chrome Store reviews say this -- and most people have strong web browsing habits forged from years of browsing. Given that, it's a great experience when Vimium feels like a natural addition to Chrome which augments, but doesn't break, the user's current browsing habits. **Principles:** 1. **Easy to understand**. Even if you're not very familiar with Vim. The Vimium video shows you all you need to know to start using Vimium and feel awesome. 2. **Reliable**. The core feature set works on most sites on the web. 3. **Immediately useful**. Vimium doesn't require any configuration or doc-reading before it's useful. Just watch the video or hit `?`. You can transition into using Vimium piecemeal; you don't need to jump in whole-hog from the start. 4. **Feels native**. Vimium doesn't drastically change the way Chrome looks or behaves. 5. **Simple**. The core feature set isn't overwhelming. This principle is particularly vulnerable as we add to Vimium, so it requires our active effort to maintain this simplicity. 6. **Code simplicity**. Developers find the Vimium codebase relatively simple and easy to jump into. This allows more people to fix bugs and implement features. ### Which pull requests get merged? **Goals of the maintainers** The maintainers of Vimium have limited bandwidth, which influences which PRs we can review and merge. Our goals are generally to keep Vimium small, maintainable, and really nail the broad appeal use cases. This is in contrast to adding and maintaining an increasing number of complex or niche features. We recommend those live in forked repos rather than the mainline Vimium repo. PRs we'll likely merge: - Reflect all of the Vimium design principles. - Are useful for lots of Vimium users. - Have simple implementations (straightforward code, few lines of code). PRs we likely won't: - Violate one or more of our design principles. - Are niche. - Have complex implementations -- more code than they're worth. Tips for preparing a PR: - If you want to check with us first before implementing something big, open an issue proposing the idea. You'll get feedback from the maintainers as to whether it's something we'll likely merge. - Try to keep PRs around 50 LOC or less. Bigger PRs create inertia for review. Here's the rationale behind this policy: - Vimium is a volunteer effort. To make it possible to keep the project up-to-date as the web and browsers evolve, the codebase has to remain small and maintainable. - If the maintainers don't use a feature, and most other users don't, then the feature will likely get neglected. - Every feature, particularly neglected ones, increase the complexity of the codebase and makes it more difficult and less pleasant to work on. - Adding a new feature is only part of the work. Once it's added, a feature must be maintained forever. - Vimium is a project which suffers from the [stadium model of open source](https://645ventures.com/voices/articles/github-at-scale-and-how-to-help-stadium-model-maintainers): there are many users but unfortunately few maintainers. As a result, there is bandwidth to maintain only a limited number of features in the main repo. ### Installing From Source Vimium is written in Javascript. To install Vimium from source: **On Chrome/Chromium:** 1. Navigate to `chrome://extensions` 1. Toggle into Developer Mode 1. Click on "Load Unpacked Extension..." 1. Select the Vimium directory you've cloned from Github. **On Firefox:** Firefox needs a modified version of the manifest.json that's used for Chrome. To generate this, run `./make.js write-firefox-manifest` After that: 1. Open Firefox 1. Enter "about:debugging" in the URL bar 1. Click "Load Temporary Add-on" 1. Open the Vimium directory you've cloned from Github, and select any file inside. ### Running the tests Our tests use [shoulda.js](https://github.com/philc/shoulda.js) and [Puppeteer](https://github.com/puppeteer/puppeteer). To run the tests: 1. Install [Deno](https://deno.land/) if you don't have it already. 1. `PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts` to install [Puppeteer](https://github.com/lucacasonato/deno-puppeteer) 1. `./make.js test` to build the code and run the tests. ### Coding Style - Run `deno fmt` at the root of the Vimium project to format your code. - We generally follow the recommendations from the [Airbnb Javascript style guide](https://github.com/airbnb/javascript). - We wrap lines at 100 characters. - When writing comments, uppercase the first letter of your sentence, and put a period at the end. - We're currently using JavaScript language features from ES2018 or earlier. If we desire to use something introduced in a later version of JavaScript, we need to remember to update the minimum Chrome and Firefox versions required. vimium-2.1.2/CREDITS000066400000000000000000000042501460337006400140460ustar00rootroot00000000000000Authors & Maintainers: Ilya Sukhar (github: ilya) Phil Crosby (github: philc) Contributors: acrollet Adam Lindberg (github: eproxus) akhilman Ângelo Otávio Nuffer Nunes (github: angelonuffer) Bernardo B. Marques (github: bernardofire) Bill Casarin (github: jb55) Bill Mill (github: llimllib) Branden Rolston (github: branden) Caleb Spare (github: cespare) Carl Helmertz (github: chelmertz) Christian Stefanescu (github: stchris) ConradIrwin Daniel MacDougall (github: dmacdougall) drizzd gpurkins hogelog int3 Johannes Emerich (github: knuton) Julian Naydichev (github: naydichev) Justin Blake (github: blaix) Knorkebrot lack markstos Matthew Cline Matt Garriott (github: mgarriott) Matthew Ryan (github: mrmr1993) Michael Hauser-Raspe (github: mijoharas) Murph (github: pandeiro) Niklas Baumstark (github: niklasb) rodimius Stephen Blott (github: smblott-github) Svein-Erik Larsen (github: feinom) Tim Morgan (github: seven1m) tsigo R.T. Lechow (github: rtlechow) Wang Ning (github:daning) Werner Laurensse (github: ab3) Timo Sand (github: deiga) Shiyong Chen (github: UncleBill) Utkarsh Upadhyay (github: PrestanceDesign) Dahan Gong (github: gdh1995) Scott Pinkelman (github: sco-tt) Darryl Pogue (github: dpogue) tobimensch Ramiro Araujo (github: ramiroaraujo) Daniel Skogly (github: poacher2k) Matt Wanchap (github: mwanchap) Leo Solidum (github: leosolid) Feel free to add real names in addition to GitHub usernames. vimium-2.1.2/MIT-LICENSE.txt000066400000000000000000000020541460337006400153000ustar00rootroot00000000000000Copyright (c) 2010 Phil Crosby, Ilya Sukhar. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.vimium-2.1.2/README.md000066400000000000000000000143771460337006400143200ustar00rootroot00000000000000# Vimium - The Hacker's Browser Vimium is a browser extension that provides keyboard-based navigation and control of the web in the spirit of the Vim editor. **Installation instructions:** * Chrome: [Chrome web store](https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb) * Edge: [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/vimium/djmieaghokpkpjfbpelnlkfgfjapaopa) * Firefox: [Firefox Add-ons](https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/) To install from source, see [here](CONTRIBUTING.md#installing-from-source). Vimium's Options page can be reached via a link on the help dialog (type `?`) or via the button next to Vimium on the extension pages of Chrome and Edge (`chrome://extensions`), or Firefox (`about:addons`). ## Keyboard Bindings 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. 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. Use ng0 to go to n-th 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. Use to cycle through options. 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, shift, 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. 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 See [CONTRIBUTING.md](CONTRIBUTING.md) for details. ## Release Notes See [CHANGELOG](CHANGELOG.md) for the major changes in each release. ## License Copyright (c) Phil Crosby, Ilya Sukhar. See [MIT-LICENSE.txt](MIT-LICENSE.txt) for details. vimium-2.1.2/background_scripts/000077500000000000000000000000001460337006400167135ustar00rootroot00000000000000vimium-2.1.2/background_scripts/background.js000066400000000000000000000010461460337006400213710ustar00rootroot00000000000000import "../lib/utils.js"; import "../lib/settings.js"; import "../lib/url_utils.js"; import "../background_scripts/tab_recency.js"; import "../background_scripts/bg_utils.js"; import "../background_scripts/commands.js"; import "../background_scripts/exclusions.js"; import "../background_scripts/completion_engines.js"; import "../background_scripts/completion_search.js"; import "../background_scripts/completion.js"; import "../background_scripts/tab_operations.js"; import "../background_scripts/marks.js"; import "../background_scripts/main.js"; vimium-2.1.2/background_scripts/bg_utils.js000066400000000000000000000016611460337006400210650ustar00rootroot00000000000000const BgUtils = { tabRecency: new TabRecency(), // We're using browser.runtime to determine the browser name and version for Firefox. That API is // only available on the background page. We're not using window.navigator because it's // unreliable. Sometimes browser vendors will provide fake values, like when // `privacy.resistFingerprinting` is enabled on `about:config` of Firefox. isFirefox() { // Only Firefox has a `browser` object defined. return globalThis.browser // We want this browser check to also cover Firefox variants, like LibreWolf. See #3773. // We could also just check browserInfo.name against Firefox and Librewolf. ? browser.runtime.getURL("").startsWith("moz") : false; }, async getFirefoxVersion() { return globalThis.browser ? (await browser.runtime.getBrowserInfo()).version : null; }, }; BgUtils.tabRecency.init(); Object.assign(globalThis, { BgUtils, }); vimium-2.1.2/background_scripts/commands.js000066400000000000000000000505021460337006400210540ustar00rootroot00000000000000// A specification for a command, as defined by the default key bindings, or as it // appears in the user's keymapping settings. class RegistryEntry { // Array of keys. keySequence; // Name of the command. command; description; // Whether this command can be used with a count key prefix. noRepeat; // The maximum number of allow repetitions of this command, to avoid user error. repeatLimit; // Whether this command has to be run by the background page. background; // Whether this command must be run only in the top frame of a page. topFrame; // The map of options for this command. This is a parsed, sanitized version of the user's options // for this command. options; // The (optional) raw list of options for this command provided in the user's settings. // E.g. "count=10" in "map j scrollDown count=10". // NOTE(philc): This is used only by the createTab command. optionList; constructor(o) { Object.seal(this); if (o) Object.assign(this, o); } } const Commands = { availableCommands: {}, // A map of keyString => RegistryEntry keyToRegistryEntry: null, // A map of typed key => key it's mapped to (via the `mapkey` config statement). mapKeyRegistry: null, async init() { await Settings.onLoaded(); for (const command of Object.keys(commandDescriptions)) { const [description, options] = commandDescriptions[command]; this.availableCommands[command] = Object.assign(options || {}, { description }); } Settings.addEventListener("change", async () => { await this.loadKeyMappings(Settings.get("keyMappings")); }); await this.loadKeyMappings(Settings.get("keyMappings")); }, // Parses the text supplied by the user in their "keyMappings" setting. // - shouldLogWarnings: if true, logs to the console when part of the user's config is invalid. // Returns { keyToRegistryEntry, keyToMappedKey }. parseKeyMappingsConfig(configText, shouldLogWarnings) { let keyToRegistryEntry = {}; let mapKeyRegistry = {}; const configLines = Utils.parseLines(configText); const logWarning = (...args) => { if (!shouldLogWarnings) return; console.warn.apply(console, args); }; for (const line of configLines) { const tokens = line.split(/\s+/); const command = tokens[0].toLowerCase(); switch (command) { case "map": if (tokens.length >= 3) { const [_, key, command, ...optionList] = tokens; if (!this.availableCommands[command]) { logWarning(`"${command}" is not a valid command in the line:`, line); continue; } const keySequence = this.parseKeySequence(key); const options = this.parseCommandOptions(command, optionList); keyToRegistryEntry[key] = new RegistryEntry( Object.assign({ keySequence, command, options, optionList, }, this.availableCommands[command]), ); } break; case "unmap": if (tokens.length != 2) { logWarning("Incorrect usage for unmap in the line:", line); continue; } const key = tokens[1]; delete keyToRegistryEntry[key]; delete mapKeyRegistry[key]; break; case "unmapall": keyToRegistryEntry = {}; mapKeyRegistry = {}; break; case "mapkey": if (tokens.length != 3) { logWarning("Incorrect usage for mapkey in the line:", line); continue; } const fromChar = this.parseKeySequence(tokens[1]); const toChar = this.parseKeySequence(tokens[2]); // NOTE(philc): I'm not sure why we enforce that the fromChar and toChar have to be // length one. It's been that way since this feature was introduced in 6596e30. const isValid = fromChar.length == toChar.length && toChar.length === 1; if (isValid) { mapKeyRegistry[fromChar[0]] = toChar[0]; } else { logWarning( "mapkey only supports mapping keys which are single characters. Line:", line, ); } break; default: logWarning(`"${command}" is not a valid config command in line:`, line); } } return { keyToRegistryEntry, keyToMappedKey: mapKeyRegistry, }; }, // Parses the user's keyMapping config text and persists the parsed key mappings into the // extension's storage, for use by the other parts of this extension. async loadKeyMappings(userKeyMappingsConfigText) { let key, command; this.keyToRegistryEntry = {}; this.mapKeyRegistry = {}; const defaultKeyConfig = Object.keys(defaultKeyMappings).map((key) => `map ${key} ${defaultKeyMappings[key]}` ).join("\n"); const parsed = this.parseKeyMappingsConfig( defaultKeyConfig + "\n" + userKeyMappingsConfigText, true, ); this.mapKeyRegistry = parsed.keyToMappedKey; this.keyToRegistryEntry = parsed.keyToRegistryEntry; await chrome.storage.session.set({ mapKeyRegistry: this.mapKeyRegistry }); await this.installKeyStateMapping(); this.prepareHelpPageData(); // Push the key mappings from any passNextKey commands into storage so that they're's available // to the front end so they can be detected during insert mode. We exclude single-key mappings // for this command (i.e. printable keys) because we're considering that a configuration error: // when users press printable keys in insert mode, they expect that character to be input, not // to be droppped into a special Vimium mode. const passNextKeys = Object.entries(this.keyToRegistryEntry) .filter(([key, v]) => v.command == "passNextKey" && key.length > 1) .map(([key, v]) => key); await chrome.storage.session.set({ passNextKeyKeys: passNextKeys }); }, // 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.js. // The return value is a sequence of keys: e.g. "b" -> ["", "", "b"]. parseKeySequence: (function () { const modifier = "(?:[acms]-)"; // E.g. "a-", "c-", "m-", "s-". const namedKey = "(?:[a-z][a-z0-9]+)"; // E.g. "left" or "f12" (always two characters or more). const modifiedKey = `(?:${modifier}+(?:.|${namedKey}))`; // E.g. "c-*" or "c-left". const specialKeyRegexp = new RegExp(`^<(${namedKey}|${modifiedKey})>(.*)`, "i"); return function (key) { if (key.length === 0) { return []; // Parse "bcd" as "" and "bcd". } else if (0 === key.search(specialKeyRegexp)) { const array = RegExp.$1.split("-"); const adjustedLength = Math.max(array.length, 1); let modifiers = array.slice(0, adjustedLength - 1); let keyChar = array[adjustedLength - 1]; if (keyChar.length !== 1) { keyChar = keyChar.toLowerCase(); } modifiers = modifiers.map((m) => m.toLowerCase()); modifiers.sort(); return [ "<" + modifiers.concat([keyChar]).join("-") + ">", ...this.parseKeySequence(RegExp.$2), ]; } else { return [key[0], ...this.parseKeySequence(key.slice(1))]; } }; })(), // Command options follow command mappings, and are of one of two forms: // key=value - a value // key - a flag parseCommandOptions(command, optionList) { const options = {}; for (const option of Array.from(optionList)) { const parse = option.split("=", 2); options[parse[0]] = parse.length === 1 ? true : parse[1]; } // We parse any `count` option immediately (to avoid having to parse it repeatedly later). if ("count" in options) { options.count = parseInt(options.count); if (isNaN(options.count) || this.availableCommands[command].noRepeat) { delete options.count; } } return options; }, // This generates and installs a nested key-to-command mapping structure. There is an example in // mode_key_handler.js. async installKeyStateMapping() { const keyStateMapping = {}; for (const keys of Object.keys(this.keyToRegistryEntry || {})) { const registryEntry = this.keyToRegistryEntry[keys]; let currentMapping = keyStateMapping; for (let index = 0; index < registryEntry.keySequence.length; index++) { const key = registryEntry.keySequence[index]; if (currentMapping[key] != null ? currentMapping[key].command : undefined) { // 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] != null ? currentMapping[key] : (currentMapping[key] = {}); } else { currentMapping[key] = Object.assign({}, registryEntry); // We don't need these properties in the content scripts. for (const prop of ["keySequence", "description"]) { delete currentMapping[key][prop]; } } } } await chrome.storage.session.set({ normalModeKeyStateMapping: keyStateMapping, // Inform `KeyboardUtils.isEscape()` whether `` should be interpreted as `Escape` (which it // is by default). useVimLikeEscape: !("" in keyStateMapping), }); }, // Build the "helpPageData" data structure which the help page needs and place it in Chrome // storage. prepareHelpPageData() { const commandToKey = {}; for (const key of Object.keys(this.keyToRegistryEntry || {})) { const registryEntry = this.keyToRegistryEntry[key]; (commandToKey[registryEntry.command] != null ? commandToKey[registryEntry.command] : (commandToKey[registryEntry.command] = [])).push(key); } const commandGroups = {}; for (const group of Object.keys(this.commandGroups || {})) { const commands = this.commandGroups[group]; commandGroups[group] = []; for (const command of commands) { commandGroups[group].push({ command, description: this.availableCommands[command].description, keys: commandToKey[command] != null ? commandToKey[command] : [], advanced: this.advancedCommands.includes(command), }); } } chrome.storage.session.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", ], }; const defaultKeyMappings = { // Navigating the current page "j": "scrollDown", "k": "scrollUp", "h": "scrollLeft", "l": "scrollRight", "gg": "scrollToTop", "G": "scrollToBottom", "zH": "scrollToLeft", "zL": "scrollToRight", "": "scrollDown", "": "scrollUp", "d": "scrollPageDown", "u": "scrollPageUp", "r": "reload", "yy": "copyCurrentUrl", "p": "openCopiedUrlInCurrentTab", "P": "openCopiedUrlInNewTab", "gi": "focusInput", "[[": "goPrevious", "]]": "goNext", "gf": "nextFrame", "gF": "mainFrame", "gu": "goUp", "gU": "goToRoot", "i": "enterInsertMode", "v": "enterVisualMode", "V": "enterVisualLineMode", // Link hints "f": "LinkHints.activateMode", "F": "LinkHints.activateModeToOpenInNewTab", "": "LinkHints.activateModeWithQueue", "yf": "LinkHints.activateModeToCopyLinkUrl", // Using find "/": "enterFindMode", "n": "performFind", "N": "performBackwardsFind", // Vomnibar "o": "Vomnibar.activate", "O": "Vomnibar.activateInNewTab", "T": "Vomnibar.activateTabSelection", "b": "Vomnibar.activateBookmarks", "B": "Vomnibar.activateBookmarksInNewTab", "ge": "Vomnibar.activateEditUrl", "gE": "Vomnibar.activateEditUrlInNewTab", // Navigating history "H": "goBack", "L": "goForward", // Manipulating tabs "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", // Marks "m": "Marks.activateCreateMode", "`": "Marks.activateGotoMode", // Misc "?": "showHelp", "gs": "toggleViewSource", }; // This is a mapping of: commandIdentifier => [description, options]. // If the noRepeat and repeatLimit options are both specified, then noRepeat takes precedence. const 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.sessions ? chrome.sessions.MAX_SESSION_RESULTS : null) || 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 }], closeTabsOnRight: ["Close tabs on the right", { background: 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 }], }; globalThis.Commands = Commands; vimium-2.1.2/background_scripts/completion.js000066400000000000000000001043221460337006400214240ustar00rootroot00000000000000// 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): "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. // Set this to true to render relevancy when debugging the ranking scores. const showRelevancy = false; // TODO(philc): Consider moving out the "computeRelevancy" function. class Suggestion { queryTerms; description; url; // A shortened URL (URI-decoded, protocol removed) suitable for dispaly purposes. shortUrl; title = ""; // A computed relevancy value. relevancy; relevancyFunction; relevancyData; // When true, then this suggestion is automatically pre-selected in the vomnibar. This only affects // the suggestion in slot 0 in the vomnibar. autoSelect = false; // When true, we highlight matched terms in the title and URL. Otherwise we don't. highlightTerms = true; // The text to insert into the vomnibar input when this suggestion is selected. insertText; // This controls whether this suggestion is a candidate for deduplication after simplifying // its URL. deDuplicate = true; // The tab represented by this suggestion. Populated by TabCompleter. tabId; // Whether this is a suggestion provided by a user's custom search engine. isCustomSearch; // Whether this is meant to be the first suggestion from the user's custom search engine which // represents their query as typed, verbatim. isPrimarySuggestion = false; // The generated HTML string for showing this suggestion in the Vomnibar. html; searchUrl; constructor(options) { Object.seal(this); Object.assign(this, options); } // Returns the relevancy score. computeRelevancy() { // We assume that, once the relevancy has been set, it won't change. Completers must set // either @relevancy or @relevancyFunction. if (this.relevancy == null) { this.relevancy = this.relevancyFunction(this); } return this.relevancy; } generateHtml() { if (this.html) return this.html; const relevancyHtml = showRelevancy ? `${this.computeRelevancy()}` : ""; const insertTextClass = this.insertText ? "vomnibarInsertText" : "vomnibarNoInsertText"; const insertTextIndicator = "↪"; // A right hooked arrow. if (this.insertText && this.isCustomSearch) { this.title = this.insertText; } let faviconHtml = ""; if (this.description === "tab" && !BgUtils.isFirefox()) { const faviconUrl = new URL(chrome.runtime.getURL("/_favicon/")); faviconUrl.searchParams.set("pageUrl", this.url); faviconUrl.searchParams.set("size", "16"); faviconHtml = ``; } if (this.isCustomSearch) { this.html = `\
${insertTextIndicator}${this.description} ${this.highlightQueryTerms(Utils.escapeHtml(this.title))} ${relevancyHtml}
\ `; } else { this.html = `\
${insertTextIndicator}${this.description} ${this.highlightQueryTerms(Utils.escapeHtml(this.title))}
${insertTextIndicator}${faviconHtml}${ this.highlightQueryTerms(Utils.escapeHtml(this.shortenUrl())) } ${relevancyHtml}
\ `; } return this.html; } // Use neat trick to snatch a domain (http://stackoverflow.com/a/8498668). getUrlRoot(url) { const a = document.createElement("a"); a.href = url; return a.protocol + "//" + a.hostname; } getHostname(url) { const a = document.createElement("a"); a.href = url; return a.hostname; } stripTrailingSlash(url) { if (url[url.length - 1] === "/") { url = url.substring(url, url.length - 1); } return url; } // Push the ranges within `string` which match `term` onto `ranges`. pushMatchingRanges(string, term, ranges) { let 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) const splits = string.split(RegexpCache.get(term, "(", ")")); for (let index = 0, end = splits.length - 2; index <= end; index += 2) { const unmatchedText = splits[index]; const 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) { if (!this.highlightTerms) return string; let ranges = []; const escapedTerms = this.queryTerms.map((term) => Utils.escapeHtml(term)); for (const term of escapedTerms) { this.pushMatchingRanges(string, term, ranges); } if (ranges.length === 0) { return string; } ranges = this.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 (const [start, end] of ranges) { string = string.substring(0, start) + `${string.substring(start, end)}` + string.substring(end); } return 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) { let previous = ranges.shift(); const mergedRanges = [previous]; ranges.forEach(function (range) { if (previous[1] >= range[0]) { previous[1] = Math.max(range[1], previous[1]); } else { mergedRanges.push(range); previous = range; } }); return mergedRanges; } // Simplify a suggestion's URL (by removing those parts which aren't useful for display or // comparison). shortenUrl() { if (this.shortUrl != null) { return this.shortUrl; } // We get easier-to-read shortened URLs if we URI-decode them. let url = (Utils.decodeURIByParts(this.url) || this.url).toLowerCase(); for (const [filter, replacements] of Suggestion.stripPatterns) { if (new RegExp(filter).test(url)) { for (const replace of replacements) { url = url.replace(replace, ""); } } } this.shortUrl = url; return this.shortUrl; } // 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). static boostRelevancyScore(factor, score) { return score + (score < 0.5 ? score * factor : (1.0 - score) * factor); } } // 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. // Suggestion.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))], ]; const folderSeparator = "/"; // If these names occur as top-level bookmark names, then they are not included in the names of // bookmark folders. const ignoredTopLevelBookmarks = { "Other Bookmarks": true, "Mobile Bookmarks": true, "Bookmarks Bar": true, }; // this.bookmarks are loaded asynchronously when refresh() is called. class BookmarkCompleter { async filter({ queryTerms }) { if (!this.bookmarks) await this.refresh(); // If the folder separator character is the first character in any query term, then use the // bookmark's full path as its title. Otherwise, just use the its regular title. let results; const usePathAndTitle = queryTerms.reduce( (prev, term) => prev || term.startsWith(folderSeparator), false, ); if (queryTerms.length > 0) { results = this.bookmarks.filter((bookmark) => { const suggestionTitle = usePathAndTitle ? bookmark.pathAndTitle : bookmark.title; if (bookmark.hasJavascriptPrefix == null) { bookmark.hasJavascriptPrefix = Utils.hasJavascriptPrefix(bookmark.url); } if (bookmark.hasJavascriptPrefix && bookmark.shortUrl == null) { bookmark.shortUrl = "javascript:..."; } const suggestionUrl = bookmark.shortUrl != null ? bookmark.shortUrl : bookmark.url; return RankingUtils.matches(queryTerms, suggestionUrl, suggestionTitle); }); } else { results = []; } const suggestions = results.map((bookmark) => { return new Suggestion({ queryTerms, description: "bookmark", url: bookmark.url, title: usePathAndTitle ? bookmark.pathAndTitle : bookmark.title, relevancyFunction: this.computeRelevancy, shortUrl: bookmark.shortUrl, deDuplicate: (bookmark.shortUrl == null), }); }); return suggestions; } async refresh() { // In case refresh() is called multiple times before chrome.bookmarks.getTree() completes, only // call chrome.bookmarks.getTree() once. if (this.bookmarksTreePromise) { await this.bookmarksTreePromise; return; } this.bookmarksTreePromise = chrome.bookmarks.getTree(); const bookmarksTree = await this.bookmarksTreePromise; this.bookmarks = this.traverseBookmarks(bookmarksTree) .filter((b) => b.url != null); this.bookmarksTreePromise = null; } // Traverses the bookmark hierarchy, and returns a flattened list of all bookmarks. traverseBookmarks(bookmarks) { const results = []; bookmarks.forEach((folder) => this.traverseBookmarksRecursive(folder, results)); return results; } // Recursive helper for `traverseBookmarks`. traverseBookmarksRecursive(bookmark, results, parent) { if (parent == null) { parent = { pathAndTitle: "" }; } if ( bookmark.title && !((parent.pathAndTitle === "") && ignoredTopLevelBookmarks[bookmark.title]) ) { bookmark.pathAndTitle = parent.pathAndTitle + folderSeparator + bookmark.title; } else { bookmark.pathAndTitle = parent.pathAndTitle; } results.push(bookmark); if (bookmark.children) { bookmark.children.forEach((child) => this.traverseBookmarksRecursive(child, results, bookmark) ); } } computeRelevancy(suggestion) { return RankingUtils.wordRelevancy( suggestion.queryTerms, suggestion.shortUrl || suggestion.url, suggestion.title, ); } } class HistoryCompleter { // - seenTabToOpenCompletionList: true if the user has typed only , and nothing else. // We interpret this to mean that they want to see all of their history in the Vomnibar, sorted // by recency. async filter({ queryTerms, seenTabToOpenCompletionList }) { await HistoryCache.onLoaded(); let results; if (queryTerms.length > 0) { results = HistoryCache.history .filter((entry) => RankingUtils.matches(queryTerms, entry.url, entry.title)); } else if (seenTabToOpenCompletionList) { // The user has typed to open the entire history (sorted by recency). results = HistoryCache.history; } else { results = []; } const suggestions = results.map((entry) => { return new Suggestion({ queryTerms, description: "history", url: entry.url, title: entry.title, relevancyFunction: this.computeRelevancy, relevancyData: entry, }); }); return suggestions; } computeRelevancy(suggestion) { const historyEntry = suggestion.relevancyData; const recencyScore = RankingUtils.recencyScore(historyEntry.lastVisitTime); // If there are no query terms, then relevancy is based on recency alone. if (suggestion.queryTerms.length === 0) return recencyScore; const 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. return (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; async filter({ queryTerms, query }) { const isMultiWordQuery = /\S\s/.test(query); if ((queryTerms.length === 0) || isMultiWordQuery) return []; if (!this.domains) await this.populateDomains(); const firstTerm = queryTerms[0]; const domains = Object.keys(this.domains || []).filter((d) => d.includes(firstTerm)); const domainsAndScores = this.sortDomainsByRelevancy(queryTerms, domains); const result = new Suggestion({ queryTerms, description: "domain", // This should be the URL or the domain, or an empty string, but not null. url: domainsAndScores[0]?.[0] || "", relevancy: 2.0, }); return result.url.length > 0 ? [result] : []; } // Returns a list of domains of the form: [ [domain, relevancy], ... ] sortDomainsByRelevancy(queryTerms, domainCandidates) { const results = []; for (const domain of domainCandidates) { const recencyScore = RankingUtils.recencyScore(this.domains[domain].entry.lastVisitTime || 0); const wordRelevancy = RankingUtils.wordRelevancy(queryTerms, domain, null); const score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2; results.push([domain, score]); } results.sort((a, b) => b[1] - a[1]); return results; } async populateDomains() { await HistoryCache.onLoaded(); this.domains = {}; HistoryCache.history.forEach((entry) => this.onVisited(entry)); chrome.history.onVisited.addListener(this.onVisited.bind(this)); chrome.history.onVisitRemoved.addListener(this.onVisitRemoved.bind(this)); } onVisited(newPage) { const domain = this.parseDomainAndScheme(newPage.url); if (domain) { const slot = this.domains[domain] || (this.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. if (slot.entry.lastVisitTime < newPage.lastVisitTime) { slot.entry = newPage; } slot.referenceCount += 1; } } onVisitRemoved(toRemove) { if (toRemove.allHistory) { this.domains = {}; } else { toRemove.urls.forEach((url) => { const domain = this.parseDomainAndScheme(url); if (domain && this.domains[domain] && ((this.domains[domain].referenceCount -= 1) === 0)) { return delete this.domains[domain]; } }); } } // Return something like "http://www.example.com" or false. parseDomainAndScheme(url) { return UrlUtils.hasFullUrlPrefix(url) && !UrlUtils.hasChromePrefix(url) && url.split("/", 3).join("/"); } } // Searches through all open tabs, matching on title and URL. // If the query is empty, then return a list of open tabs, sorted by recency. class TabCompleter { async filter({ queryTerms }) { await BgUtils.tabRecency.init(); // We search all tabs, not just those in the current window. const tabs = await chrome.tabs.query({}); const results = tabs.filter((tab) => RankingUtils.matches(queryTerms, tab.url, tab.title)); const suggestions = results .map((tab) => { const suggestion = new Suggestion({ queryTerms, description: "tab", url: tab.url, title: tab.title, tabId: tab.id, deDuplicate: false, }); suggestion.relevancy = this.computeRelevancy(suggestion); return suggestion; }) .sort((a, b) => b.relevancy - a.relevancy); // Boost relevancy with a multiplier so a relevant tab doesn't get crowded out by results from // competing completers. To prevent tabs from crowding out everything else in turn, penalize // them for being further down the results list by scaling on a hyperbola starting at 1 and // approaching 0 asymptotically for higher indexes. The multiplier and the curve fall-off were // subjectively chosen on the grounds that they seem to work pretty well. suggestions.forEach(function (suggestion, i) { suggestion.relevancy *= 8; suggestion.relevancy /= (i / 4) + 1; }); return suggestions; } computeRelevancy(suggestion) { if (suggestion.queryTerms.length > 0) { return RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title); } else { return BgUtils.tabRecency.recencyScore(suggestion.tabId); } } } class SearchEngineCompleter { cancel() { CompletionSearch.cancel(); } // TODO(philc): Consider moving to UserSearchEngines getUserSearchEngineForQuery(query) { const parts = query.trimStart().split(/\s+/); // For a keyword "w", we match "w search terms" and "w ", but not "w" on its own. if (parts.length <= 1) return; const keyword = parts[0]; return UserSearchEngines.keywordToEngine[keyword]; } refresh() { UserSearchEngines.set(Settings.get("searchEngines")); } async filter(request) { const { queryTerms } = request; const keyword = queryTerms[0]; const queryTermsWithoutKeyword = queryTerms.slice(1); const userSearchEngine = UserSearchEngines.keywordToEngine[keyword]; if (!userSearchEngine) return []; const searchUrl = userSearchEngine.url; const completions = await CompletionSearch.complete(searchUrl, queryTermsWithoutKeyword); const makeSuggestion = (query) => { const url = UrlUtils.createSearchUrl(query, searchUrl); return new Suggestion({ queryTerms, description: userSearchEngine.description, url, title: query, searchUrl, highlightTerms: false, isCustomSearch: true, relevancy: null, relevancyFunction: this.computeRelevancy, }); }; const suggestions = completions.map((completion) => { const s = makeSuggestion(completion); s.insertText = completion; return s; }); if (suggestions[0]) suggestions[0].relevancy = 1.0; // This is a suggestion which contains the user's query. It's the "search for exactly what I // just typed" option. It should always appear first in the list. const primarySuggestion = makeSuggestion(queryTermsWithoutKeyword.join(" ")); primarySuggestion.relevancy = 2; primarySuggestion.isPrimarySuggestion = true; primarySuggestion.autoSelect = true; suggestions.unshift(primarySuggestion); return suggestions; } computeRelevancy({ 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. return Suggestion.boostRelevancyScore( 0.5, 0.7 * RankingUtils.wordRelevancy(queryTerms, title, title), ); } } SearchEngineCompleter.debug = false; // 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. const maxResults = 10; class MultiCompleter { constructor(completers) { this.completers = completers; } refresh() { for (const c of this.completers) { if (c.refresh) c.refresh(); } } cancel() { for (const c of this.completers) { c.cancel?.(); } } async filter(request) { const searchEngineCompleter = this.completers.find((c) => c instanceof SearchEngineCompleter); const query = request.query; const queryTerms = request.queryTerms; // The only UX where we support showing results when there are no query terms is via // Vomnibar.activateTabSelection, where we show the list of open tabs by recency. const isTabCompleter = this.completers.length == 1 && this.completers[0] instanceof TabCompleter; if (queryTerms.length == 0 && !isTabCompleter) { return []; } const queryMatchesUserSearchEngine = searchEngineCompleter?.getUserSearchEngineForQuery(query); // If the user's query matches one of their custom search engines, then use only that engine to // provide completions for their query. const completers = queryMatchesUserSearchEngine ? [searchEngineCompleter] : this.completers.filter((c) => c != searchEngineCompleter); RegexpCache.clear(); const promises = completers.map((c) => c.filter(request)); let results = (await Promise.all(promises)).flat(1); results = this.postProcessSuggestions(request, queryTerms, results); return results; } // Rank them, simplify the URLs, and de-duplicate suggestions with the same simplified URL. postProcessSuggestions(request, queryTerms, suggestions) { for (const s of suggestions) { s.computeRelevancy(queryTerms); } suggestions.sort((a, b) => b.relevancy - a.relevancy); // Simplify URLs and remove duplicates (duplicate simplified URLs, that is). let count = 0; const seenUrls = {}; const dedupedSuggestions = []; for (const s of suggestions) { const url = s.shortenUrl(); if (s.deDuplicate && seenUrls[url]) continue; if (count++ === maxResults) break; seenUrls[url] = s; dedupedSuggestions.push(s); } // Give each completer the opportunity to tweak the suggestions. for (const completer of this.completers) { if (completer.postProcessSuggestions) { completer.postProcessSuggestions(request, dedupedSuggestions); } } // Generate HTML for the remaining suggestions and return them. for (const s of dedupedSuggestions) { s.generateHtml(request); } return dedupedSuggestions; } } // Utilities which help us compute a relevancy score for a given item. const 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 (const term of queryTerms) { const regexp = RegexpCache.get(term); let matchedTerm = false; for (const thing of things) { if (!matchedTerm) { matchedTerm = thing.match(regexp); } } if (!matchedTerm) return false; } return 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) { let score = 0; let count = 0; const 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; } } } return [score, count < string.length ? count : string.length]; }, // Returns a number between [0, 1] indicating how often the query terms appear in the url and title. wordRelevancy(queryTerms, url, title) { let titleCount, titleScore; let urlScore = (titleScore = 0.0); let urlCount = (titleCount = 0); // Calculate initial scores. for (const term of queryTerms) { let [s, c] = RankingUtils.scoreTerm(term, url); urlScore += s; urlCount += c; if (title) { [s, c] = RankingUtils.scoreTerm(term, title); titleScore += s; titleCount += c; } } const 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. if (urlScore < titleScore) { urlScore = titleScore; } // Return the average. return (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) { if (!this.oneMonthAgo) { this.oneMonthAgo = 1000 * 60 * 60 * 24 * 30; } const recency = Date.now() - lastAccessedTime; const recencyDifference = Math.max(0, this.oneMonthAgo - recency) / this.oneMonthAgo; // recencyScore is between [0, 1]. It is 1 when recenyDifference is 0. This quadratic equation // will incresingly discount older history entries. let recencyScore = recencyDifference * recencyDifference * recencyDifference; // Calibrate recencyScore vis-a-vis word-relevancy scores. return recencyScore *= RankingUtils.matchWeights.recencyCalibrator; }, // Takes the difference of two numbers and returns a number between [0, 1] (the percentage difference). normalizeDifference(a, b) { const max = Math.max(a, b); return (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. const RegexpCache = { init() { this.initialized = true; this.clear(); }, clear() { this.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) { if (prefix == null) prefix = ""; if (suffix == null) suffix = ""; if (!this.initialized) this.init(); let regexpString = Utils.escapeRegexSpecialCharacters(string); // Avoid cost of constructing new strings if prefix/suffix are empty (which is expected to be a // common case). if (prefix) regexpString = prefix + regexpString; if (suffix) regexpString = regexpString + suffix; // Smartcase: Regexp is case insensitive, unless `string` contains a capital letter (testing // `string`, not `regexpString`). return this.cache[regexpString] || (this.cache[regexpString] = new RegExp(regexpString, Utils.hasUpperCase(string) ? "" : "i")); }, }; // Provides cached access to Chrome's history. As the user browses to new pages, we add those pages // to this history cache. const HistoryCache = { size: 20000, // An array of History items returned from Chrome. history: null, reset() { this.history = null; chrome.history.onVisited.removeListener(this._onVisitedListener); chrome.history.onVisitRemoved.removeListener(this._onVisitRemovedListener); }, async onLoaded() { if (this.history) return; await this.fetchHistory(); }, async fetchHistory() { if (this.chromeHistoryPromise) { await this.chromeHistoryPromise; return; } this.chromeHistoryPromise = chrome.history.search({ text: "", maxResults: this.size, startTime: 0, }); const history = await this.chromeHistoryPromise; // On Firefox, some history entries do not have titles. for (const entry of history) { if (entry.title == null) entry.title = ""; } history.sort(this.compareHistoryByUrl); this.history = history; chrome.history.onVisited.addListener(this._onVisitedListener); chrome.history.onVisitRemoved.addListener(this._onVisitRemovedListener); this.chromeHistoryPromise = null; }, compareHistoryByUrl(a, b) { if (a.url === b.url) return 0; if (a.url > b.url) return 1; return -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. onVisited(newPage) { // On Firefox, some history entries do not have titles. if (newPage.title == null) newPage.title = ""; const i = HistoryCache.binarySearch(newPage, this.history, this.compareHistoryByUrl); const pageWasFound = this.history[i]?.url == newPage.url; if (pageWasFound) { this.history[i] = newPage; } else { this.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) { this.history = []; } else { toRemove.urls.forEach((url) => { const i = HistoryCache.binarySearch({ url }, this.history, this.compareHistoryByUrl); if ((i < this.history.length) && (this.history[i].url === url)) { this.history.splice(i, 1); } }); } }, }; HistoryCache._onVisitedListener = HistoryCache.onVisited.bind(HistoryCache); HistoryCache._onVisitRemovedListener = HistoryCache.onVisitRemoved.bind(HistoryCache); // 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 = function (targetElement, array, compareFunction) { let element, middle; let high = array.length - 1; let low = 0; while (low <= high) { middle = Math.floor((low + high) / 2); element = array[middle]; const 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. if (compareFunction(element, targetElement) < 0) { return middle + 1; } else { return middle; } }; Object.assign(globalThis, { Suggestion, BookmarkCompleter, MultiCompleter, HistoryCompleter, DomainCompleter, TabCompleter, SearchEngineCompleter, HistoryCache, RankingUtils, RegexpCache, }); vimium-2.1.2/background_scripts/completion_engines.js000066400000000000000000000161271460337006400231410ustar00rootroot00000000000000// 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 the text body of an HTTP response 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". This info is shown as documentation to the user. // // 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.js". // // 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". // TODO(philc): This base class is doing very little. We should remove it and use composition. class BaseEngine { constructor(options) { Object.assign(this, options); this.regexps = this.regexps.map((regexp) => new RegExp(regexp)); } match(searchUrl) { return Utils.matchesAnyRegexp(this.regexps, searchUrl); } getUrl(queryTerms) { return UrlUtils.createSearchUrl(queryTerms, this.engineUrl); } } class Google extends BaseEngine { constructor() { super({ engineUrl: "http://suggestqueries.google.com/complete/search?client=chrome&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", }, }); } parse(text) { return JSON.parse(text)[1]; } } const googleMapsPrefix = "map of "; class GoogleMaps extends BaseEngine { constructor() { super({ engineUrl: `http://suggestqueries.google.com/complete/search?client=chrome&ds=yt&q=${googleMapsPrefix}%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(text) { return JSON.parse(text)[1] .filter((suggestion) => suggestion.startsWith(googleMapsPrefix)) .map((suggestion) => suggestion.slice(googleMapsPrefix)); } } class Youtube extends BaseEngine { constructor() { super({ engineUrl: "http://suggestqueries.google.com/complete/search?client=chrome&ds=yt&q=%s", regexps: ["^https?://[a-z]+\\.youtube\\.com/results"], example: { searchUrl: "https://www.youtube.com/results?search_query=%s", keyword: "y", }, }); } parse(text) { return JSON.parse(text)[1]; } } 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(text) { return JSON.parse(text)[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(text) { return JSON.parse(text)[1]; } } class Amazon extends BaseEngine { constructor() { super({ engineUrl: "https://completion.amazon.com/api/2017/suggestions?mid=ATVPDKIKX0DER&alias=aps&prefix=%s", regexps: ["^https?://(www|smile)\\.amazon\\.(com|co\\.uk|ca|de|com\\.au)/s/"], example: { searchUrl: "https://www.amazon.com/s/?field-keywords=%s", keyword: "a", }, }); } parse(text) { return JSON.parse(text).suggestions.map((suggestion) => suggestion.value); } } 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(text) { return JSON.parse(text).map((suggestion) => suggestion.phrase); } } 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(text) { return JSON.parse(text).docs.map((suggestion) => suggestion.word); } } // Qwant is a privacy-friendly search engine. 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(text) { return JSON.parse(text).data.items.map((suggestion) => suggestion.value); } } // Brave is a privacy-friendly search engine. class Brave extends BaseEngine { constructor() { super({ engineUrl: "https://search.brave.com/api/suggest?rich=false&q=%s", regexps: ["^https?://search\\.brave\\.com/"], example: { searchUrl: "https://search.brave.com/search?q=%s", keyword: "br", }, }); } parse(text) { return JSON.parse(text)[1]; } } // On the user-facing documentation page pages/completion_engines.html, these completion search // engines will be shown to the user in this order. const CompletionEngines = [ Youtube, GoogleMaps, Google, DuckDuckGo, Wikipedia, Bing, Amazon, Webster, Qwant, Brave, ]; globalThis.CompletionEngines = CompletionEngines; export { Amazon, Brave, DuckDuckGo, Qwant, Webster }; vimium-2.1.2/background_scripts/completion_search.js000066400000000000000000000137261460337006400227600ustar00rootroot00000000000000// 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) { this.searchUrl = searchUrl; this.engine = 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(this.searchUrl)) { let terms = this.searchUrl.replace(/\+%s.*/, ""); terms = terms.replace(/.*=/, ""); terms = terms.replace(/\+/g, " "); queryTerms = [...terms.split(" "), ...queryTerms]; const prefix = `${terms} `; this.transformSuggestionsFn = (suggestions) => { return suggestions .filter((s) => s.startsWith(prefix)) .map((s) => s.slice(prefix.length)); }; } return this.engine.getUrl(queryTerms); } parse(responseText) { const suggestions = this.engine.parse(responseText); return this.transformSuggestionsFn ? this.transformSuggestionsFn(suggestions) : suggestions; } } const 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, // This gets incremented each time we make a request to the completion engine. This allows us to // dedupe requets which overlap, which is the case when the user is typing fast. requestId: 0, async get(url) { const timeoutDuration = 2500; const controller = new AbortController(); let isError = false; let responseText; const timer = Utils.setTimeout(timeoutDuration, () => controller.abort()); try { const response = await fetch(url, { signal: controller.signal }); responseText = await response.text(); } catch { // Fetch throws an error if the network is unreachable, etc. isError = true; } clearTimeout(timer); return isError ? null : responseText; }, // Look up the completion engine for this searchUrl. lookupEngine(searchUrl) { if (this.engineCache.has(searchUrl)) { return this.engineCache.get(searchUrl); } else { for (let engine of Array.from(CompletionEngines)) { engine = new engine(); if (engine.match(searchUrl)) { return this.engineCache.set(searchUrl, engine); } } } }, // 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. async complete(searchUrl, queryTerms) { const query = queryTerms.join(" ").toLowerCase(); // We don't complete queries which are too short: the results are usually useless. if (query.length < 4) return []; // We don't complete regular URLs or Javascript URLs. if (queryTerms.length == 1 && await UrlUtils.isUrl(query)) return []; if (UrlUtils.hasJavascriptPrefix(query)) return []; const engine = this.lookupEngine(searchUrl); if (!engine) return []; const completionCacheKey = JSON.stringify([searchUrl, queryTerms]); if (this.completionCache.has(completionCacheKey)) { if (this.debug) console.log("hit", completionCacheKey); return this.completionCache.get(completionCacheKey); } const createTimeoutPromise = (ms) => { return new Promise((resolve) => { setTimeout(() => { resolve(); }, ms); }); }; this.requestId++; const lastRequestId = this.requestId; // We delay sending a completion request in case the user is still typing. await createTimeoutPromise(this.delay); // If the user has issued a new query while we were waiting, then this query is old; abort it. if (lastRequestId != this.requestId) return []; const engineWrapper = new EnginePrefixWrapper(searchUrl, engine); const url = engineWrapper.getUrl(queryTerms); if (this.debug) console.log("GET", url); const responseText = await this.get(url); // Parsing the response may fail if we receive 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 engine implementations. let suggestions = []; let isError = responseText == null; if (!isError) { try { suggestions = engineWrapper.parse(responseText) // Make all suggestions lower case. It looks odd when suggestions from one // completion engine are upper case, and those from another are lower case. .map((s) => s.toLowerCase()) // Filter out the query itself. It's not adding anything. .filter((s) => s !== query); } catch (error) { if (this.debug) console.log("error:", error); isError = true; } } if (isError) { // We allow failures to be cached too, but remove them after just thirty seconds. Utils.setTimeout( 30 * 1000, () => this.completionCache.set(completionCacheKey, null), ); } this.completionCache.set(completionCacheKey, suggestions); return suggestions; }, // Cancel any pending (ie. blocked on @delay) queries. Does not cancel in-flight queries. This is // called whenever the user is typing. cancel() { this.requestId++; }, }; globalThis.CompletionSearch = CompletionSearch; vimium-2.1.2/background_scripts/exclusions.js000066400000000000000000000055401460337006400214510ustar00rootroot00000000000000const ExclusionRegexpCache = { cache: {}, clear(cache) { this.cache = cache || {}; }, get(pattern) { if (pattern in this.cache) { return this.cache[pattern]; } else { let result; // We use try/catch to ensure that a broken regexp doesn't wholly cripple Vimium. try { result = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$"); } catch { if (!globalThis.isUnitTests) { console.log(`bad regexp in exclusion rule: ${pattern}`); } result = /^$/; // Match the empty string. } this.cache[pattern] = result; return result; } }, }; // 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. const Exclusions = { // Make RegexpCache, which is required on the page popup, accessible via the Exclusions object. RegexpCache: ExclusionRegexpCache, // 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) { if (rules == null) { rules = Settings.get("exclusionRules"); } const matchingRules = rules.filter((r) => r.pattern && (url.search(ExclusionRegexpCache.get(r.pattern)) >= 0) ); // An absolute exclusion rule (one with no passKeys) takes priority. for (const rule of matchingRules) { if (!rule.passKeys) return rule; } // Strip whitespace from all matching passKeys strings, and join them together. const passKeys = matchingRules.map((r) => r.passKeys.split(/\s+/).join("")).join(""); // TODO(philc): Remove this commented out code. // passKeys = (rule.passKeys.split(/\s+/).join "" for rule in matchingRules).join "" if (matchingRules.length > 0) { return { passKeys: Utils.distinctCharacters(passKeys) }; } else { return null; } }, isEnabledForUrl(url) { const rule = Exclusions.getRule(url); return { isEnabledForUrl: !rule || (rule.passKeys.length > 0), passKeys: rule ? rule.passKeys : "", }; }, setRules(rules) { // Callers map a rule to null to have it deleted, and rules without a pattern are useless. const newRules = rules.filter((rule) => rule?.pattern); Settings.set("exclusionRules", newRules); }, onSettingsUpdated() { // NOTE(mrmr1993): In FF, the |rules| argument will be garbage collected when the exclusions // popup is closed. Do NOT store it/use it asynchronously. ExclusionRegexpCache.clear(); }, }; Settings.addEventListener("change", () => Exclusions.onSettingsUpdated()); globalThis.Exclusions = Exclusions; vimium-2.1.2/background_scripts/main.js000066400000000000000000000756261460337006400202150ustar00rootroot00000000000000// NOTE(philc): This file has many superfluous return statements in its functions, as a result of // converting from coffeescript to es6. Many can be removed, but I didn't take the time to // diligently track down precisely which return statements could be removed when I was doing the // conversion. import * as TabOperations from "./tab_operations.js"; // Allow Vimium's content scripts to access chrome.storage.session. Otherwise, // chrome.storage.session will be null in content scripts. chrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" }); // This is exported for use by "marks.js". globalThis.tabLoadedHandlers = {}; // tabId -> function() // A Vimium secret, available only within the current browser session. The secret is a generated // strong random string. const randomArray = globalThis.crypto.getRandomValues(new Uint8Array(32)); // 32-byte random token. const secretToken = randomArray.reduce((a, b) => a.toString(16) + b.toString(16)); chrome.storage.session.set({ vimiumSecret: secretToken }); const completionSources = { bookmarks: new BookmarkCompleter(), history: new HistoryCompleter(), domains: new DomainCompleter(), tabs: new TabCompleter(), searchEngines: new SearchEngineCompleter(), }; const completers = { omni: new MultiCompleter([ completionSources.bookmarks, completionSources.history, completionSources.domains, completionSources.tabs, completionSources.searchEngines, ]), bookmarks: new MultiCompleter([completionSources.bookmarks]), tabs: new MultiCompleter([completionSources.tabs]), }; const onURLChange = (details) => { // sendMessage will throw "Error: Could not establish connection. Receiving end does not exist." // if there is no Vimium content script loaded in the given tab. This can occur if the user // navigated to a page where Vimium doesn't have permissions, like chrome:// URLs. This error is // noisy and mysterious (it usually doesn't have a valid line number), so we silence it. chrome.tabs.sendMessage(details.tabId, { handler: "checkEnabledAfterURLChange", silenceLogging: true, }, { frameId: details.frameId, }) .catch(() => {}); }; // Re-check whether Vimium is enabled for a frame when the URL changes without a reload. // There's no reliable way to detect when the URL has changed in the content script, so we // have to use the webNavigation API in our background script. chrome.webNavigation.onHistoryStateUpdated.addListener(onURLChange); // history.pushState. chrome.webNavigation.onReferenceFragmentUpdated.addListener(onURLChange); // Hash changed. if (!globalThis.isUnitTests) { // Cache "content_scripts/vimium.css" in chrome.storage.session for UI components. (function () { const url = chrome.runtime.getURL("content_scripts/vimium.css"); fetch(url).then(async (response) => { if (response.ok) { chrome.storage.session.set({ vimiumCSSInChromeStorage: await response.text() }); } }); })(); } const muteTab = (tab) => chrome.tabs.update(tab.id, { muted: !tab.mutedInfo.muted }); const toggleMuteTab = (request, sender) => { const currentTab = request.tab; const tabId = request.tabId; const registryEntry = request.registryEntry; if ((registryEntry.options.all != null) || (registryEntry.options.other != null)) { // If there are any audible, unmuted tabs, then we mute them; otherwise we unmute any muted tabs. chrome.tabs.query({ audible: true }, function (tabs) { let tab; if (registryEntry.options.other != null) { tabs = tabs.filter((t) => t.id !== currentTab.id); } const audibleUnmutedTabs = tabs.filter((t) => t.audible && !t.mutedInfo.muted); if (audibleUnmutedTabs.length >= 0) { chrome.tabs.sendMessage(tabId, { frameId: sender.frameId, handler: "showMessage", message: `Muting ${audibleUnmutedTabs.length} tab(s).`, }); for (tab of audibleUnmutedTabs) { muteTab(tab); } } else { chrome.tabs.sendMessage(tabId, { frameId: sender.frameId, handler: "showMessage", message: "Unmuting all muted tabs.", }); for (tab of tabs) { if (tab.mutedInfo.muted) { muteTab(tab); } } } }); } else { if (currentTab.mutedInfo.muted) { chrome.tabs.sendMessage(tabId, { frameId: sender.frameId, handler: "showMessage", message: "Unmuted tab.", }); } else { chrome.tabs.sendMessage(tabId, { frameId: sender.frameId, handler: "showMessage", message: "Muted tab.", }); } muteTab(currentTab); } }; // // Selects the tab with the ID specified in request.id // async function selectSpecificTab(request) { const tab = await chrome.tabs.get(request.id); // Focus the tab's window. TODO(philc): Why are we null-checking chrome.windows here? if (chrome.windows != null) { await chrome.windows.update(tab.windowId, { focused: true }); } await chrome.tabs.update(request.id, { active: true }); } const moveTab = function ({ count, tab, registryEntry }) { if (registryEntry.command === "moveTabLeft") { count = -count; } return chrome.tabs.query({ currentWindow: true }, function (tabs) { const pinnedCount = (tabs.filter((tab) => tab.pinned)).length; const minIndex = tab.pinned ? 0 : pinnedCount; const maxIndex = (tab.pinned ? pinnedCount : tabs.length) - 1; return chrome.tabs.move(tab.id, { index: Math.max(minIndex, Math.min(maxIndex, tab.index + count)), }); }); }; // TODO(philc): Rename to createRepeatCommand. const mkRepeatCommand = (command) => (function (request) { request.count--; if (request.count >= 0) { // TODO(philc): I think we can remove this return statement, and all returns // from commands built using mkRepeatCommand. return 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. const 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(async function (request, callback) { if (request.urls == null) { if (request.url) { // If the request contains a URL, then use it. request.urls = [request.url]; } else { // Otherwise, if we have a registryEntry containing URLs, then use them. // TODO(philc): This would be clearer if we try to detect options (a=b) rather than URLs, // because the syntax for options is well defined ([a-zA-Z]+=\S+). const promises = request.registryEntry.optionList.map((opt) => UrlUtils.isUrl(opt)); const isUrl = await Promise.all(promises); const urlList = request.registryEntry.optionList.filter((_, i) => isUrl[i]); if (urlList.length > 0) { request.urls = urlList; } else { // Otherwise, just create a new tab. let 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. newTabUrl = request.tab.incognito ? Settings.defaultOptions.newTabUrl : chrome.runtime.getURL(newTabUrl); } request.urls = [newTabUrl]; } } } if (request.registryEntry.options.incognito || request.registryEntry.options.window) { // Firefox does not allow an incognito window to be created with the URL about:newtab. It // throws this error: "Illegal URL: about:newtab". const urls = request.urls.filter((u) => u != Settings.defaultOptions.newTabUrl); const windowConfig = { url: urls, incognito: request.registryEntry.options.incognito || false, }; await chrome.windows.create(windowConfig); callback(request); } else { let openNextUrl; const urls = request.urls.slice().reverse(); if (request.position == null) { request.position = request.registryEntry.options.position; } return (openNextUrl = function (request) { if (urls.length > 0) { return TabOperations.openUrlInNewTab( Object.assign(request, { url: urls.pop() }), openNextUrl, ); } else { return callback(request); } })(request); } }), duplicateTab: mkRepeatCommand((request, callback) => { return chrome.tabs.duplicate( request.tabId, (tab) => callback(Object.assign(request, { tab, tabId: tab.id })), ); }), moveTabToNewWindow({ count, tab }) { chrome.tabs.query({ currentWindow: true }, function (tabs) { const activeTabIndex = tab.index; const startTabIndex = Math.max(0, Math.min(activeTabIndex, tabs.length - count)); [tab, ...tabs] = tabs.slice(startTabIndex, startTabIndex + count); chrome.windows.create({ tabId: tab.id, incognito: tab.incognito }, function (window) { chrome.tabs.move(tabs.map((t) => t.id), { windowId: window.id, index: -1 }); }); }); }, nextTab(request) { return selectTab("next", request); }, previousTab(request) { return selectTab("previous", request); }, firstTab(request) { return selectTab("first", request); }, lastTab(request) { return selectTab("last", request); }, removeTab({ count, tab }) { return forCountTabs(count, tab, (tab) => { // In Firefox, Ctrl-W will not close a pinned tab, but on Chrome, it will. We try to be // consistent with each browser's UX for pinned tabs. if (tab.pinned && BgUtils.isFirefox()) return; chrome.tabs.remove(tab.id); }); }, restoreTab: mkRepeatCommand((request, callback) => chrome.sessions.restore(null, callback(request)) ), togglePinTab({ count, tab }) { return forCountTabs(count, tab, (tab) => chrome.tabs.update(tab.id, { pinned: !tab.pinned })); }, toggleMuteTab, moveTabLeft: moveTab, moveTabRight: moveTab, async nextFrame({ count, tabId }) { // We're assuming that these frames are returned in the order that they appear on the page. This // seems to be the case empirically. If it's ever needed, we could also sort by frameId. let frameIds = await getFrameIdsForTab(tabId); const promises = frameIds.map(async (frameId) => { // It is possible that this sendMessage call fails, if a frame gets unloaded while the request // is in flight. let isError = false; const status = await (chrome.tabs.sendMessage(tabId, { handler: "getFocusStatus" }, { frameId: frameId, }).catch((_) => { isError = true; })); return { frameId, status, isError }; }); const frameResponses = (await Promise.all(promises)).filter((r) => !r.isError); const focusedFrameId = frameResponses.find(({ status }) => status.focused)?.frameId; // It's theoretically possible that focusedFrameId is null if the user switched tabs or away // from the browser while the request is in flight. if (focusedFrameId == null) return; // Prune any frames which gave an error response (i.e. they disappeared). frameIds = frameResponses.filter((r) => r.status.focusable).map((r) => r.frameId); const index = frameIds.indexOf(focusedFrameId); count = count ?? 1; const nextIndex = (index + count) % frameIds.length; if (index == nextIndex) return; await chrome.tabs.sendMessage(tabId, { handler: "focusFrame", highlight: true }, { frameId: frameIds[nextIndex], }); }, async closeTabsOnLeft(request) { await removeTabsRelative("before", request); }, async closeTabsOnRight(request) { await removeTabsRelative("after", request); }, async closeOtherTabs(request) { await removeTabsRelative("both", request); }, async visitPreviousTab({ count, tab }) { await BgUtils.tabRecency.init(); let tabIds = BgUtils.tabRecency.getTabsByRecency(); tabIds = tabIds.filter((tabId) => tabId !== tab.id); if (tabIds.length > 0) { const id = tabIds[(count - 1) % tabIds.length]; selectSpecificTab({ id }); } }, reload({ count, tabId, registryEntry, tab: { windowId } }) { const bypassCache = registryEntry.options.hard != null ? registryEntry.options.hard : false; return chrome.tabs.query({ windowId }, function (tabs) { const position = (function () { for (let index = 0; index < tabs.length; index++) { const tab = tabs[index]; if (tab.id === tabId) return index; } })(); tabs = [...tabs.slice(position), ...tabs.slice(0, position)]; count = Math.min(count, tabs.length); for (const tab of tabs.slice(0, count)) { chrome.tabs.reload(tab.id, { bypassCache }); } }); }, }; const forCountTabs = (count, currentTab, callback) => chrome.tabs.query({ currentWindow: true }, function (tabs) { const activeTabIndex = currentTab.index; const startTabIndex = Math.max(0, Math.min(activeTabIndex, tabs.length - count)); for (const tab of tabs.slice(startTabIndex, startTabIndex + count)) { callback(tab); } }); // Remove tabs before, after, or either side of the currently active tab const removeTabsRelative = async (direction, { count, tab }) => { // count is null if the user didn't type a count prefix before issuing this command and didn't // specify a count=n option in their keymapping settings. Interpret this as closing all tabs on // either side. if (count == null) count = 99999; const activeTab = tab; const tabs = await chrome.tabs.query({ currentWindow: true }); const toRemove = tabs.filter((tab) => { if (tab.pinned || tab.id == activeTab.id) { return false; } switch (direction) { case "before": return tab.index < activeTab.index && tab.index >= activeTab.index - count; case "after": return tab.index > activeTab.index && tab.index <= activeTab.index + count; case "both": return true; } }); await chrome.tabs.remove(toRemove.map((t) => t.id)); }; // Selects a tab before or after the currently selected tab. // - direction: "next", "previous", "first" or "last". const selectTab = (direction, { count, tab }) => chrome.tabs.query({ currentWindow: true }, function (tabs) { if (tabs.length > 1) { const toSelect = (() => { switch (direction) { case "next": return (tab.index + count) % tabs.length; case "previous": return ((tab.index - count) + (count * tabs.length)) % tabs.length; case "first": return Math.min(tabs.length - 1, count - 1); case "last": return Math.max(0, tabs.length - count); } })(); chrome.tabs.update(tabs[toSelect].id, { active: true }); } }); chrome.webNavigation.onCommitted.addListener(async ({ tabId, frameId }) => { // Vimium can't run on all tabs (e.g. chrome:// URLs). insertCSS will throw an error on such tabs, // which is expected, and noise. Swallow that error. const swallowError = () => {}; await Settings.onLoaded(); await chrome.scripting.insertCSS({ css: Settings.get("userDefinedLinkHintCss"), target: { tabId: tabId, frameIds: [frameId], }, }).catch(swallowError); }); // Returns all frame IDs for the given tab. Note that in Chrome, this will omit frame IDs for frames // or iFrames which contain chrome-extension:// URLs, even if those pages are listed in Vimium's // web_accessible_resources in manifest.json. async function getFrameIdsForTab(tabId) { // getAllFrames unfortunately excludes frames and iframes from chrome-extension:// URLs. // In Firefox, by contrast, pages with moz-extension:// URLs are included. const frames = await chrome.webNavigation.getAllFrames({ tabId: tabId }); return frames.map((f) => f.frameId); } const HintCoordinator = { // Forward the message in "request" to all frames the in sender's tab. broadcastLinkHintsMessage(request, sender) { chrome.tabs.sendMessage( sender.tab.id, Object.assign(request, { handler: "linkHintsMessage" }), ); }, // This is sent by the content script once the user issues the link hints command. async prepareToActivateLinkHintsMode( tabId, originatingFrameId, { modeIndex, isVimiumHelpDialog, isVimiumOptionsPage }, ) { const frameIds = await getFrameIdsForTab(tabId); // If link hints was triggered on the Options page, or the Vimium help dialog (which is shown // inside an iframe), we cannot directly retrieve those frameIds using the getFrameIdsForTab. // However, as a workaround, if those pages were the pages activating hints, their frameId is // equal to originatingFrameId const isExtensionPage = isVimiumHelpDialog || isVimiumOptionsPage; if (isExtensionPage && !frameIds.includes(originatingFrameId)) { frameIds.push(originatingFrameId); } const timeout = 3000; let promises = frameIds.map(async (frameId) => { let promise = chrome.tabs.sendMessage( tabId, { handler: "linkHintsMessage", messageType: "getHintDescriptors", modeIndex, isVimiumHelpDialog, }, { frameId }, ); promise = Utils.promiseWithTimeout(promise, timeout) .catch((error) => Utils.debugLog("Swallowed getHintDescriptors error:", error)); const descriptors = await promise; return { frameId, descriptors, }; }); const responses = (await Promise.all(promises)) .filter((r) => r.descriptors != null); const frameIdToDescriptors = {}; for (const { frameId, descriptors } of responses) { frameIdToDescriptors[frameId] = descriptors; } promises = responses.map(({ frameId }) => { // Don't send this frame's own link hints back to it -- they're already stored in that frame's // content script. At the time that we wrote this, this resulted in a 150% speedup for link // busy sites like Reddit. const outgoingFrameIdToHintDescriptors = Object.assign({}, frameIdToDescriptors); delete outgoingFrameIdToHintDescriptors[frameId]; return chrome.tabs.sendMessage( tabId, { handler: "linkHintsMessage", messageType: "activateMode", frameId: frameId, originatingFrameId: originatingFrameId, frameIdToHintDescriptors: outgoingFrameIdToHintDescriptors, modeIndex: modeIndex, }, { frameId }, ).catch((error) => { Utils.debugLog( "Swallowed linkHints activateMode error:", error, "tabId", tabId, "frameId", frameId, ); }); }); await Promise.all(promises); }, }; const sendRequestHandlers = { runBackgroundCommand(request, sender) { return BackgroundCommands[request.registryEntry.command](request, sender); }, // 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 }) { return tab.url; }, openUrlInNewTab: mkRepeatCommand((request, callback) => TabOperations.openUrlInNewTab(request, callback) ), openUrlInNewWindow(request) { return TabOperations.openUrlInNewWindow(request); }, async openUrlInIncognito(request) { return chrome.windows.create({ incognito: true, url: await UrlUtils.convertToUrl(request.url), }); }, openUrlInCurrentTab: TabOperations.openUrlInCurrentTab, openOptionsPageInNewTab(request) { return chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: request.tab.index + 1, }); }, domReady(_, sender) { const isTopFrame = sender.frameId == 0; if (!isTopFrame) return; const tabId = sender.tab.id; // The only feature that uses tabLoadedHandlers is marks. if (tabLoadedHandlers[tabId]) tabLoadedHandlers[tabId](); delete tabLoadedHandlers[tabId]; }, nextFrame: BackgroundCommands.nextFrame, selectSpecificTab, createMark: Marks.create.bind(Marks), gotoMark: Marks.goto.bind(Marks), // Send a message to all frames in the current tab. If request.frameId is provided, then send // messages to only the frame with that ID. sendMessageToFrames(request, sender) { const newRequest = Object.assign({}, request.message); const options = request.frameId != null ? { frameId: request.frameId } : {}; chrome.tabs.sendMessage(sender.tab.id, newRequest, options); }, broadcastLinkHintsMessage(request, sender) { HintCoordinator.broadcastLinkHintsMessage(request, sender); }, prepareToActivateLinkHintsMode(request, sender) { HintCoordinator.prepareToActivateLinkHintsMode(sender.tab.id, sender.frameId, request); }, async initializeFrame(request, sender) { // Check whether the extension is enabled for the top frame's URL, rather than the URL of the // specific frame that sent this request. const enabledState = Exclusions.isEnabledForUrl(sender.tab.url); const isTopFrame = sender.frameId == 0; if (isTopFrame) { let whichIcon; if (!enabledState.isEnabledForUrl) { whichIcon = "disabled"; } else if (enabledState.passKeys.length > 0) { whichIcon = "partial"; } else { whichIcon = "enabled"; } const iconSet = { "enabled": { "16": "../icons/action_enabled_16.png", "32": "../icons/action_enabled_32.png", }, "partial": { "16": "../icons/action_partial_16.png", "32": "../icons/action_partial_32.png", }, "disabled": { "16": "../icons/action_disabled_16.png", "32": "../icons/action_disabled_32.png", }, }; chrome.action.setIcon({ path: iconSet[whichIcon], tabId: sender.tab.id }); } const response = Object.assign({ isFirefox: BgUtils.isFirefox(), firefoxVersion: await BgUtils.getFirefoxVersion(), frameId: sender.frameId, }, enabledState); return response; }, async getBrowserInfo() { return { isFirefox: BgUtils.isFirefox(), firefoxVersion: await BgUtils.getFirefoxVersion(), }; }, async reloadVimiumExtension() { // Clear the background page's console log, if its console window is open. console.clear(); browser.runtime.reload(); // Refresh all open tabs, so they get the latest content scripts, and a clear console. const tabs = await chrome.tabs.query({}); for (const tab of tabs) { // Don't refresh the console window for the background page again. We just did that, // effectively. if (tab.url.startsWith("about:debugging")) continue; // Our extension's reload.html page should automatically close when the extension is reloaded, // but if there's an error in manifest.json, it will not, and the extension will enter a // continuous reload loop. Avoid that by not reloading the reload.html page. if (tab.url.endsWith("reload.html")) continue; chrome.tabs.reload(tab.id); } }, async filterCompletions(request) { const completer = completers[request.completerName]; let response = await completer.filter(request); // 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)); return response; }, refreshCompletions(request) { const completer = completers[request.completerName]; completer.refresh(); }, cancelCompletions(request) { const completer = completers[request.completerName]; completer.cancel(); }, }; Utils.addChromeRuntimeOnMessageListener( Object.keys(sendRequestHandlers), async function (request, sender) { Utils.debugLog( "main.js: onMessage:%ourl:%otab:%oframe:%o", request.handler, sender.url.replace(/https?:\/\//, ""), sender.tab?.id, sender.frameId, // request // Often useful for debugging. ); // NOTE(philc): We expect all messages to come from a content script in a tab. I've observed in // Firefox when the extension is first installed, domReady and initializeFrame messages come from // content scripts in about:blank URLs, which have a null sender.tab. I don't know what this // corresponds to. Since we expect a valid sender.tab, ignore those messages. if (sender.tab == null) return; await Settings.onLoaded(); request = Object.assign({ count: 1 }, request, { tab: sender.tab, tabId: sender.tab.id, }); const handler = sendRequestHandlers[request.handler]; const result = handler ? await handler(request, sender) : null; return result; }, ); // 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(function (tabId) { if (tabLoadedHandlers[tabId]) { delete tabLoadedHandlers[tabId]; } chrome.storage.session.get("findModeRawQueryListIncognito", function (items) { if (items.findModeRawQueryListIncognito) { return chrome.windows != null ? chrome.windows.getAll(null, function (windows) { for (const window of windows) { if (window.incognito) return; } // There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set. return chrome.storage.session.remove("findModeRawQueryListIncognito"); }) : undefined; } }); }); // Convenience function for development use. globalThis.runTests = () => open(chrome.runtime.getURL("tests/dom_tests/dom_tests.html")); // // Begin initialization. // // True if the major version of Vimium has changed. // - previousVersion: this will be null for new installs. function majorVersionHasIncreased(previousVersion) { const currentVersion = Utils.getCurrentVersion(); if (previousVersion == null) return false; const currentMajorVersion = currentVersion.split(".").slice(0, 2).join("."); const previousMajorVersion = previousVersion.split(".").slice(0, 2).join("."); return Utils.compareVersions(currentMajorVersion, previousMajorVersion) == 1; } // Show notification on upgrade. const showUpgradeMessageIfNecessary = async function (onInstalledDetails) { const currentVersion = Utils.getCurrentVersion(); // We do not show an upgrade message for patch/silent releases. Such releases have the same // major and minor version numbers. if (!majorVersionHasIncreased(onInstalledDetails.previousVersion)) { return; } // NOTE(philc): These notifications use the system notification UI. So, if you don't have // notifications enabled from your browser (e.g. in Notification Settings in OSX), then // chrome.notification.create will succeed, but you won't see it. const notificationId = "VimiumUpgradeNotification"; await chrome.notifications.create( notificationId, { 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.runtime.lastError) { chrome.notifications.onClicked.addListener(async function (id) { if (id != notificationId) return; const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tab = tabs[0]; return TabOperations.openUrlInNewTab({ tab, tabId: tab.id, url: "https://github.com/philc/vimium/blob/master/CHANGELOG.md", }); }); } }; async function injectContentScriptsAndCSSIntoExistingTabs() { const manifest = chrome.runtime.getManifest(); const contentScriptConfig = manifest.content_scripts[0]; const contentScripts = contentScriptConfig.js; const cssFiles = contentScriptConfig.css; // The scripting.executeScript and scripting.insertCSS APIs can fail if we don't have permissions // to run scripts in a given tab. Examples are: chrome:// URLs, file:// pages (if the user hasn't // granted Vimium access to file URLs), and probably incognito tabs (unconfirmed). Calling these // APIs on such tabs results in an error getting logged on the background page. To avoid this // noise, we swallow the failures. We could instead try to determine if the tab is scriptable by // checking its URL scheme before calling these APIs, but that approach has some nuance to it. // This is simpler. const swallowError = (_) => {}; const tabs = await chrome.tabs.query({ status: "complete" }); for (const tab of tabs) { const target = { tabId: tab.id, allFrames: true }; // Inject all of our content javascripts. chrome.scripting.executeScript({ files: contentScripts, target: target, }).catch(swallowError); // Inject our extension's CSS. chrome.scripting.insertCSS({ files: cssFiles, target: target, }).catch(swallowError); // Inject the user's link hint CSS. chrome.scripting.insertCSS({ css: Settings.get("userDefinedLinkHintCss"), target: target, }).catch(swallowError); } } async function initializeExtension() { await Settings.onLoaded(); await Commands.init(); } // The browser may have tabs already open. We inject the content scripts and Vimium's CSS // immediately so that the extension is running on the pages immediately after install, rather than // having to reload those pages. chrome.runtime.onInstalled.addListener(async (details) => { Utils.debugLog("chrome.runtime.onInstalled"); // NOTE(philc): In my testing, when the onInstalled event occurs, the onStartup event does not // also occur, so we need to initialize Vimium here. await initializeExtension(); const shouldInjectContentScripts = // NOTE(philc): 2023-06-16: we do not install the content scripts in all tabs on Firefox. // I believe this is because Firefox does this already. See https://stackoverflow.com/a/37132144 // for commentary. !BgUtils.isFirefox() && (["chrome_update", "shared_module_update"].includes(details.reason)); if (shouldInjectContentScripts) injectContentScriptsAndCSSIntoExistingTabs(); await showUpgradeMessageIfNecessary(details); }); // Note that this event is not fired when an incognito profile is started. chrome.runtime.onStartup.addListener(async () => { Utils.debugLog("chrome.runtime.onStartup"); await initializeExtension(); }); Object.assign(globalThis, { TabOperations, // Exported for tests: HintCoordinator, BackgroundCommands, majorVersionHasIncreased, }); // The chrome.runtime.onStartup and onInstalled events are not fired when disabling and then // re-enabling the extension in developer mode, so we also initialize the extension here. initializeExtension(); vimium-2.1.2/background_scripts/marks.js000066400000000000000000000134311460337006400203700ustar00rootroot00000000000000import * as TabOperations from "./tab_operations.js"; const Marks = { // This returns the key which is used for storing mark locations in chrome.storage.sync. getLocationKey(markName) { return `vimiumGlobalMark|${markName}`; }, // Get the part of a URL we use for matching here (that is, everything up to the first anchor). getBaseUrl(url) { return 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. async create(req, sender) { const items = await chrome.storage.session.get("vimiumSecret"); const markInfo = { vimiumSecret: items.vimiumSecret, markName: req.markName, url: this.getBaseUrl(sender.tab.url), tabId: sender.tab.id, scrollX: req.scrollX, scrollY: req.scrollY, }; if ((markInfo.scrollX != null) && (markInfo.scrollY != null)) { this.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, { handler: "getScrollPosition" }, (response) => { this.saveMark( Object.assign(markInfo, { scrollX: response.scrollX, scrollY: response.scrollY }), ); }); } }, saveMark(markInfo) { const item = {}; item[this.getLocationKey(markInfo.markName)] = markInfo; chrome.storage.local.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. async goto(req) { const vimiumSecret = (await chrome.storage.session.get("vimiumSecret"))["vimiumSecret"]; const key = this.getLocationKey(req.markName); const items = await chrome.storage.local.get(key); const markInfo = items[key]; if (markInfo.vimiumSecret !== vimiumSecret) { // This is a different Vimium instantiation, so markInfo.tabId is definitely out of date. Utils.debugLog("marks: vimiumSecret is incorrect."); await this.focusOrLaunch(markInfo, req); } else { // Check whether markInfo.tabId still exists. According to // https://developer.chrome.com/extensions/tabs, tab Ids are unqiue within a Chrome // session. So, if we find a match, we can use it. let tab; // This will throw an error if the tab doesn't exist. try { tab = await chrome.tabs.get(markInfo.tabId); } catch { // Swallow. } const originalTabStillExists = tab?.url && (markInfo.url === this.getBaseUrl(tab.url)); if (originalTabStillExists) { await this.gotoPositionInTab(markInfo); } else { await this.focusOrLaunch(markInfo, req); } } }, // Focus an existing tab and scroll to the given position within it. async gotoPositionInTab({ tabId, scrollX, scrollY }) { const tab = await chrome.tabs.update(tabId, { active: true }); chrome.windows.update(tab.windowId, { focused: true }); chrome.tabs.sendMessage(tabId, { handler: "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. async 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). const markIsScrolled = markInfo.scrollX > 0 || markInfo.scrollY > 0; const query = markIsScrolled ? markInfo.url : `${markInfo.url}*`; const tabs = await chrome.tabs.query({ url: query }); if (tabs.length > 0) { // There is at least one matching tab. Pick one and go to it. const tab = await this.pickTab(tabs); this.gotoPositionInTab(Object.assign(markInfo, { tabId: tab.id })); } else { // There is no existing matching tab. We'll have to create one. TabOperations.openUrlInNewTab( Object.assign(req, { url: this.getBaseUrl(markInfo.url) }), (tab) => { // Note. tabLoadedHandlers is defined in "main.js". The handler below will be called // when the tab is loaded, its DOM is ready and it registers with the background page. return tabLoadedHandlers[tab.id] = () => this.gotoPositionInTab(Object.assign(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. async pickTab(tabs) { // NOTE(philc): We assume getCurrent() can return null, but I didn't confirm this. Also, it // should be impossible for the user to invoke Vimium-related keys if all windows are closed. const window = await chrome.windows.getCurrent(); const windowId = window?.id; // Prefer tabs in the current window, if there are any. const tabsInWindow = tabs.filter((tab) => tab.windowId === windowId); if (tabsInWindow.length > 0) tabs = tabsInWindow; // 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). if (tabs.length > 1) { tabs = tabs.filter((t) => !t.active); } // Prefer shorter URLs. tabs.sort((a, b) => a.url.length - b.url.length); return tabs[0]; }, }; globalThis.Marks = Marks; vimium-2.1.2/background_scripts/reload.js000066400000000000000000000005231460337006400205170ustar00rootroot00000000000000// Used as part of a debugging workflow when developing the extension. (async () => { await chrome.runtime.sendMessage({ handler: "reloadVimiumExtension" }); // NOTE(philc): This page's window is supposed to automatically close when the extension reloads // itself, but I've noticed sometimes this fails. globalThis.close(); })(); vimium-2.1.2/background_scripts/tab_operations.js000066400000000000000000000070711460337006400222670ustar00rootroot00000000000000// // Methods for opening URLs in tabs. // // TODO(philc): Convert these to Promise-based APIs. const chromeNewTabUrl = "about:newtab"; // Opens the url in the current tab. // If the URL is a JavaScript snippet, execute that snippet in the current tab. async function openUrlInCurrentTab(request) { // Note that when injecting JavaScript, it's subject to the site's CSP. Sites with strict CSPs // (like github.com, developer.mozilla.org) will raise an error when we try to run this code. See // https://github.com/philc/vimium/issues/4331. if (UrlUtils.hasJavascriptPrefix(request.url)) { const scriptingArgs = { target: { tabId: request.tabId }, func: (text) => { const prefix = "javascript:"; text = text.slice(prefix.length).trim(); text = decodeURIComponent(text); try { text = decodeURIComponent(text); } catch { // Swallow } const el = document.createElement("script"); el.textContent = text; document.head.appendChild(el); }, args: [request.url], }; if (!BgUtils.isFirefox()) { // The MAIN world -- where the webpage runs -- is less privileged than the ISOLATED world. // Specifying a world is required for Chrome, but not Firefox. // As of Firefox 118, specifying "MAIN" as the world is not yet supported. scriptingArgs.world = "MAIN"; } chrome.scripting.executeScript(scriptingArgs); } else { chrome.tabs.update(request.tabId, { url: await UrlUtils.convertToUrl(request.url) }); } } // Opens request.url in new tab and switches to it. async function openUrlInNewTab(request, callback) { if (callback == null) { callback = function () {}; } const tabConfig = { url: await UrlUtils.convertToUrl(request.url), active: true, windowId: request.tab.windowId, }; const position = request.position; let tabIndex = null; switch (position) { case "start": tabIndex = 0; break; case "before": tabIndex = request.tab.index; break; // 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 case "end": tabIndex = BgUtils.isFirefox() ? 9999 : null; break; // "after" is the default case when there are no options. default: tabIndex = request.tab.index + 1; } tabConfig.index = tabIndex; if (request.active != null) { tabConfig.active = request.active; } // Firefox does not support "about:newtab" in chrome.tabs.create. if (tabConfig["url"] === chromeNewTabUrl) { delete tabConfig["url"]; } tabConfig.openerTabId = request.tab.id; // clean position and active, so following `openUrlInNewTab(request)` will create a tab just next // to this new tab return chrome.tabs.create( tabConfig, (tab) => callback(Object.assign(request, { tab, tabId: tab.id, position: "", active: false })), ); } // Opens request.url in new window and switches to it. async function openUrlInNewWindow(request, callback) { if (callback == null) { callback = function () {}; } const winConfig = { url: await UrlUtils.convertToUrl(request.url), active: true, }; if (request.active != null) { winConfig.active = request.active; } // Firefox does not support "about:newtab" in chrome.tabs.create. if (tabConfig["url"] === chromeNewTabUrl) { delete winConfig["url"]; } return chrome.windows.create(winConfig, callback); } export { openUrlInCurrentTab, openUrlInNewTab, openUrlInNewWindow }; vimium-2.1.2/background_scripts/tab_recency.js000066400000000000000000000105471460337006400215360ustar00rootroot00000000000000// TabRecency associates an integer with each tab id representing how recently it has been accessed. // The order of tabs as tracked by TabRecency is used to provide a recency-based ordering in the // tabs vomnibar. // // The values are persisted to chrome.storage.session so that they're not lost when the extension's // background page is unloaded. // // Callers must await TabRecency.init before calling recencyScore or getTabsByRecency. // // In theory, the browser's tab.lastAccessed timestamp field should allow us to sort tabs by // recency, but in practice it does not work across several edge cases. See the comments on #4368. class TabRecency { constructor() { this.counter = 1; this.tabIdToCounter = {}; this.loaded = false; this.queuedActions = []; } // Add listeners to chrome.tabs, and load the index from session storage. async init() { if (this.initPromise) { await this.initPromise; return; } let resolveFn; this.initPromise = new Promise((resolve, _reject) => { resolveFn = resolve; }); chrome.tabs.onActivated.addListener((activeInfo) => { this.queueAction("register", activeInfo.tabId); }); chrome.tabs.onRemoved.addListener((tabId) => { this.queueAction("deregister", tabId); }); chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => { this.queueAction("deregister", removedTabId); this.queueAction("register", addedTabId); }); chrome.windows.onFocusChanged.addListener(async (windowId) => { if (windowId == chrome.windows.WINDOW_ID_NONE) return; const tabs = await chrome.tabs.query({ windowId, active: true }); if (tabs[0]) { this.queueAction("register", tabs[0].id); } }); await this.loadFromStorage(); while (this.queuedActions.length > 0) { const [action, tabId] = this.queuedActions.shift(); this.handleAction(action, tabId); } this.loaded = true; resolveFn(); } // Loads the index from session storage. async loadFromStorage() { const tabsPromise = chrome.tabs.query({}); const storagePromise = chrome.storage.session.get("tabRecency"); const [tabs, storage] = await Promise.all([tabsPromise, storagePromise]); if (storage.tabRecency == null) return; let maxCounter = 0; for (const counter of Object.values(storage.tabRecency)) { if (maxCounter < counter) maxCounter = counter; } if (this.counter < maxCounter) { this.counter = maxCounter; } this.tabIdToCounter = Object.assign({}, storage.tabRecency); // Remove any tab IDs which aren't currently loaded. const tabIds = new Set(tabs.map((t) => t.id)); for (const id in this.tabIdToCounter) { if (!tabIds.has(parseInt(id))) { delete this.tabIdToCounter[id]; } } } async saveToStorage() { await chrome.storage.session.set({ tabRecency: this.tabIdToCounter }); } // - action: "register" or "unregister". queueAction(action, tabId) { if (!this.loaded) { this.queuedActions.push([action, tabId]); } else { this.handleAction(action, tabId); } } // - action: "register" or "unregister". handleAction(action, tabId) { if (action == "register") { this.register(tabId); } else if (action == "deregister") { this.deregister(tabId); } else { throw new Error(`Unexpected action type: ${action}`); } } register(tabId) { this.counter++; this.tabIdToCounter[tabId] = this.counter; this.saveToStorage(); } deregister(tabId) { delete this.tabIdToCounter[tabId]; this.saveToStorage(); } // Recently-visited tabs get a higher score (except the current tab, which gets a low score). recencyScore(tabId) { if (!this.loaded) throw new Error("TabRecency hasn't yet been loaded."); const tabCounter = this.tabIdToCounter[tabId]; const isCurrentTab = tabCounter == this.counter; if (isCurrentTab) return 0; return (tabCounter ?? 1) / this.counter; // tabCounter may be null. } // Returns a list of tab Ids sorted by recency, most recent tab first. getTabsByRecency() { if (!this.loaded) throw new Error("TabRecency hasn't yet been loaded."); const ids = Object.keys(this.tabIdToCounter); ids.sort((a, b) => this.tabIdToCounter[b] - this.tabIdToCounter[a]); return ids.map((id) => parseInt(id)); } } Object.assign(globalThis, { TabRecency }); vimium-2.1.2/content_scripts/000077500000000000000000000000001460337006400162465ustar00rootroot00000000000000vimium-2.1.2/content_scripts/file_urls.css000066400000000000000000000005271460337006400207500ustar00rootroot00000000000000/* Chrome file:// URLs set draggable=true for links to files (CSS selector .icon.file). This * automatically sets -webkit-user-select: none, which disables selecting the file names and so * prevents Vimium's search from working as expected. Here, we reset the value back to default. */ .icon.file { -webkit-user-select: auto !important; } vimium-2.1.2/content_scripts/hud.js000066400000000000000000000211511460337006400173640ustar00rootroot00000000000000// // A heads-up-display (HUD) for showing Vimium page operations. // Note: you cannot interact with the HUD until document.body is available. // const HUD = { tween: null, hudUI: null, findMode: null, abandon() { if (this.hudUI) { this.hudUI.hide(false); } }, // Set by @pasteFromClipboard to handle the value returned by pasteResponse pasteListener: null, // 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. async init(focusable) { await Settings.onLoaded(); if (focusable == null) { focusable = true; } if (this.hudUI == null) { const queryString = window.vimiumDomTestsAreRunning ? "?dom_tests=true" : ""; this.hudUI = new UIComponent(`pages/hud.html${queryString}`, "vimiumHUDFrame", ({ data }) => { if (this[data.name]) { return this[data.name](data); } }); // Allow to access to the clipboard through iframes. // This is only valid/necessary for Chrome. Firefox will show this console warning: // 'Feature Policy: Skipping unsupported feature name "clipboard-read"' if (!Utils.isFirefox()) { this.hudUI.iframeElement.allow = "clipboard-read; clipboard-write"; } } // this[data.name]? data if (this.tween == null) { this.tween = new Tween( "iframe.vimiumHUDFrame.vimiumUIComponentVisible", this.hudUI.shadowDOM, ); } if (focusable) { this.hudUI.toggleIframeElementClasses("vimiumNonClickable", "vimiumClickable"); // Note(gdh1995): Chrome 74 only acknowledges text selection when a frame has been visible. // See more in #3277. // Note(mrmr1993): Show the HUD frame, so Firefox will actually perform the paste. this.hudUI.toggleIframeElementClasses("vimiumUIComponentHidden", "vimiumUIComponentVisible"); // Force the re-computation of styles, so Chrome sends a visibility change message to the // child frame. See https://github.com/philc/vimium/pull/3277#issuecomment-487363284 getComputedStyle(this.hudUI.iframeElement).display; } else { this.hudUI.toggleIframeElementClasses("vimiumClickable", "vimiumNonClickable"); } }, // duration - if omitted, the message will show until dismissed. show(text, duration) { DomUtils.documentComplete(async () => { clearTimeout(this._showForDurationTimerId); // @hudUI.activate will take charge of making it visible await this.init(false); this.hudUI.activate({ name: "show", text }); this.tween.fade(1.0, 150); if (duration != null) { this._showForDurationTimerId = setTimeout(() => this.hide(), duration); } }); }, showFindMode(findMode = null) { this.findMode = findMode; DomUtils.documentComplete(async () => { await this.init(); this.hudUI.activate({ name: "showFindMode" }); this.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. this.findMode.findInPlace(data.query, { "postFindFocus": this.hudUI.iframeElement.contentWindow, }); // Show the number of matches in the HUD UI. const matchCount = FindMode.query.parsedQuery.length > 0 ? FindMode.query.matchCount : 0; const showMatchText = FindMode.query.rawQuery.length > 0; this.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, updateIndicator) { if (immediate == null) { immediate = false; } if (updateIndicator == null) { updateIndicator = true; } if ((this.hudUI != null) && (this.tween != null)) { clearTimeout(this._showForDurationTimerId); this.tween.stop(); if (immediate) { if (updateIndicator) { Mode.setIndicator(); } else { this.hudUI.hide(); } } else { this.tween.fade(0, 150, () => this.hide(true, updateIndicator)); } } }, // These parameters describe the reason find mode is exiting, and come from the HUD UI component. hideFindMode({ exitEventIsEnter, exitEventIsEscape }) { let postExit; this.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(); const focusNode = DomUtils.getSelectionFocusElement(); if (document.activeElement != null) { document.activeElement.blur(); } if (focusNode && focusNode.focus) { focusNode.focus(); } if (exitEventIsEnter) { FindMode.handleEnter(); if (FindMode.query.hasResults) { postExit = () => newPostFindMode(); } } 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; } this.findMode.exit(); if (postExit) { 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(async () => { await this.init(); this.hudUI.postMessage({ name: "copyToClipboard", data: text }); }); }, pasteFromClipboard(pasteListener) { this.pasteListener = pasteListener; DomUtils.documentComplete(async () => { await this.init(); this.tween.fade(0, 0); this.hudUI.postMessage({ name: "pasteFromClipboard" }); }); }, pasteResponse({ data }) { // Hide the HUD frame again. this.hudUI.toggleIframeElementClasses("vimiumUIComponentVisible", "vimiumUIComponentHidden"); this.unfocusIfFocused(); this.pasteListener(data); }, unfocusIfFocused() { // On Firefox, if an vimium-2.1.2/test_harnesses/image_map.png000066400000000000000000000016501460337006400205060ustar00rootroot00000000000000PNG  IHDR,2* pHYs  sRGBgAMA a=IDATx?Hqp 34 <EbPX6KKCh^x % \t o\y/8Nf<՚̞b w/*GjP9 Xd,2 @ `A Xd,2 @ `QkOՄ̫-Y]C% ]~sr L>XwoDy6 UɄ }!)VxTx FS` kc}xIﰒQёzVPnW.mkn&:6&,f+gC Gz`?Xmla)>boO)ayi)ڂ߽ɏe{^ SV:$vQMXa1WxÏ~i4j~Oq;#KC\q@ujH\kp~!~!H*X*H}hٴU'^4RT33T%CZM* @ `A Xd,2 @ `A Xd,2 E*uuIENDB`vimium-2.1.2/test_harnesses/page_with_links.html000066400000000000000000000030511460337006400221130ustar00rootroot00000000000000 Page with many links This will be a link spanning two
lines




This link has a lot of vertical padding







This link has a lot of vertical padding on the top

div with an onclick attribute


An anchor with just a name

Next and previous links.

Below is an image map:
Section A Section B vimium-2.1.2/test_harnesses/visibility_test.html000066400000000000000000000134401460337006400221750ustar00rootroot00000000000000 Visibility test
Node/Test
test
test
test
test
test
test
test
vimium-2.1.2/test_harnesses/vomnibar.html000066400000000000000000000056331460337006400205710ustar00rootroot00000000000000 Vomnibar harness Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. vimium-2.1.2/tests/000077500000000000000000000000001460337006400141675ustar00rootroot00000000000000vimium-2.1.2/tests/dom_tests/000077500000000000000000000000001460337006400161705ustar00rootroot00000000000000vimium-2.1.2/tests/dom_tests/dom_test_setup.js000066400000000000000000000005731460337006400215710ustar00rootroot00000000000000window.vimiumDomTestsAreRunning = true; import * as shoulda from "../vendor/shoulda.js"; // Attach shoulda's functions -- like setup, context, should -- to the global namespace. Object.assign(window, shoulda); window.shoulda = shoulda; document.addEventListener("DOMContentLoaded", async () => { isEnabledForUrl = true; await Settings.onLoaded(); await HUD.init(); }); vimium-2.1.2/tests/dom_tests/dom_tests.html000066400000000000000000000035401460337006400210610ustar00rootroot00000000000000

Vimium Tests

vimium-2.1.2/tests/dom_tests/dom_tests.js000066400000000000000000001220541460337006400205330ustar00rootroot00000000000000let commandCount = null; let commandName = null; // Some tests have side effects on the handler stack and the active mode, so these are reset on // setup. Also, some tests affect the focus (e.g. Vomnibar tests), so we make sure the window has // the focus. const initializeModeState = () => { window.focus(); Mode.reset(); handlerStack.reset(); const normalMode = installModes(); normalMode.setPassKeys("p"); normalMode.setKeyMapping({ m: { options: {}, command: "m" }, // A mapped key. p: { options: {}, command: "p" }, // A pass key. z: { p: { options: {}, command: "zp" } }, // Not a pass key. }); normalMode.setCommandHandler(({ command, count }) => { [commandName, commandCount] = [command.command, count]; }); commandName = null; commandCount = null; return normalMode; }; // // Retrieve the hint markers as an array object. // const getHintMarkerEls = () => Array.from(document.querySelectorAll(".vimiumHintMarker")); const stubSettings = (key, value) => stub(Settings._settings, key, value); HintCoordinator.sendMessage = (name, request) => { if (request == null) { request = {}; } if (HintCoordinator[name]) { HintCoordinator[name](request); } return request; }; const activateLinkHintsMode = () => { HintCoordinator.getHintDescriptors({ modeIndex: 0 }, {}, () => {}); HintCoordinator.activateMode({ frameIdToHintDescriptors: {}, modeIndex: 0, originatingFrameId: frameId, }); return HintCoordinator.linkHintsMode; }; // // Generate tests that are common to both default and filtered // link hinting modes. // const createGeneralHintTests = (isFilteredMode) => { window.vimiumOnClickAttributeName = "does-not-matter"; context("Link hints", () => { setup(() => { initializeModeState(); const testContent = "testtress"; document.getElementById("test-div").innerHTML = testContent; stubSettings("filterLinkHints", isFilteredMode); stubSettings("linkHintCharacters", "ab"); stubSettings("linkHintNumbers", "12"); stub(window, "windowIsFocused", () => true); }); teardown(() => document.getElementById("test-div").innerHTML = ""); should("create hints when activated, discard them when deactivated", () => { const mode = activateLinkHintsMode(); assert.isFalse(mode.hintMarkerContainingDiv == null); mode.deactivateMode(); assert.isTrue(mode.hintMarkerContainingDiv == null); }); should("position items correctly", () => { const assertStartPosition = (element1, element2) => { assert.equal(element1.getClientRects()[0].left, element2.getClientRects()[0].left); assert.equal(element1.getClientRects()[0].top, element2.getClientRects()[0].top); }; stub(document.body.style, "position", "static"); let mode = activateLinkHintsMode(); let markerEls = getHintMarkerEls(); assertStartPosition(document.getElementsByTagName("a")[0], markerEls[0]); assertStartPosition(document.getElementsByTagName("a")[1], markerEls[1]); mode.deactivateMode(); stub(document.body.style, "position", "relative"); mode = activateLinkHintsMode(); markerEls = getHintMarkerEls(); assertStartPosition(document.getElementsByTagName("a")[0], markerEls[0]); assertStartPosition(document.getElementsByTagName("a")[1], markerEls[1]); mode.deactivateMode(); }); }); }; createGeneralHintTests(false); createGeneralHintTests(true); context("False positives in link-hint", () => { setup(() => { const testContent = 'false positiveclickable' + 'clickable'; document.getElementById("test-div").innerHTML = testContent; stubSettings("filterLinkHints", true); stubSettings("linkHintNumbers", "12"); stub(window, "windowIsFocused", () => true); }); teardown(() => document.getElementById("test-div").innerHTML = ""); should("handle false positives", () => { const mode = activateLinkHintsMode(); mode.deactivateMode(); assert.equal(["clickable", "clickable"], mode.hintMarkers.map((m) => m.linkText)); }); }); context("jsaction matching", () => { let element; setup(() => { stubSettings("filterLinkHints", true); const testContent = '

clickable

'; document.getElementById("test-div").innerHTML = testContent; element = document.getElementById("test-paragraph"); }); teardown(() => document.getElementById("test-div").innerHTML = ""); should("select jsaction elements", () => { for (const text of ["click:namespace.actionName", "namespace.actionName"]) { element.setAttribute("jsaction", text); const mode = activateLinkHintsMode(); mode.deactivateMode(); assert.equal(1, mode.hintMarkers.length); assert.equal("clickable", mode.hintMarkers[0].linkText); assert.equal(element, mode.hintMarkers[0].localHint.element); } }); should("not select inactive jsaction elements", () => { const attributes = [ "mousedown:namespace.actionName", "click:namespace._", "none", "namespace:_", ]; for (const attribute of attributes) { element.setAttribute("jsaction", attribute); const linkHints = activateLinkHintsMode(); const hintMarkers = getHintMarkerEls().filter((marker) => marker.linkText !== "Frame."); linkHints.deactivateMode(); assert.equal(0, hintMarkers.length); } }); }); context("link hints for image maps", () => { setup(() => { const testContent = '' + '' + '' + '' + ""; document.getElementById("test-div").innerHTML = testContent; }); teardown(() => document.getElementById("test-div").innerHTML = ""); should("generate a hint for each area in the image map", () => { const mode = activateLinkHintsMode(); const markerEls = getHintMarkerEls(); assert.equal(2, markerEls.length); mode.deactivateMode(); }); }); const sendKeyboardEvent = (key, type, extra) => { if (type == null) type = "keydown"; if (extra == null) extra = {}; handlerStack.bubbleEvent( type, Object.assign(extra, { type, key, preventDefault() {}, stopImmediatePropagation() {}, }), ); }; const sendKeyboardEvents = (keys) => { for (const key of keys.split("")) { sendKeyboardEvent(key); } }; // TODO(philc): For some reason, this test corrupts the state linkhints state for other tests, in particular, // the alphabet hints tests. I haven't yet dug into why. // const inputs = []; // context("Test link hints for focusing input elements correctly", () => { // let linkHintsMode; // setup(() => { // let input; // initializeModeState(); // const testDiv = document.getElementById("test-div"); // testDiv.innerHTML = ""; // stubSettings("filterLinkHints", false); // stubSettings("linkHintCharacters", "ab"); // // Every HTML5 input type except for hidden. We should be able to activate all of them with link hints. // // NOTE(philc): I'm not sure why, but "image" doesn't get a link hint in Puppeteer, so I've omitted it. // const inputTypes = ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file", // "month", "number", "password", "radio", "range", "reset", "search", "submit", "tel", "text", // "time", "url", "week"]; // for (let type of inputTypes) { // input = document.createElement("input"); // input.type = type; // testDiv.appendChild(input); // inputs.push(input); // } // // Manually add also a select element to test focus. // input = document.createElement("select"); // testDiv.appendChild(input); // inputs.push(input); // }); // teardown(() => { // document.getElementById("test-div").innerHTML = ""; // // linkHintsMode.deactivateMode(); // TODO(philc): I don't think this should be necessary. // }); // should("Focus each input when its hint text is typed", () => { // for (var input of inputs) { // input.scrollIntoView(); // Ensure the element is visible so we create a link hint for it. // const activeListener = ensureCalled(function(event) { // if (event.type === "focus") { return input.blur(); } // }); // input.addEventListener("focus", activeListener, false); // input.addEventListener("click", activeListener, false); // linkHintsMode = activateLinkHintsMode(); // const [hint] = getHintMarkerEls(). // filter(hint => input === HintCoordinator.getLocalHint(hint.hintDescriptor).element); // for (let char of hint.hintString) // sendKeyboardEvent(char); // linkHintsMode.deactivateMode(); // input.removeEventListener("focus", activeListener, false); // input.removeEventListener("click", activeListener, false); // } // }); // }); context("Test link hints for changing mode", () => { let linkHints; setup(() => { initializeModeState(); const testDiv = document.getElementById("test-div"); testDiv.innerHTML = "link"; linkHints = activateLinkHintsMode(); }); teardown(() => { document.getElementById("test-div").innerHTML = ""; linkHints.deactivateMode(); }); should("change mode on shift", () => { assert.equal("curr-tab", linkHints.mode.name); sendKeyboardEvent("Shift", "keydown"); assert.equal("bg-tab", linkHints.mode.name); sendKeyboardEvent("Shift", "keyup"); assert.equal("curr-tab", linkHints.mode.name); }); should("change mode on ctrl", () => { assert.equal("curr-tab", linkHints.mode.name); sendKeyboardEvent("Control", "keydown"); assert.equal("fg-tab", linkHints.mode.name); sendKeyboardEvent("Control", "keyup"); assert.equal("curr-tab", linkHints.mode.name); }); }); const createLinks = function (n) { for (let i = 0, end = n; i < end; i++) { const link = document.createElement("a"); link.textContent = "test"; document.getElementById("test-div").appendChild(link); } }; context("Alphabetical link hints", () => { let mode; setup(() => { initializeModeState(); stubSettings("filterLinkHints", false); stubSettings("linkHintCharacters", "ab"); stub(window, "windowIsFocused", () => true); document.getElementById("test-div").innerHTML = ""; // Three hints will trigger double hint chars. createLinks(3); mode = activateLinkHintsMode(); }); teardown(() => { mode.deactivateMode(); document.getElementById("test-div").innerHTML = ""; }); should("label the hints correctly", () => { assert.equal( ["aa", "b", "ab"], mode.hintMarkers.map((m) => m.hintString), ); }); should("narrow the hints", () => { sendKeyboardEvent("a"); assert.equal( ["", "none", ""], mode.hintMarkers.map((m) => m.element.style.display), ); }); should("generate the correct number of alphabet hints", () => { const alphabetHints = new AlphabetHints(); for (const n of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { const hintStrings = alphabetHints.hintStrings(n); assert.equal(n, hintStrings.length); } }); should("generate non-overlapping alphabet hints", () => { const alphabetHints = new AlphabetHints(); for (const n of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { const hintStrings = alphabetHints.hintStrings(n); for (const h1 of hintStrings) { for (const h2 of hintStrings) { if (h1 !== h2) { assert.isFalse(0 === h1.indexOf(h2)); } } } } }); }); context("Filtered link hints", () => { // Note. In all of these tests, the order of the elements returned by getHintMarkerEls() may be // different from the order they are listed in the test HTML content. This is because // LinkHints.activateMode() sorts the elements. let mode; setup(() => { stubSettings("filterLinkHints", true); stubSettings("linkHintNumbers", "0123456789"); stub(window, "windowIsFocused", () => true); }); context("Text hints", () => { setup(() => { initializeModeState(); const testContent = "testtresstraittrackalt text"; document.getElementById("test-div").innerHTML = testContent; mode = activateLinkHintsMode(); }); teardown(() => { document.getElementById("test-div").innerHTML = ""; mode.deactivateMode(); }); should("label the hints", () => { const hintMarkers = getHintMarkerEls(); const expectedMarkers = [1, 2, 3, 4].map((m) => m.toString()); const actualMarkers = [0, 1, 2, 3].map((i) => hintMarkers[i].textContent.toLowerCase()); assert.equal(expectedMarkers.length, actualMarkers.length); for (const marker of expectedMarkers) { assert.isTrue(actualMarkers.includes(marker)); } }); should("narrow the hints", () => { sendKeyboardEvent("t"); sendKeyboardEvent("r"); assert.equal( ["none", "", "", ""], mode.hintMarkers.map((m) => m.element.style.display), ); assert.equal("3", mode.hintMarkers[1].hintString); sendKeyboardEvent("a"); assert.equal("1", mode.hintMarkers[3].hintString); }); // This test is the same as above, but with an extra non-matching character. The effect should // be the same. should("narrow the hints and ignore typing mistakes", () => { sendKeyboardEvent("t"); sendKeyboardEvent("r"); sendKeyboardEvent("x"); assert.equal( ["none", "", "", ""], mode.hintMarkers.map((m) => m.element.style.display), ); assert.equal("3", mode.hintMarkers[1].hintString); sendKeyboardEvent("a"); assert.equal("1", mode.hintMarkers[3].hintString); }); }); context("Image hints", () => { setup(() => { initializeModeState(); const testContent = "alt text" + "alt text" + "" + ""; document.getElementById("test-div").innerHTML = testContent; mode = activateLinkHintsMode(); }); teardown(() => { document.getElementById("test-div").innerHTML = ""; mode.deactivateMode(); }); should("label the images", () => { let hintMarkers = getHintMarkerEls().map((m) => m.textContent.toLowerCase()); // We don't know the actual hint numbers which will be assigned, so we replace them with "N". hintMarkers = hintMarkers.map((str) => str.replace(/^[1-4]/, "N")); assert.equal(4, hintMarkers.length); assert.isTrue(hintMarkers.includes("N: alt text")); assert.isTrue(hintMarkers.includes("N: some title")); assert.isTrue(hintMarkers.includes("N: alt text")); assert.isTrue(hintMarkers.includes("N")); }); }); context("Input hints", () => { setup(() => { initializeModeState(); const testContent = ` \ \ \ `; document.getElementById("test-div").innerHTML = testContent; mode = activateLinkHintsMode(); }); teardown(() => { document.getElementById("test-div").innerHTML = ""; mode.deactivateMode(); }); should("label the input elements", () => { let hintMarkers = getHintMarkerEls(); hintMarkers = getHintMarkerEls().map((m) => m.textContent.toLowerCase()); // We don't know the actual hint numbers which will be assigned, so we replace them with "N". hintMarkers = hintMarkers.map((str) => str.replace(/^[0-9]+/, "N")); assert.equal(5, hintMarkers.length); assert.isTrue(hintMarkers.includes("N")); assert.isTrue(hintMarkers.includes("N")); assert.isTrue(hintMarkers.includes("N: a label")); assert.isTrue(hintMarkers.includes("N: a label")); assert.isTrue(hintMarkers.includes("N")); }); }); context("Text hint scoring", () => { let getActiveHintMarker; setup(() => { initializeModeState(); const testContent = [ { id: 0, text: "the xboy stood on the xburning deck" }, // Noise. { id: 1, text: "the boy stood on the xburning deck" }, // Whole word (boy). { id: 2, text: "on the xboy stood the xburning deck" }, // Start of text (on). { id: 3, text: "the xboy stood on the xburning deck" }, // Noise. { id: 4, text: "the xboy stood on the xburning deck" }, // Noise. { id: 5, text: "the xboy stood on the xburning" }, // Shortest text.. { id: 6, text: "the xboy stood on the burning xdeck" }, // Start of word (bu) { id: 7, text: "test abc one - longer" }, // For tab test - 2. { id: 8, text: "test abc one" }, // For tab test - 1. { id: 9, text: "test abc one - longer still" }, // For tab test - 3. ].map(({ id, text }) => `${text}`).join(" "); document.getElementById("test-div").innerHTML = testContent; mode = activateLinkHintsMode(); getActiveHintMarker = () => { return HintCoordinator.getLocalHint( mode.markerMatcher.activeHintMarker.hintDescriptor, ).element.id; }; }); teardown(() => { document.getElementById("test-div").innerHTML = ""; mode.deactivateMode(); }); should("score start-of-word matches highly", () => { sendKeyboardEvents("bu"); assert.equal("6", getActiveHintMarker()); }); should("score start-of-text matches highly (br)", () => { sendKeyboardEvents("on"); assert.equal("2", getActiveHintMarker()); }); should("score whole-word matches highly", () => { sendKeyboardEvents("boy"); assert.equal("1", getActiveHintMarker()); }); should("score shorter texts more highly", () => { sendKeyboardEvents("stood"); assert.equal("5", getActiveHintMarker()); }); should("use tab to select the active hint", () => { sendKeyboardEvents("abc"); assert.equal("8", getActiveHintMarker()); sendKeyboardEvent("Tab", "keydown"); assert.equal("7", getActiveHintMarker()); sendKeyboardEvent("Tab", "keydown"); assert.equal("9", getActiveHintMarker()); }); }); }); context("Input focus", () => { setup(() => { initializeModeState(); const testContent = ` \ `; document.getElementById("test-div").innerHTML = testContent; }); teardown(() => document.getElementById("test-div").innerHTML = ""), should("focus the first element", () => { NormalModeCommands.focusInput(1); assert.equal("first", document.activeElement.id); }); should("focus the nth element", () => { NormalModeCommands.focusInput(100); assert.equal("third", document.activeElement.id); }); should("activate insert mode on the first element", () => { NormalModeCommands.focusInput(1); assert.isTrue(InsertMode.permanentInstance.isActive()); }); should("activate insert mode on the first element", () => { NormalModeCommands.focusInput(100); assert.isTrue(InsertMode.permanentInstance.isActive()); }); should("activate the most recently-selected input if the count is 1", () => { NormalModeCommands.focusInput(3); NormalModeCommands.focusInput(1); assert.equal("third", document.activeElement.id); }); should("not trigger insert if there are no inputs", () => { document.getElementById("test-div").innerHTML = ""; NormalModeCommands.focusInput(1); assert.isFalse(InsertMode.permanentInstance.isActive()); }); }); // TODO: these find prev/next link tests could be refactored into unit tests which invoke a function // which has a tighter contract than goNext(), since they test minor aspects of goNext()'s link // matching behavior, and we don't need to construct external state many times over just to test // that. i.e. these tests should look something like: // assert.equal(findLink(html("))[0].href, "first") // These could then move outside of the dom_tests file. context("Find prev / next links", () => { setup(() => { initializeModeState(); window.location.hash = ""; }); should("find exact matches", () => { document.getElementById("test-div").innerHTML = `\ nextcorrupted next page\ `; stubSettings("nextPatterns", "next"); NormalModeCommands.goNext(); assert.equal("#second", window.location.hash); }); should("match against non-word patterns", () => { document.getElementById("test-div").innerHTML = `\ >>\ `; stubSettings("nextPatterns", ">>"); NormalModeCommands.goNext(); assert.equal("#first", window.location.hash); }); should("favor matches with fewer words", () => { document.getElementById("test-div").innerHTML = `\ lorem ipsum next next!\ `; stubSettings("nextPatterns", "next"); NormalModeCommands.goNext(); assert.equal("#second", window.location.hash); }); should("find link relation in header", () => { document.getElementById("test-div").innerHTML = `\ \ `; NormalModeCommands.goNext(); assert.equal("#first", window.location.hash); }); should("favor link relation to text matching", () => { document.getElementById("test-div").innerHTML = `\ next\ `; NormalModeCommands.goNext(); assert.equal("#first", window.location.hash); }); should("match mixed case link relation", () => { document.getElementById("test-div").innerHTML = `\ \ `; NormalModeCommands.goNext(); assert.equal("#first", window.location.hash); }); should("match against the title attribute", () => { document.getElementById("test-div").innerHTML = `\ unhelpful text\ `; NormalModeCommands.goNext(); assert.equal("#first", window.location.hash); }); should("match against the aria-label attribute", () => { document.getElementById("test-div").innerHTML = `\ unhelpful text\ `; NormalModeCommands.goNext(); assert.equal("#first", window.location.hash); }); }); context("Key mapping", () => { let normalMode, handlerCalled, handlerCalledCount; setup(() => { normalMode = initializeModeState(); handlerCalled = false; handlerCalledCount = 0; normalMode.setCommandHandler(({ count }) => { handlerCalled = true; handlerCalledCount = count; }); }); should("recognize first mapped key", () => { assert.isTrue(normalMode.isMappedKey("m")); }); should("recognize second mapped key", () => { assert.isFalse(normalMode.isMappedKey("p")); sendKeyboardEvent("z"); assert.isTrue(normalMode.isMappedKey("p")); }); should("recognize pass keys", () => { assert.isTrue(normalMode.isPassKey("p")); }); should("not mis-recognize pass keys", () => { assert.isFalse(normalMode.isMappedKey("p")); sendKeyboardEvent("z"); assert.isTrue(normalMode.isMappedKey("p")); }); should("recognize initial count keys", () => { assert.isTrue(normalMode.isCountKey("1")); assert.isTrue(normalMode.isCountKey("9")); }); should("not recognize '0' as initial count key", () => { assert.isFalse(normalMode.isCountKey("0")); }); should("recognize subsequent count keys", () => { sendKeyboardEvent("1"); assert.isTrue(normalMode.isCountKey("0")); assert.isTrue(normalMode.isCountKey("9")); }); should("set and call command handler", () => { sendKeyboardEvent("m"); assert.isTrue(handlerCalled); }); should("not call command handler for pass keys", () => { sendKeyboardEvent("p"); assert.isFalse(handlerCalled); }); should("accept a count prefix with a single digit", () => { sendKeyboardEvent("2"); sendKeyboardEvent("m"); assert.equal(2, handlerCalledCount); }); should("accept a count prefix with multiple digits", () => { sendKeyboardEvent("2"); sendKeyboardEvent("0"); sendKeyboardEvent("m"); assert.equal(20, handlerCalledCount); }); should("cancel a count prefix", () => { sendKeyboardEvent("2"); sendKeyboardEvent("z"); sendKeyboardEvent("m"); assert.equal(true, handlerCalled); assert.equal(null, handlerCalledCount); }); should("accept a count prefix for multi-key command mappings", () => { sendKeyboardEvent("5"); sendKeyboardEvent("z"); sendKeyboardEvent("p"); assert.equal(5, handlerCalledCount); }); should("cancel a key prefix", () => { sendKeyboardEvent("z"); assert.equal(false, handlerCalled); sendKeyboardEvent("m"); assert.equal(true, handlerCalled); }); should("cancel a count prefix after a prefix key", () => { sendKeyboardEvent("2"); sendKeyboardEvent("z"); sendKeyboardEvent("m"); assert.equal(null, handlerCalledCount); }); should("cancel a prefix key on escape", () => { sendKeyboardEvent("z"); sendKeyboardEvent("Escape", "keydown"); sendKeyboardEvent("p"); assert.equal(0, handlerCalledCount); }); }); context("Normal mode", () => { setup(() => initializeModeState()); should("invoke commands for mapped keys", () => { sendKeyboardEvent("m"); assert.equal("m", commandName); }); should("invoke commands for mapped keys with a mapped prefix", () => { sendKeyboardEvent("z"); sendKeyboardEvent("m"); assert.equal("m", commandName); }); should("invoke commands for mapped keys with an unmapped prefix", () => { sendKeyboardEvent("a"); sendKeyboardEvent("m"); assert.equal("m", commandName); }); should("not invoke commands for pass keys", () => { sendKeyboardEvent("p"); assert.equal(null, commandName); }); should("not invoke commands for pass keys with an unmapped prefix", () => { sendKeyboardEvent("a"); sendKeyboardEvent("p"); assert.equal(null, commandName); }); should("invoke commands for pass keys with a count", () => { sendKeyboardEvent("1"); sendKeyboardEvent("p"); assert.equal("p", commandName); }); should("invoke commands for pass keys with a key queue", () => { sendKeyboardEvent("z"); sendKeyboardEvent("p"); assert.equal("zp", commandName); }); should("accept count prefixes of length 1", () => { sendKeyboardEvent("2"); sendKeyboardEvent("m"); assert.equal(2, commandCount); }); should("accept count prefixes of length 2", () => { sendKeyboardEvents("12"); sendKeyboardEvent("m"); assert.equal(12, commandCount); }); should("get the correct count for mixed inputs (single key)", () => { sendKeyboardEvent("2"); sendKeyboardEvent("z"); sendKeyboardEvent("m"); assert.equal(null, commandCount); }); should("get the correct count for mixed inputs (multi key)", () => { sendKeyboardEvent("2"); sendKeyboardEvent("z"); sendKeyboardEvent("p"); assert.equal(2, commandCount); }); should("get the correct count for mixed inputs (multi key, duplicates)", () => { sendKeyboardEvent("2"); sendKeyboardEvent("z"); sendKeyboardEvent("z"); sendKeyboardEvent("p"); assert.equal(null, commandCount); }); should("get the correct count for mixed inputs (with leading mapped keys)", () => { sendKeyboardEvent("z"); sendKeyboardEvent("2"); sendKeyboardEvent("m"); assert.equal(2, commandCount); }); should("get the correct count for mixed inputs (with leading unmapped keys)", () => { sendKeyboardEvent("a"); sendKeyboardEvent("2"); sendKeyboardEvent("m"); assert.equal(2, commandCount); }); should("not get a count after unmapped keys", () => { sendKeyboardEvent("2"); sendKeyboardEvent("a"); sendKeyboardEvent("m"); assert.equal(null, commandCount); }); should("get the correct count after unmapped keys", () => { sendKeyboardEvent("2"); sendKeyboardEvent("a"); sendKeyboardEvent("3"); sendKeyboardEvent("m"); assert.equal(3, commandCount); }); should("not handle unmapped keys", () => { sendKeyboardEvent("u"); assert.equal(null, commandCount); }); }); context("Insert mode", () => { let insertMode; setup(() => { initializeModeState(); insertMode = new InsertMode({ global: true }); }); should("exit on escape", () => { assert.isTrue(insertMode.modeIsActive); sendKeyboardEvent("Escape", "keydown"); assert.isFalse(insertMode.modeIsActive); }); should("resume normal mode after leaving insert mode", () => { assert.equal(null, commandName); insertMode.exit(); sendKeyboardEvent("m"); assert.equal("m", commandName); }); }); context("Triggering insert mode", () => { setup(() => { initializeModeState(); const testContent = ` \ \ \

\

`; document.getElementById("test-div").innerHTML = testContent; }); teardown(() => { if (document.activeElement != null) { document.activeElement.blur(); } document.getElementById("test-div").innerHTML = ""; }); should("trigger insert mode on focus of text input", () => { assert.isFalse(InsertMode.permanentInstance.isActive()); document.getElementById("first").focus(); assert.isTrue(InsertMode.permanentInstance.isActive()); }); should("trigger insert mode on focus of password input", () => { assert.isFalse(InsertMode.permanentInstance.isActive()); document.getElementById("third").focus(); assert.isTrue(InsertMode.permanentInstance.isActive()); }); should("trigger insert mode on focus of contentEditable elements", () => { assert.isFalse(InsertMode.permanentInstance.isActive()); document.getElementById("fourth").focus(); assert.isTrue(InsertMode.permanentInstance.isActive()); }); should("not trigger insert mode on other elements", () => { assert.isFalse(InsertMode.permanentInstance.isActive()); document.getElementById("fifth").focus(); assert.isFalse(InsertMode.permanentInstance.isActive()); }); }); // NOTE(philc): I'm disabling the caret and visual mode tests because I think they're fallen into // disrepair, or we merged changes to master and neglected to update the tests. We should return to // these and fix+re-enable them. // context("Caret mode", // setup(() => { // document.getElementById("test-div").innerHTML = `\ //

//   It is an ancient Mariner,
//   And he stoppeth one of three.
//   By thy long grey beard and glittering eye,
//   Now wherefore stopp'st thou me?
// 

\ // `; // initializeModeState(); // this.initialVisualMode = new VisualMode; // }); // teardown(() => document.getElementById("test-div").innerHTML = ""), // should("enter caret mode", () => { // assert.isFalse(this.initialVisualMode.modeIsActive); // assert.equal("I", getSelection()); // }); // should("exit caret mode on escape", () => { // sendKeyboardEvent("Escape", "keydown"); // assert.equal("", getSelection()); // }); // should("move caret with l and h", () => { // assert.equal("I", getSelection()); // sendKeyboardEvent("l"); // assert.equal("t", getSelection()); // sendKeyboardEvent("h"); // assert.equal("I", getSelection()); // }); // should("move caret with w and b", () => { // assert.equal("I", getSelection()); // sendKeyboardEvent("w"); // assert.equal("i", getSelection()); // sendKeyboardEvent("b"); // assert.equal("I", getSelection()); // }); // should("move caret with e", () => { // assert.equal("I", getSelection()); // sendKeyboardEvent("e"); // assert.equal(" ", getSelection()); // sendKeyboardEvent("e"); // assert.equal(" ", getSelection()); // }); // should("move caret with j and k", () => { // assert.equal("I", getSelection()); // sendKeyboardEvent("j"); // assert.equal("A", getSelection()); // sendKeyboardEvent("k"); // assert.equal("I", getSelection()); // }); // should("re-use an existing selection", () => { // assert.equal("I", getSelection()); // sendKeyboardEvents("ww"); // assert.equal("a", getSelection()); // sendKeyboardEvent("Escape", "keydown"); // new VisualMode; // assert.equal("a", getSelection()); // }); // should("not move the selection on caret/visual mode toggle", () => { // sendKeyboardEvents("ww"); // assert.equal("a", getSelection()); // for (let key of "vcvcvc".split()) { // sendKeyboardEvent(key); // assert.equal("a", getSelection()); // } // }) // ); // // TODO(philc): Re-enable // context("Visual mode", // setup(() => { // document.getElementById("test-div").innerHTML = `\ //

//   It is an ancient Mariner,
//   And he stoppeth one of three.
//   By thy long grey beard and glittering eye,
//   Now wherefore stopp'st thou me?
// 

\ // `; // initializeModeState(); // this.initialVisualMode = new VisualMode; // sendKeyboardEvent("w"); // sendKeyboardEvent("w"); // // We should now be at the "a" of "an". // sendKeyboardEvent("v"); // }); // teardown(() => document.getElementById("test-div").innerHTML = ""), // should("select word with e", () => { // assert.equal("a", getSelection()); // sendKeyboardEvent("e"); // assert.equal("an", getSelection()); // sendKeyboardEvent("e"); // assert.equal("an ancient", getSelection()); // }); // should("select opposite end of the selection with o", () => { // assert.equal("a", getSelection()); // sendKeyboardEvent("e"); // assert.equal("an", getSelection()); // sendKeyboardEvent("e"); // assert.equal("an ancient", getSelection()); // sendKeyboardEvents("ow"); // assert.equal("ancient", getSelection()); // sendKeyboardEvents("oe"); // assert.equal("ancient Mariner", getSelection()); // }); // should("accept a count", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("2e"); // assert.equal("an ancient", getSelection()); // }); // should("select a word", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("aw"); // assert.equal("an", getSelection()); // }); // should("select a word with a count", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("2aw"); // assert.equal("an ancient", getSelection()); // }); // should("select a word with a count", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("2aw"); // assert.equal("an ancient", getSelection()); // }); // should("select to start of line", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("0"); // assert.equal("It is", getSelection().trim()); // }); // should("select to end of line", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("$"); // assert.equal("an ancient Mariner,", getSelection()); // }); // should("re-enter caret mode", () => { // assert.equal("a", getSelection()); // sendKeyboardEvents("cww"); // assert.equal("M", getSelection()); // }) // ); const createMode = (options) => { const mode = new Mode(); mode.init(options); return mode; }; context("Mode utilities", () => { setup(() => { initializeModeState(); const testContent = ` \ \ `; document.getElementById("test-div").innerHTML = testContent; }); teardown(() => document.getElementById("test-div").innerHTML = ""); should("not have duplicate singletons", () => { let mode; let count = 0; class Test extends Mode { constructor() { count += 1; super(); super.init({ singleton: "test" }); } exit() { count -= 1; return super.exit(); } } assert.isTrue(count === 0); for (let i = 1; i <= 10; i++) { mode = new Test(); assert.isTrue(count === 1); } mode.exit(); assert.isTrue(count === 0); }); should("exit on escape", () => { const test = createMode({ exitOnEscape: true }); assert.isTrue(test.modeIsActive); sendKeyboardEvent("Escape", "keydown"); assert.isFalse(test.modeIsActive); }); should("not exit on escape if not enabled", () => { const test = createMode({ exitOnEscape: false }); assert.isTrue(test.modeIsActive); sendKeyboardEvent("Escape", "keydown"); assert.isTrue(test.modeIsActive); }); should("exit on blur", () => { const element = document.getElementById("first"); element.focus(); const test = createMode({ exitOnBlur: element }); assert.isTrue(test.modeIsActive); element.blur(); assert.isFalse(test.modeIsActive); }); should("not exit on blur if not enabled", () => { const element = document.getElementById("first"); element.focus(); const test = createMode({ exitOnBlur: false }); assert.isTrue(test.modeIsActive); element.blur(); assert.isTrue(test.modeIsActive); }); }); context("PostFindMode", () => { let postFindMode; setup(() => { initializeModeState(); const testContent = ""; document.getElementById("test-div").innerHTML = testContent; document.getElementById("first").focus(); postFindMode = new PostFindMode(); }); teardown(() => document.getElementById("test-div").innerHTML = ""), should("be a singleton", () => { assert.isTrue(postFindMode.modeIsActive); new PostFindMode(); assert.isFalse(postFindMode.modeIsActive); }); should("suppress unmapped printable keys", () => { sendKeyboardEvent("a"); assert.equal(null, commandCount); }); should("be deactivated on click events", () => { handlerStack.bubbleEvent("click", { target: document.activeElement }); assert.isFalse(postFindMode.modeIsActive); }); should("enter insert mode on immediate escape", () => { sendKeyboardEvent("Escape", "keydown"); assert.equal(null, commandCount); assert.isFalse(postFindMode.modeIsActive); }); should("not enter insert mode on subsequent escapes", () => { sendKeyboardEvent("a"); sendKeyboardEvent("Escape", "keydown"); assert.isTrue(postFindMode.modeIsActive); }); }); context("WaitForEnter", () => { let isSuccess, waitForEnter; setup(() => { initializeModeState(); isSuccess = null; waitForEnter = new WaitForEnter((value) => { isSuccess = value; }); }); should("exit with success on Enter", () => { assert.isTrue(waitForEnter.modeIsActive); assert.isFalse(isSuccess != null); sendKeyboardEvent("Enter", "keydown"); assert.isFalse(waitForEnter.modeIsActive); assert.isTrue((isSuccess != null) && (isSuccess === true)); }); should("exit without success on Escape", () => { assert.isTrue(waitForEnter.modeIsActive); assert.isFalse(isSuccess != null); sendKeyboardEvent("Escape", "keydown"); assert.isFalse(waitForEnter.modeIsActive); assert.isTrue((isSuccess != null) && (isSuccess === false)); }); should("not exit on other keyboard events", () => { assert.isTrue(waitForEnter.modeIsActive); assert.isFalse(isSuccess != null); sendKeyboardEvents("abc"); assert.isTrue(waitForEnter.modeIsActive); assert.isFalse(isSuccess != null); }); }); context("GrabBackFocus", () => { setup(() => { const testContent = ""; document.getElementById("test-div").innerHTML = testContent; stubSettings("grabBackFocus", true); }); teardown(() => document.getElementById("test-div").innerHTML = ""), should("blur an already focused input", () => { document.getElementById("input").focus(); assert.isTrue(document.activeElement); assert.isTrue(DomUtils.isEditable(document.activeElement)); initializeModeState(); assert.isTrue(document.activeElement); assert.isFalse(DomUtils.isEditable(document.activeElement)); }); should("blur a newly focused input", () => { initializeModeState(); document.getElementById("input").focus(); assert.isTrue(document.activeElement); assert.isFalse(DomUtils.isEditable(document.activeElement)); }); should("exit on a key event", () => { initializeModeState(); sendKeyboardEvent("a"); document.getElementById("input").focus(); assert.isTrue(document.activeElement); assert.isTrue(DomUtils.isEditable(document.activeElement)); }); should("exit on a mousedown event", () => { initializeModeState(); handlerStack.bubbleEvent("mousedown", { target: document.body }); document.getElementById("input").focus(); assert.isTrue(document.activeElement); assert.isTrue(DomUtils.isEditable(document.activeElement)); }); }); vimium-2.1.2/tests/dom_tests/dom_utils_test.js000066400000000000000000000134251460337006400215710ustar00rootroot00000000000000context("DOM content loaded", () => { // The DOM content has already loaded, this should be called immediately. should("call callback immediately.", () => { let called = false; DomUtils.documentReady(() => called = true); assert.isTrue(called); }); // See ./dom_tests.html; the callback there was installed before the document was ready. should("already have called callback embedded in test page.", () => { assert.isTrue(window.documentReadyListenerCalled); }); }); context("Check visibility", () => { should("detect visible elements as visible", () => { document.getElementById("test-div").innerHTML = `\
test
`; assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("foo"), true)) !== null); }); should("detect display:none links as hidden", () => { document.getElementById("test-div").innerHTML = `\ `; assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("foo"), true)); }); should("detect visibility:hidden links as hidden", () => { document.getElementById("test-div").innerHTML = `\ `; assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("foo"), true)); }); should("detect elements nested in display:none elements as hidden", () => { document.getElementById("test-div").innerHTML = `\ `; assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("foo"), true)); }); should("detect links nested in visibility:hidden elements as hidden", () => { document.getElementById("test-div").innerHTML = `\ `; assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("foo"), true)); }); should("detect links outside viewport as hidden", () => { document.getElementById("test-div").innerHTML = `\ test test`; assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("foo"), true)); assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("bar"), true)); }); should("detect links only partially outside viewport as visible", () => { document.getElementById("test-div").innerHTML = `\ test test`; assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("foo"), true)) !== null); assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("bar"), true)) !== null); }); should("detect links that contain only floated / absolutely-positioned divs as visible", () => { document.getElementById("test-div").innerHTML = `\
test
`; assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("foo"), true)) !== null); document.getElementById("test-div").innerHTML = `\
test
`; assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("foo"), true)) !== null); }); should("detect links that contain only invisible floated divs as invisible", () => { document.getElementById("test-div").innerHTML = `\
test
`; assert.equal(null, DomUtils.getVisibleClientRect(document.getElementById("foo"), true)); }); should( "detect font-size: 0; and display: inline; links when their children are display: inline", () => { // This test represents the minimal test case covering issue #1554. document.getElementById("test-div").innerHTML = `\
test
`; assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("foo"), true)) !== null); }, ); should("detect links inside opacity:0 elements as visible", () => { // XXX This is an expected failure. See issue #16. document.getElementById("test-div").innerHTML = `\ `; assert.isTrue((DomUtils.getVisibleClientRect(document.getElementById("foo"), true)) !== null); }); }); context("getClientRectsForAreas", () => { let img, area; setup(() => { img = document.createElement("img"); area = document.createElement("area"); }); should("return the associated rect for an image map", () => { area.setAttribute("coords", "1,2,3,4"); const result = DomUtils.getClientRectsForAreas(img, [area]); assert.equal([{ element: area, rect: Rect.create(1, 2, 3, 4) }], result); }); should("skip when a map's coords are malformed", () => { area.setAttribute("coords", "1,2,3"); // This is only 3 coords rather than 4. assert.equal([], DomUtils.getClientRectsForAreas(img, [area])); area.setAttribute("coords", "1,2,3,junk-value"); assert.equal([], DomUtils.getClientRectsForAreas(img, [area])); }); }); // NOTE(philc): This test doesn't pass on puppeteer. It's unclear from the XXX comment if it's // supposed to. // should("Detect links within SVGs as visible"), () => { // # XXX this is an expected failure // document.getElementById("test-div").innerHTML = """ // // // test // // // """ // assert.equal(null, (DomUtils.getVisibleClientRect (document.getElementById 'foo'), true)); // } vimium-2.1.2/tests/unit_tests/000077500000000000000000000000001460337006400163705ustar00rootroot00000000000000vimium-2.1.2/tests/unit_tests/bg_utils_test.js000066400000000000000000000002351460337006400215750ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/url_utils.js"; import "../../background_scripts/tab_recency.js"; import "../../background_scripts/bg_utils.js"; vimium-2.1.2/tests/unit_tests/commands_test.js000066400000000000000000000136701460337006400215750ustar00rootroot00000000000000import "./test_helper.js"; import "../../background_scripts/bg_utils.js"; import "../../lib/settings.js"; import "../../lib/keyboard_utils.js"; import "../../background_scripts/commands.js"; import "../../content_scripts/mode.js"; import "../../content_scripts/mode_key_handler.js"; // Include mode_normal to check that all commands have been implemented. import "../../content_scripts/mode_normal.js"; import "../../content_scripts/link_hints.js"; import "../../content_scripts/marks.js"; import "../../content_scripts/vomnibar.js"; await Commands.init(); context("parseKeySequence", () => { const testKeySequence = (key, expectedKeyText, expectedKeyLength) => { const keySequence = Commands.parseKeySequence(key); assert.equal(expectedKeyText, keySequence.join("/")); assert.equal(expectedKeyLength, keySequence.length); }; should("lowercase keys correctly", () => { testKeySequence("a", "a", 1); testKeySequence("A", "A", 1); testKeySequence("ab", "a/b", 2); }); should("recognise non-alphabetic keys", () => { testKeySequence("#", "#", 1); testKeySequence(".", ".", 1); testKeySequence("##", "#/#", 2); testKeySequence("..", "./.", 2); }); should("parse keys with modifiers", () => { testKeySequence("", "", 1); testKeySequence("", "", 1); testKeySequence("", "", 1); testKeySequence("", "/", 2); testKeySequence("", "", 1); testKeySequence("z", "z/", 2); }); should("normalize with modifiers", () => { // Modifiers should be in alphabetical order. testKeySequence("", "", 1); }); should("parse and normalize named keys", () => { testKeySequence("", "", 1); testKeySequence("", "", 1); testKeySequence("", "", 1); testKeySequence("", "", 1); testKeySequence("", "", 1); }); should("handle angle brackets which are part of not modifiers", () => { testKeySequence("<", "<", 1); testKeySequence(">", ">", 1); testKeySequence("<<", ">", ">/>", 2); testKeySequence("<>", "", 2); testKeySequence("<>", "", 2); testKeySequence("<", "", 2); testKeySequence(">", ">", 1); testKeySequence("", "", 3); }); should("negative tests", () => { // These should not be parsed as modifiers. testKeySequence("", "", 5); testKeySequence("", "", 6); }); }); context("parseKeyMappingConfig", () => { should("handle map statements", () => { const { keyToRegistryEntry } = Commands.parseKeyMappingsConfig("map a scrollDown"); assert.equal("scrollDown", keyToRegistryEntry["a"]?.command); }); should("ignore mappings for unknown commands", () => { assert.equal({}, Commands.parseKeyMappingsConfig("map a unknownCommand").keyToRegistryEntry); }); should("handle mapkey statements", () => { const { keyToMappedKey } = Commands.parseKeyMappingsConfig("mapkey a b"); assert.equal({ "a": "b" }, keyToMappedKey); }); should("handle unmap statements", () => { const input = "mapkey a b \n unmap a"; const { keyToMappedKey } = Commands.parseKeyMappingsConfig(input); assert.equal({}, keyToMappedKey); }); should("handle unmapall statements", () => { const input = "mapkey a b \n unmapall \n mapkey b c"; const { keyToMappedKey } = Commands.parseKeyMappingsConfig(input); assert.equal({ "b": "c" }, keyToMappedKey); }); should("ignore commands with the wrong number of tokens", () => { assert.equal({}, Commands.parseKeyMappingsConfig("mapkey a b c").keyToMappedKey); assert.equal({}, Commands.parseKeyMappingsConfig("map a").keyToRegistryEntry); assert.equal( { "a": "b" }, Commands.parseKeyMappingsConfig("mapkey a b \n unmap a a").keyToMappedKey, ); }); }); context("Validate commands and options", () => { // TODO(smblott) For this and each following test, is there a way to structure the tests such that the name // of the offending command appears in the output, if the test fails? should("have either noRepeat or repeatLimit, but not both", () => { for (const command of Object.keys(Commands.availableCommands)) { const options = Commands.availableCommands[command]; assert.isTrue(!(options.noRepeat && options.repeatLimit)); } }); should("describe each command", () => { for (const command of Object.keys(Commands.availableCommands)) { const options = Commands.availableCommands[command]; assert.equal("string", typeof options.description); } }); should("define each command in each command group", () => { for (const group of Object.keys(Commands.commandGroups)) { const commands = Commands.commandGroups[group]; for (const command of commands) { assert.equal("string", typeof command); assert.isTrue(Commands.availableCommands[command]); } } }); should("have valid commands for each advanced command", () => { for (const command of Commands.advancedCommands) { assert.equal("string", typeof command); assert.isTrue(Commands.availableCommands[command]); } }); should("have valid commands for each default key mapping", () => { const count = Object.keys(Commands.keyToRegistryEntry).length; assert.isTrue(0 < count); for (const key of Object.keys(Commands.keyToRegistryEntry)) { const command = Commands.keyToRegistryEntry[key]; assert.equal("object", typeof command); assert.isTrue(Commands.availableCommands[command.command]); } }); }); context("Validate advanced commands", () => { should("include each advanced command in a command group", () => { const allCommands = Object.keys(Commands.commandGroups).map((k) => Commands.commandGroups[k]) .flat(1); for (const command of Commands.advancedCommands) { assert.isTrue(allCommands.includes(command)); } }); }); vimium-2.1.2/tests/unit_tests/completion_engines_test.js000066400000000000000000000033641460337006400236540ustar00rootroot00000000000000import "./test_helper.js"; import "../../background_scripts/bg_utils.js"; import * as Engines from "../../background_scripts/completion_engines.js"; import "../../background_scripts/completion.js"; context("Amazon completion", () => { should("parses results", () => { const response = JSON.stringify({ "suggestions": [ { "value": "one" }, { "value": "two" }, ], }); const results = new Engines.Amazon().parse(response); assert.equal(["one", "two"], results); }); }); context("Brave completion", () => { should("parses results", () => { const response = JSON.stringify(["the-query", ["one", "two"]]); const results = new Engines.Brave().parse(response); assert.equal(["one", "two"], results); }); }); context("DuckDuckGo completion", () => { should("parses results", () => { const response = JSON.stringify([ { "phrase": "one" }, { "phrase": "two" }, ]); const results = new Engines.DuckDuckGo().parse(response); assert.equal(["one", "two"], results); }); }); context("Qwant completion", () => { should("parses results", () => { const response = JSON.stringify({ "data": { "items": [ { "value": "one" }, { "value": "two" }, ], }, }); const results = new Engines.Qwant().parse(response); assert.equal(["one", "two"], results); }); }); // Engines which have trivial parsers are omitted from these tests. context("Webster completion", () => { should("parses results", () => { const response = JSON.stringify({ "docs": [ { "word": "one" }, { "word": "two" }, ], }); const results = new Engines.Webster().parse(response); assert.equal(["one", "two"], results); }); }); vimium-2.1.2/tests/unit_tests/completion_test.js000066400000000000000000000554541460337006400221530ustar00rootroot00000000000000import "./test_helper.js"; import "../../background_scripts/bg_utils.js"; import "../../background_scripts/completion_engines.js"; import "../../background_scripts/completion_search.js"; import "../../background_scripts/completion.js"; const hours = (n) => 1000 * 60 * 60 * n; // A convenience wrapper around completer.filter() so it can be called synchronously in tests. const filterCompleter = async (completer, queryTerms) => { return await completer.filter({ queryTerms, query: queryTerms.join(" "), }); }; context("bookmark completer", () => { const bookmark3 = { title: "bookmark3", url: "bookmark3.com" }; const bookmark2 = { title: "bookmark2", url: "bookmark2.com" }; const bookmark1 = { title: "bookmark1", url: "bookmark1.com", children: [bookmark2] }; let completer; setup(() => { stub(window.chrome.bookmarks, "getTree", () => [bookmark1]); completer = new BookmarkCompleter(); }); should("flatten a list of bookmarks with inorder traversal", async () => { const result = await completer.traverseBookmarks([bookmark1, bookmark3]); assert.equal([bookmark1, bookmark2, bookmark3], result); }); should("return matching bookmarks when searching", async () => { completer.refresh(); const results = await filterCompleter(completer, ["mark2"]); assert.equal([bookmark2.url], results.map((suggestion) => suggestion.url)); }); should("return *no* matching bookmarks when there is no match", async () => { completer.refresh(); const results = await filterCompleter(completer, ["does-not-match"]); assert.equal([], results.map((suggestion) => suggestion.url)); }); should("construct bookmark paths correctly", async () => { completer.refresh(); await filterCompleter(completer, ["mark2"]); assert.equal("/bookmark1/bookmark2", bookmark2.pathAndTitle); }); should( "return matching bookmark *titles* when searching *without* the folder separator character", async () => { completer.refresh(); const results = await filterCompleter(completer, ["mark2"]); assert.equal(["bookmark2"], results.map((suggestion) => suggestion.title)); }, ); should( "return matching bookmark *paths* when searching with the folder separator character", async () => { completer.refresh(); const results = await filterCompleter(completer, ["/bookmark1", "mark2"]); assert.equal(["/bookmark1/bookmark2"], results.map((suggestion) => suggestion.title)); }, ); }); context("HistoryCache", () => { const compare = (a, b) => a - b; context("binary search", () => { should("find elements to the left of the middle", () => { assert.equal(0, HistoryCache.binarySearch(3, [3, 5, 8], compare)); }); should("find elements to the right of the middle", () => { assert.equal(2, HistoryCache.binarySearch(8, [3, 5, 8], compare)); }); context("unfound elements", () => { should("return 0 if it should be the head of the list", () => { assert.equal(0, HistoryCache.binarySearch(1, [3, 5, 8], compare)); }); should("return length - 1 if it should be at the end of the list", () => { assert.equal(0, HistoryCache.binarySearch(3, [3, 5, 8], compare)); }); should( "return one passed end of array (so: array.length) if greater than last element in array", () => { assert.equal(3, HistoryCache.binarySearch(10, [3, 5, 8], compare)); }, ); should("found return the position if it's between two elements", () => { assert.equal(1, HistoryCache.binarySearch(4, [3, 5, 8], compare)); assert.equal(2, HistoryCache.binarySearch(7, [3, 5, 8], compare)); }); }); }); context("fetchHistory", () => { const history1 = { url: "b.com", lastVisitTime: 5 }; const history2 = { url: "a.com", lastVisitTime: 10 }; let onVisitedListener, onVisitRemovedListener; setup(async () => { const history = [history1, history2]; // const history = [history2, history1]; onVisitedListener = null; onVisitRemovedListener = null; stub(window.chrome, "history", { search: (_options) => history, onVisited: { addListener(listener) { onVisitedListener = listener; }, removeListener() {}, }, onVisitRemoved: { addListener(listener) { onVisitRemovedListener = listener; }, removeListener() {}, }, }); HistoryCache.reset(); await HistoryCache.fetchHistory(); }); should("store visits sorted by url ascending", () => { assert.equal([history2, history1], HistoryCache.history); }); should("add new visits to the history", () => { const newSite = { url: "ab.com" }; onVisitedListener(newSite); assert.equal([history2, newSite, history1], HistoryCache.history); }); should("replace new visits in the history", () => { assert.equal([history2, history1], HistoryCache.history); const newSite = { url: "a.com", lastVisitTime: 15 }; onVisitedListener(newSite); assert.equal([newSite, history1], HistoryCache.history); }); should( "(not) remove page from the history, when page is not in history (it should be a no-op)", () => { assert.equal([history2, history1], HistoryCache.history); const toRemove = { urls: ["x.com"], allHistory: false }; onVisitRemovedListener(toRemove); assert.equal([history2, history1], HistoryCache.history); }, ); should("remove pages from the history", () => { assert.equal([history2, history1], HistoryCache.history); const toRemove = { urls: ["a.com"], allHistory: false }; onVisitRemovedListener(toRemove); assert.equal([history1], HistoryCache.history); }); should("remove all pages from the history", () => { assert.equal([history2, history1], HistoryCache.history); const toRemove = { allHistory: true }; onVisitRemovedListener(toRemove); assert.equal([], HistoryCache.history); }); }); }); context("history completer", () => { const history1 = { title: "history1", url: "history1.com", lastVisitTime: hours(1) }; const history2 = { title: "history2", url: "history2.com", lastVisitTime: hours(5) }; let completer; setup(() => { completer = new HistoryCompleter(); stub(window.chrome, "history", { search: (_options) => [history1, history2], onVisited: { addListener() {}, removeListener() {} }, onVisitRemoved: { addListener() {}, removeListener() {} }, }); HistoryCache.reset(); }); should("return matching history entries when searching", async () => { const results = await filterCompleter(completer, ["story1"]); assert.equal([history1.url], results.map((s) => s.url)); }); should("rank recent results higher than nonrecent results", async () => { stub(Date, "now", returns(hours(24))); const results = await filterCompleter(completer, ["hist"]); results.forEach((result) => result.computeRelevancy()); results.sort((a, b) => b.relevancy - a.relevancy); assert.equal([history2.url, history1.url], results.map((result) => result.url)); }); }); context("domain completer", () => { const history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(1) }; const history2 = { title: "history2", url: "http://history2.com", lastVisitTime: hours(1) }; const undef = { title: "history2", url: "http://undefined.net", lastVisitTime: hours(1) }; let completer = null; setup(() => { stub(window.chrome, "history", { search: (_options) => [history1, history2, undef], onVisited: { addListener() {}, removeListener() {} }, onVisitRemoved: { addListener() {}, removeListener() {} }, }); stub(Date, "now", returns(hours(24))); completer = new DomainCompleter(); HistoryCache.reset(); }); should("return only a single matching domain", async () => { const results = await filterCompleter(completer, ["story"]); assert.equal(["http://history1.com"], results.map((r) => r.url)); }); should("pick domains which are more recent", async () => { // These domains are the same except for their last visited time. let result = await filterCompleter(completer, ["story"]); assert.equal("http://history1.com", result[0].url); history2.lastVisitTime = hours(3); result = await filterCompleter(completer, ["story"]); assert.equal("http://history2.com", result[0].url); }); should( "returns no results when there's more than one query term, because clearly it's not a domain", async () => { assert.equal([], await filterCompleter(completer, ["his", "tory"])); }, ); should("not return any results for empty queries", async () => { assert.equal([], await filterCompleter(completer, [])); }); }); context("domain completer (removing entries)", () => { const history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(2) }; const history2 = { title: "history2", url: "http://history2.com", lastVisitTime: hours(1) }; const history3 = { title: "history2something", url: "http://history2.com/something", lastVisitTime: hours(0), }; let onVisitRemovedListener, completer; setup(async () => { onVisitRemovedListener = null; stub(window.chrome, "history", { search: (_options) => [history1, history2, history3], onVisited: { addListener(_listener) { }, }, onVisitRemoved: { addListener(listener) { onVisitRemovedListener = listener; }, }, }); stub(Date, "now", returns(hours(24))); completer = new DomainCompleter(); // Force installation of listeners. await filterCompleter(completer, ["story"]); }); should("remove 1 entry for domain with reference count of 1", async () => { onVisitRemovedListener({ allHistory: false, urls: [history1.url] }); let result = await filterCompleter(completer, ["story"]); assert.equal("http://history2.com", result[0].url); result = await filterCompleter(completer, ["story1"]); assert.equal(0, result.length); }); should("remove 2 entries for domain with reference count of 2", async () => { onVisitRemovedListener({ allHistory: false, urls: [history2.url] }); let result = await filterCompleter(completer, ["story2"]); assert.equal("http://history2.com", result[0].url); onVisitRemovedListener({ allHistory: false, urls: [history3.url] }); result = await filterCompleter(completer, ["story2"]); assert.equal(0, result.length); result = await filterCompleter(completer, ["story"]); assert.equal("http://history1.com", result[0].url); }); should("remove 3 (all) matching domain entries", async () => { onVisitRemovedListener({ allHistory: false, urls: [history2.url] }); onVisitRemovedListener({ allHistory: false, urls: [history1.url] }); onVisitRemovedListener({ allHistory: false, urls: [history3.url] }); const result = await filterCompleter(completer, ["story"]); assert.equal(0, result.length); }); should("remove 3 (all) matching domain entries, and do it all at once", async () => { onVisitRemovedListener({ allHistory: false, urls: [history2.url, history1.url, history3.url] }); const result = await filterCompleter(completer, ["story"]); assert.equal(0, result.length); }); should("remove *all* domain entries", async () => { onVisitRemovedListener({ allHistory: true }); const result = await filterCompleter(completer, ["story"]); assert.equal(0, result.length); }); }); context("multi completer", () => { const tabs = [{ url: "tab1.com", title: "tab1", id: 1 },]; const tabCompleter = new TabCompleter(); let multiCompleter; setup(() => { stub(chrome.tabs, "query", () => tabs); multiCompleter = new MultiCompleter([tabCompleter, new DomainCompleter()]); }); should("return an empty list when the query is empty", async () => { // Even though a TabCompleter returns results when the query is empty, a MultiCompleter which // wraps a TabCompleter should not. assert.equal(1, (await filterCompleter(tabCompleter, [])).length); assert.equal([], (await filterCompleter(multiCompleter, []))); }); }); context("tab completer", () => { const tabs = [ { url: "tab1.com", title: "tab1", id: 1 }, { url: "tab2.com", title: "tab2", id: 2 }, ]; let completer; setup(() => { stub(chrome.tabs, "query", () => tabs); completer = new TabCompleter(); }); should("return tabs by recency when query is empty", async () => { const results = await filterCompleter(completer, []); assert.equal(["tab1.com", "tab2.com"], results.map((tab) => tab.url)); }); should("return matching tabs", async () => { const results = await filterCompleter(completer, ["tab2"]); assert.equal(["tab2.com"], results.map((tab) => tab.url)); assert.equal([2], results.map((tab) => tab.tabId)); }); }); context("SearchEngineCompleter", () => { const googleSearchUrl = "http://www.google.com/search?q="; let completer; const createResponse = (responseText) => { return { text: () => responseText }; }; setup(() => { completer = new SearchEngineCompleter(); const searchEngineConfig = `g: ${googleSearchUrl}%s`; UserSearchEngines.set(searchEngineConfig); }); should("complete search results using the given completer", async () => { const googleResults = ["blue", ["blue1", "blue2"]]; stub(window, "fetch", () => createResponse(JSON.stringify(googleResults))); const results = await filterCompleter(completer, ["g", "blue"]); assert.equal( [googleSearchUrl + "blue", googleSearchUrl + "blue1", googleSearchUrl + "blue2"], results.map((suggestion) => suggestion.url), ); }); }); context("suggestions", () => { setup(() => { stub(chrome.runtime, "getURL", returns("https://test/")); }); should("escape html in page titles", () => { const suggestion = new Suggestion({ queryTerms: ["queryterm"], description: "tab", url: "url", title: "title ", relevancyFunction: returns(1), }); assert.isTrue(suggestion.generateHtml({}).indexOf("title <span>") >= 0); }); should("highlight query words", () => { const suggestion = new Suggestion({ queryTerms: ["ninj", "words"], description: "tab", url: "url", title: "ninjawords", relevancyFunction: returns(1), }); const expected = "ninjawords"; assert.isTrue(suggestion.generateHtml({}).indexOf(expected) >= 0); }); should("highlight query words correctly when whey they overlap", () => { const suggestion = new Suggestion({ queryTerms: ["ninj", "jaword"], description: "tab", url: "url", title: "ninjawords", relevancyFunction: returns(1), }); const expected = "ninjawords"; assert.isTrue(suggestion.generateHtml({}).indexOf(expected) >= 0); }); should("shorten urls", () => { const suggestion = new Suggestion({ queryTerms: ["queryterm"], description: "history", url: "http://ninjawords.com", title: "ninjawords", relevancyFunction: returns(1), }); assert.equal(-1, suggestion.generateHtml({}).indexOf("http://ninjawords.com")); }); }); context("RankingUtils.wordRelevancy", () => { should("score higher in shorter URLs", () => { const highScore = RankingUtils.wordRelevancy( ["stack"], "http://stackoverflow.com/short", "a-title", ); const lowScore = RankingUtils.wordRelevancy( ["stack"], "http://stackoverflow.com/longer", "a-title", ); assert.isTrue(highScore > lowScore); }); should("score higher in shorter titles", () => { const highScore = RankingUtils.wordRelevancy(["milk"], "a-url", "Milkshakes"); const lowScore = RankingUtils.wordRelevancy(["milk"], "a-url", "Milkshakes rocks"); assert.isTrue(highScore > lowScore); }); should("score higher for matching the start of a word (in a URL)", () => { const lowScore = RankingUtils.wordRelevancy( ["stack"], "http://Xstackoverflow.com/same", "a-title", ); const highScore = RankingUtils.wordRelevancy( ["stack"], "http://stackoverflowX.com/same", "a-title", ); assert.isTrue(highScore > lowScore); }); should("score higher for matching the start of a word (in a title)", () => { const lowScore = RankingUtils.wordRelevancy(["te"], "a-url", "Dist racted"); const highScore = RankingUtils.wordRelevancy(["te"], "a-url", "Distrac ted"); assert.isTrue(highScore > lowScore); }); should("score higher for matching a whole word (in a URL)", () => { const lowScore = RankingUtils.wordRelevancy( ["com"], "http://stackoverflow.comX/same", "a-title", ); const highScore = RankingUtils.wordRelevancy( ["com"], "http://stackoverflowX.com/same", "a-title", ); assert.isTrue(highScore > lowScore); }); should("score higher for matching a whole word (in a title)", () => { const lowScore = RankingUtils.wordRelevancy(["com"], "a-url", "abc comX"); const highScore = RankingUtils.wordRelevancy(["com"], "a-url", "abcX com"); assert.isTrue(highScore > lowScore); }); }); // TODO: (smblott) // Word relevancy should take into account the number of matches (it doesn't currently). should // "score higher for multiple matches (in a URL)", -> // lowScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflow.com/Xxxxxx", "a-title") // highScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflow.com/Xstack", "a-title") // assert.isTrue highScore > lowScore // should "score higher for multiple matches (in a title)", -> // lowScore = RankingUtils.wordRelevancy(["bbc"], "http://stackoverflow.com/same", "BBC Radio 4 (XBCr4)") // highScore = RankingUtils.wordRelevancy(["bbc"], "http://stackoverflow.com/same", "BBC Radio 4 (BBCr4)") // assert.isTrue highScore > lowScore context("Suggestion.pushMatchingRanges", () => { should("extract ranges matching term (simple case, two matches)", () => { const ranges = []; const [one, two, three] = ["one", "two", "three"]; const suggestion = new Suggestion([], "", "", "", returns(1)); suggestion.pushMatchingRanges(`${one}${two}${three}${two}${one}`, two, ranges); assert.equal( 2, Utils.zip([ranges, [[3, 6], [11, 14]]]).filter((pair) => (pair[0][0] === pair[1][0]) && (pair[0][1] === pair[1][1]) ).length, ); }); should("extract ranges matching term (two matches, one at start of string)", () => { const ranges = []; const [one, two, three] = ["one", "two", "three"]; const suggestion = new Suggestion([], "", "", "", returns(1)); suggestion.pushMatchingRanges(`${two}${three}${two}${one}`, two, ranges); assert.equal( 2, Utils.zip([ranges, [[0, 3], [8, 11]]]).filter((pair) => (pair[0][0] === pair[1][0]) && (pair[0][1] === pair[1][1]) ).length, ); }); should("extract ranges matching term (two matches, one at end of string)", () => { const ranges = []; const [one, two, three] = ["one", "two", "three"]; const suggestion = new Suggestion([], "", "", "", returns(1)); suggestion.pushMatchingRanges(`${one}${two}${three}${two}`, two, ranges); assert.equal( 2, Utils.zip([ranges, [[3, 6], [11, 14]]]).filter((pair) => (pair[0][0] === pair[1][0]) && (pair[0][1] === pair[1][1]) ).length, ); }); should("extract ranges matching term (no matches)", () => { const ranges = []; const [one, two, three] = ["one", "two", "three"]; const suggestion = new Suggestion([], "", "", "", returns(1)); suggestion.pushMatchingRanges(`${one}${two}${three}${two}${one}`, "does-not-match", ranges); assert.equal(0, ranges.length); }); }); context("RankingUtils", () => { should("do a case insensitive match", () => { assert.isTrue(RankingUtils.matches(["ari"], "maRio")); }); should("do a case insensitive match on full term", () => { assert.isTrue(RankingUtils.matches(["mario"], "MARio")); }); should("do a case insensitive match on several terms", () => { assert.isTrue( RankingUtils.matches(["ari"], "DOES_NOT_MATCH", "DOES_NOT_MATCH_EITHER", "MARio"), ); }); should("do a smartcase match (positive)", () => { assert.isTrue(RankingUtils.matches(["Mar"], "Mario")); }); should("do a smartcase match (negative)", () => { assert.isFalse(RankingUtils.matches(["Mar"], "mario")); }); should("do a match with regexp meta-characters (positive)", () => { assert.isTrue(RankingUtils.matches(["ma.io"], "ma.io")); }); should("do a match with regexp meta-characters (negative)", () => { assert.isFalse(RankingUtils.matches(["ma.io"], "mario")); }); should("do a smartcase match on full term", () => { assert.isTrue(RankingUtils.matches(["Mario"], "Mario")); assert.isFalse(RankingUtils.matches(["Mario"], "mario")); }); should("do case insensitive word relevancy (matching)", () => { assert.isTrue(RankingUtils.wordRelevancy(["ari"], "MARIO", "MARio") > 0.0); }); should("do case insensitive word relevancy (not matching)", () => { assert.isTrue(RankingUtils.wordRelevancy(["DOES_NOT_MATCH"], "MARIO", "MARio") === 0.0); }); should("every query term must match at least one thing (matching)", () => { assert.isTrue(RankingUtils.matches(["cat", "dog"], "catapult", "hound dog")); }); should("every query term must match at least one thing (not matching)", () => { assert.isTrue(!RankingUtils.matches(["cat", "dog", "wolf"], "catapult", "hound dog")); }); }); context("RegexpCache", () => { should("RegexpCache is in fact caching (positive case)", () => { assert.isTrue(RegexpCache.get("this") === RegexpCache.get("this")); }); should("RegexpCache is in fact caching (negative case)", () => { assert.isTrue(RegexpCache.get("this") !== RegexpCache.get("that")); }); should("RegexpCache prefix/suffix wrapping is working (positive case)", () => { assert.isTrue(RegexpCache.get("this", "(", ")") === RegexpCache.get("this", "(", ")")); }); should("RegexpCache prefix/suffix wrapping is working (negative case)", () => { assert.isTrue(RegexpCache.get("this", "(", ")") !== RegexpCache.get("this")); }); should("search for a string", () => { assert.isTrue("hound dog".search(RegexpCache.get("dog")) === 6); }); should("search for a string which isn't there", () => { assert.isTrue("hound dog".search(RegexpCache.get("cat")) === -1); }); should("search for a string with a prefix/suffix (positive case)", () => { assert.isTrue("hound dog".search(RegexpCache.get("dog", "\\b", "\\b")) === 6); }); should("search for a string with a prefix/suffix (negative case)", () => { assert.isTrue("hound dog".search(RegexpCache.get("do", "\\b", "\\b")) === -1); }); }); vimium-2.1.2/tests/unit_tests/exclusion_test.js000066400000000000000000000053221460337006400220000ustar00rootroot00000000000000import "./test_helper.js"; Utils.getCurrentVersion = () => "1.44"; import "../../lib/settings.js"; import "../../background_scripts/bg_utils.js"; import "../../background_scripts/exclusions.js"; import "../../background_scripts/commands.js"; const isEnabledForUrl = (request) => Exclusions.isEnabledForUrl(request.url); // These tests cover only the most basic aspects of excluded URLs and passKeys. context("Excluded URLs and pass keys", () => { setup(async () => { await Settings.onLoaded(); await Settings.set("exclusionRules", [ { pattern: "http*://mail.google.com/*", passKeys: "" }, { pattern: "http*://www.facebook.com/*", passKeys: "abab" }, { pattern: "http*://www.facebook.com/*", passKeys: "cdcd" }, { pattern: "http*://www.bbc.com/*", passKeys: "" }, { pattern: "http*://www.bbc.com/*", passKeys: "ab" }, { pattern: "http*://www.example.com/*", passKeys: "a bb c bba a" }, { pattern: "http*://www.duplicate.com/*", passKeys: "ace" }, { pattern: "http*://www.duplicate.com/*", passKeys: "bdf" }, ]); }); teardown(async () => { await Settings.clear(); }); should("be disabled for excluded sites", () => { const rule = isEnabledForUrl({ url: "http://mail.google.com/calendar/page" }); assert.isFalse(rule.isEnabledForUrl); assert.isFalse(rule.passKeys); }); should("be disabled for excluded sites, one exclusion", () => { const rule = isEnabledForUrl({ url: "http://www.bbc.com/calendar/page" }); assert.isFalse(rule.isEnabledForUrl); assert.isFalse(rule.passKeys); }); should("be enabled, but with pass keys", () => { const rule = isEnabledForUrl({ url: "https://www.facebook.com/something" }); assert.isTrue(rule.isEnabledForUrl); assert.equal(rule.passKeys, "abcd"); }); should("be enabled", () => { const rule = isEnabledForUrl({ url: "http://www.twitter.com/pages" }); assert.isTrue(rule.isEnabledForUrl); assert.isFalse(rule.passKeys); }); should("handle spaces and duplicates in passkeys", () => { const rule = isEnabledForUrl({ url: "http://www.example.com/pages" }); assert.isTrue(rule.isEnabledForUrl); assert.equal("abc", rule.passKeys); }); should("handle multiple passkeys rules", () => { const rule = isEnabledForUrl({ url: "http://www.duplicate.com/pages" }); assert.isTrue(rule.isEnabledForUrl); assert.equal("abcdef", rule.passKeys); }); should("be enabled when given malformed regular expressions", async () => { await Settings.set("exclusionRules", [ { pattern: "http*://www.bad-regexp.com/*[a-", passKeys: "" }, ]); const rule = isEnabledForUrl({ url: "http://www.bad-regexp.com/pages" }); assert.isTrue(rule.isEnabledForUrl); }); }); vimium-2.1.2/tests/unit_tests/handler_stack_test.js000066400000000000000000000075561460337006400226040ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/handler_stack.js"; context("handlerStack", () => { let handlerStack, handler1Called, handler2Called; setup(() => { stub(window, "DomUtils", {}); stub(DomUtils, "consumeKeyup", () => {}); stub(DomUtils, "suppressEvent", () => {}); stub(DomUtils, "suppressPropagation", () => {}); handlerStack = new HandlerStack(); handler1Called = false; handler2Called = false; }); should("bubble events", () => { handlerStack.push({ keydown: () => { return handler1Called = true; }, }); handlerStack.push({ keydown: () => { return handler2Called = true; }, }); handlerStack.bubbleEvent("keydown", {}); assert.isTrue(handler2Called); assert.isTrue(handler1Called); }); should("terminate bubbling on falsy return value", () => { handlerStack.push({ keydown: () => { return handler1Called = true; }, }); handlerStack.push({ keydown: () => { handler2Called = true; return false; }, }); handlerStack.bubbleEvent("keydown", {}); assert.isTrue(handler2Called); assert.isFalse(handler1Called); }); should("terminate bubbling on passEventToPage, and be true", () => { handlerStack.push({ keydown: () => { return handler1Called = true; }, }); handlerStack.push({ keydown: () => { handler2Called = true; return handlerStack.passEventToPage; }, }); assert.isTrue(handlerStack.bubbleEvent("keydown", {})); assert.isTrue(handler2Called); assert.isFalse(handler1Called); }); should("terminate bubbling on passEventToPage, and be false", () => { handlerStack.push({ keydown: () => { return handler1Called = true; }, }); handlerStack.push({ keydown: () => { handler2Called = true; return handlerStack.suppressPropagation; }, }); assert.isFalse(handlerStack.bubbleEvent("keydown", {})); assert.isTrue(handler2Called); assert.isFalse(handler1Called); }); should("restart bubbling on restartBubbling", () => { handler1Called = 0; handler2Called = 0; const id = handlerStack.push({ keydown: () => { handler1Called++; handlerStack.remove(id); return handlerStack.restartBubbling; }, }); handlerStack.push({ keydown: () => { handler2Called++; return true; }, }); assert.isTrue(handlerStack.bubbleEvent("keydown", {})); assert.isTrue(handler1Called === 1); assert.isTrue(handler2Called === 2); }); should("remove handlers correctly", () => { handlerStack.push({ keydown: () => { handler1Called = true; }, }); const handlerId = handlerStack.push({ keydown: () => { handler2Called = true; }, }); handlerStack.remove(handlerId); handlerStack.bubbleEvent("keydown", {}); assert.isFalse(handler2Called); assert.isTrue(handler1Called); }); should("remove handlers correctly", () => { const handlerId = handlerStack.push({ keydown: () => { handler1Called = true; }, }); handlerStack.push({ keydown: () => { handler2Called = true; }, }); handlerStack.remove(handlerId); handlerStack.bubbleEvent("keydown", {}); assert.isTrue(handler2Called); assert.isFalse(handler1Called); }); should("handle self-removing handlers correctly", () => { handlerStack.push({ keydown: () => { handler1Called = true; }, }); handlerStack.push({ keydown() { handler2Called = true; this.remove(); return true; }, }); handlerStack.bubbleEvent("keydown", {}); assert.isTrue(handler2Called); assert.isTrue(handler1Called); assert.equal(handlerStack.stack.length, 1); }); }); vimium-2.1.2/tests/unit_tests/main_test.js000066400000000000000000000046751460337006400207250ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/settings.js"; import "../../background_scripts/commands.js"; import "../../background_scripts/completion.js"; import "../../background_scripts/marks.js"; import "../../background_scripts/main.js"; context("HintCoordinator", () => { should("prepareToActivateLinKhintsMode", async () => { let receivedMessages = []; const frameIdToHintDescriptors = { "0": { frameId: 0, localIndex: 123, linkText: null }, "1": { frameId: 1, localIndex: 456, linkText: null }, }; stub(chrome.webNavigation, "getAllFrames", () => [{ frameId: 0 }, { frameId: 1 }]); stub(chrome.tabs, "sendMessage", async (_tabId, message, options) => { if (message.messageType == "getHintDescriptors") { return frameIdToHintDescriptors[options.frameId]; } else if (message.messageType == "activateMode") { receivedMessages.push(message); } }); await HintCoordinator.prepareToActivateLinkHintsMode(0, 0, { modeIndex: 0, isVimiumHelpDialog: false, }); receivedMessages = receivedMessages.map( (m) => Utils.pick(m, ["frameId", "frameIdToHintDescriptors"]), ); // Each frame should receive only the hint descriptors from the other frames. assert.equal([ { frameId: 0, frameIdToHintDescriptors: { "1": frameIdToHintDescriptors[1] } }, { frameId: 1, frameIdToHintDescriptors: { "0": frameIdToHintDescriptors[0] } }, ], receivedMessages); }); }); context("Selecting frames", () => { should("nextFrame", async () => { const focusedFrames = []; stub(chrome.webNavigation, "getAllFrames", () => [{ frameId: 1 }, { frameId: 2 }]); stub(chrome.tabs, "sendMessage", async (_tabId, message, options) => { if (message.handler == "getFocusStatus") { return { focused: options.frameId == 2, focusable: true }; } else if (message.handler == "focusFrame") { focusedFrames.push(options.frameId); } }); await BackgroundCommands.nextFrame(1, 0); assert.equal([1], focusedFrames); }); }); context("majorVersionHasIncreased", () => { should("return whether the major version has changed", () => { assert.equal(false, majorVersionHasIncreased(null)); shoulda.stub(Utils, "getCurrentVersion", () => "2.0.1"); assert.equal(false, majorVersionHasIncreased("2.0.0")); shoulda.stub(Utils, "getCurrentVersion", () => "2.1.0"); assert.equal(true, majorVersionHasIncreased("2.0.0")); }); }); vimium-2.1.2/tests/unit_tests/marks_test.js000066400000000000000000000035251460337006400211070ustar00rootroot00000000000000import "./test_helper.js"; import "../../background_scripts/marks.js"; context("marks", () => { const createMark = async (markProperties, tabProperties) => { const mark = Object.assign({ scrollX: 0, scrollY: 0 }, markProperties); const tab = Object.assign({ url: "http://example.com" }, tabProperties); const sender = { tab: tab }; await Marks.create(mark, sender); }; setup(() => { chrome.storage.session.clear(); chrome.storage.session.set({ vimiumSecret: "secret" }); }); teardown(() => { chrome.storage.session.clear(); chrome.storage.local.clear(); }); should("record the vimium secret in the mark's info", async () => { await createMark({ markName: "a" }); const key = Marks.getLocationKey("a"); const savedMark = (await chrome.storage.local.get(key))[key]; assert.equal("secret", savedMark.vimiumSecret); }); should("goto a mark when its tab exists", async () => { await createMark({ markName: "A" }, { id: 1 }); const tab = { url: "http://example.com" }; stub(window.chrome.tabs, "get", (id) => id == 1 ? tab : null); const updatedTabs = []; stub(window.chrome.tabs, "update", (id, properties) => updatedTabs[id] = properties); await Marks.goto({ markName: "A" }); assert.isTrue(updatedTabs[1] && updatedTabs[1].active); }); should("find a new tab if a mark's tab no longer exists", async () => { await createMark({ markName: "A" }, { id: 1 }); const tab = { url: "http://example.com", id: 2 }; stub(window.chrome.tabs, "get", (_id) => { throw new Error(); }); stub(window.chrome.tabs, "query", (_) => [tab]); const updatedTabs = []; stub(window.chrome.tabs, "update", (id, properties) => updatedTabs[id] = properties); await Marks.goto({ markName: "A" }); assert.isTrue(updatedTabs[2] && updatedTabs[2].active); }); }); vimium-2.1.2/tests/unit_tests/rect_test.js000066400000000000000000000256651460337006400207400ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/rect.js"; context("Rect", () => { should("set rect properties correctly", () => { const [x1, y1, x2, y2] = [1, 2, 3, 4]; const rect = Rect.create(x1, y1, x2, y2); assert.equal(rect.left, x1); assert.equal(rect.top, y1); assert.equal(rect.right, x2); assert.equal(rect.bottom, y2); assert.equal(rect.width, x2 - x1); assert.equal(rect.height, y2 - y1); }); should("translate rect horizontally", () => { const [x1, y1, x2, y2] = [1, 2, 3, 4]; const x = 5; const rect1 = Rect.create(x1, y1, x2, y2); const rect2 = Rect.translate(rect1, x); assert.equal(rect1.left + x, rect2.left); assert.equal(rect1.right + x, rect2.right); assert.equal(rect1.width, rect2.width); assert.equal(rect1.height, rect2.height); assert.equal(rect1.top, rect2.top); assert.equal(rect1.bottom, rect2.bottom); }); should("translate rect vertically", () => { const [x1, y1, x2, y2] = [1, 2, 3, 4]; const y = 5; const rect1 = Rect.create(x1, y1, x2, y2); const rect2 = Rect.translate(rect1, undefined, y); assert.equal(rect1.top + y, rect2.top); assert.equal(rect1.bottom + y, rect2.bottom); assert.equal(rect1.width, rect2.width); assert.equal(rect1.height, rect2.height); assert.equal(rect1.left, rect2.left); assert.equal(rect1.right, rect2.right); }); }); context("Rect subtraction", () => { context("unchanged by rects outside", () => { should("left, above", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(-2, -2, -1, -1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("left", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(-2, 0, -1, 1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("left, below", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(-2, 2, -1, 3); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("right, above", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(2, -2, 3, -1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("right", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(2, 0, 3, 1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("right, below", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(2, 2, 3, 3); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("above", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(0, -2, 1, -1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("below", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(0, 2, 1, 3); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); }); context("unchanged by rects touching", () => { should("left, above", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(-1, -1, 0, 0); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("left", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(-1, 0, 0, 1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("left, below", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(-1, 1, 0, 2); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("right, above", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(1, -1, 2, 0); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("right", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(1, 0, 2, 1); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("right, below", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(1, 1, 2, 2); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("above", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(0, -1, 1, 0); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); should("below", () => { const rect1 = Rect.create(0, 0, 1, 1); const rect2 = Rect.create(0, 1, 1, 2); const rects = Rect.subtract(rect1, rect2); assert.equal(rects.length, 1); const rect = rects[0]; assert.isTrue(Rect.equals(rect1, rect)); }); }); should("have nothing when subtracting itself", () => { const rect = Rect.create(0, 0, 1, 1); const rects = Rect.subtract(rect, rect); assert.equal(rects.length, 0); }); should("not overlap subtracted rect", () => { const rect = Rect.create(0, 0, 3, 3); for (let x = -2; x <= 2; x++) { for (let y = -2; y <= 2; y++) { for (let width = 1; width <= 3; width++) { for (let height = 1; height <= 3; height++) { const subtractRect = Rect.create(x, y, x + width, y + height); const resultRects = Rect.subtract(rect, subtractRect); for (const resultRect of resultRects) { assert.isFalse(Rect.intersects(subtractRect, resultRect)); } } } } } }); should("be contained in original rect", () => { const rect = Rect.create(0, 0, 3, 3); for (let x = -2; x <= 2; x++) { for (let y = -2; y <= 2; y++) { for (let width = 1; width <= 3; width++) { for (let height = 1; height <= 3; height++) { const subtractRect = Rect.create(x, y, x + width, y + height); const resultRects = Rect.subtract(rect, subtractRect); for (const resultRect of resultRects) { assert.isTrue(Rect.intersects(rect, resultRect)); } } } } } }); should("contain the subtracted rect in the original minus the results", () => { const rect = Rect.create(0, 0, 3, 3); for (let x = -2; x <= 2; x++) { for (let y = -2; y <= 2; y++) { for (let width = 1; width <= 3; width++) { for (let height = 1; height <= 3; height++) { const subtractRect = Rect.create(x, y, x + width, y + height); const resultRects = Rect.subtract(rect, subtractRect); let resultComplement = [Rect.copy(rect)]; for (const resultRect of resultRects) { resultComplement = Array.prototype.concat.apply( [], resultComplement.map((rect) => Rect.subtract(rect, resultRect)), ); } assert.isTrue((resultComplement.length === 0) || (resultComplement.length === 1)); if (resultComplement.length === 1) { const complementRect = resultComplement[0]; assert.isTrue(Rect.intersects(subtractRect, complementRect)); } } } } } }); }); context("Rect overlaps", () => { should("detect that a rect overlaps itself", () => { const rect = Rect.create(2, 2, 4, 4); assert.isTrue(Rect.intersectsStrict(rect, rect)); }); should("detect that non-overlapping rectangles do not overlap on the left", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(0, 2, 1, 4); assert.isFalse(Rect.intersectsStrict(rect1, rect2)); }); should("detect that non-overlapping rectangles do not overlap on the right", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(5, 2, 6, 4); assert.isFalse(Rect.intersectsStrict(rect1, rect2)); }); should("detect that non-overlapping rectangles do not overlap on the top", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(2, 0, 2, 1); assert.isFalse(Rect.intersectsStrict(rect1, rect2)); }); should("detect that non-overlapping rectangles do not overlap on the bottom", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(2, 5, 2, 6); assert.isFalse(Rect.intersectsStrict(rect1, rect2)); }); should("detect overlapping rectangles on the left", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(0, 2, 2, 4); assert.isTrue(Rect.intersectsStrict(rect1, rect2)); }); should("detect overlapping rectangles on the right", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(4, 2, 5, 4); assert.isTrue(Rect.intersectsStrict(rect1, rect2)); }); should("detect overlapping rectangles on the top", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(2, 4, 4, 5); assert.isTrue(Rect.intersectsStrict(rect1, rect2)); }); should("detect overlapping rectangles on the bottom", () => { const rect1 = Rect.create(2, 2, 4, 4); const rect2 = Rect.create(2, 0, 4, 2); assert.isTrue(Rect.intersectsStrict(rect1, rect2)); }); should("detect overlapping rectangles when second rectangle is contained in first", () => { const rect1 = Rect.create(1, 1, 4, 4); const rect2 = Rect.create(2, 2, 3, 3); assert.isTrue(Rect.intersectsStrict(rect1, rect2)); }); should("detect overlapping rectangles when first rectangle is contained in second", () => { const rect1 = Rect.create(1, 1, 4, 4); const rect2 = Rect.create(2, 2, 3, 3); assert.isTrue(Rect.intersectsStrict(rect2, rect1)); }); }); vimium-2.1.2/tests/unit_tests/settings_test.js000066400000000000000000000016301460337006400216250ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/settings.js"; context("settings", () => { setup(async () => { // Prior to Vimium 2.0.0, the settings values were encoded as JSON strings. await chrome.storage.sync.set({ scrollStepSize: JSON.stringify(123) }); }); teardown(() => { Settings.clear(); }); should("Run v2.0.0 migration when loading settings", async () => { let storage = await chrome.storage.sync.get(null); assert.equal("123", storage.scrollStepSize); // The JSON value should've been migrated to an int when loading settings. await Settings.load(); const settings = Settings.getSettings(); assert.equal(123, settings["scrollStepSize"]); // When writing settings, the JSON value should be persisted back to storage. await Settings.set(settings); storage = await chrome.storage.sync.get(null); assert.equal(123, storage.scrollStepSize); }); }); vimium-2.1.2/tests/unit_tests/tab_recency_test.js000066400000000000000000000063411460337006400222470ustar00rootroot00000000000000import "./test_helper.js"; import "../../background_scripts/tab_recency.js"; context("TabRecency", () => { let tabRecency; setup(() => tabRecency = new TabRecency()); context("order", () => { setup(async () => { stub(chrome.tabs, "query", () => Promise.resolve([])); await tabRecency.init(); tabRecency.queueAction("register", (1)); tabRecency.queueAction("register", (2)); tabRecency.queueAction("register", (3)); tabRecency.queueAction("register", (4)); tabRecency.queueAction("deregister", (4)); tabRecency.queueAction("register", (2)); }); should("have the correct entries in the correct order", () => { const expected = [2, 3, 1]; assert.equal(expected, tabRecency.getTabsByRecency()); }); should("score tabs by recency; current tab should be last", () => { const score = (id) => tabRecency.recencyScore(id); assert.equal(0, score(2)); assert.isTrue(score(2) < score(1)); assert.isTrue(score(1) < score(3)); }); }); should("navigate actions are queued until state from storage is loaded", async () => { let onActivated; stub(chrome.tabs.onActivated, "addListener", (fn) => { onActivated = fn; }); let resolveStorage; const storagePromise = new Promise((resolve, _) => resolveStorage = resolve); stub(chrome.storage.session, "get", () => storagePromise); tabRecency.init(); // Here, chrome.tabs.onActivated listeners have been added by tabrecency, but the // chrome.storage.session data hasn't yet loaded. onActivated({ tabId: 5 }); resolveStorage({}); await tabRecency.init(); assert.equal([5], tabRecency.getTabsByRecency()); }); should("loadFromStorage handles empty values", async () => { stub(chrome.tabs, "query", () => Promise.resolve([{ id: 1 }])); stub(chrome.storage.session, "get", () => Promise.resolve({})); await tabRecency.init(); assert.equal([], tabRecency.getTabsByRecency()); stub(chrome.storage.session, "get", () => Promise.resolve({ tabRecency: {} })); await tabRecency.loadFromStorage(); assert.equal([], tabRecency.getTabsByRecency()); }); should("loadFromStorage works", async () => { const tabs = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; stub(chrome.tabs, "query", () => Promise.resolve(tabs)); const storage = { tabRecency: { 1: 5, 2: 6 } }; stub(chrome.storage.session, "get", () => Promise.resolve(storage)); // Even though the in-storage tab counters are higher than the in-memory tabs, during // loading, the in-memory tab counters are adjusted to be the most recent. await tabRecency.init(); assert.equal([2, 1], tabRecency.getTabsByRecency()); tabRecency.queueAction("register", (3)); tabRecency.queueAction("register", (1)); assert.equal([1, 3, 2], tabRecency.getTabsByRecency()); }); should("loadFromStorage prunes out tabs which are no longer active", async () => { const tabs = [{ id: 1 }]; stub(chrome.tabs, "query", () => Promise.resolve(tabs)); const storage = { tabRecency: { 1: 5, 2: 6 } }; stub(chrome.storage.session, "get", () => Promise.resolve(storage)); await tabRecency.init(); assert.equal([1], tabRecency.getTabsByRecency()); }); }); vimium-2.1.2/tests/unit_tests/test_chrome_stubs.js000066400000000000000000000102451460337006400224640ustar00rootroot00000000000000// // This file contains stubs for a number of browser and chrome APIs which are missing in Deno. // window.document = { createElement() { return {}; }, addEventListener() {}, }; // There are 3 chrome.storage.* objects with identical APIs. // - areaName: one of "local", "sync", "session". const createStorageAPI = (areaName) => { const storage = { store: {}, async set(items) { let key, value; chrome.runtime.lastError = undefined; for (key of Object.keys(items)) { value = items[key]; this.store[key] = value; } for (key of Object.keys(items)) { value = items[key]; window.chrome.storage.onChanged.call(key, value, areaName); } }, async get(keysArg) { chrome.runtime.lastError = undefined; if (keysArg == null) { return globalThis.structuredClone(this.store); } else if (typeof keysArg == "string") { const result = {}; result[keysArg] = globalThis.structuredClone(this.store[keysArg]); return result; } else { const result = {}; for (key of keysArg) { result[key] = globalThis.structuredClone(this.store[key]); } return result; } }, async remove(key) { chrome.runtime.lastError = undefined; if (key in this.store) { delete this.store[key]; } window.chrome.storage.onChanged.callEmpty(key); }, async clear() { // TODO: Consider firing the change listener if Chrome's API implementation does. this.store = {}; }, }; // The "session" storage has one API that the others don't. if (areaName == "session") storage.setAccessLevel = () => {}; return storage; }; window.chrome = { areRunningVimiumTests: true, runtime: { getURL() { return ""; }, getManifest() { return { version: "1.2.3" }; }, onConnect: { addListener() { return true; }, }, onMessage: { addListener() { return true; }, }, onInstalled: { addListener() {}, }, onStartup: { addListener() {}, }, }, extension: { getURL(path) { return path; }, getBackgroundPage() { return {}; }, getViews() { return []; }, }, tabs: { get(_id) {}, onUpdated: { addListener() { return true; }, }, onAttached: { addListener() { return true; }, }, onMoved: { addListener() { return true; }, }, onRemoved: { addListener() { return true; }, }, onActivated: { addListener() { return true; }, }, onReplaced: { addListener() { return true; }, }, query() { return true; }, sendMessage(_id, _properties) {}, update(_id, _properties) {}, }, webNavigation: { onHistoryStateUpdated: { addListener() {}, }, onReferenceFragmentUpdated: { addListener() {}, }, onCommitted: { addListener() {}, }, }, windows: { onRemoved: { addListener() { return true; }, }, getAll() { return true; }, getCurrent() { return {}; }, onFocusChanged: { addListener() { return true; }, }, update(_id, _properties) {}, }, browserAction: { setBadgeBackgroundColor() {}, }, sessions: { MAX_SESSION_RESULTS: 25, }, storage: { onChanged: { addListener(func) { this.func = func; }, // Fake a callback from chrome.storage.sync. call(key, value, area) { chrome.runtime.lastError = undefined; const key_value = {}; key_value[key] = { newValue: value }; if (this.func) return this.func(key_value, area); }, callEmpty(key) { chrome.runtime.lastError = undefined; if (this.func) { const items = {}; items[key] = {}; this.func(items, "sync"); } }, }, local: createStorageAPI("sync"), sync: createStorageAPI("sync"), session: createStorageAPI("session"), }, bookmarks: { getTree: () => [], }, }; vimium-2.1.2/tests/unit_tests/test_helper.js000066400000000000000000000010311460337006400212370ustar00rootroot00000000000000import * as shoulda from "../vendor/shoulda.js"; import "./test_chrome_stubs.js"; import "../../lib/utils.js"; const shouldaSubset = { assert: shoulda.assert, context: shoulda.context, ensureCalled: shoulda.ensureCalled, setup: shoulda.setup, should: shoulda.should, shoulda: shoulda, stub: shoulda.stub, returns: shoulda.returns, teardown: shoulda.teardown, }; globalThis.isUnitTests = true; // Attach shoulda's functions, like setup, context, should, to the global namespace. Object.assign(window, shouldaSubset); vimium-2.1.2/tests/unit_tests/url_utils_test.js000066400000000000000000000103171460337006400220110ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/settings.js"; import "../../lib/url_utils.js"; context("isUrl", () => { should("accept valid URLs", async () => { assert.isTrue(await UrlUtils.isUrl("www.google.com")); assert.isTrue(await UrlUtils.isUrl("www.bbc.co.uk")); assert.isTrue(await UrlUtils.isUrl("yahoo.com")); assert.isTrue(await UrlUtils.isUrl("nunames.nu")); assert.isTrue(await UrlUtils.isUrl("user:pass@ftp.xyz.com/test")); assert.isTrue(await UrlUtils.isUrl("localhost/index.html")); assert.isTrue(await UrlUtils.isUrl("127.0.0.1:8192/test.php")); // IPv6 assert.isTrue(await UrlUtils.isUrl("[::]:9000")); // Long TLDs assert.isTrue(await UrlUtils.isUrl("testing.social")); assert.isTrue(await UrlUtils.isUrl("testing.onion")); // // Internal URLs. assert.isTrue( await UrlUtils.isUrl( "moz-extension://c66906b4-3785-4a60-97bc-094a6366017e/pages/options.html", ), ); }); should("reject invalid URLs", async () => { assert.isFalse(await UrlUtils.isUrl("a.x")); assert.isFalse(await UrlUtils.isUrl("www-domain-tld")); assert.isFalse(await UrlUtils.isUrl("http://www.example.com/ has-space")); }); }); context("convertToUrl", async () => { should("detect and clean up valid URLs", async () => { assert.equal("http://www.google.com/", await UrlUtils.convertToUrl("http://www.google.com/")); assert.equal( "http://www.google.com/", await UrlUtils.convertToUrl(" http://www.google.com/ "), ); assert.equal("http://www.google.com", await UrlUtils.convertToUrl("www.google.com")); assert.equal("http://google.com", await UrlUtils.convertToUrl("google.com")); assert.equal("http://localhost", await UrlUtils.convertToUrl("localhost")); assert.equal("http://xyz.museum", await UrlUtils.convertToUrl("xyz.museum")); assert.equal("chrome://extensions", await UrlUtils.convertToUrl("chrome://extensions")); assert.equal( "http://user:pass@ftp.xyz.com/test", await UrlUtils.convertToUrl("user:pass@ftp.xyz.com/test"), ); assert.equal("http://127.0.0.1", await UrlUtils.convertToUrl("127.0.0.1")); assert.equal("http://127.0.0.1:8080", await UrlUtils.convertToUrl("127.0.0.1:8080")); assert.equal("http://[::]:8080", await UrlUtils.convertToUrl("[::]:8080")); assert.equal("view-source: 0.0.0.0", await UrlUtils.convertToUrl("view-source: 0.0.0.0")); assert.equal( "javascript:alert('25 % 20 * 25%20');", await UrlUtils.convertToUrl("javascript:alert('25 % 20 * 25%20');"), ); }); should("convert non-URL terms into search queries", async () => { await Settings.load(); assert.equal("https://www.google.com/search?q=google", await UrlUtils.convertToUrl("google")); assert.equal( "https://www.google.com/search?q=go%20ogle.com", await UrlUtils.convertToUrl("go ogle.com"), ); assert.equal( "https://www.google.com/search?q=%40twitter", await UrlUtils.convertToUrl("@twitter"), ); }); }); context("createSearchUrl", () => { should("replace %S without encoding", () => { assert.equal( "https://www.github.com/philc/vimium/pulls", UrlUtils.createSearchUrl("vimium/pulls", "https://www.github.com/philc/%S"), ); }); }); context("hasChromePrefix", () => { should("detect chrome prefixes of URLs", () => { assert.isTrue(UrlUtils.hasChromePrefix("about:foobar")); assert.isTrue(UrlUtils.hasChromePrefix("view-source:foobar")); assert.isTrue(UrlUtils.hasChromePrefix("chrome-extension:foobar")); assert.isTrue(UrlUtils.hasChromePrefix("data:foobar")); assert.isTrue(UrlUtils.hasChromePrefix("data:")); assert.isFalse(UrlUtils.hasChromePrefix("")); assert.isFalse(UrlUtils.hasChromePrefix("about")); assert.isFalse(UrlUtils.hasChromePrefix("view-source")); assert.isFalse(UrlUtils.hasChromePrefix("chrome-extension")); assert.isFalse(UrlUtils.hasChromePrefix("data")); assert.isFalse(UrlUtils.hasChromePrefix("data :foobar")); }); }); context("hasJavascriptPrefix", () => { should("detect javascript: URLs", () => { assert.isTrue(UrlUtils.hasJavascriptPrefix("javascript:foobar")); assert.isFalse(UrlUtils.hasJavascriptPrefix("http:foobar")); }); }); vimium-2.1.2/tests/unit_tests/utils_test.js000066400000000000000000000120601460337006400211240ustar00rootroot00000000000000import "./test_helper.js"; import "../../lib/settings.js"; context("extractQuery", () => { should("extract queries from search URLs", () => { assert.equal( "bbc sport 1", Utils.extractQuery( "https://www.google.ie/search?q=%s", "https://www.google.ie/search?q=bbc+sport+1", ), ); assert.equal( "bbc sport 2", Utils.extractQuery( "http://www.google.ie/search?q=%s", "https://www.google.ie/search?q=bbc+sport+2", ), ); assert.equal( "bbc sport 3", Utils.extractQuery( "https://www.google.ie/search?q=%s", "http://www.google.ie/search?q=bbc+sport+3", ), ); assert.equal( "bbc sport 4", Utils.extractQuery( "https://www.google.ie/search?q=%s", "http://www.google.ie/search?q=bbc+sport+4&blah", ), ); }); }); context("decodeURIByParts", () => { should("decode javascript: URLs", () => { assert.equal("foobar", Utils.decodeURIByParts("foobar")); assert.equal(" ", Utils.decodeURIByParts("%20")); assert.equal("25 % 20 25 ", Utils.decodeURIByParts("25 % 20 25%20")); }); }); context("compare versions", () => { should("compare correctly", () => { assert.equal(0, Utils.compareVersions("1.40.1", "1.40.1")); assert.equal(0, Utils.compareVersions("1.40", "1.40.0")); assert.equal(0, Utils.compareVersions("1.40.0", "1.40")); assert.equal(-1, Utils.compareVersions("1.40.1", "1.40.2")); assert.equal(-1, Utils.compareVersions("1.40.1", "1.41")); assert.equal(-1, Utils.compareVersions("1.40", "1.40.1")); assert.equal(1, Utils.compareVersions("1.41", "1.40")); assert.equal(1, Utils.compareVersions("1.41.0", "1.40")); assert.equal(1, Utils.compareVersions("1.41.1", "1.41")); }); }); context("makeIdempotent", () => { let func; let count = 0; setup(() => { count = 0; func = Utils.makeIdempotent((n) => { if (n == null) { n = 1; } count += n; }); }); should("call a function once", () => { func(); assert.equal(1, count); }); should("call a function once with an argument", () => { func(2); assert.equal(2, count); }); should("not call a function a second time", () => { func(); assert.equal(1, count); }); should("not call a function a second time", () => { func(); assert.equal(1, count); func(); assert.equal(1, count); }); }); context("distinctCharacters", () => { should( "eliminate duplicate characters", () => assert.equal("abc", Utils.distinctCharacters("bbabaabbacabbbab")), ); }); context("escapeRegexSpecialCharacters", () => { should("escape regexp special characters", () => { const str = "-[]/{}()*+?.^$|"; const regexp = new RegExp(Utils.escapeRegexSpecialCharacters(str)); assert.isTrue(regexp.test(str)); }); }); context("extractQuery", () => { should("extract the query terms from a URL", () => { const url = "https://www.google.ie/search?q=star+wars&foo&bar"; const searchUrl = "https://www.google.ie/search?q=%s"; assert.equal("star wars", Utils.extractQuery(searchUrl, url)); }); should("require trailing URL components", () => { const url = "https://www.google.ie/search?q=star+wars&foo&bar"; const searchUrl = "https://www.google.ie/search?q=%s&foobar=x"; assert.equal(null, Utils.extractQuery(searchUrl, url)); }); should("accept trailing URL components", () => { const url = "https://www.google.ie/search?q=star+wars&foo&bar&foobar=x"; const searchUrl = "https://www.google.ie/search?q=%s&foobar=x"; assert.equal("star wars", Utils.extractQuery(searchUrl, url)); }); }); context("pick", () => { should("omit properties", () => { assert.equal({ a: 1, b: 2 }, Utils.pick({ a: 1, b: 2, c: 3 }, ["a", "b", "d"])); }); }); context("parseLines", () => { should("omit whitespace", () => { assert.equal(0, Utils.parseLines(" \n \n ").length); }); should("omit comments", () => { assert.equal(0, Utils.parseLines(' # comment \n " comment \n ').length); }); should("join lines", () => { assert.equal(1, Utils.parseLines("a\\\nb").length); assert.equal("ab", Utils.parseLines("a\\\nb")[0]); }); should("trim lines", () => { assert.equal(2, Utils.parseLines(" a \n b").length); assert.equal("a", Utils.parseLines(" a \n b")[0]); assert.equal("b", Utils.parseLines(" a \n b")[1]); }); }); context("UserSearchEngines", () => { should("parse out search engine text", () => { const config = [ "g: http://google.com/%s Google Search", "random line", "# comment", " w: http://wikipedia.org/%s", ].join("\n"); const results = UserSearchEngines.parseConfig(config); assert.equal( { g: new UserSearchEngine({ keyword: "g", url: "http://google.com/%s", description: "Google Search", }), w: new UserSearchEngine({ keyword: "w", url: "http://wikipedia.org/%s", description: "search (w)", }), }, results, ); }); }); vimium-2.1.2/tests/vendor/000077500000000000000000000000001460337006400154645ustar00rootroot00000000000000vimium-2.1.2/tests/vendor/shoulda.js000066400000000000000000000231521460337006400174640ustar00rootroot00000000000000/* * A unit testing micro framework. Tests are grouped into "contexts", each of which can share common * setup functions. */ /* * Assertions. */ const assert = { isTrue: function (value) { if (!value) { this.fail("Expected true, but got " + value); } }, isFalse: function (value) { if (value) { this.fail("Expected false, but got " + value); } }, // Does a deep-equal check on complex objects. equal: function (expected, actual) { const areEqual = typeof expected === "object" ? JSON.stringify(expected) === JSON.stringify(actual) : expected === actual; if (!areEqual) { this.fail( `\nExpected:\n${this._print(expected)}\nGot:\n${this._print(actual)}\n`, ); } }, // We cannot name this function simply "throws", because it's a reserved JavaScript keyword. throwsError: function (expression, errorName) { try { expression(); } catch (error) { if (errorName) { if (error.name == errorName) { return; } else { assert.fail( `Expected error ${errorName} to be thrown but error ${error.name} was thrown instead.`, ); } } else { return; } } if (errorName) { assert.fail(`Expected error ${errorName} but no error was thrown.`); } else { assert.fail("Expected error but none was thrown."); } }, fail: function (message) { throw new AssertionError(message); }, // Used for printing the arguments passed to assertions. _print: function (object) { if (object === null) return "null"; else if (object === undefined) return "undefined"; else if (typeof object === "string") return '"' + object + '"'; else { try { // Pretty-print with indentation. return JSON.stringify(object, undefined, 2); } catch (_) { // `object` might not be stringifiable (e.g. DOM nodes), or JSON.stringify may not exist. return object.toString(); } } }, }; /* * ensureCalled ensures the given function is called by the end of the test case. This is useful * when testing APIs that use callbacks. */ function ensureCalled(fn) { const wrappedFunction = function () { const i = Tests.requiredCallbacks.indexOf(wrappedFunction); if (i >= 0) { Tests.requiredCallbacks.splice(i, 1); // Delete. } return fn.apply(null, arguments); }; Tests.requiredCallbacks.push(wrappedFunction); return wrappedFunction; } function AssertionError(message) { this.name = AssertionError; this.message = message; } AssertionError.prototype = new Error(); AssertionError.prototype.constructor = AssertionError; /* * A Context is a named set of test methods and nested contexts, with optional setup and teardown * blocks. */ function Context(name) { this.name = name; this.setupMethod = null; this.teardownMethod = null; this.contexts = []; this.tests = []; } const contextStack = []; /* * See the usage documentation for details on how to use the "context" and "should" functions. */ function context(name, fn) { if (typeof fn != "function") { throw new Error("context() requires a function argument."); } const newContext = new Context(name); if (contextStack.length > 0) { contextStack[contextStack.length - 1].tests.push(newContext); } else { Tests.topLevelContexts.push(newContext); } contextStack.push(newContext); fn(); contextStack.pop(); return newContext; } context.only = (name, fn) => { const c = context(name, fn); c.isFocused = true; Tests.focusIsUsed = true; }; function setup(fn) { contextStack[contextStack.length - 1].setupMethod = fn; } function teardown(fn) { contextStack[contextStack.length - 1].teardownMethod = fn; } function should(name, fn) { const test = { name, fn }; contextStack[contextStack.length - 1].tests.push(test); return test; } should.only = (name, fn) => { const test = should(name, fn); test.isFocused = true; Tests.focusIsUsed = true; }; /* * Tests is used to run tests and keep track of the count of successes and failures. */ const Tests = { topLevelContexts: [], testsRun: 0, testsFailed: 0, // The list of callbacks to ensure are called by the end of the test. This list is appended to by // `ensureCalled`. requiredCallbacks: [], // True if, during the collection phase, should.only or context.only was used. focusIsUsed: false, /* * Run all contexts which have been defined. * - testNameFilter: a String. If provided, only run tests which match testNameFilter will be run. */ run: async function (testNameFilter) { // Run every top level context (i.e. those not defined within another context). These will in // turn run any nested contexts. The very last context ever added to Tests.testContexts is a top // level context. Note that any contexts which have not already been run by a previous top level // context must themselves be top level contexts. this.testsRun = 0; this.testsFailed = 0; for (const context of this.topLevelContexts) { await this.runContext(context, [], testNameFilter); } this.printTestSummary(); return this.testsFailed == 0; }, /* * This resets (clears) the state of shoulda, including the tests which have been defined. This is * useful when running shoulda tests in a REPL environment, to prevent tests from getting defined * multiple times when a file is re-evaluated. */ reset: function () { this.topLevelContexts = []; this.focusedTests = []; this.focusIsUsed = false; }, /* * Run a context. This runs the test methods defined in the context first, and then any nested * contexts. */ runContext: async function (context, parentContexts, testNameFilter) { parentContexts = parentContexts.concat([context]); for (const test of context.tests) { if (test instanceof Context) { await this.runContext(test, parentContexts, testNameFilter); } else { await this.runTest(test, parentContexts, testNameFilter); } } }, /* * Run a test method. This will run all setup methods in all contexts, and then all teardown * methods. * - testMethod: an object with keys name, fn. * - contexts: an array of Contexts, ordered outer to inner. * - testNameFilter: A String. If provided, only run the test if it matches testNameFilter. */ runTest: async function (testMethod, contexts, testNameFilter) { if ( this.focusIsUsed && !testMethod.isFocused && !contexts.some((c) => c.isFocused) ) { return; } const fullTestName = this.fullyQualifiedName(testMethod.name, contexts); if (testNameFilter && !fullTestName.includes(testNameFilter)) { return; } this.testsRun++; let failureMessage = null; // This is the scope which all references to "this" in the setup and test methods will resolve to. const testScope = {}; try { try { for (const context of contexts) { if (context.setupMethod) { await context.setupMethod.call(testScope, testScope); } } await testMethod.fn.call(testScope, testScope); } finally { for (const context of contexts) { if (context.teardownMethod) { await context.teardownMethod.call(testScope, testScope); } } } } catch (error) { // Note that error can be either a String or an Error. const failedAssertion = error instanceof AssertionError; failureMessage = failedAssertion ? error.message : error.toString(); if (!failedAssertion && error.stack) { failureMessage += "\n" + error.stack; } } if (!failureMessage && this.requiredCallbacks.length > 0) { failureMessage = "A callback function should have been called during this test, but it wasn't."; } if (failureMessage) { Tests.testsFailed++; Tests.printFailure(fullTestName, failureMessage); } this.requiredCallbacks = []; clearStubs(); }, // The fully-qualified name of the test or context, e.g. "context1: context2: testName". fullyQualifiedName: function (testName, contexts) { return contexts.map((c) => c.name).concat(testName).join(": "); }, printTestSummary: function () { if (this.testsFailed > 0) { console.log(`Fail (${Tests.testsFailed}/${Tests.testsRun})`); } else { console.log(`Pass (${Tests.testsRun}/${Tests.testsRun})`); } }, printFailure: function (testName, failureMessage) { console.log(`Fail "${testName}"`, failureMessage); }, }; function run(testNameFilter) { return Tests.run(testNameFilter); } function reset() { Tests.reset(); } /* * Stats of the latest test run. */ function getStats() { return { failed: Tests.testsFailed, run: Tests.testsRun, }; } /* * Stubs */ const stubbedObjects = []; function stub(object, propertyName, returnValue) { stubbedObjects.push({ object: object, propertyName: propertyName, original: object[propertyName], }); object[propertyName] = returnValue; } /* * returns creates a function which returns the given value. This is useful for stubbing functions * to return a hardcoded value. */ function returns(value) { return () => value; } function clearStubs() { // Restore stubs in the reverse order they were defined in, in case the same property was stubbed // twice. for (let i = stubbedObjects.length - 1; i >= 0; i--) { const stubProperties = stubbedObjects[i]; stubProperties.object[stubProperties.propertyName] = stubProperties.original; } } export { assert, context, ensureCalled, getStats, reset, returns, run, setup, should, stub, teardown, };