Full Code of philc/vimium for AI

master e5163eaa32d4 cached
118 files
749.5 KB
197.4k tokens
826 symbols
1 requests
Download .txt
Showing preview only (787K chars total). Download the full file or copy to clipboard to get everything.
Repository: philc/vimium
Branch: master
Commit: e5163eaa32d4
Files: 118
Total size: 749.5 KB

Directory structure:
gitextract_bk_2yr_t/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── pull_request_template.md
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── CREDITS
├── MIT-LICENSE.txt
├── README.md
├── background_scripts/
│   ├── all_commands.js
│   ├── bg_utils.js
│   ├── commands.js
│   ├── completion/
│   │   ├── completers.js
│   │   ├── ranking.js
│   │   ├── search_engines.js
│   │   └── search_wrapper.js
│   ├── exclusions.js
│   ├── main.js
│   ├── marks.js
│   ├── reload.js
│   ├── tab_operations.js
│   ├── tab_recency.js
│   └── user_search_engines.js
├── build_scripts/
│   └── write_command_listing_page.js
├── content_scripts/
│   ├── file_urls.css
│   ├── hud.js
│   ├── link_hints.js
│   ├── marks.js
│   ├── mode.js
│   ├── mode_find.js
│   ├── mode_insert.js
│   ├── mode_key_handler.js
│   ├── mode_normal.js
│   ├── mode_visual.js
│   ├── scroller.js
│   ├── ui_component.js
│   ├── vimium.css
│   ├── vimium_frontend.js
│   └── vomnibar.js
├── deno.json
├── lib/
│   ├── chrome_api_stubs.js
│   ├── dom_utils.js
│   ├── find_mode_history.js
│   ├── handler_stack.js
│   ├── keyboard_utils.js
│   ├── rect.js
│   ├── settings.js
│   ├── types.js
│   ├── url_utils.js
│   └── utils.js
├── make.js
├── manifest.json
├── pages/
│   ├── action.css
│   ├── action.html
│   ├── action.js
│   ├── all_content_scripts.js
│   ├── command_listing.css
│   ├── command_listing.html
│   ├── command_listing.js
│   ├── doc.css
│   ├── doc_search_completion.html
│   ├── doc_search_completion.js
│   ├── exclusion_rules_editor.js
│   ├── help_dialog_page.css
│   ├── help_dialog_page.html
│   ├── help_dialog_page.js
│   ├── hud_page.css
│   ├── hud_page.html
│   ├── hud_page.js
│   ├── key_mappings.css
│   ├── options.css
│   ├── options.html
│   ├── options.js
│   ├── reload.html
│   ├── ui_component_messenger.js
│   ├── vomnibar_page.css
│   ├── vomnibar_page.html
│   └── vomnibar_page.js
├── resources/
│   └── tlds.txt
├── test_harnesses/
│   ├── cross_origin_iframe.html
│   ├── event_capture.html
│   ├── form.html
│   ├── has_popup_and_link_hud.html
│   ├── iframe.html
│   ├── page_with_links.html
│   ├── visibility_test.html
│   ├── vomnibar_harness.html
│   └── vomnibar_harness.js
└── tests/
    ├── dom_tests/
    │   ├── dom_test_setup.js
    │   ├── dom_tests.html
    │   ├── dom_tests.js
    │   └── dom_utils_test.js
    ├── unit_tests/
    │   ├── bg_utils_test.js
    │   ├── command_listing_test.js
    │   ├── commands_test.js
    │   ├── completion/
    │   │   ├── completers_test.js
    │   │   ├── ranking_test.js
    │   │   └── search_engines_test.js
    │   ├── doc_search_completion_test.js
    │   ├── exclusion_test.js
    │   ├── handler_stack_test.js
    │   ├── help_dialog_test.js
    │   ├── hud_page_test.js
    │   ├── link_hints_test.js
    │   ├── main_test.js
    │   ├── marks_test.js
    │   ├── options_page_test.js
    │   ├── rect_test.js
    │   ├── settings_test.js
    │   ├── tab_operations_test.js
    │   ├── tab_recency_test.js
    │   ├── test_chrome_stubs.js
    │   ├── test_helper.js
    │   ├── ui_component_test.js
    │   ├── url_utils_test.js
    │   ├── user_search_engines_test.js
    │   ├── utils_test.js
    │   └── vomnibar_page_test.js
    └── vendor/
        └── shoulda.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
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.


================================================
FILE: .github/pull_request_template.md
================================================
## 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`.


================================================
FILE: .gitignore
================================================
dist

================================================
FILE: CHANGELOG.md
================================================
2.4.1, 2.4.2 (2026-03-07)

- Fix issue where existing users were mistakenly opted-in to
  [Vimium's new tab page](https://github.com/philc/vimium/pull/4795) for the `createTab` command.
  (https://github.com/philc/vimium/issues/4859)
- Fix exclusion rules is empty in downloaded backups. (https://github.com/philc/vimium/issues/4839)

2.4.0 (2026-01-27)

- Support a Vimium new tab experience: the browser can be configured to open a blank Vimium page as
  the new tab page. In Vimium's settings, the Vomnibar can configured to open on new tabs. See
  [instructions](https://github.com/philc/vimium?tab=readme-ov-file#how-to-allow-vimium-to-work-on-new-tab-pages).
  (https://github.com/philc/vimium/pull/4795)
- Make Google search result links work on sub-tabs like "Web".
  (https://github.com/philc/vimium/issues/4750)

2.3.1 (2025-11-12)

- Fix Vimium to work with Chrome 144. (https://github.com/philc/vimium/issues/4785)

2.3.0 (2025-06-30)

- Add a command listing page, which documents all commands and their options. Access it
  [on the web](https://vimium.github.io/commands/), or from the Vimium Options page > Show available
  commands.
- Some internal CSS classes were changed for Vimium's UI. This may affect those who have customized
  Vimium's CSS via the options page. (https://github.com/philc/vimium/issues/4668)
- Breaking change: when creating a mapping for `setZoom`, a `level` argument is now required. E.g.:
  `map z2 setZoom level=2.0`.
- Make `Vomnibar.activateBookmark` accept a `query` option.
  (https://github.com/philc/vimium/pull/4591)
- Fix `openCopiedUrlInCurrentTab` doesn't launch search queries.
  (https://github.com/philc/vimium/issues/4657)
- Make `openCopiedUrlInCurrentTab` accept a `position` option.
- Update `goPrevious` and `goNext` commands to handle google.com's new layout.
  (https://github.com/philc/vimium/issues/4650)
- Add a "hide update notifications" option for silencing "Vimium has been updated" notifications.
  (https://github.com/philc/vimium/issues/4346)
- Use dark mode styles in the HUD when the browser is in dark mode.
- Bug fixes.

2.2.1 (2025-03-20)

- Fix findSelected and findSelectedBackwards commands (https://github.com/philc/vimium/issues/4655)
- Fix openCopiedUrlInCurrentTab (https://github.com/philc/vimium/issues/4654)

2.2.0 (2025-03-08)

- Use the browser's default search engine. [(#2598)](https://github.com/philc/vimium/issues/2598)
- Add "reload hard" command (R). ([#4445](https://github.com/philc/vimium/pull/4445)).
- Add zoomIn (zi), zoomOut (zo), zoomReset (z0), and setZoom commands.
  ([#4488](https://github.com/philc/vimium/pull/4488))
- Add findSelected and findSelectedBackwards commands.
  ([#4502](https://github.com/philc/vimium/pull/4502))
- Options page: improve UI, add error validation.
- Make tab commands handle Firefox hidden tabs.
- Bug fixes.

2.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)

- 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. `<s-left>`)
  ([#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 `<Enter>` 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 `<tab>`, `<enter>`, `<delete>`, `<insert>`, `<home>` and `<end>`.
- 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
  `<Shift>` for hint characters.
- With `map R reload hard`, the reload command now asks Chrome to bypass its cache.
- You can now map `<c-[>` 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: `<c-a-X>`.
  - 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 `<a-m>`), see also
    [advanced usage](https://github.com/philc/vimium/wiki/Tips-and-Tricks#muting-tabs).
- Other new features:
  - You can now map `<backspace>` to a Vimium command (e.g. `map <backspace> goBack`).
  - For link hints, when one hint marker is covered by another, `<Space>` now rotates the stacking
    order. If you use filtered hints, you'll need to use a modifier (e.g. `<c-Space>`).
- 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.
  - `<c-a-[>` is no longer handled (incorrectly) as `Escape`. This also affects `<Alt-Gr-[>`.
  - 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 <c-]> passNextKey`, you
    can close Facebook's messenger popups with `<c-]><Esc>`.
- 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:
  - `<c-a-[>` 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 <tt>\`\`</tt> to jump back to the previous position after jump-like movements: <br/> (`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)

- `<c-d>`, `<c-f>` 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 (`<a-f>`) 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 &lt;left&gt;, &lt;right&gt;, &lt;up&gt;,
  &lt;down&gt;, &lt;f1&gt;, &lt;f2&gt;, 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)

- `<c-f>` and `<c-b>` 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.
- `<c-[>` is now equivalent to `Esc`, to match the behavior of VIM.
- `<c-e>` and `<c-y>` 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.


================================================
FILE: CONTRIBUTING.md
================================================
# 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://github.com/philc/book-notes/blob/master/engineering/working%20in%20public%20-%20nadia%20eghbal.md#the-structure-of-an-open-source-project-chap-2):
  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 "This Firefox" on the left side
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.
2. `deno run -A npm:puppeteer browsers install chrome` to install puppeteer
3. `./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.


================================================
FILE: CREDITS
================================================
Authors & Maintainers:
  Ilya Sukhar <ilya.sukhar@gmail.com> (github: ilya)
  Phil Crosby <phil.crosby@gmail.com> (github: philc)

Contributors:
  acrollet
  Adam Lindberg <hello@alind.io> (github: eproxus)
  akhilman
  Ângelo Otávio Nuffer Nunes <angelonuffer@gmail.com> (github: angelonuffer)
  Bernardo B. Marques <bernardo.fire@gmail.com> (github: bernardofire)
  Bill Casarin <jb@jb55.com> (github: jb55)
  Bill Mill (github: llimllib)
  Branden Rolston <brolston@gmail.com> (github: branden)
  Caleb Spare <cespare@gmail.com> (github: cespare)
  Carl Helmertz <helmertz@gmail.com> (github: chelmertz)
  Christian Stefanescu (github: stchris)
  ConradIrwin
  Daniel MacDougall <dmacdougall@gmail.com> (github: dmacdougall)
  drizzd
  gpurkins
  hogelog
  int3
  Johannes Emerich (github: knuton)
  Julian Naydichev <rublind@gmail.com> (github: naydichev)
  Justin Blake <justin@hentzia.com> (github: blaix)
  Knorkebrot
  lack
  markstos
  Matthew Cline <matt@nightrealms.com>
  Matt Garriott (github: mgarriott)
  Matthew Ryan (github: mrmr1993)
  Michael Hauser-Raspe (github: mijoharas)
  Murph (github: pandeiro)
  Niklas Baumstark <niklas.baumstark@gmail.com> (github: niklasb)
  rodimius
  Stephen Blott (github: smblott-github)
  Svein-Erik Larsen <feinom@gmail.com> (github: feinom)
  Tim Morgan <tim@timmorgan.org> (github: seven1m)
  tsigo
  R.T. Lechow <rtlechow@gmail.com> (github: rtlechow)
  Wang Ning <daning106@gmail.com> (github:daning)
  Werner Laurensse (github: ab3)
  Timo Sand <timo.j.sand@gmail.com> (github: deiga)
  Shiyong Chen <billbill290@gmail.com> (github: UncleBill)
  Utkarsh Upadhyay <musically.ut@gmail.com) (github: musically-ut)
  Michael Salihi <admin@prestance-informatique.fr> (github: PrestanceDesign)
  Dahan Gong <gdh1995@qq.com> (github: gdh1995)
  Scott Pinkelman <scott@scottpinkelman.com> (github: sco-tt)
  Darryl Pogue <darryl@dpogue.ca> (github: dpogue)
  tobimensch
  Ramiro Araujo <rama.araujo@gmail.com> (github: ramiroaraujo)
  Daniel Skogly <daniel@wishy.gift> (github: poacher2k)
  Matt Wanchap <matt@wanchap.com> (github: mwanchap)
  Leo Solidum <leo.g.solidum@gmail.com> (github: leosolid)

Feel free to add real names in addition to GitHub usernames.


================================================
FILE: MIT-LICENSE.txt
================================================
Copyright (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.

================================================
FILE: README.md
================================================
# 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.

[Watch the demo video](https://www.youtube.com/watch?v=t67Sn0RGK54).

**Installation instructions:**

- Chrome:
  [Chrome web store](https://chromewebstore.google.com/detail/vimium/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 `<c-x>`, `<m-x>`, and `<a-x>` for ctrl+x, meta+x, and alt+x
respectively. For shift+x and ctrl-shift-x, just type `X` and `<c-X>`. 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
    <a-p>   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
    <a-f>   open multiple links in a new tab
    gi      focus the first (or n-th) text input box on the page. Use <tab> 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
    R       Hard reload the page (skip the cache)

Vimium supports command repetition so, for example, hitting `5t` will open 5 tabs in rapid
succession. `<Esc>` (or `<c-[>`) will clear any partial commands in the queue and will also exit
insert and find modes.

There are additional commands which aren't included in this README; 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 statements per line:

- `map key command`: Maps a key to a Vimium command. Overrides Chrome's default behavior for that
  key, 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 <c-d> scrollPageDown` maps ctrl+d to scrolling the page down. Chrome's default behavior of
  showing a bookmark dialog is suppressed.
- `map r reload hard` maps the r key to reloading the page, and also includes the "hard" option to
  hard-reload the page.
- `unmap <c-d>` removes any mapping for ctrl+d and restores Chrome's default behavior.
- `unmap r` removes any mapping for the r key.

See the [docs](https://vimium.github.io/commands/) for every Vimium command and its options.

You can add comments to key mappings by starting a line with `"` or `#`.

The following special keys are available for mapping:

- `<c-*>`, `<a-*>`, `<s-*>`, `<m-*>` for ctrl, alt, shift, and meta (command on Mac) respectively
  with any key. Replace `*` with the key of choice.
- `<left>`, `<right>`, `<up>`, `<down>` for the arrow keys.
- `<f1>` through `<f12>` for the function keys.
- `<space>` for the space key.
- `<tab>`, `<enter>`, `<delete>`, `<backspace>`, `<insert>`, `<home>` and `<end>` for the
  corresponding non-printable keys.

Shifts are automatically detected so, for example, `<c-&>` corresponds to ctrl+shift+7 on an English
keyboard.

## How to allow Vimium to work on new tab pages

- Vimium will work on new tab pages which are opened with Vimium's `createTab` command (mapped to
  `t` by default).
- To have Vimium work on <em>all</em> new tab pages opened by the browser (e.g. via `cmd-t` or
  `ctrl-t` shortcuts), a companion
  [Vimium New Tab Page extension](https://github.com/philc/vimium-new-tab/) is required.
- Once that is installed, all new tabs will open a blank Vimium new tab page.

## More documentation

- [FAQ](https://github.com/philc/vimium/wiki/FAQ)
- [Command listing](https://vimium.github.io/commands/)
- [Vimium's GitHub wiki](https://github.com/philc/vimium/wiki): documentation for the more advanced
  features.

## 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.


================================================
FILE: background_scripts/all_commands.js
================================================
// This is the order they will be shown in the help dialog.
//
// Properties:
// - advanced: advanced commands are not shown in the help dialog by default.
// - background: whether this command has to be run by the background page.
// - desc: shown in the help dialog and command listing page.
// - details: extra help information that will only be shown on the command listing page.
// - group: commands are displayed in groups in the help dialog and command listing.
// - noRepeat: whether this command can be used with a count key prefix.
// - repeatLimit: the number of allowed repetitions of this command before the user is prompted for
//   confirmation.
// - topFrame: whether this command must be run only in the top frame of a page.
//
const allCommands = [
  //
  // Navigation
  //

  {
    name: "scrollDown",
    desc: "Scroll down",
    group: "navigation",
  },

  {
    name: "scrollUp",
    desc: "Scroll up",
    group: "navigation",
  },

  {
    name: "scrollToTop",
    desc: "Scroll to the top of the page",
    group: "navigation",
  },

  {
    name: "scrollToBottom",
    desc: "Scroll to the bottom of the page",
    group: "navigation",
  },

  {
    name: "scrollPageDown",
    desc: "Scroll a half page down",
    group: "navigation",
  },

  {
    name: "scrollPageUp",
    desc: "Scroll a half page up",
    group: "navigation",
  },

  {
    name: "scrollFullPageDown",
    desc: "Scroll a full page down",
    group: "navigation",
  },

  {
    name: "scrollFullPageUp",
    desc: "Scroll a full page up",
    group: "navigation",
  },

  {
    name: "scrollLeft",
    desc: "Scroll left",
    group: "navigation",
  },

  {
    name: "scrollRight",
    desc: "Scroll right",
    group: "navigation",
    advanced: true,
  },

  {
    name: "scrollToLeft",
    desc: "Scroll all the way to the left",
    group: "navigation",
    advanced: true,
  },

  {
    name: "scrollToRight",
    desc: "Scroll all the way to the right",
    group: "navigation",
  },

  {
    name: "reload",
    desc: "Reload the page",
    group: "navigation",
    background: true,
    options: {
      hard: "Perform a hard reload, forcing the browser to bypass its cache.",
    },
  },

  {
    name: "copyCurrentUrl",
    desc: "Copy the current URL to the clipboard",
    group: "navigation",
    noRepeat: true,
  },

  {
    name: "openCopiedUrlInCurrentTab",
    desc: "Open the clipboard's URL in the current tab",
    group: "navigation",
    noRepeat: true,
  },

  {
    name: "openCopiedUrlInNewTab",
    desc: "Open the clipboard's URL in a new tab",
    group: "navigation",
    noRepeat: true,
    options: {
      position: "Where to place the tab in the tab bar. " +
        "One of `start`, `before`, `after`, `end`. `after` is the default.",
    },
  },

  {
    name: "goUp",
    desc: "Go up the URL hierarchy",
    group: "navigation",
    advanced: true,
  },

  {
    name: "goToRoot",
    desc: "Go to the root of current URL hierarchy",
    group: "navigation",
    advanced: true,
  },

  {
    name: "enterInsertMode",
    desc: "Enter insert mode",
    group: "navigation",
    noRepeat: true,
  },

  {
    name: "enterVisualMode",
    desc: "Enter visual mode",
    group: "navigation",
    noRepeat: true,
  },

  {
    name: "enterVisualLineMode",
    desc: "Enter visual line mode",
    group: "navigation",
    advanced: true,
    noRepeat: true,
  },

  {
    name: "passNextKey",
    desc: "Pass the next key to the page",
    options: {
      normal: "Optional. Enter Vimium's normal mode, and ignore any defined pass keys.",
    },
    group: "navigation",
    advanced: true,
  },

  {
    name: "focusInput",
    desc: "Focus the first text input on the page",
    group: "navigation",
  },

  {
    name: "LinkHints.activateMode",
    desc: "Open a link in the current tab",
    options: {
      action: "one of `hover`, `focus`, `copy-text`. When a link is selected, " +
        "instead of clicking on the link, perform the specified action.",
    },
    group: "navigation",
    advanced: true,
  },

  {
    name: "LinkHints.activateModeToOpenInNewTab",
    desc: "Open a link in a new tab",
    group: "navigation",
  },

  {
    name: "LinkHints.activateModeToOpenInNewForegroundTab",
    desc: "Open a link in a new tab & switch to it",
    group: "navigation",
  },

  {
    name: "LinkHints.activateModeWithQueue",
    desc: "Open multiple links in a new tab",
    group: "navigation",
    advanced: true,
    noRepeat: true,
  },

  {
    name: "LinkHints.activateModeToDownloadLink",
    desc: "Download link url",
    group: "navigation",
    advanced: true,
  },

  {
    name: "LinkHints.activateModeToOpenIncognito",
    desc: "Open a link in incognito window",
    group: "navigation",
    advanced: true,
  },

  {
    name: "LinkHints.activateModeToCopyLinkUrl",
    desc: "Copy a link URL to the clipboard",
    group: "navigation",
    advanced: true,
  },

  {
    name: "goPrevious",
    desc: "Follow the link labeled previous or <",
    group: "navigation",
    advanced: true,
    noRepeat: true,
  },

  {
    name: "goNext",
    desc: "Follow the link labeled next or >",
    group: "navigation",
    advanced: true,
    noRepeat: true,
  },

  {
    name: "nextFrame",
    desc: "Select the next frame on the page",
    group: "navigation",
    background: true,
  },

  {
    name: "mainFrame",
    desc: "Select the page's main/top frame",
    group: "navigation",
    topFrame: true,
    noRepeat: true,
  },

  {
    name: "Marks.activateCreateMode",
    desc: "Create a new mark",
    details: "Do this by typing the key bound to this command, and then a letter. " +
      "This will set a mark bound to that letter. Lowercase letters are local marks and uppercase " +
      "letters are global marks.",
    options: {
      swap: "Swap global and local marks. This option exists because in a browser, global marks " +
        "are generally more useful than local marks, and so it may be desirable to make lowercase " +
        "letters represent global marks rather than local marks.",
    },
    group: "navigation",
    advanced: true,
    noRepeat: true,
  },

  {
    name: "Marks.activateGotoMode",
    desc: "Jump to a mark",
    options: {
      swap: "Swap global and local marks. This option exists because in a browser, global marks " +
        "are generally more useful than local marks, and so it may be desirable to make lowercase " +
        "letters represent global marks rather than local marks.",
    },
    group: "navigation",
    advanced: true,
    noRepeat: true,
  },

  //
  // Vomnibar
  //

  {
    name: "Vomnibar.activate",
    desc: "Open URL, bookmark or history entry",
    options: {
      query: "The text to prefill the Vomnibar with.",
      keyword: 'The keyword of a search engine defined in the "Custom search engines" ' +
        "section of the Vimium Options page. The Vomnibar will be scoped to use that search engine.",
    },
    group: "vomnibar",
    topFrame: true,
  },

  {
    name: "Vomnibar.activateInNewTab",
    desc: "Open URL, bookmark or history entry in a new tab",
    group: "vomnibar",
    options: {
      query: "The text to prefill the Vomnibar with.",
      keyword: 'The keyword of a search engine defined in the "Custom search engines" ' +
        "section of the Vimium Options page. The Vomnibar will be scoped to use that search engine.",
    },
    topFrame: true,
  },

  {
    name: "Vomnibar.activateBookmarks",
    desc: "Open a bookmark",
    group: "vomnibar",
    options: {
      query: "The text to prefill the Vomnibar with.",
    },
    topFrame: true,
  },

  {
    name: "Vomnibar.activateBookmarksInNewTab",
    desc: "Open a bookmark in a new tab",
    group: "vomnibar",
    options: {
      query: "The text to prefill the Vomnibar with.",
    },
    topFrame: true,
  },

  {
    name: "Vomnibar.activateTabSelection",
    desc: "Search through your open tabs",
    group: "vomnibar",
    topFrame: true,
  },

  {
    name: "Vomnibar.activateEditUrl",
    desc: "Edit the current URL",
    group: "vomnibar",
    topFrame: true,
  },

  {
    name: "Vomnibar.activateEditUrlInNewTab",
    desc: "Edit the current URL and open in a new tab",
    group: "vomnibar",
    topFrame: true,
  },

  //
  // Find
  //

  {
    name: "enterFindMode",
    desc: "Enter find mode.",
    group: "find",
    noRepeat: true,
  },

  {
    name: "performFind",
    desc: "Cycle forward to the next find match",
    group: "find",
  },

  {
    name: "performBackwardsFind",
    desc: "Cycle backward to the previous find match",
    group: "find",
  },

  {
    name: "findSelected",
    desc: "Find the selected text",
    group: "find",
    advanced: true,
  },

  {
    name: "findSelectedBackwards",
    desc: "Find the selected text, searching backwards",
    group: "find",
    advanced: true,
  },

  //
  // History
  //

  {
    name: "goBack",
    desc: "Go back in history",
    group: "history",
  },

  {
    name: "goForward",
    desc: "Go forward in history",
    group: "history",
  },

  //
  // Tabs
  //

  {
    name: "createTab",
    desc: "Create new tab",
    options: {
      "(any url)": "Open this URL, rather than the browser's new tab page. " +
        "E.g.: `map X createTab https://example.com`",
      window: "Create the tab in a new window",
      incognito: "Create the tab in an incognito window",
      position: "Where to place the tab in the tab bar. " +
        "One of `start`, `before`, `after`, `end`. `after` is the default.",
    },
    group: "tabs",
    background: true,
    repeatLimit: 20,
  },

  {
    name: "previousTab",
    desc: "Go one tab left",
    group: "tabs",
    background: true,
  },

  {
    name: "nextTab",
    desc: "Go one tab right",
    group: "tabs",
    background: true,
  },

  {
    name: "visitPreviousTab",
    desc: "Go to previously-visited tab",
    group: "tabs",
    background: true,
  },

  {
    name: "firstTab",
    desc: "Go to the first tab",
    group: "tabs",
    background: true,
  },

  {
    name: "lastTab",
    desc: "Go to the last tab",
    group: "tabs",
    background: true,
  },

  {
    name: "duplicateTab",
    desc: "Duplicate current tab",
    group: "tabs",
    background: true,
    repeatLimit: 20,
  },

  {
    name: "togglePinTab",
    desc: "Pin or unpin current tab",
    group: "tabs",
    background: true,
  },

  {
    name: "toggleMuteTab",
    desc: "Mute or unmute current tab",
    options: {
      all: "Mute all tabs.",
      other: "Mute every tab except the current one.",
    },
    group: "tabs",
    background: true,
    noRepeat: true,
  },

  {
    name: "removeTab",
    desc: "Close current tab",
    group: "tabs",
    background: true,
    // Don't close (in one command invocation) more than the number of tabs that can be re-opened by
    // the browser.
    repeatLimit: chrome.sessions?.MAX_SESSION_RESULTS || 25,
  },

  {
    name: "restoreTab",
    desc: "Restore closed tab",
    group: "tabs",
    background: true,
    repeatLimit: 20,
  },

  {
    name: "moveTabToNewWindow",
    desc: "Move tab to new window",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "closeTabsOnLeft",
    desc: "Close tabs on the left",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "closeTabsOnRight",
    desc: "Close tabs on the right",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "closeOtherTabs",
    desc: "Close all other tabs",
    group: "tabs",
    advanced: true,
    background: true,
    noRepeat: true,
  },

  {
    name: "moveTabLeft",
    desc: "Move tab to the left",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "moveTabRight",
    desc: "Move tab to the right",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "setZoom",
    desc: "Set zoom",
    group: "tabs",
    advanced: true,
    background: true,
    options: {
      level: "The zoom level. This can be a range of [0.25, 5.0]. 1.0 is the default.",
    },
  },

  {
    name: "zoomIn",
    desc: "Zoom in",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "zoomOut",
    desc: "Zoom out",
    group: "tabs",
    advanced: true,
    background: true,
  },

  {
    name: "zoomReset",
    desc: "Reset zoom",
    group: "tabs",
    advanced: true,
    background: true,
  },

  //
  // Misc
  //

  {
    name: "toggleViewSource",
    desc: "View page source",
    group: "misc",
    advanced: true,
    noRepeat: true,
  },

  {
    name: "showHelp",
    desc: "Show help",
    group: "misc",
    noRepeat: true,
    topFrame: true,
  },
];

export { allCommands };


================================================
FILE: background_scripts/bg_utils.js
================================================
import { TabRecency } from "./tab_recency.js";

// 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.
export function isFirefox() {
  // 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.
  return globalThis.browser?.runtime.getURL("").startsWith("moz") ?? false;
}

export async function getFirefoxVersion() {
  return isFirefox() ? (await browser.runtime.getBrowserInfo()).version : null;
}

// TODO(philc): tabRecency imports bg_utils. We should resovle the cycle for the sake of clarity.
export const tabRecency = new TabRecency();
tabRecency.init();


================================================
FILE: background_scripts/commands.js
================================================
import { allCommands } from "./all_commands.js";

// A specification for a command that's currently bound to a key sequence, as defined by the default
// key bindings, or as it appears in the user's keymapping settings.
export class RegistryEntry {
  // Array of keys.
  keySequence;
  // Name of the command.
  command;
  // Whether this command can be used with a count key prefix.
  noRepeat;
  // The number of allowed repetitions of this command before the user is prompted for confirmation.
  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;

  constructor(o) {
    Object.seal(this);
    if (o) Object.assign(this, o);
  }
}

// This is intentionally a superset of valid modifiers (a, c, m, s).
const modifier = "(?:[a-zA-Z]-)";
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");

// Remove comments and leading/trailing whitespace from a list of lines, and merge lines where the
// last character on the preceding line is "\".
function parseLines(text) {
  return text.replace(/\\\n/g, "")
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => (line.length > 0) && !(Array.from('#"').includes(line[0])));
}

// Returns the index of the nth occurrence of the regexp in the string. -1 if not found.
function nthRegexIndex(str, regex, n) {
  if (!regex.global) {
    regex = new RegExp(regex.source, regex.flags + "g");
  }
  let match;
  let count = 0;
  while ((match = regex.exec(str)) !== null) {
    count++;
    if (count === n) {
      return match.index;
    }
    // Prevent infinite loop for zero-length matches.
    if (match.index === regex.lastIndex) {
      regex.lastIndex++;
    }
  }
  return -1;
}

const KeyMappingsParser = {
  // 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, validationErrors }.
  parse(configText, shouldLogWarnings) {
    let keyToRegistryEntry = {};
    let mapKeyRegistry = {};
    let errors = [];
    const configLines = parseLines(configText);
    const commandsByName = Utils.keyBy(allCommands, "name");

    const validModifiers = ["a", "c", "m", "s"];
    const validateParsedKey = function (key) {
      if (!key?.match(modifiedKey)) return;
      // Check that the modifier is valid and not capitalized.
      const mod = key.split("-")[0].slice(1);
      if (!validModifiers.includes(mod)) {
        return `${key} has an invalid modifier; valid modifiers are ${validModifiers}`;
      }
    };
    const validateUrl = function (str) {
      try {
        new URL(str);
        return true;
      } catch {
        return false;
      }
    };

    for (const line of configLines) {
      const tokens = line.split(/\s+/);
      const action = tokens[0].toLowerCase();
      switch (action) {
        case "map": {
          if (tokens.length < 3) {
            errors.push(`"map requires at least 2 arguments on line ${line}`);
            continue;
          }
          const [_, key, command] = tokens;
          let optionString;
          const optionsStart = nthRegexIndex(line, /\s+/, 3);
          if (optionsStart == -1) {
            optionString = "";
          } else {
            optionString = line.slice(optionsStart).trim();
          }
          const commandInfo = commandsByName[command];
          if (!commandInfo) {
            errors.push(`"${command}" is not a valid command in the line: ${line}`);
            continue;
          }
          const keySequence = this.parseKeySequence(key);
          const keyErrors = keySequence.map((k) => validateParsedKey(k)).filter((e) => e);
          if (keyErrors.length > 0) {
            errors = errors.concat(keyErrors);
            continue;
          }
          const options = this.parseCommandOptions(optionString);
          const allowedOptions = Object.keys(commandInfo.options || {});
          if (!commandInfo.noRepeat) {
            allowedOptions.push("count");
          }
          let hasUnknownOption = false;
          for (const option of Object.keys(options)) {
            if (allowedOptions.includes(option)) continue;
            if (allowedOptions.includes("(any url)")) {
              // Since this command allows for any URL as an argument, we perform some basic
              // validation to ensure the provided option string is indeed a URL.
              if (validateUrl(option)) continue;
              hasUnknownOption = true;
              errors.push(
                `Command ${command} does not support option ${option}. ` +
                  `Is this meant to be a valid URL?`,
              );
              break;
            } else {
              hasUnknownOption = true;
              errors.push(`Command ${command} does not support option ${option}`);
              break;
            }
          }
          if (hasUnknownOption) break;
          keyToRegistryEntry[key] = new RegistryEntry({
            keySequence,
            command,
            noRepeat: commandInfo.noRepeat,
            repeatLimit: commandInfo.repeatLimit,
            background: commandInfo.background,
            topFrame: commandInfo.topFrame,
            options,
          });
          break;
        }
        case "unmap": {
          if (tokens.length != 2) {
            errors.push(`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) {
            errors.push(`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 {
            errors.push(
              `mapkey only supports mapping keys which are single characters. Line: ${line}`,
            );
          }
          break;
        }
        default:
          errors.push(`"${action}" is not a valid config command in line: ${line}`);
      }
    }

    return {
      keyToRegistryEntry,
      keyToMappedKey: mapKeyRegistry,
      validationErrors: errors,
    };
  },

  // Lower-case the appropriate portions of named keys.
  //
  // A key name is one of three forms exemplified by <c-a> <left> or <c-f12> (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 <Left> or <C-a>.
  // On the other hand, <c-a> and <c-A> 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. "<Space><c-A>b" -> ["<space>", "<c-A>", "b"].
  parseKeySequence(key) {
    if (key.length === 0) {
      return [];
      // Parse "<c-a>bcd" as "<c-a>" 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 these forms:
  //   key=value     - a value
  //   key="value"   - a value surrounded by quotes
  //   key           - a flag
  parseCommandOptions(optionString) {
    const options = {};
    while (optionString != "") {
      // Note that option names are allowed to be letters only; no numbers.
      let match, matchedString, key, value;
      // Case: option value surrounded by quotes (key= "a b"). Spaces are allowed in the value.
      if (match = optionString.match(/^([a-zA-Z]+)="([^"]+)"(\s+|$)/)) {
        matchedString = match[0];
        key = match[1];
        value = match[2];
      } // Case: option value not surrounded by quotes (key=value). Spaces aren't allowed.
      else if (match = optionString.match(/^([a-zA-Z]+)=(\S+)(\s+|$)/)) {
        matchedString = match[0];
        key = match[1];
        value = match[2];
      } // Case: single option (flag), or "any URL". This correctly parses URLs because URLs cannot
      // contain unescaped equals or space characters. The key will be the option's name (or the
      // URL), and the value will be true.
      else if (match = optionString.match(/^([^\s"]+)(\s+|$)/)) {
        matchedString = match[0];
        key = match[1];
        value = true;
      }
      // NOTE(philc): If this string doesn't match any of our option regexps, we could throw an
      // error here or use an assert. I think this might only happen in the case where there's a
      // single equals sign. For now, just add the whole string as a flag option. If the command in
      // question doesn't accept this option, then an error will get surfaced to the user.
      if (match == null) {
        console.log(`Warning: '${optionString}' isn't a valid option string.`);
        options[optionString] = true;
        break;
      }

      options[key] = value;
      optionString = optionString.slice(matchedString.length);
    }

    // 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)) {
        delete options.count;
      }
    }

    return options;
  },
};

const Commands = {
  // 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();
    Settings.addEventListener("change", async () => {
      await this.loadKeyMappings(Settings.get("keyMappings"));
    });
    await this.loadKeyMappings(Settings.get("keyMappings"));
  },

  // 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 = KeyMappingsParser.parse(
      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 });
  },

  // 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"]) {
            delete currentMapping[key][prop];
          }
        }
      }
    }
    await chrome.storage.session.set({
      normalModeKeyStateMapping: keyStateMapping,
      // Inform `KeyboardUtils.isEscape()` whether `<c-[>` should be interpreted as `Escape` (which it
      // is by default).
      useVimLikeEscape: !("<c-[>" in keyStateMapping),
    });
  },

  // Build the "commandToOptionsToKeys" data structure and place it in chrome's session storage.
  // This is used by the help page and commands listing.
  prepareHelpPageData() {
    /*
      Map of commands to option sets to keys to trigger that command option set.
      Commands with no options will have the empty string options set.
      Example:
      {
        "zoomReset": {
          "": ["z0", "zz"] // No options, with two key maps, ie: `map zz zoomReset`
        },
        "setZoom": {
          "1.1": ["z1"], // `map z1 setZoom 1.1`
          "1.2": ["z2"], // `map z2 setZoom 1.2`
        }
      }
    */
    const commandToOptionsToKeys = {};
    const formatOptionString = (options) => {
      return Object.entries(options)
        .map(([k, v]) => {
          // When the value of an option is true, then it was parsed as a flag.
          if (v === true) {
            return k;
          } else {
            return `${k}=${v}`;
          }
        })
        .join(" ");
    };
    for (const key of Object.keys(this.keyToRegistryEntry || {})) {
      const registryEntry = this.keyToRegistryEntry[key];
      const optionString = formatOptionString(registryEntry.options || {});
      commandToOptionsToKeys[registryEntry.command] ||= {};
      commandToOptionsToKeys[registryEntry.command][optionString] ||= [];
      commandToOptionsToKeys[registryEntry.command][optionString].push(key);
    }
    chrome.storage.session.set({ commandToOptionsToKeys });
  },
};

const defaultKeyMappings = {
  // Navigating the current page
  "j": "scrollDown",
  "k": "scrollUp",
  "h": "scrollLeft",
  "l": "scrollRight",
  "gg": "scrollToTop",
  "G": "scrollToBottom",
  "zH": "scrollToLeft",
  "zL": "scrollToRight",
  "<c-e>": "scrollDown",
  "<c-y>": "scrollUp",
  "d": "scrollPageDown",
  "u": "scrollPageUp",
  "r": "reload",
  "R": "reload hard",
  "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",
  "<a-f>": "LinkHints.activateModeWithQueue",
  "yf": "LinkHints.activateModeToCopyLinkUrl",

  // Using find
  "/": "enterFindMode",
  "n": "performFind",
  "N": "performBackwardsFind",
  "*": "findSelected",
  "#": "findSelectedBackwards",

  // 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",
  "<a-p>": "togglePinTab",
  "<a-m>": "toggleMuteTab",
  "zi": "zoomIn",
  "zo": "zoomOut",
  "z0": "zoomReset",

  // Marks
  "m": "Marks.activateCreateMode",
  "`": "Marks.activateGotoMode",

  // Misc
  "?": "showHelp",
  "gs": "toggleViewSource",
};

export {
  Commands,
  // Exported for unit tests.
  defaultKeyMappings,
  KeyMappingsParser,
  parseLines,
};


================================================
FILE: background_scripts/completion/completers.js
================================================
// This file contains the definition of the completers used for the Vomnibar's suggestion UI. A
// completer will take a query (whatever the user typed into the Vomnibar) and return a list of
// Suggestions, e.g. bookmarks, domains, URLs from history.
//
// The Vomnibar 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 Vomnibar.
//  - refresh(): (optional) refreshes the completer's data source (e.g. refetches the list of
//    bookmarks).
//  - cancel(): (optional) cancels any pending, cancelable action.

import * as bgUtils from "./../bg_utils.js";
import * as completionSearch from "./search_wrapper.js";
import * as userSearchEngines from "../user_search_engines.js";
import * as ranking from "./ranking.js";
import { RegexpCache } from "./ranking.js";

// Set this to true to render relevancy when debugging the ranking scores.
const showRelevancy = false;

// TODO(philc): Consider moving out the "computeRelevancy" function.
export 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
      ? `<span class='relevancy'>${this.computeRelevancy()}</span>`
      : "";
    const insertTextClass = this.insertText ? "" : "no-insert-text";
    const insertTextIndicator = "&#8618;"; // 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 = `<img class="icon" src="${faviconUrl.toString()}" />`;
    }
    if (this.isCustomSearch) {
      this.html = `\
<div class="top-half">
   <span class="source ${insertTextClass}">${insertTextIndicator}</span><span class="source">${this.description}</span>
   <span class="title">${this.highlightQueryTerms(Utils.escapeHtml(this.title))}</span>
   ${relevancyHtml}
 </div>\
`;
    } else {
      this.html = `\
<div class="top-half">
   <span class="source ${insertTextClass}">${insertTextIndicator}</span><span class="source">${this.description}</span>
   <span class="title">${this.highlightQueryTerms(Utils.escapeHtml(this.title))}</span>
 </div>
 <div class="bottom-half">
  <span class="source no-insert-text">${insertTextIndicator}</span>${faviconHtml}<span class="url">${
        this.highlightQueryTerms(Utils.escapeHtml(this.shortenUrl()))
      }</span>
  ${relevancyHtml}
</div>\
`;
    }
    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 <span>.
  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) +
        `<span class='match'>${string.substring(start, end)}</span>` +
        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];
    for (const range of ranges) {
      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.
export 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.hasJavascriptProtocol == null) {
          bookmark.hasJavascriptProtocol = UrlUtils.hasJavascriptProtocol(bookmark.url);
        }
        if (bookmark.hasJavascriptProtocol && bookmark.shortUrl == null) {
          bookmark.shortUrl = "javascript:...";
        }
        const suggestionUrl = bookmark.shortUrl != null ? bookmark.shortUrl : bookmark.url;
        return ranking.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 = [];
    for (const folder of bookmarks) {
      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) {
      for (const child of bookmark.children) {
        this.traverseBookmarksRecursive(child, results, bookmark);
      }
    }
  }

  computeRelevancy(suggestion) {
    return ranking.wordRelevancy(
      suggestion.queryTerms,
      suggestion.shortUrl || suggestion.url,
      suggestion.title,
    );
  }
}

export class HistoryCompleter {
  // - seenTabToOpenCompletionList: true if the user has typed only <Tab>, 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) => ranking.matches(queryTerms, entry.url, entry.title));
    } else if (seenTabToOpenCompletionList) {
      // The user has typed <Tab> 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 = ranking.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 = ranking.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.
export class DomainCompleter {
  // A map of domain -> { entry: <historyEntry>, referenceCount: <count> }
  // - `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 = ranking.recencyScore(this.domains[domain].entry.lastVisitTime || 0);
      const wordRelevancy = ranking.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 = {};
    for (const entry of HistoryCache.history) {
      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 map 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 {
      for (const url of toRemove.urls) {
        const domain = this.parseDomainAndScheme(url);
        const entry = this.domains[domain];
        if (entry == null) continue;
        entry.referenceCount--;
        if (entry.referenceCount <= 0) {
          delete this.domains[domain];
        }
      }
    }
  }

  // Return something like "http://www.example.com" or false.
  parseDomainAndScheme(url) {
    if (UrlUtils.urlHasProtocol(url) && !UrlUtils.hasChromeProtocol(url)) {
      return 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.
export 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) => ranking.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 ranking.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title);
    } else {
      return bgUtils.tabRecency.recencyScore(suggestion.tabId);
    }
  }
}

export class SearchEngineCompleter {
  cancel() {
    completionSearch.cancel();
  }

  // Returns the UserSearchEngine for the given query. Returns null if the query does not begin with
  // a keyword from one of the user's search engines.
  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.
    const keyword = parts[0];
    if (parts.length <= 1) return null;
    // Don't match queries for built-in properties like "constructor". See #4396.
    if (Object.hasOwn(userSearchEngines.keywordToEngine, keyword)) {
      return userSearchEngines.keywordToEngine[keyword];
    }
    return null;
  }

  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 * ranking.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;

export 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;
  }
}

// Provides cached access to Chrome's history. As the user browses to new pages, we add those pages
// to this history cache.
export 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 {
      for (const url of toRemove.urls) {
        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;
  }
};


================================================
FILE: background_scripts/completion/ranking.js
================================================
// Utilities which help us compute a relevancy score for a given item.

// 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.
export function 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.
const 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.
function scoreTerm(term, string) {
  let score = 0;
  let count = 0;
  const nonMatching = string.split(RegexpCache.get(term));
  if (nonMatching.length > 1) {
    // Have match.
    score = 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 += matchWeights.matchStartOfWord;
      if (RegexpCache.get(term, "\\b", "\\b").test(string)) {
        // Have match of whole word.
        score += 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.
export function 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] = scoreTerm(term, url);
    urlScore += s;
    urlCount += c;
    if (title) {
      [s, c] = scoreTerm(term, title);
      titleScore += s;
      titleCount += c;
    }
  }

  const maximumPossibleScore = matchWeights.maximumScore * queryTerms.length;

  // Normalize scores.
  urlScore /= maximumPossibleScore;
  urlScore *= normalizeDifference(urlCount, url.length);

  if (title) {
    titleScore /= maximumPossibleScore;
    titleScore *= 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)

let oneMonthAgo = 1000 * 60 * 60 * 24 * 30;

// 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.
export function recencyScore(lastAccessedTime) {
  const recency = Date.now() - lastAccessedTime;
  const recencyDifference = Math.max(0, oneMonthAgo - recency) / oneMonthAgo;

  // recencyScore is between [0, 1]. It is 1 when recenyDifference is 0. This quadratic equation
  // will incresingly discount older history entries.
  let recencyScore = recencyDifference * recencyDifference * recencyDifference;

  // Calibrate recencyScore vis-a-vis word-relevancy scores.
  return recencyScore *= matchWeights.recencyCalibrator;
}

// Takes the difference of two numbers and returns a number between [0, 1] (the percentage difference).
function 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.
export 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"));
  },
};


================================================
FILE: background_scripts/completion/search_engines.js
================================================
// An engine provides search suggestions for a online search engine.
//
// An "engineUrl" is used for fetching suggestions, whereas a "searchUrl" is used for the actual
// search itself.
//
// Each 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 "./completers.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.join(" "), this.engineUrl);
  }
}

export 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 ";

export 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 "<tt>map of </tt>" 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));
  }
}

export 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];
  }
}

export 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];
  }
}

export 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];
  }
}

export 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);
  }
}

export 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);
  }
}

export 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.
export 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.
export 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];
  }
}

// Kagi is a paid ad-free search engine
export class Kagi extends BaseEngine {
  constructor() {
    super({
      engineUrl: "https://kagi.com/autosuggest?q=%s",
      regexps: ["^https?://www\\.kagi\\.com/"],
      example: {
        searchUrl: "https://www.kagi.com/search?q=%s",
        keyword: "k",
      },
    });
  }

  parse(text) {
    return JSON.parse(text).map((suggestion) => suggestion.t);
  }
}

// On the user-facing documentation page pages/doc_search_completion.html, the completion search
// engines will be shown in this order.
export const list = [
  Youtube,
  GoogleMaps,
  Google,
  DuckDuckGo,
  Wikipedia,
  Bing,
  Amazon,
  Webster,
  Brave,
  Qwant,
  Kagi,
];


================================================
FILE: background_scripts/completion/search_wrapper.js
================================================
import * as searchEngines from "./search_engines.js";

// 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;
  }
}

let debug = false;
const inTransit = {};
const completionCache = new SimpleCache(2 * 60 * 60 * 1000, 5000); // Two hours, 5000 entries.
const 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).
const 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.
let requestId = 0;

async function 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.
function lookupEngine(searchUrl) {
  if (engineCache.has(searchUrl)) {
    return engineCache.get(searchUrl);
  } else {
    for (const engineClass of searchEngines.list) {
      const engine = new engineClass();
      if (engine.match(searchUrl)) {
        return 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.
export async function 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.hasJavascriptProtocol(query)) return [];

  const engine = lookupEngine(searchUrl);
  if (!engine) return [];

  const completionCacheKey = JSON.stringify([searchUrl, queryTerms]);
  if (completionCache.has(completionCacheKey)) {
    if (debug) console.log("hit", completionCacheKey);
    return completionCache.get(completionCacheKey);
  }

  const createTimeoutPromise = (ms) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  };

  requestId++;
  const lastRequestId = requestId;

  // We delay sending a completion request in case the user is still typing.
  await createTimeoutPromise(DELAY);

  // If the user has issued a new query while we were waiting, then this query is old; abort it.
  if (lastRequestId != requestId) return [];

  const engineWrapper = new EnginePrefixWrapper(searchUrl, engine);
  const url = engineWrapper.getUrl(queryTerms);

  if (debug) console.log("GET", url);
  const responseText = await 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 (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,
      () => completionCache.set(completionCacheKey, null),
    );
  }

  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.
export function cancel() {
  requestId++;
}


================================================
FILE: background_scripts/exclusions.js
================================================
// This module manages 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 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;
    }
  },
};

// Make RegexpCache, which is required on the page popup, accessible via the Exclusions object.
const 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.
function 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;
  }
}

export function isEnabledForUrl(url) {
  const rule = getRule(url);
  return {
    isEnabledForUrl: !rule || (rule.passKeys.length > 0),
    passKeys: rule ? rule.passKeys : "",
  };
}

function 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);
}

function 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", () => onSettingsUpdated());


================================================
FILE: background_scripts/main.js
================================================
import "../lib/utils.js";
import "../lib/settings.js";
import "../lib/url_utils.js";
import "../background_scripts/tab_recency.js";
import * as bgUtils from "../background_scripts/bg_utils.js";
import "../background_scripts/all_commands.js";
import { Commands } from "../background_scripts/commands.js";
import * as exclusions from "../background_scripts/exclusions.js";
import "../background_scripts/completion/search_engines.js";
import "../background_scripts/completion/search_wrapper.js";
import "../background_scripts/completion/completers.js";
import "../background_scripts/tab_operations.js";
import * as marks from "../background_scripts/marks.js";

import {
  BookmarkCompleter,
  DomainCompleter,
  HistoryCompleter,
  MultiCompleter,
  SearchEngineCompleter,
  TabCompleter,
} from "./completion/completers.js";

// 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]),
};

// A query dictionary for `chrome.tabs.query` that will return only the visible tabs.
const visibleTabsQueryArgs = { currentWindow: true };
if (bgUtils.isFirefox()) {
  // Only Firefox supports hidden tabs.
  visibleTabsQueryArgs.hidden = false;
}

function 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.
  const message = {
    handler: "checkEnabledAfterURLChange",
    silenceLogging: true,
  };
  chrome.tabs.sendMessage(details.tabId, message, { 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() });
      }
    });
  })();
}

function muteTab(tab) {
  chrome.tabs.update(tab.id, { muted: !tab.mutedInfo.muted });
}

function 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);
  }
}

// Find a tab's actual index in a given tab array returned by chrome.tabs.query. In Firefox, there
// may be hidden tabs, so tab.tabIndex may not be the actual index into the array of visible tabs.
function getTabIndex(tab, tabs) {
  // First check if the tab is where we expect it, to avoid searching the array.
  if (tabs.length > tab.index && tabs[tab.index].index === tab.index) {
    return tab.index;
  } else {
    return tabs.findIndex((t) => t.index === tab.index);
  }
}

//
// 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 });
}

function moveTab({ count, tab, registryEntry }) {
  if (registryEntry.command === "moveTabLeft") {
    count = -count;
  }
  return chrome.tabs.query(visibleTabsQueryArgs, function (tabs) {
    const pinnedCount = (tabs.filter((tab) => tab.pinned)).length;
    const minIndex = tab.pinned ? 0 : pinnedCount;
    const maxIndex = (tab.pinned ? pinnedCount : tabs.length) - 1;
    // The tabs array index of the new position.
    const moveIndex = Math.max(minIndex, Math.min(maxIndex, getTabIndex(tab, tabs) + count));
    return chrome.tabs.move(tab.id, {
      index: tabs[moveIndex].index,
    });
  });
}

function createRepeatCommand(command) {
  return async function (request) {
    let i = request.count - 1;
    const r = Object.assign({}, request);
    delete r.count;
    while (i >= 0) {
      i--;
      await command(r);
    }
  };
}

function nextZoomLevel(currentZoom, steps) {
  // Chrome's default zoom levels.
  const chromeLevels = [0.25, 0.33, 0.5, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
  // Firefox's default zoom levels.
  const firefoxLevels = [0.3, 0.5, 0.67, 0.8, 0.9, 1, 1.1, 1.2, 1.33, 1.5, 1.7, 2, 2.4, 3, 4, 5];

  let zoomLevels = chromeLevels; // Chrome by default
  if (bgUtils.isFirefox()) {
    zoomLevels = firefoxLevels;
  }

  if (steps === 0) { // Nothing
    return currentZoom;
  } else if (steps > 0) { // In
    // Chrome sometimes returns values with floating point errors.
    // Example: Chrome gives 0.32999999999999996 instead of 0.33.
    currentZoom += 0.0000001; // This is needed to solve floating point bugs in Chrome.
    const nextIndex = zoomLevels.findIndex((level) => level > currentZoom);
    const floorIndex = nextIndex == -1 ? zoomLevels.length : nextIndex - 1;
    return zoomLevels[Math.min(zoomLevels.length - 1, floorIndex + steps)];
  } else if (steps < 0) { // Out
    currentZoom -= 0.0000001; // This is needed to solve floating point bugs in Chrome.
    let ceilIndex = zoomLevels.findIndex((level) => level >= currentZoom);
    ceilIndex = ceilIndex == -1 ? zoomLevels.length : ceilIndex;
    return zoomLevels[Math.max(0, ceilIndex + steps)];
  }
}

// These are commands which are bound to keystrokes which must be handled by the background page.
// They are mapped in commands.js.
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: createRepeatCommand(async function (request) {
    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.
        const options = Object.keys(request.registryEntry.options);
        const promises = options.map((opt) => UrlUtils.isUrl(opt));
        const isUrl = await Promise.all(promises);
        const urlList = options.filter((_, i) => isUrl[i]);
        if (urlList.length > 0) {
          request.urls = urlList;
        } else {
          // Otherwise, just create a new tab.
          let url;
          const destination = Settings.get("newTabDestination");
          const customUrl = Settings.get("newTabCustomUrl");
          if (destination == Settings.newTabDestinations.vimiumNewTabPage) {
            url = Settings.vimiumNewTabPageUrl;
          } else if (destination == Settings.newTabDestinations.customUrl && customUrl.length > 0) {
            url = customUrl;
          } else {
            url = UrlUtils.chromeNewTabUrl;
          }
          request.urls = [url];
        }
      }
    }
    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 != UrlUtils.chromeNewTabUrl);
      const windowConfig = {
        url: urls,
        incognito: request.registryEntry.options.incognito || false,
      };
      await chrome.windows.create(windowConfig);
    } else {
      const urls = request.urls.slice().reverse();
      if (request.position == null) {
        request.position = request.registryEntry.options.position;
      }
      while (urls.length > 0) {
        const url = urls.pop();
        const tab = await TabOperations.openUrlInNewTab(Object.assign(request, { url }));
        // Ensure subsequent invocations of this command place the next tab directly after this one.
        Object.assign(request, { tab, position: "after", active: false });
      }
    }
  }),

  duplicateTab: createRepeatCommand(async (request) => {
    const tab = await chrome.tabs.duplicate(request.tabId);
    // Ensure subsequent invocations of this command place the next tab directly after this one.
    request.tabId = tab.id;
  }),

  moveTabToNewWindow({ count, tab }) {
    // TODO(philc): Switch to the promise API of chrome.tabs.query.
    chrome.tabs.query(visibleTabsQueryArgs, function (tabs) {
      const activeTabIndex = getTabIndex(tab, tabs);
      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);
  },
  async removeTab({ count, tab }) {
    await 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: createRepeatCommand(async (request) => {
    await chrome.sessions.restore(null);
  }),
  async togglePinTab({ count, tab }) {
    await forCountTabs(count, tab, (tab) => {
      chrome.tabs.update(tab.id, { pinned: !tab.pinned });
    });
  },
  toggleMuteTab,
  moveTabLeft: moveTab,
  moveTabRight: moveTab,

  async setZoom({ tabId, registryEntry }) {
    const level = registryEntry.options?.["level"] ?? "1";
    const newZoom = parseFloat(level);
    if (!isNaN(newZoom)) {
      chrome.tabs.setZoom(tabId, newZoom);
    }
  },
  async zoomIn({ count, tabId }) {
    const currentZoom = await chrome.tabs.getZoom(tabId);
    const newZoom = nextZoomLevel(currentZoom, count);
    chrome.tabs.setZoom(tabId, newZoom);
  },
  async zoomOut({ count, tabId }) {
    const currentZoom = await chrome.tabs.getZoom(tabId);
    const newZoom = nextZoomLevel(currentZoom, -count);
    chrome.tabs.setZoom(tabId, newZoom);
  },
  async zoomReset({ tabId }) {
    chrome.tabs.setZoom(tabId, 0); // setZoom of 0 sets to the tab default.
  },

  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 });
    }
  },

  async reload({ count, tab, registryEntry }) {
    const bypassCache = registryEntry.options.hard != null ? registryEntry.options.hard : false;
    await forCountTabs(count, tab, (tab) => {
      chrome.tabs.reload(tab.id, { bypassCache });
    });
  },
};

async function forCountTabs(count, currentTab, callback) {
  const tabs = await chrome.tabs.query(visibleTabsQueryArgs);
  const activeTabIndex = getTabIndex(currentTab, tabs);
  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
async function removeTabsRelative(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(visibleTabsQueryArgs);
  const activeIndex = getTabIndex(activeTab, tabs);
  const toRemove = tabs.filter((tab, tabIndex) => {
    if (tab.pinned || tab.id == activeTab.id) {
      return false;
    }
    switch (direction) {
      case "before":
        return tabIndex < activeIndex &&
          tabIndex >= activeIndex - count;
      case "after":
        return tabIndex > activeIndex &&
          tabIndex <= activeIndex + 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".
function selectTab(direction, { count, tab }) {
  chrome.tabs.query(visibleTabsQueryArgs, function (tabs) {
    if (tabs.length > 1) {
      const toSelect = (() => {
        switch (direction) {
          case "next":
            return (getTabIndex(tab, tabs) + count) % tabs.length;
          case "previous":
            return ((getTabIndex(tab, tabs) - 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, requestedByHelpDialog, isExtensionPage },
  ) {
    const frameIds = await getFrameIdsForTab(tabId);
    // If link hints was triggered on a Vimium extension page (like the vimium help dialog or
    // options page), we cannot directly retrieve the frameIds for those pages using the
    // getFrameIdsForTab. However, as a workaround, if those pages were the pages activating hints,
    // their frameId is equal to originatingFrameId.
    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,
          requestedByHelpDialog,
        },
        { 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: createRepeatCommand(async (request, callback) => {
    await TabOperations.openUrlInNewTab(request, callback);
  }),
  async openUrlInNewWindow(request) {
    await TabOperations.openUrlInNewWindow(request);
  },
  async openUrlInIncognito(request) {
    await 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,
    });
  },

  launchSearchQuery({ query, openInNewTab }) {
    const disposition = openInNewTab ? "NEW_TAB" : "CURRENT_TAB";
    chrome.search.query({ disposition, text: query });
  },

  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,
  gotoMark: marks.goto,
  // 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";
      }

      let 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",
        },
      };

      if (bgUtils.isFirefox()) {
        // Only Firefox supports SVG icons.
        iconSet = {
          "enabled": "../icons/action_enabled.svg",
          "partial": "../icons/action_partial.svg",
          "disabled": "../icons/action_disabled.svg",
        };
      }

      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 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.
async function showUpgradeMessageIfNecessary(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) ||
    Settings.get("hideUpdateNotifications")
  ) {
    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/icon128.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];
      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,
  nextZoomLevel,
});

// 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();


================================================
FILE: background_scripts/marks.js
================================================
import * as TabOperations from "./tab_operations.js";

// This returns the key which is used for storing mark locations in chrome.storage.sync.
// Exported for tests.
export function getLocationKey(markName) {
  return `vimiumGlobalMark|${markName}`;
}

// Get the part of a URL we use for matching here (that is, everything up to the first anchor).
function 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.
export async function create(req, sender) {
  const items = await chrome.storage.session.get("vimiumSecret");
  const markInfo = {
    vimiumSecret: items.vimiumSecret,
    markName: req.markName,
    url: getBaseUrl(sender.tab.url),
    tabId: sender.tab.id,
    scrollX: req.scrollX,
    scrollY: req.scrollY,
  };

  if ((markInfo.scrollX != null) && (markInfo.scrollY != null)) {
    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) => {
      saveMark(Object.assign(markInfo, { scrollX: response.scrollX, scrollY: response.scrollY }));
    });
  }
}

function saveMark(markInfo) {
  const item = {};
  item[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.
export async function goto(req) {
  const vimiumSecret = (await chrome.storage.session.get("vimiumSecret"))["vimiumSecret"];
  const key = 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 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 === getBaseUrl(tab.url));
    if (originalTabStillExists) {
      await gotoPositionInTab(markInfo);
    } else {
      await focusOrLaunch(markInfo, req);
    }
  }
}

// Focus an existing tab and scroll to the given position within it.
async function 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 function 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 pickTab(tabs);
    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: 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] = () =>
          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 function 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];
}


================================================
FILE: background_scripts/reload.js
================================================
// Used as part of a debugging workflow when developing the extension.

const tabs = await chrome.tabs.query({});
// Clear the background page's console log, if its console window is open.
console.clear();
await chrome.runtime.reload();

// Chrome does not execute past this point. This is for Firefox-based browsers. Note that Chrome
// will not reload every tab that Vimium was open in. That must be done outside of Vimium, e.g. via
// an Applescript on Mac.

// Firefox will reload every tab as a result of chrome.runtime.reload(). However, the console
// on those pages does not get cleared for some reason, so we manually clear it.
for (const tab of tabs) {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      console.clear();
    },
  });
}

// We want to close the reload.html page as part of reloading the extension. In both Chrome and
// Firefox, the browser will automatically close every tab that's specific to this extension,
// including this page. However, in Firefox, if there's an error in manifest.json and the extension
// can't reload, then the extension's pages will not get closed, so close this page manually.
// globalThis.close();


================================================
FILE: background_scripts/tab_operations.js
================================================
//
// Functions for opening URLs in tabs.
//

import * as bgUtils from "../background_scripts/bg_utils.js";
import "../lib/url_utils.js";

// Opens request.url in the current tab. If the URL is keywords, search for them in the default
// search engine. If the URL is a javascript: snippet, execute it in the current tab.
export async function openUrlInCurrentTab(request) {
  const urlStr = await UrlUtils.convertToUrl(request.url);
  if (urlStr == null) {
    // The requested destination is not a URL, so treat it like a search query.
    chrome.search.query({ text: request.url });
  } else if (UrlUtils.hasJavascriptProtocol(urlStr)) {
    // 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.
    const scriptingArgs = {
      target: { tabId: request.tabId },
      func: (text) => {
        const prefix = "javascript:";
        text = text.slice(prefix.length).trim();
        // TODO(philc): Why do we try to double decode here? Discover and then document it.
        text = decodeURIComponent(text);
        try {
          text = decodeURIComponent(text);
        } catch {
          // Swallow
        }
        const el = document.createElement("script");
        el.textContent = text;
        document.head.appendChild(el);
      },
      args: [urlStr],
    };
    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 {
    // The requested destination is a regular URL.
    chrome.tabs.update(request.tabId, { url: urlStr });
  }
}

// Opens request.url in new tab and switches to it.
// Returns the created tab.
export async function openUrlInNewTab(request) {
  const urlStr = await UrlUtils.convertToUrl(request.url);
  const tabConfig = { 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;
  tabConfig.active = request.active ?? true;
  tabConfig.openerTabId = request.tab.id;

  let newTab;

  if (urlStr == null) {
    // The requested destination is not a URL, so treat it like a search query.
    //
    // The chrome.search.query API lets us open the search in a new tab, but it doesn't let us
    // control the precise position of that tab. So, we open a new blank tab using our position
    // parameter, and then execute the search in that tab.

    // In Chrome, if we create a blank tab and call chrome.search.query, the omnibar is focused,
    // which we don't want. To work around that, first create an empty page. This is not needed in
    // Firefox. And in fact, firefox doesn't support a data:text URL to the chrome.tab.create API.
    tabConfig.url = bgUtils.isFirefox() ? null : "data:text/html,<html></html>";
    newTab = await chrome.tabs.create(tabConfig);
    const query = request.url;
    await chrome.search.query({ text: query, tabId: newTab.id });
  } else {
    // The requested destination is a regular URL.
    // Firefox does not support "about:newtab" in chrome.tabs.create, so omit it.
    if (urlStr != UrlUtils.chromeNewTabUrl) {
      tabConfig.url = urlStr;
    }
    newTab = await chrome.tabs.create(tabConfig);
  }
  return newTab;
}

// Open request.url in new window and switch to it.
export async function openUrlInNewWindow(request) {
  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, so omit it.
  if (tabConfig["url"] === UrlUtils.chromeNewTabUrl) {
    delete winConfig["url"];
  }
  await chrome.windows.create(winConfig);
}


================================================
FILE: background_scripts/tab_recency.js
================================================
// 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));
  }
}

export { TabRecency };


================================================
FILE: background_scripts/user_search_engines.js
================================================
import "../lib/url_utils.js";
import * as commands from "./commands.js";

// A struct representing a search engine entry in the "searchEngine" setting.
export class UserSearchEngine {
  keyword;
  url;
  description;
  constructor(o) {
    Object.seal(this);
    if (o) Object.assign(this, o);
  }
}

// Parses a user's search engine configuration from Settings, and stores the parsed results.
// TODO(philc): Should this be responsible for updating itself when Settings changes, rather than
// the callers doing so? Or, remove this class and re-parse the configuration every keystroke in
// Vomnibar, so we don't introduce another layer of caching in the code.
export let keywordToEngine = {};

// Returns a result of the shape: { keywordToEngine, validationErrors }.
export function parseConfig(configText) {
  const results = {};
  const errors = [];
  for (const line of commands.parseLines(configText)) {
    const tokens = line.split(/\s+/);
    if (tokens.length < 2) {
      errors.push(`This line has less than two tokens: ${line}`);
      continue;
    }
    if (!tokens[0].includes(":")) {
      errors.push(`This line doesn't include a ":" character: ${line}`);
      continue;
    }
    const keyword = tokens[0].split(":")[0];
    const url = tokens[1];
    const description = tokens.length > 2 ? tokens.slice(2).join(" ") : `search (${keyword})`;

    if (!UrlUtils.urlHasProtocol(url) && !UrlUtils.hasJavascriptProtocol(url)) {
      errors.push(`This search engine doesn't have a valid URL: ${line}`);
      continue;
    }
    results[keyword] = new UserSearchEngine({ keyword, url, description });
  }
  return {
    keywordToEngine: results,
    validationErrors: errors,
  };
}

export function set(searchEnginesConfigText) {
  keywordToEngine = parseConfig(searchEnginesConfigText).keywordToEngine;
}


================================================
FILE: build_scripts/write_command_listing_page.js
================================================
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env
// Write a static version of the command_listing.html page to dist, to be hosted on vimium.github.io
// as an online reference.

import * as testHelper from "../tests/unit_tests/test_helper.js";
import "../tests/unit_tests/test_chrome_stubs.js";
import * as commandListing from "../pages/command_listing.js";
import * as fs from "@std/fs";
import * as path from "@std/path";

const scriptDir = path.dirname(path.fromFileUrl(import.meta.url));

chrome.storage.session.get = async (key) => {
  if (key == "commandToOptionsToKeys") {
    return { commandToOptionsToKeys: {} };
  }
};

await testHelper.jsdomStub(path.join(scriptDir, "../pages/command_listing.html"));
await Settings.onLoaded();

await commandListing.populatePage();

const dist = path.join(scriptDir, "../dist/command_listing_page");
if (await fs.exists(dist)) {
  await Deno.remove(dist, { recursive: true });
}

await Deno.mkdir(dist, { recursive: true });

// Write out all required CSS files to disk.
const linkEls = document.head.querySelectorAll("link[rel=stylesheet]");
for (const el of linkEls) {
  const cssPath = el.getAttribute("href");
  const src = path.join(scriptDir, "../pages/" + cssPath);
  const dest = path.join(dist, path.basename(cssPath));
  await Deno.copyFile(src, dest);
  el.setAttribute("href", path.basename(cssPath));
}

// Remove any external javascripts. Since this page's HTML has already been generated, it doesn't
// need JS at runtime.
for (const el of document.head.querySelectorAll("script")) {
  el.remove();
}

// Indicate that this is the hosted version of the page. This causes a link back to the
// Github repo to be shown.
document.querySelector("html").classList.add("hosted-version");

// Use the website's favicon.
const favicon = document.createElement("link");
favicon.setAttribute("rel", "shortcut icon");
favicon.href = "../vimium_logo.svg";
document.head.appendChild(favicon);

// The doctype tag is not included in outerHTML; add it back in.
const html = "<!DOCTYPE html>" + document.documentElement.outerHTML;
await Deno.writeTextFile(path.join(dist, "index.html"), html);


================================================
FILE: content_scripts/file_urls.css
================================================
/* 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;
}


================================================
FILE: content_scripts/hud.js
================================================
//
// 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.

  handleUIComponentMessage({ data }) {
    const handlers = {
      hideFindMode: this.hideFindMode,
      search: this.search,
      unfocusIfFocused: this.unfocusIfFocused,
      pasteResponse: this.pasteResponse,
      showClipboardUnavailableMessage: this.showClipboardUnavailableMessage,
    };
    const handler = handlers[data.name];
    if (handler) {
      return handler.bind(this)(data);
    }
  },

  async init(focusable) {
    await Settings.onLoaded();
    if (focusable == null) {
      focusable = true;
    }
    if (this.hudUI == null) {
      const queryString = globalThis.vimiumDomTestsAreRunning ? "?dom_tests=true" : "";
      this.hudUI = new UIComponent();
      this.hudUI.load(
        `pages/hud_page.html${queryString}`,
        "vimium-hud-frame",
        this.handleUIComponentMessage.bind(this),
      );
    }
    // this[data.name]? data
    if (this.tween == null) {
      this.tween = new Tween(
        "iframe.vimium-hud-frame.vimium-ui-component-visible",
        this.hudUI.shadowDOM,
      );
    }
    const classList = this.hudUI.iframeElement.classList;
    if (focusable) {
      classList.remove("vimium-non-clickable");
      classList.add("vimium-clickable");
      // 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.setIframeVisible(true);
      // 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 {
      classList.remove("vimium-non-clickable");
      classList.add("vimium-clickable");
    }
  },

  // duration - if omitted, the message will show until dismissed.
  async show(text, duration) {
    await DomUtils.documentComplete();
    clearTimeout(this._showForDurationTimerId);
    // @hudUI.activate will take charge of making it visible
    await this.init(false);
    this.hudUI.show({ name: "show", text });
    this.tween.fade(1.0, 150);

    if (duration != null) {
      this._showForDurationTimerId = setTimeout(() => this.hide(), duration);
    }
  },

  async showFindMode(findMode = null) {
    this.findMode = findMode;
    await DomUtils.documentComplete();
    await this.init();
    this.hudUI.show({ 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.
    globalThis.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.
  async copyToClipboard(text) {
    await DomUtils.documentComplete();
    await this.init();
    this.hudUI.postMessage({ name: "copyToClipboard", data: text });
  },

  async pasteFromClipboard(pasteListener) {
    this.pasteListener = pasteListener;
    await DomUtils.documentComplete();
    await this.init();
    this.tween.fade(0, 0);
    this.hudUI.postMessage({ name: "pasteFromClipboard" });
  },

  pasteResponse({ data }) {
    // Hide the HUD frame again.
    this.hudUI.setIframeVisible(false);
    this.unfocusIfFocused();
    this.pasteListener(data);
  },

  unfocusIfFocused() {
    // On Firefox, if an <iframe> disappears when it's focused, then it will keep "focused", which
    // means keyboard events will always be dispatched to the HUD iframe
    if (this.hudUI && this.hudUI.showing) {
      this.hudUI.iframeElement.blur();
      globalThis.focus();
    }
  },

  // Navigator.clipboard is only available in secure contexts. Show a warning when clipboard actions
  // fail on non-HTTPS sites. See #4572.
  async showClipboardUnavailableMessage() {
    await DomUtils.documentComplete();
    await this.init();
    // Since the message is long and surprising, show it for longer to allow more time to reading.
    this.show("Clipboard actions available only on HTTPS sites", 4000);
  },
};

class Tween {
  constructor(cssSelector, insertionPoint) {
    this.opacity = 0;
    this.intervalId = -1;
    this.styleElement = null;
    this.cssSelector = cssSelector;
    if (insertionPoint == null) insertionPoint = document.documentElement;
    this.styleElement = DomUtils.createElement("style");

    if (!this.styleElement.style) {
      // We're in an XML document, so we shouldn't inject any elements. See the comment in
      // UIComponent.
      Tween.prototype.fade = Tween.prototype.stop = Tween.prototype.updateStyle = function () {};
      return;
    }

    this.styleElement.type = "text/css";
    this.styleElement.innerHTML = "";
    insertionPoint.appendChild(this.styleElement);
  }

  fade(toAlpha, duration, onComplete) {
    clearInterval(this.intervalId);
    const startTime = (new Date()).getTime();
    const fromAlpha = this.opacity;
    const alphaStep = toAlpha - fromAlpha;

    const performStep = () => {
      const elapsed = (new Date()).getTime() - startTime;
      if (elapsed >= duration) {
        clearInterval(this.intervalId);
        this.updateStyle(toAlpha);
        if (onComplete) {
          onComplete();
        }
      } else {
        const value = ((elapsed / duration) * alphaStep) + fromAlpha;
        this.updateStyle(value);
      }
    };

    this.updateStyle(this.opacity);
    this.intervalId = setInterval(performStep, 50);
  }

  stop() {
    clearInterval(this.intervalId);
  }

  updateStyle(opacity) {
    this.opacity = opacity;
    this.styleElement.innerHTML = `\
${this.cssSelector} {
  opacity: ${this.opacity};
}\
`;
  }
}

globalThis.HUD = HUD;


================================================
FILE: content_scripts/link_hints.js
================================================
//
// This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items
// on the page have a hint marker displayed containing a sequence of letters. Typing those letters
// will select a link.
//
// In our 'default' mode, the characters we use to show link hints are a user-configurable option.
// By default they're the home row. The CSS which is used on the link hints is also a configurable
// option.
//
// In 'filter' mode, our link hints are numbers, and the user can narrow down the range of
// possibilities by typing the text of the link itself.
//

// A DOM element that sits on top of a link, showing the key the user should type to select the
// link.
class HintMarker {
  hintDescriptor;
  localHint;
  linkText; // Used in FilterHints
  hintString; // Used in AlphabetHints
  markerRect; // Cached rectangle of the element, used for rotating hints.
  // Element is null if the hint marker reflects a hint that's owned by another frame.
  element;
  // Cached book-keeping when computing a marker's score against a query.
  linkWords;
  score;
  stableSortCount;
  constructor() {
    Object.seal(this);
  }
  isLocalMarker() {
    return this.localHint != null;
  }
}

// A clickable element in the current frame, plus metadata about how to show a hint marker for it.
class LocalHint {
  element; // The clickable element.
  image; // When element is an <area> (image map), `image` is its associated image.
  rect; // The rectangle where the hint should shown, to avoid overlapping with other hints.
  linkText; // Used in FilterHints.
  showLinkText; // Used in FilterHints.
  // The reason that an element has a link hint when the reason isn't obvious, e.g. the body of a
  // frame so that the frame can be focused. This reason is shown to the user in the hint's caption.
  reason;
  // "secondClassCitizen" means the element isn't clickable, but does have a tab index. We show
  // hints for these elements unless their hit box collides with another clickable element.
  secondClassCitizen;
  // An element that may be clickable based on our heuristics. It's a "false positive" if one of its
  // child elements is detected as clickable.
  possibleFalsePositive;
  constructor(o) {
    Object.seal(this);
    if (o) Object.assign(this, o);
  }
}

// Metadata about each LocalHint which is transferred to other frames in the current tab, so that
// every frame can be aware of every other frame's local hints.
class HintDescriptor {
  frameId; // The frameId that the hint is local to.
  localIndex; // An index into the owner frame's localHints.
  linkText; // The link's text. This is non-null only for FilterHints.
  constructor(o) {
    Object.seal(this);
    if (o) Object.assign(this, o);
  }
}

// The "name" property below is a short-form name to appear in the link-hints mode's name. It's for
// debug only.
//
const isMac = KeyboardUtils.platform === "Mac";
const OPEN_IN_CURRENT_TAB = {
  name: "curr-tab",
  indicator: "Open link in current tab",
};
const OPEN_IN_NEW_BG_TAB = {
  name: "bg-tab",
  indicator: "Open link in new tab",
  clickModifiers: { metaKey: isMac, ctrlKey: !isMac },
};
const OPEN_IN_NEW_FG_TAB = {
  name: "fg-tab",
  indicator: "Open link in new tab and switch to it",
  clickModifiers: { shiftKey: true, metaKey: isMac, ctrlKey: !isMac },
};
const OPE
Download .txt
gitextract_bk_2yr_t/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── pull_request_template.md
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── CREDITS
├── MIT-LICENSE.txt
├── README.md
├── background_scripts/
│   ├── all_commands.js
│   ├── bg_utils.js
│   ├── commands.js
│   ├── completion/
│   │   ├── completers.js
│   │   ├── ranking.js
│   │   ├── search_engines.js
│   │   └── search_wrapper.js
│   ├── exclusions.js
│   ├── main.js
│   ├── marks.js
│   ├── reload.js
│   ├── tab_operations.js
│   ├── tab_recency.js
│   └── user_search_engines.js
├── build_scripts/
│   └── write_command_listing_page.js
├── content_scripts/
│   ├── file_urls.css
│   ├── hud.js
│   ├── link_hints.js
│   ├── marks.js
│   ├── mode.js
│   ├── mode_find.js
│   ├── mode_insert.js
│   ├── mode_key_handler.js
│   ├── mode_normal.js
│   ├── mode_visual.js
│   ├── scroller.js
│   ├── ui_component.js
│   ├── vimium.css
│   ├── vimium_frontend.js
│   └── vomnibar.js
├── deno.json
├── lib/
│   ├── chrome_api_stubs.js
│   ├── dom_utils.js
│   ├── find_mode_history.js
│   ├── handler_stack.js
│   ├── keyboard_utils.js
│   ├── rect.js
│   ├── settings.js
│   ├── types.js
│   ├── url_utils.js
│   └── utils.js
├── make.js
├── manifest.json
├── pages/
│   ├── action.css
│   ├── action.html
│   ├── action.js
│   ├── all_content_scripts.js
│   ├── command_listing.css
│   ├── command_listing.html
│   ├── command_listing.js
│   ├── doc.css
│   ├── doc_search_completion.html
│   ├── doc_search_completion.js
│   ├── exclusion_rules_editor.js
│   ├── help_dialog_page.css
│   ├── help_dialog_page.html
│   ├── help_dialog_page.js
│   ├── hud_page.css
│   ├── hud_page.html
│   ├── hud_page.js
│   ├── key_mappings.css
│   ├── options.css
│   ├── options.html
│   ├── options.js
│   ├── reload.html
│   ├── ui_component_messenger.js
│   ├── vomnibar_page.css
│   ├── vomnibar_page.html
│   └── vomnibar_page.js
├── resources/
│   └── tlds.txt
├── test_harnesses/
│   ├── cross_origin_iframe.html
│   ├── event_capture.html
│   ├── form.html
│   ├── has_popup_and_link_hud.html
│   ├── iframe.html
│   ├── page_with_links.html
│   ├── visibility_test.html
│   ├── vomnibar_harness.html
│   └── vomnibar_harness.js
└── tests/
    ├── dom_tests/
    │   ├── dom_test_setup.js
    │   ├── dom_tests.html
    │   ├── dom_tests.js
    │   └── dom_utils_test.js
    ├── unit_tests/
    │   ├── bg_utils_test.js
    │   ├── command_listing_test.js
    │   ├── commands_test.js
    │   ├── completion/
    │   │   ├── completers_test.js
    │   │   ├── ranking_test.js
    │   │   └── search_engines_test.js
    │   ├── doc_search_completion_test.js
    │   ├── exclusion_test.js
    │   ├── handler_stack_test.js
    │   ├── help_dialog_test.js
    │   ├── hud_page_test.js
    │   ├── link_hints_test.js
    │   ├── main_test.js
    │   ├── marks_test.js
    │   ├── options_page_test.js
    │   ├── rect_test.js
    │   ├── settings_test.js
    │   ├── tab_operations_test.js
    │   ├── tab_recency_test.js
    │   ├── test_chrome_stubs.js
    │   ├── test_helper.js
    │   ├── ui_component_test.js
    │   ├── url_utils_test.js
    │   ├── user_search_engines_test.js
    │   ├── utils_test.js
    │   └── vomnibar_page_test.js
    └── vendor/
        └── shoulda.js
Download .txt
SYMBOL INDEX (826 symbols across 54 files)

FILE: background_scripts/bg_utils.js
  function isFirefox (line 7) | function isFirefox() {
  function getFirefoxVersion (line 13) | async function getFirefoxVersion() {

FILE: background_scripts/commands.js
  class RegistryEntry (line 5) | class RegistryEntry {
    method constructor (line 22) | constructor(o) {
  function parseLines (line 36) | function parseLines(text) {
  function nthRegexIndex (line 44) | function nthRegexIndex(str, regex, n) {
  method parse (line 67) | parse(configText, shouldLogWarnings) {
  method parseKeySequence (line 211) | parseKeySequence(key) {
  method parseCommandOptions (line 238) | parseCommandOptions(optionString) {
  method init (line 293) | async init() {
  method loadKeyMappings (line 303) | async loadKeyMappings(userKeyMappingsConfigText) {
  method installKeyStateMapping (line 336) | async installKeyStateMapping() {
  method prepareHelpPageData (line 370) | prepareHelpPageData() {

FILE: background_scripts/completion/completers.js
  class Suggestion (line 24) | class Suggestion {
    method constructor (line 57) | constructor(options) {
    method computeRelevancy (line 63) | computeRelevancy() {
    method generateHtml (line 72) | generateHtml() {
    method getUrlRoot (line 115) | getUrlRoot(url) {
    method getHostname (line 121) | getHostname(url) {
    method stripTrailingSlash (line 127) | stripTrailingSlash(url) {
    method pushMatchingRanges (line 135) | pushMatchingRanges(string, term, ranges) {
    method highlightQueryTerms (line 159) | highlightQueryTerms(string) {
    method mergeRanges (line 184) | mergeRanges(ranges) {
    method shortenUrl (line 200) | shortenUrl() {
    method boostRelevancyScore (line 221) | static boostRelevancyScore(factor, score) {
  class BookmarkCompleter (line 262) | class BookmarkCompleter {
    method filter (line 263) | async filter({ queryTerms }) {
    method refresh (line 302) | async refresh() {
    method traverseBookmarks (line 318) | traverseBookmarks(bookmarks) {
    method traverseBookmarksRecursive (line 327) | traverseBookmarksRecursive(bookmark, results, parent) {
    method computeRelevancy (line 347) | computeRelevancy(suggestion) {
  class HistoryCompleter (line 356) | class HistoryCompleter {
    method filter (line 360) | async filter({ queryTerms, seenTabToOpenCompletionList }) {
    method computeRelevancy (line 387) | computeRelevancy(suggestion) {
  class DomainCompleter (line 406) | class DomainCompleter {
    method filter (line 413) | async filter({ queryTerms, query }) {
    method sortDomainsByRelevancy (line 432) | sortDomainsByRelevancy(queryTerms, domainCandidates) {
    method populateDomains (line 444) | async populateDomains() {
    method onVisited (line 454) | onVisited(newPage) {
    method onVisitRemoved (line 468) | onVisitRemoved(toRemove) {
    method parseDomainAndScheme (line 485) | parseDomainAndScheme(url) {
  class TabCompleter (line 494) | class TabCompleter {
    method filter (line 495) | async filter({ queryTerms }) {
    method computeRelevancy (line 526) | computeRelevancy(suggestion) {
  class SearchEngineCompleter (line 535) | class SearchEngineCompleter {
    method cancel (line 536) | cancel() {
    method getUserSearchEngineForQuery (line 542) | getUserSearchEngineForQuery(query) {
    method refresh (line 554) | refresh() {
    method filter (line 558) | async filter(request) {
    method computeRelevancy (line 605) | computeRelevancy({ queryTerms, title }) {
  class MultiCompleter (line 623) | class MultiCompleter {
    method constructor (line 624) | constructor(completers) {
    method refresh (line 628) | refresh() {
    method cancel (line 634) | cancel() {
    method filter (line 640) | async filter(request) {
    method postProcessSuggestions (line 670) | postProcessSuggestions(request, queryTerms, suggestions) {
  method reset (line 712) | reset() {
  method onLoaded (line 718) | async onLoaded() {
  method fetchHistory (line 723) | async fetchHistory() {
  method compareHistoryByUrl (line 747) | compareHistoryByUrl(a, b) {
  method onVisited (line 755) | onVisited(newPage) {
  method onVisitRemoved (line 768) | onVisitRemoved(toRemove) {

FILE: background_scripts/completion/ranking.js
  function matches (line 6) | function matches(queryTerms, ...things) {
  function scoreTerm (line 39) | function scoreTerm(term, string) {
  function wordRelevancy (line 60) | function wordRelevancy(queryTerms, url, title) {
  function recencyScore (line 111) | function recencyScore(lastAccessedTime) {
  function normalizeDifference (line 124) | function normalizeDifference(a, b) {
  method init (line 132) | init() {
  method clear (line 137) | clear() {
  method get (line 151) | get(string, prefix, suffix) {

FILE: background_scripts/completion/search_engines.js
  class BaseEngine (line 36) | class BaseEngine {
    method constructor (line 37) | constructor(options) {
    method match (line 42) | match(searchUrl) {
    method getUrl (line 45) | getUrl(queryTerms) {
  class Google (line 50) | class Google extends BaseEngine {
    method constructor (line 51) | constructor() {
    method parse (line 62) | parse(text) {
  class GoogleMaps (line 69) | class GoogleMaps extends BaseEngine {
    method constructor (line 70) | constructor() {
    method parse (line 87) | parse(text) {
  class Youtube (line 94) | class Youtube extends BaseEngine {
    method constructor (line 95) | constructor() {
    method parse (line 106) | parse(text) {
  class Wikipedia (line 111) | class Wikipedia extends BaseEngine {
    method constructor (line 112) | constructor() {
    method parse (line 123) | parse(text) {
  class Bing (line 128) | class Bing extends BaseEngine {
    method constructor (line 129) | constructor() {
    method parse (line 140) | parse(text) {
  class Amazon (line 145) | class Amazon extends BaseEngine {
    method constructor (line 146) | constructor() {
    method parse (line 158) | parse(text) {
  class DuckDuckGo (line 163) | class DuckDuckGo extends BaseEngine {
    method constructor (line 164) | constructor() {
    method parse (line 175) | parse(text) {
  class Webster (line 180) | class Webster extends BaseEngine {
    method constructor (line 181) | constructor() {
    method parse (line 193) | parse(text) {
  class Qwant (line 199) | class Qwant extends BaseEngine {
    method constructor (line 200) | constructor() {
    method parse (line 211) | parse(text) {
  class Brave (line 217) | class Brave extends BaseEngine {
    method constructor (line 218) | constructor() {
    method parse (line 229) | parse(text) {
  class Kagi (line 235) | class Kagi extends BaseEngine {
    method constructor (line 236) | constructor() {
    method parse (line 247) | parse(text) {

FILE: background_scripts/completion/search_wrapper.js
  class EnginePrefixWrapper (line 11) | class EnginePrefixWrapper {
    method constructor (line 12) | constructor(searchUrl, engine) {
    method getUrl (line 17) | getUrl(queryTerms) {
    method parse (line 38) | parse(responseText) {
  constant DELAY (line 51) | const DELAY = 100;
  function get (line 57) | async function get(url) {
  function lookupEngine (line 78) | function lookupEngine(searchUrl) {
  function complete (line 95) | async function complete(searchUrl, queryTerms) {
  function cancel (line 169) | function cancel() {

FILE: background_scripts/exclusions.js
  method clear (line 6) | clear(cache) {
  method get (line 9) | get(pattern) {
  function getRule (line 36) | function getRule(url, rules) {
  function isEnabledForUrl (line 58) | function isEnabledForUrl(url) {
  function setRules (line 66) | function setRules(rules) {
  function onSettingsUpdated (line 72) | function onSettingsUpdated() {

FILE: background_scripts/main.js
  function onURLChange (line 71) | function onURLChange(details) {
  function muteTab (line 102) | function muteTab(tab) {
  function toggleMuteTab (line 106) | function toggleMuteTab(request, sender) {
  function getTabIndex (line 161) | function getTabIndex(tab, tabs) {
  function selectSpecificTab (line 173) | async function selectSpecificTab(request) {
  function moveTab (line 182) | function moveTab({ count, tab, registryEntry }) {
  function createRepeatCommand (line 198) | function createRepeatCommand(command) {
  function nextZoomLevel (line 210) | function nextZoomLevel(currentZoom, steps) {
  method moveTabToNewWindow (line 302) | moveTabToNewWindow({ count, tab }) {
  method nextTab (line 314) | nextTab(request) {
  method previousTab (line 317) | previousTab(request) {
  method firstTab (line 320) | firstTab(request) {
  method lastTab (line 323) | lastTab(request) {
  method removeTab (line 326) | async removeTab({ count, tab }) {
  method togglePinTab (line 337) | async togglePinTab({ count, tab }) {
  method setZoom (line 346) | async setZoom({ tabId, registryEntry }) {
  method zoomIn (line 353) | async zoomIn({ count, tabId }) {
  method zoomOut (line 358) | async zoomOut({ count, tabId }) {
  method zoomReset (line 363) | async zoomReset({ tabId }) {
  method nextFrame (line 367) | async nextFrame({ count, tabId }) {
  method closeTabsOnLeft (line 402) | async closeTabsOnLeft(request) {
  method closeTabsOnRight (line 405) | async closeTabsOnRight(request) {
  method closeOtherTabs (line 408) | async closeOtherTabs(request) {
  method visitPreviousTab (line 412) | async visitPreviousTab({ count, tab }) {
  method reload (line 422) | async reload({ count, tab, registryEntry }) {
  function forCountTabs (line 430) | async function forCountTabs(count, currentTab, callback) {
  function removeTabsRelative (line 440) | async function removeTabsRelative(direction, { count, tab }) {
  function selectTab (line 469) | function selectTab(direction, { count, tab }) {
  function getFrameIdsForTab (line 506) | async function getFrameIdsForTab(tabId) {
  method broadcastLinkHintsMessage (line 515) | broadcastLinkHintsMessage(request, sender) {
  method prepareToActivateLinkHintsMode (line 523) | async prepareToActivateLinkHintsMode(
  method runBackgroundCommand (line 601) | runBackgroundCommand(request, sender) {
  method getCurrentTabUrl (line 606) | getCurrentTabUrl({ tab }) {
  method openUrlInNewWindow (line 612) | async openUrlInNewWindow(request) {
  method openUrlInIncognito (line 615) | async openUrlInIncognito(request) {
  method openOptionsPageInNewTab (line 622) | openOptionsPageInNewTab(request) {
  method launchSearchQuery (line 629) | launchSearchQuery({ query, openInNewTab }) {
  method domReady (line 634) | domReady(_, sender) {
  method sendMessageToFrames (line 649) | sendMessageToFrames(request, sender) {
  method broadcastLinkHintsMessage (line 654) | broadcastLinkHintsMessage(request, sender) {
  method prepareToActivateLinkHintsMode (line 657) | prepareToActivateLinkHintsMode(request, sender) {
  method initializeFrame (line 661) | async initializeFrame(request, sender) {
  method getBrowserInfo (line 713) | async getBrowserInfo() {
  method filterCompletions (line 720) | async filterCompletions(request) {
  method refreshCompletions (line 733) | refreshCompletions(request) {
  method cancelCompletions (line 738) | cancelCompletions(request) {
  function majorVersionHasIncreased (line 802) | function majorVersionHasIncreased(previousVersion) {
  function showUpgradeMessageIfNecessary (line 811) | async function showUpgradeMessageIfNecessary(onInstalledDetails) {
  function injectContentScriptsAndCSSIntoExistingTabs (line 851) | async function injectContentScriptsAndCSSIntoExistingTabs() {
  function initializeExtension (line 890) | async function initializeExtension() {

FILE: background_scripts/marks.js
  function getLocationKey (line 5) | function getLocationKey(markName) {
  function getBaseUrl (line 10) | function getBaseUrl(url) {
  function create (line 17) | async function create(req, sender) {
  function saveMark (line 39) | function saveMark(markInfo) {
  function goto (line 49) | async function goto(req) {
  function gotoPositionInTab (line 79) | async function gotoPositionInTab({ tabId, scrollX, scrollY }) {
  function focusOrLaunch (line 87) | async function focusOrLaunch(markInfo, req) {
  function pickTab (line 114) | async function pickTab(tabs) {

FILE: background_scripts/tab_operations.js
  function openUrlInCurrentTab (line 10) | async function openUrlInCurrentTab(request) {
  function openUrlInNewTab (line 52) | async function openUrlInNewTab(request) {
  function openUrlInNewWindow (line 105) | async function openUrlInNewWindow(request) {

FILE: background_scripts/tab_recency.js
  class TabRecency (line 12) | class TabRecency {
    method constructor (line 13) | constructor() {
    method init (line 21) | async init() {
    method loadFromStorage (line 61) | async loadFromStorage() {
    method saveToStorage (line 86) | async saveToStorage() {
    method queueAction (line 91) | queueAction(action, tabId) {
    method handleAction (line 100) | handleAction(action, tabId) {
    method register (line 110) | register(tabId) {
    method deregister (line 116) | deregister(tabId) {
    method recencyScore (line 122) | recencyScore(tabId) {
    method getTabsByRecency (line 131) | getTabsByRecency() {

FILE: background_scripts/user_search_engines.js
  class UserSearchEngine (line 5) | class UserSearchEngine {
    method constructor (line 9) | constructor(o) {
  function parseConfig (line 22) | function parseConfig(configText) {
  function set (line 51) | function set(searchEnginesConfigText) {

FILE: content_scripts/hud.js
  constant HUD (line 5) | const HUD = {
  method abandon (line 9) | abandon() {
  method handleUIComponentMessage (line 23) | handleUIComponentMessage({ data }) {
  method init (line 37) | async init(focusable) {
  method show (line 76) | async show(text, duration) {
  method showFindMode (line 89) | async showFindMode(findMode = null) {
  method search (line 97) | search(data) {
  method hide (line 115) | hide(immediate, updateIndicator) {
  method hideFindMode (line 138) | hideFindMode({ exitEventIsEnter, exitEventIsEscape }) {
  method copyToClipboard (line 179) | async copyToClipboard(text) {
  method pasteFromClipboard (line 185) | async pasteFromClipboard(pasteListener) {
  method pasteResponse (line 193) | pasteResponse({ data }) {
  method unfocusIfFocused (line 200) | unfocusIfFocused() {
  method showClipboardUnavailableMessage (line 211) | async showClipboardUnavailableMessage() {
  class Tween (line 219) | class Tween {
    method constructor (line 220) | constructor(cssSelector, insertionPoint) {
    method fade (line 240) | fade(toAlpha, duration, onComplete) {
    method stop (line 264) | stop() {
    method updateStyle (line 268) | updateStyle(opacity) {

FILE: content_scripts/link_hints.js
  class HintMarker (line 16) | class HintMarker {
    method constructor (line 28) | constructor() {
    method isLocalMarker (line 31) | isLocalMarker() {
  class LocalHint (line 37) | class LocalHint {
    method constructor (line 52) | constructor(o) {
  class HintDescriptor (line 60) | class HintDescriptor {
    method constructor (line 64) | constructor(o) {
  constant OPEN_IN_CURRENT_TAB (line 74) | const OPEN_IN_CURRENT_TAB = {
  constant OPEN_IN_NEW_BG_TAB (line 78) | const OPEN_IN_NEW_BG_TAB = {
  constant OPEN_IN_NEW_FG_TAB (line 83) | const OPEN_IN_NEW_FG_TAB = {
  constant OPEN_WITH_QUEUE (line 88) | const OPEN_WITH_QUEUE = {
  constant COPY_LINK_URL (line 93) | const COPY_LINK_URL = {
  method linkActivator (line 96) | linkActivator(link) {
  constant OPEN_INCOGNITO (line 108) | const OPEN_INCOGNITO = {
  method linkActivator (line 111) | linkActivator(link) {
  constant DOWNLOAD_LINK_URL (line 115) | const DOWNLOAD_LINK_URL = {
  constant COPY_LINK_TEXT (line 120) | const COPY_LINK_TEXT = {
  method linkActivator (line 123) | linkActivator(link) {
  constant HOVER_LINK (line 134) | const HOVER_LINK = {
  method linkActivator (line 137) | linkActivator(link) {
  constant FOCUS_LINK (line 141) | const FOCUS_LINK = {
  method linkActivator (line 144) | linkActivator(link) {
  method willHandleMessage (line 174) | willHandleMessage(messageType) {
  method sendMessage (line 181) | sendMessage(messageType, request) {
  method prepareToActivateMode (line 187) | prepareToActivateMode(mode, onExit) {
  method getHintDescriptors (line 222) | getHintDescriptors({ modeIndex, requestedByHelpDialog }, _sender) {
  method activateMode (line 248) | activateMode({ frameId, frameIdToHintDescriptors, modeIndex, originating...
  method updateKeyState (line 275) | updateKeyState(request) {
  method rotateHints (line 278) | rotateHints() {
  method setOpenLinkMode (line 281) | setOpenLinkMode({ modeIndex }) {
  method activateActiveHintMarker (line 284) | activateActiveHintMarker() {
  method getLocalHint (line 287) | getLocalHint(hint) {
  method exit (line 291) | exit({ isSuccess }) {
  method mouseOutOfLastClickedElement (line 301) | mouseOutOfLastClickedElement() {
  method activateMode (line 312) | activateMode(count, { mode, registryEntry }) {
  method activateModeToOpenInNewTab (line 339) | activateModeToOpenInNewTab(count) {
  method activateModeToOpenInNewForegroundTab (line 342) | activateModeToOpenInNewForegroundTab(count) {
  method activateModeToCopyLinkUrl (line 345) | activateModeToCopyLinkUrl(count) {
  method activateModeWithQueue (line 348) | activateModeWithQueue() {
  method activateModeToOpenIncognito (line 351) | activateModeToOpenIncognito(count) {
  method activateModeToDownloadLink (line 354) | activateModeToDownloadLink(count) {
  class LinkHintsMode (line 359) | class LinkHintsMode {
    method constructor (line 361) | constructor(hintDescriptors, mode) {
    method renderHints (line 412) | renderHints() {
    method setOpenLinkMode (line 450) | setOpenLinkMode(mode, shouldPropagateToOtherFrames) {
    method setIndicator (line 464) | setIndicator() {
    method createMarkerFor (line 476) | createMarkerFor(desc) {
    method onKeyDownInMode (line 501) | onKeyDownInMode(event) {
    method updateVisibleMarkers (line 583) | updateVisibleMarkers() {
    method updateKeyState (line 592) | updateKeyState({ hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount ...
    method markerOverlapsStack (line 615) | markerOverlapsStack(marker, stack) {
    method rotateHints (line 625) | rotateHints() {
    method activateLink (line 696) | activateLink(linkMatched, userMightOverType) {
    method showMarker (line 767) | showMarker(linkMarker, matchingCharCount) {
    method hideMarker (line 780) | hideMarker(marker) {
    method deactivateMode (line 786) | deactivateMode() {
    method removeHintMarkers (line 791) | removeHintMarkers() {
  class AlphabetHints (line 800) | class AlphabetHints {
    method constructor (line 801) | constructor() {
    method fillInMarkers (line 811) | fillInMarkers(hintMarkers) {
    method hintStrings (line 831) | hintStrings(linkCount) {
    method getMatchingHints (line 847) | getMatchingHints(hintMarkers) {
    method pushKeyChar (line 854) | pushKeyChar(keyChar) {
    method popKeyChar (line 858) | popKeyChar() {
    method shouldRotateHints (line 863) | shouldRotateHints() {
  class FilterHints (line 869) | class FilterHints {
    method constructor (line 870) | constructor() {
    method generateHintString (line 888) | generateHintString(linkHintNumber) {
    method renderMarker (line 899) | renderMarker(marker) {
    method fillInMarkers (line 908) | fillInMarkers(hintMarkers) {
    method getMatchingHints (line 920) | getMatchingHints(hintMarkers, tabCount) {
    method pushKeyChar (line 950) | pushKeyChar(keyChar) {
    method popKeyChar (line 970) | popKeyChar() {
    method filterLinkHints (line 975) | filterLinkHints(hintMarkers) {
    method scoreLinkHint (line 1005) | scoreLinkHint(linkSearchString) {
    method shouldRotateHints (line 1052) | shouldRotateHints(event) {
  method getLocalHintsForElement (line 1073) | getLocalHintsForElement(element) {
  method getElementFromPoint (line 1300) | getElementFromPoint(x, y, root, stack) {
  method getLocalHints (line 1327) | getLocalHints(requireHref) {
  method generateLinkText (line 1447) | generateLinkText(hint) {
  class TypingProtector (line 1496) | class TypingProtector extends Mode {
    method constructor (line 1497) | constructor(delay, callback) {
  class WaitForEnter (line 1517) | class WaitForEnter extends Mode {
    method constructor (line 1518) | constructor(callback) {
  class HoverMode (line 1540) | class HoverMode extends Mode {
    method constructor (line 1541) | constructor(link) {

FILE: content_scripts/marks.js
  method exit (line 7) | exit(continuation = null) {
  method getLocationKey (line 18) | getLocationKey(keyChar) {
  method getMarkString (line 22) | getMarkString() {
  method setPreviousPosition (line 30) | setPreviousPosition() {
  method showMessage (line 37) | showMessage(message, keyChar) {
  method isGlobalMark (line 45) | isGlobalMark(event, keyChar) {
  method activateCreateMode (line 53) | activateCreateMode(_count, { registryEntry }) {
  method activateGotoMode (line 90) | activateGotoMode(_count, { registryEntry }) {

FILE: content_scripts/mode.js
  class Mode (line 29) | class Mode {
    method init (line 33) | init(options) {
    method setIndicator (line 205) | setIndicator(indicator) {
    method setIndicator (line 212) | static setIndicator() {
    method push (line 216) | push(handlers) {
    method unshift (line 223) | unshift(handlers) {
    method onExit (line 230) | onExit(handler) {
    method exit (line 234) | exit(...args) {
    method logModes (line 258) | logModes() {
    method log (line 267) | log(...args) {
    method top (line 274) | static top() {
    method reset (line 279) | static reset() {
  class SuppressAllKeyboardEvents (line 292) | class SuppressAllKeyboardEvents extends Mode {
    method constructor (line 293) | constructor(options) {
  class CacheAllKeydownEvents (line 306) | class CacheAllKeydownEvents extends SuppressAllKeyboardEvents {
    method constructor (line 307) | constructor(options) {
    method replayKeydownEvents (line 322) | replayKeydownEvents() {

FILE: content_scripts/mode_find.js
  class SuppressPrintable (line 5) | class SuppressPrintable extends Mode {
    method constructor (line 6) | constructor(options) {
  class PostFindMode (line 45) | class PostFindMode extends SuppressPrintable {
    method constructor (line 46) | constructor() {
  class FindMode (line 83) | class FindMode extends Mode {
    method constructor (line 84) | constructor(options) {
    method exit (line 123) | exit(event) {
    method restoreSelection (line 131) | restoreSelection() {
    method findInPlace (line 141) | findInPlace(query, options) {
    method updateQuery (line 156) | static updateQuery(query) {
    method updateActiveRegexIndices (line 215) | static updateActiveRegexIndices() {
    method getQueryFromRegexMatches (line 239) | static getQueryFromRegexMatches() {
    method getNextQueryFromRegexMatches (line 248) | static getNextQueryFromRegexMatches(backwards) {
    method getQuery (line 275) | static getQuery(backwards) {
    method saveQuery (line 286) | static saveQuery() {
    method execute (line 291) | static execute(query, options) {
    method handleEscape (line 347) | static handleEscape() {
    method handleEnter (line 361) | static handleEnter() {
    method findNext (line 367) | static findNext(backwards) {
    method checkReturnToViewPort (line 386) | checkReturnToViewPort() {
  function getAllTextNodes (line 492) | function getAllTextNodes(node) {

FILE: content_scripts/mode_insert.js
  class InsertMode (line 1) | class InsertMode extends Mode {
    method constructor (line 2) | constructor(options) {
    method isActive (line 76) | isActive(event) {
    method getActiveElement (line 86) | getActiveElement() {
    method suppressEvent (line 94) | static suppressEvent(event) {
  class PassNextKeyMode (line 103) | class PassNextKeyMode extends Mode {
    method constructor (line 104) | constructor(count) {

FILE: content_scripts/mode_key_handler.js
  class KeyHandlerMode (line 14) | class KeyHandlerMode extends Mode {
    method setKeyMapping (line 15) | setKeyMapping(keyMapping) {
    method setPassKeys (line 19) | setPassKeys(passKeys) {
    method setCommandHandler (line 25) | setCommandHandler(commandHandler) {
    method reset (line 30) | reset(countPrefix) {
    method init (line 36) | init(options) {
    method onKeydown (line 60) | onKeydown(event) {
    method isMappedKey (line 89) | isMappedKey(keyChar) {
    method isCountKey (line 96) | isCountKey(keyChar) {
    method isPassKey (line 105) | isPassKey(keyChar) {
    method isInResetState (line 119) | isInResetState() {
    method handleKeyChar (line 123) | handleKeyChar(keyChar) {

FILE: content_scripts/mode_normal.js
  class NormalMode (line 1) | class NormalMode extends KeyHandlerMode {
    method init (line 2) | init(options) {
    method commandHandler (line 27) | commandHandler({ command: registryEntry, count }) {
  function findSelectedHelper (line 79) | function findSelectedHelper(backwards) {
  method scrollToBottom (line 89) | scrollToBottom() {
  method scrollToTop (line 93) | scrollToTop(count) {
  method scrollToLeft (line 97) | scrollToLeft() {
  method scrollToRight (line 100) | scrollToRight() {
  method scrollUp (line 103) | scrollUp(count) {
  method scrollDown (line 106) | scrollDown(count) {
  method scrollPageUp (line 109) | scrollPageUp(count) {
  method scrollPageDown (line 112) | scrollPageDown(count) {
  method scrollFullPageUp (line 115) | scrollFullPageUp(count) {
  method scrollFullPageDown (line 118) | scrollFullPageDown(count) {
  method scrollLeft (line 121) | scrollLeft(count) {
  method scrollRight (line 124) | scrollRight(count) {
  method goBack (line 129) | goBack(count) {
  method goForward (line 132) | goForward(count) {
  method goUp (line 137) | goUp(count) {
  method goToRoot (line 151) | goToRoot() {
  method toggleViewSource (line 155) | toggleViewSource() {
  method copyCurrentUrl (line 166) | copyCurrentUrl() {
  method openCopiedUrlInNewTab (line 179) | openCopiedUrlInNewTab(count, request) {
  method openCopiedUrlInCurrentTab (line 190) | openCopiedUrlInCurrentTab() {
  method enterInsertMode (line 197) | enterInsertMode() {
  method enterVisualMode (line 203) | enterVisualMode() {
  method enterVisualLineMode (line 209) | enterVisualLineMode() {
  method enterFindMode (line 215) | enterFindMode() {
  method performFind (line 221) | performFind(count) {
  method performBackwardsFind (line 227) | performBackwardsFind(count) {
  method findSelected (line 233) | findSelected() {
  method findSelectedBackwards (line 237) | findSelectedBackwards() {
  method mainFrame (line 242) | mainFrame() {
  method showHelp (line 245) | showHelp(sourceFrameId) {
  method passNextKey (line 249) | passNextKey(count, options) {
  method goPrevious (line 258) | goPrevious() {
  method goNext (line 265) | goNext() {
  method focusInput (line 272) | focusInput(count) {
  function findLink (line 406) | function findLink(linkStrings) {
  function findElementWithRelValue (line 487) | function findElementWithRelValue(value) {
  class FocusSelector (line 499) | class FocusSelector extends Mode {
    method constructor (line 500) | constructor(hints, visibleInputs, selectedInputIndex) {
    method exit (line 539) | exit() {

FILE: content_scripts/mode_visual.js
  class Movement (line 12) | class Movement {
    method constructor (line 13) | constructor(alterMethod) {
    method getNextForwardCharacter (line 21) | getNextForwardCharacter() {
    method nextCharacterIsWordCharacter (line 36) | nextCharacterIsWordCharacter() {
    method runMovement (line 56) | runMovement(...args) {
    method reverseSelection (line 112) | reverseSelection() {
    method extendByOneCharacter (line 136) | extendByOneCharacter(direction) {
    method getDirection (line 146) | getDirection() {
    method collapseSelectionToAnchor (line 163) | collapseSelectionToAnchor() {
    method collapseSelectionToFocus (line 172) | collapseSelectionToFocus() {
    method setSelectionRange (line 181) | setSelectionRange(range) {
    method selectLexicalEntity (line 189) | selectLexicalEntity(entity, count) {
    method selectLine (line 205) | selectLine(count) {
    method scrollIntoView (line 218) | scrollIntoView() {
  class VisualMode (line 229) | class VisualMode extends KeyHandlerMode {
    method init (line 230) | init(options) {
    method commandHandler (line 366) | commandHandler({ command: { command }, count }) {
    method find (line 382) | find(count, backwards) {
    method yank (line 407) | yank(args) {
  method "aw" (line 444) | "aw"(count) {
  method "as" (line 447) | "as"(count) {
  method "n" (line 451) | "n"(count) {
  method "N" (line 454) | "N"(count) {
  method "/" (line 457) | "/"() {
  method "y" (line 462) | "y"() {
  method "Y" (line 465) | "Y"(count) {
  method "p" (line 469) | "p"() {
  method "P" (line 472) | "P"() {
  method "v" (line 475) | "v"() {
  method "V" (line 478) | "V"() {
  method "c" (line 481) | "c"() {
  method "o" (line 492) | "o"() {
  class VisualLineMode (line 497) | class VisualLineMode extends VisualMode {
    method init (line 498) | init(options) {
    method commandHandler (line 506) | commandHandler({ command: { command }, count }) {
    method extendSelection (line 537) | extendSelection() {
  class CaretMode (line 551) | class CaretMode extends VisualMode {
    method init (line 552) | init(options) {
    method commandHandler (line 579) | commandHandler(...args) {
    method establishInitialSelectionAnchor (line 592) | establishInitialSelectionAnchor() {

FILE: content_scripts/scroller.js
  method init (line 188) | init() {
  method installCancelEventListener (line 195) | installCancelEventListener() {
  method wouldNotInitiateScroll (line 226) | wouldNotInitiateScroll() {
  method scroll (line 243) | scroll(element, direction, amount, continuous) {
  method init (line 337) | init() {
  method reset (line 355) | reset() {
  method scrollBy (line 362) | scrollBy(direction, amount, factor, continuous) {
  method scrollTo (line 396) | scrollTo(direction, pos) {
  method isScrollableElement (line 412) | isScrollableElement(element) {
  method scrollIntoView (line 422) | scrollIntoView(element) {

FILE: content_scripts/ui_component.js
  class UIComponent (line 11) | class UIComponent {
    method load (line 29) | async load(iframeUrl, className, messageHandler) {
    method handleDarkReaderFilter (line 128) | handleDarkReaderFilter() {
    method setIframeVisible (line 146) | setIframeVisible(visible) {
    method postMessage (line 159) | async postMessage(data) {
    method show (line 169) | async show(messageData = {}, focusOptions = {}) {
    method hide (line 182) | async hide(shouldRefocusOriginalFrame) {

FILE: content_scripts/vimium_frontend.js
  function windowIsFocused (line 13) | function windowIsFocused() {
  function initWindowIsFocused (line 17) | function initWindowIsFocused() {
  function isWindowFocusable (line 42) | function isWindowFocusable() {
  class GrabBackFocus (line 49) | class GrabBackFocus extends Mode {
    method constructor (line 50) | constructor() {
    method grabBackFocus (line 109) | grabBackFocus(element) {
  method click (line 130) | click(event) {
  function installModes (line 160) | function installModes() {
  function installListener (line 204) | function installListener(element, event, callback) {
  function initializeOnDomReady (line 253) | async function initializeOnDomReady() {
  function setScrollPosition (line 277) | function setScrollPosition({ scrollX, scrollY }) {
  function focusThisFrame (line 319) | function focusThisFrame(request) {
  method getFocusStatus (line 355) | getFocusStatus(_request, _sender) {
  method focusFrame (line 361) | focusFrame(request) {
  method getScrollPosition (line 364) | getScrollPosition(_ignoredA, _ignoredB) {
  method runInTopFrame (line 371) | runInTopFrame({ sourceFrameId, registryEntry }) {
  method linkHintsMessage (line 378) | linkHintsMessage(request, sender) {
  method showMessage (line 383) | showMessage(request) {
  function handleMessage (line 388) | async function handleMessage(request, sender) {
  function initializePreDomReady (line 414) | async function initializePreDomReady() {
  function checkIfEnabledForUrl (line 428) | async function checkIfEnabledForUrl() {
  method isShowing (line 457) | isShowing() {
  method abort (line 462) | abort() {
  method toggle (line 469) | async toggle(request) {

FILE: content_scripts/vomnibar.js
  method activate (line 10) | activate(sourceFrameId, registryEntry) {
  method activateInNewTab (line 15) | activateInNewTab(sourceFrameId, registryEntry) {
  method activateTabSelection (line 20) | activateTabSelection(sourceFrameId) {
  method activateBookmarks (line 27) | activateBookmarks(sourceFrameId, registryEntry) {
  method activateBookmarksInNewTab (line 35) | activateBookmarksInNewTab(sourceFrameId, registryEntry) {
  method activateEditUrl (line 44) | activateEditUrl(sourceFrameId) {
  method activateEditUrlInNewTab (line 52) | activateEditUrlInNewTab(sourceFrameId) {
  method init (line 61) | init() {
  method open (line 75) | open(sourceFrameId, vomnibarShowOptions) {

FILE: lib/chrome_api_stubs.js
  method connect (line 26) | connect() {
  method addListener (line 38) | addListener() {}
  method sendMessage (line 40) | sendMessage(message) {
  method getManifest (line 46) | getManifest() {
  method getURL (line 49) | getURL(url) {
  method get (line 55) | async get() {
  method set (line 58) | async set() {}
  method get (line 61) | async get() {
  method set (line 64) | async set() {}
  method get (line 67) | async get() {
  method set (line 70) | async set() {}
  method addListener (line 73) | addListener() {}
  method getURL (line 78) | getURL(url) {

FILE: lib/dom_utils.js
  method isReady (line 5) | isReady() {
  method documentReady (line 10) | documentReady() {
  method documentComplete (line 21) | documentComplete() {
  method createElement (line 40) | createElement(tagName) {
  method removeElement (line 58) | removeElement(el) {
  method isTopFrame (line 65) | isTopFrame() {
  method makeXPath (line 74) | makeXPath(elementArray) {
  method evaluateXPath (line 84) | evaluateXPath(xpath, resultType) {
  method getVisibleClientRect (line 102) | getVisibleClientRect(element, testChildren) {
  method cropRectToVisible (line 174) | cropRectToVisible(rect) {
  method getClientRectsForAreas (line 195) | getClientRectsForAreas(imgClientRect, areaEls) {
  method isSelectable (line 247) | isSelectable(element) {
  method isEditable (line 268) | isEditable(element) {
  method isEmbed (line 274) | isEmbed(element) {
  method isFocusable (line 279) | isFocusable(element) {
  method isDOMDescendant (line 283) | isDOMDescendant(parent, child) {
  method isSelected (line 293) | isSelected(element) {
  method simulateSelect (line 310) | simulateSelect(element) {
  method simulateClick (line 341) | simulateClick(element, modifiers) {
  method simulateMouseEvent (line 359) | simulateMouseEvent(event, element, modifiers) {
  method simulateClickDefaultAction (line 386) | simulateClickDefaultAction(element, modifiers) {
  method simulateHover (line 420) | simulateHover(element, modifiers) {
  method simulateUnhover (line 425) | simulateUnhover(element, modifiers) {
  method addFlashRect (line 430) | addFlashRect(rect) {
  method getViewportTopLeft (line 442) | getViewportTopLeft() {
  method suppressPropagation (line 464) | suppressPropagation(event) {
  method suppressEvent (line 468) | suppressEvent(event) {
  method keyup (line 484) | keyup(event) {
  method blur (line 495) | blur(event) {
  method getElementWithFocus (line 516) | getElementWithFocus(selection, backwards) {
  method getSelectionFocusElement (line 531) | getSelectionFocusElement() {
  method getContainingElement (line 549) | getContainingElement(element) {
  method windowIsTooSmall (line 556) | windowIsTooSmall() {
  method injectUserCss (line 563) | injectUserCss(parent) {

FILE: lib/find_mode_history.js
  method init (line 9) | async init() {
  method getQuery (line 34) | getQuery(index) {
  method saveQuery (line 39) | async saveQuery(query) {
  method refreshRawQueryList (line 59) | refreshRawQueryList(query, rawQueryList) {

FILE: lib/handler_stack.js
  class HandlerStack (line 1) | class HandlerStack {
    method constructor (line 2) | constructor() {
    method push (line 29) | push(handler) {
    method unshift (line 37) | unshift(handler) {
    method bubbleEvent (line 49) | bubbleEvent(type, event) {
    method remove (line 96) | remove(id) {
    method isChromeEvent (line 111) | isChromeEvent(event) {
    method alwaysContinueBubbling (line 120) | alwaysContinueBubbling(handler = null) {
    method alwaysSuppressPropagation (line 127) | alwaysSuppressPropagation(handler = null) {
    method logResult (line 135) | logResult(eventNumber, type, event, handler, result) {
    method show (line 158) | show() {
    method reset (line 166) | reset() {

FILE: lib/keyboard_utils.js
  method init (line 17) | init() {
  method getKeyChar (line 34) | getKeyChar(event) {
  method getKeyCharString (line 76) | getKeyCharString(event) {
  method isBackspace (line 112) | isBackspace(event) {
  method isPrintable (line 116) | isPrintable(event) {
  method isModifier (line 121) | isModifier(event) {

FILE: lib/rect.js
  method create (line 4) | create(x1, y1, x2, y2) {
  method copy (line 15) | copy(rect) {
  method translate (line 27) | translate(rect, x, y) {
  method subtract (line 41) | subtract(rect1, rect2) {
  method intersects (line 83) | intersects(rect1, rect2) {
  method intersectsStrict (line 91) | intersectsStrict(rect1, rect2) {
  method equals (line 96) | equals(rect1, rect2) {
  method intersect (line 103) | intersect(rect1, rect2) {

FILE: lib/settings.js
  method onLoaded (line 98) | async onLoaded() {
  method chromeStorageOnChanged (line 104) | async chromeStorageOnChanged(_changes, area) {
  method load (line 114) | async load() {
  method isLoaded (line 131) | isLoaded() {
  method get (line 135) | get(key) {
  method set (line 142) | async set(key, value) {
  method getSettings (line 150) | getSettings() {
  method migratePre2_0 (line 154) | migratePre2_0(settings) {
  method migratePre2_4 (line 186) | migratePre2_4(settings) {
  method migratePre2_4_1 (line 209) | migratePre2_4_1(settings) {
  method migrateSettingsIfNecessary (line 231) | migrateSettingsIfNecessary(settings) {
  method setSettings (line 238) | async setSettings(settings) {
  method pruneOutDefaultValues (line 256) | pruneOutDefaultValues(settings) {
  method clear (line 267) | async clear() {

FILE: lib/url_utils.js
  method init (line 15) | async init() {
  method isUrl (line 36) | async isUrl(str) {
  method convertToUrl (line 92) | async convertToUrl(string) {
  method hasChromeProtocol (line 108) | hasChromeProtocol(url) {
  method hasJavascriptProtocol (line 112) | hasJavascriptProtocol(url) {
  method urlHasProtocol (line 117) | urlHasProtocol(url) {
  method createSearchUrl (line 122) | createSearchUrl(query, searchUrl) {

FILE: lib/utils.js
  method debugLog (line 27) | debugLog() {
  method isFirefox (line 42) | isFirefox() {
  method firefoxVersion (line 49) | firefoxVersion() {
  method getCurrentVersion (line 54) | getCurrentVersion() {
  method populateBrowserInfo (line 58) | async populateBrowserInfo() {
  method escapeHtml (line 73) | escapeHtml(string) {
  method decodeURIByParts (line 86) | decodeURIByParts(uri) {
  method isString (line 126) | isString(obj) {
  method distinctCharacters (line 131) | distinctCharacters(str) {
  method compareVersions (line 138) | compareVersions(versionA, versionB) {
  method keyBy (line 161) | keyBy(array, key) {
  method zip (line 173) | zip(arrays) {
  method pick (line 178) | pick(object, propertyList) {
  method hasUpperCase (line 189) | hasUpperCase(s) {
  method matchesAnyRegexp (line 194) | matchesAnyRegexp(regexps, string) {
  method setTimeout (line 202) | setTimeout(ms, func) {
  method nextTick (line 207) | nextTick(func) {
  method promiseWithTimeout (line 211) | promiseWithTimeout(promise, ms) {
  method makeIdempotent (line 219) | makeIdempotent(func) {
  method monitorChromeSessionStorage (line 230) | monitorChromeSessionStorage(key, setter) {
  method assert (line 244) | assert(expression, ...messages) {
  method addChromeRuntimeOnMessageListener (line 265) | addChromeRuntimeOnMessageListener(requestsHandled, listenerFn) {
  method assertType (line 286) | assertType(schema, o) {
  class SimpleCache (line 321) | class SimpleCache {
    method constructor (line 325) | constructor(expiry, maxEntries) {
    method has (line 335) | has(key) {
    method set (line 341) | set(key, value = null) {
    method get (line 352) | get(key) {
    method rotate (line 365) | rotate(force) {
    method clear (line 379) | clear() {
  method addEventListener (line 387) | addEventListener(eventName, listener) {
  method dispatchEvent (line 393) | dispatchEvent(eventName) {
  method removeEventListener (line 400) | removeEventListener(eventName, listener) {

FILE: make.js
  function shell (line 17) | async function shell(procName, argsArray = []) {
  function createFirefoxManifest (line 33) | function createFirefoxManifest(manifest) {
  function parseManifestFile (line 87) | async function parseManifestFile() {
  function checkForCommonBuildIssues (line 94) | async function checkForCommonBuildIssues() {
  function checkFilesFromManifestArePresent (line 114) | async function checkFilesFromManifestArePresent(manifest) {
  function getPathsFromManifest (line 133) | function getPathsFromManifest(manifest) {
  function buildStorePackage (line 184) | async function buildStorePackage() {
  function runUnitTests (line 259) | async function runUnitTests() {
  function setupPuppeteerPageForTests (line 272) | function setupPuppeteerPageForTests(page) {
  function runPuppeteerTest (line 313) | async function runPuppeteerTest(page, url) {
  function isPortAvailable (line 343) | function isPortAvailable(number) {
  function getAvailablePort (line 353) | function getAvailablePort() {
  function testDom (line 375) | async function testDom() {

FILE: pages/action.js
  method init (line 9) | async init() {
  method isVimiumInstalledInTab (line 85) | async isVimiumInstalledInTab(tabId) {
  method showValidationErrors (line 97) | showValidationErrors() {
  method showExclusionRulesEditor (line 114) | showExclusionRulesEditor() {
  method syncEnabledKeysCaption (line 119) | syncEnabledKeysCaption() {
  method onSave (line 129) | async onSave() {
  method getPatternRegExp (line 140) | getPatternRegExp(patternStr) {
  method generateDefaultPattern (line 146) | generateDefaultPattern(url) {

FILE: pages/command_listing.js
  function compareKeys (line 5) | function compareKeys(a, b) {
  function replaceBackticksWithCodeTags (line 17) | function replaceBackticksWithCodeTags(str) {
  function populatePage (line 25) | async function populatePage() {

FILE: pages/doc_search_completion.js
  function cleanUpRegexp (line 4) | function cleanUpRegexp(re) {
  function populatePage (line 11) | function populatePage() {

FILE: pages/exclusion_rules_editor.js
  method init (line 7) | init() {
  method setForm (line 15) | setForm(exclusionRules = []) {
  method addRow (line 30) | addRow(pattern, passKeys) {
  method getRules (line 51) | getRules() {

FILE: pages/help_dialog_page.js
  function compareKeys (line 6) | function compareKeys(a, b) {
  function ellipsize (line 20) | function ellipsize(s, maxLength) {
  function isAdvancedCommand (line 26) | function isAdvancedCommand(command, options) {
  method getShowAdvancedCommands (line 36) | getShowAdvancedCommands() {
  method init (line 40) | init() {
  method getRowsForDialog (line 76) | getRowsForDialog(commandToOptionsToKeys) {
  method getRowEl (line 94) | getRowEl(command, options, keys) {
  method show (line 126) | async show() {
  method hide (line 148) | hide() {
  method toggleAdvancedCommands (line 155) | toggleAdvancedCommands(event) {
  method showAdvancedCommands (line 169) | showAdvancedCommands(visible) {
  function init (line 180) | function init() {

FILE: pages/hud_page.js
  constant TIME_TO_WAIT_FOR_IPC_MESSAGES (line 14) | const TIME_TO_WAIT_FOR_IPC_MESSAGES = 17;
  function setTextInInputElement (line 17) | function setTextInInputElement(inputEl, text) {
  function onKeyEvent (line 29) | function onKeyEvent(event) {
  function ensureClipboardIsAvailable (line 80) | function ensureClipboardIsAvailable() {
  method show (line 90) | show(data) {
  method hidden (line 98) | hidden() {
  method showFindMode (line 107) | showFindMode() {
  method updateMatchesCount (line 161) | updateMatchesCount({ matchCount, showMatchText }) {
  method copyToClipboard (line 175) | copyToClipboard(message) {
  method pasteFromClipboard (line 193) | pasteFromClipboard() {
  function init (line 212) | function init() {

FILE: pages/options.js
  function init (line 30) | async function init() {
  function getOptionEl (line 102) | function getOptionEl(optionName) {
  function resetInputValue (line 107) | function resetInputValue(event) {
  function setFormFromSettings (line 117) | function setFormFromSettings(settings) {
  function getSettingsFromForm (line 147) | function getSettingsFromForm() {
  function getValidationErrors (line 180) | function getValidationErrors() {
  function addValidationMessage (line 217) | function addValidationMessage(el, message) {
  function showValidationErrors (line 227) | function showValidationErrors() {
  function removeDuplicateChars (line 254) | function removeDuplicateChars(str) {
  function saveOptions (line 266) | async function saveOptions() {
  function showElement (line 280) | function showElement(el, visible) {
  function maintainNewTabUrlView (line 286) | function maintainNewTabUrlView() {
  function maintainLinkHintsView (line 300) | function maintainLinkHintsView() {
  function prepareBackupSettings (line 317) | function prepareBackupSettings() {
  function onDownloadBackupClicked (line 327) | function onDownloadBackupClicked() {
  function onUploadBackupClicked (line 333) | function onUploadBackupClicked() {

FILE: pages/ui_component_messenger.js
  function registerPortWithOwnerPage (line 8) | async function registerPortWithOwnerPage(event) {
  function unregister (line 25) | async function unregister() {
  function init (line 30) | function init() {
  function openPort (line 34) | function openPort(port) {
  function registerHandler (line 44) | function registerHandler(messageHandlerFn) {
  function postMessage (line 48) | function postMessage(data) {
  function dispatchReadyEventWhenReady (line 57) | function dispatchReadyEventWhenReady() {

FILE: pages/vomnibar_page.js
  function reset (line 20) | function reset() {
  function activate (line 24) | async function activate(options) {
  class VomnibarUI (line 53) | class VomnibarUI {
    method constructor (line 54) | constructor() {
    method setQuery (line 67) | setQuery(query) {
    method setActiveUserSearchEngine (line 70) | setActiveUserSearchEngine(userSearchEngine) {
    method setInitialSelectionValue (line 74) | setInitialSelectionValue(initialSelectionValue) {
    method setForceNewTab (line 77) | setForceNewTab(forceNewTab) {
    method setCompleterName (line 80) | setCompleterName(name) {
    method isUserSearchEngineActive (line 86) | isUserSearchEngineActive() {
    method hide (line 98) | hide(onHiddenCallback = null) {
    method onHidden (line 109) | onHidden() {
    method reset (line 115) | reset() {
    method updateSelection (line 126) | updateSelection() {
    method actionFromKeyEvent (line 152) | actionFromKeyEvent(event) {
    method onKeyEvent (line 186) | async onKeyEvent(event) {
    method handleEnterKey (line 252) | async handleEnterKey(event) {
    method getInputValueAsQuery (line 311) | getInputValueAsQuery() {
    method updateCompletions (line 316) | async updateCompletions() {
    method renderCompletions (line 343) | renderCompletions(completions) {
    method refreshCompletions (line 348) | refreshCompletions() {
    method cancelCompletions (line 355) | cancelCompletions() {
    method onInput (line 364) | onInput() {
    method getUserSearchEngineForQuery (line 386) | getUserSearchEngineForQuery() {
    method update (line 399) | async update() {
    method openCompletion (line 404) | openCompletion(completion, openInNewTab) {
    method launchUrl (line 412) | async launchUrl(url, openInNewTab) {
    method initDom (line 424) | initDom() {
  function init (line 447) | function init() {

FILE: test_harnesses/vomnibar_harness.js
  function setup (line 4) | function setup() {

FILE: tests/dom_tests/dom_tests.js
  method preventDefault (line 191) | preventDefault() {}
  method stopImmediatePropagation (line 192) | stopImmediatePropagation() {}
  class Test (line 1146) | class Test extends Mode {
    method constructor (line 1147) | constructor() {
    method exit (line 1152) | exit() {

FILE: tests/unit_tests/completion/completers_test.js
  method addListener (line 129) | addListener(listener) {
  method removeListener (line 132) | removeListener() {}
  method addListener (line 135) | addListener(listener) {
  method removeListener (line 138) | removeListener() {}
  method addListener (line 198) | addListener() {}
  method removeListener (line 198) | removeListener() {}
  method addListener (line 199) | addListener() {}
  method removeListener (line 199) | removeListener() {}
  method addListener (line 227) | addListener() {}
  method removeListener (line 227) | removeListener() {}
  method addListener (line 228) | addListener() {}
  method removeListener (line 228) | removeListener() {}
  method addListener (line 279) | addListener(_listener) {
  method addListener (line 283) | addListener(listener) {

FILE: tests/unit_tests/handler_stack_test.js
  method keydown (line 146) | keydown() {

FILE: tests/unit_tests/hud_page_test.js
  function newKeyEvent (line 6) | function newKeyEvent(properties) {

FILE: tests/unit_tests/test_chrome_stubs.js
  method set (line 10) | async set(items) {
  method get (line 23) | async get(keysArg) {
  method remove (line 40) | async remove(key) {
  method clear (line 48) | async clear() {
  method getURL (line 76) | getURL() {
  method getManifest (line 79) | getManifest() {
  method addListener (line 83) | addListener() {
  method addListener (line 88) | addListener() {
  method addListener (line 93) | addListener() {}
  method addListener (line 96) | addListener() {}
  method getURL (line 101) | getURL(path) {
  method getBackgroundPage (line 104) | getBackgroundPage() {
  method getViews (line 107) | getViews() {
  method executeScript (line 113) | executeScript() {}
  method query (line 117) | query() {}
  method get (line 121) | get(_id) {}
  method addListener (line 123) | addListener() {
  method addListener (line 128) | addListener() {
  method addListener (line 133) | addListener() {
  method addListener (line 138) | addListener() {
  method addListener (line 143) | addListener() {
  method addListener (line 148) | addListener() {
  method query (line 152) | query() {
  method sendMessage (line 155) | sendMessage(_id, _properties) {}
  method update (line 156) | update(_id, _properties) {}
  method addListener (line 161) | addListener() {}
  method addListener (line 164) | addListener() {}
  method addListener (line 167) | addListener() {}
  method addListener (line 173) | addListener() {
  method getAll (line 177) | getAll() {
  method getCurrent (line 180) | getCurrent() {
  method addListener (line 184) | addListener() {
  method update (line 188) | update(_id, _properties) {}
  method setBadgeBackgroundColor (line 192) | setBadgeBackgroundColor() {}
  method addListener (line 201) | addListener(func) {
  method call (line 206) | call(key, value, area) {
  method callEmpty (line 213) | callEmpty(key) {

FILE: tests/unit_tests/test_helper.js
  function jsdomStub (line 23) | async function jsdomStub(htmlFile) {

FILE: tests/unit_tests/ui_component_test.js
  function stubPostMessage (line 6) | function stubPostMessage(iframeEl, fn) {

FILE: tests/unit_tests/vomnibar_page_test.js
  function newKeyEvent (line 6) | function newKeyEvent(properties) {

FILE: tests/vendor/shoulda.js
  method isTrue (line 10) | isTrue(value) {
  method isFalse (line 16) | isFalse(value) {
  method equal (line 23) | equal(expected, actual) {
  method throwsError (line 33) | throwsError(expression, errorName) {
  method fail (line 56) | fail(message) {
  method _print (line 61) | _print(object) {
  function ensureCalled (line 81) | function ensureCalled(fn) {
  class AssertionError (line 93) | class AssertionError extends Error {
    method constructor (line 94) | constructor(message) {
  function Context (line 106) | function Context(name) {
  function context (line 119) | function context(name, fn) {
  function setup (line 141) | function setup(fn) {
  function teardown (line 145) | function teardown(fn) {
  function should (line 149) | function should(name, fn) {
  method run (line 179) | async run(testNameFilter) {
  method reset (line 198) | reset() {
  method runContext (line 208) | async runContext(context, parentContexts, testNameFilter) {
  method runTest (line 225) | async runTest(testMethod, contexts, testNameFilter) {
  method fullyQualifiedName (line 306) | fullyQualifiedName(testName, contexts) {
  method printTestSummary (line 310) | printTestSummary() {
  method printFailure (line 318) | printFailure(testName, failureMessage) {
  function run (line 323) | function run(testNameFilter) {
  function reset (line 327) | function reset() {
  function getStats (line 334) | function getStats() {
  function stub (line 346) | function stub(object, propertyName, returnValue) {
  function returns (line 359) | function returns(value) {
  function clearStubs (line 363) | function clearStubs() {
Condensed preview — 118 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (810K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 581,
    "preview": "---\nname: Bug report\nabout: File a bug\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Describe the bug**\n\nInclude a clear bug"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 193,
    "preview": "## Description\n\nProvide a rationale for this PR, and a reference to the corresponding issue, if there is one.\n\nPlease re"
  },
  {
    "path": ".gitignore",
    "chars": 4,
    "preview": "dist"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 29837,
    "preview": "2.4.1, 2.4.2 (2026-03-07)\n\n- Fix issue where existing users were mistakenly opted-in to\n  [Vimium's new tab page](https:"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 5613,
    "preview": "# Contributing to Vimium\n\n## Reporting a bug\n\nFile the issue [here](https://github.com/philc/vimium/issues).\n\n## Contrib"
  },
  {
    "path": "CREDITS",
    "chars": 2214,
    "preview": "Authors & Maintainers:\n  Ilya Sukhar <ilya.sukhar@gmail.com> (github: ilya)\n  Phil Crosby <phil.crosby@gmail.com> (githu"
  },
  {
    "path": "MIT-LICENSE.txt",
    "chars": 1068,
    "preview": "Copyright (c) 2010 Phil Crosby, Ilya Sukhar.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a co"
  },
  {
    "path": "README.md",
    "chars": 7001,
    "preview": "# Vimium - The Hacker's Browser\n\nVimium is a browser extension that provides keyboard-based navigation and control of th"
  },
  {
    "path": "background_scripts/all_commands.js",
    "chars": 12745,
    "preview": "// This is the order they will be shown in the help dialog.\n//\n// Properties:\n// - advanced: advanced commands are not s"
  },
  {
    "path": "background_scripts/bg_utils.js",
    "chars": 952,
    "preview": "import { TabRecency } from \"./tab_recency.js\";\n\n// We're using browser.runtime to determine the browser name and version"
  },
  {
    "path": "background_scripts/commands.js",
    "chars": 17859,
    "preview": "import { allCommands } from \"./all_commands.js\";\n\n// A specification for a command that's currently bound to a key seque"
  },
  {
    "path": "background_scripts/completion/completers.js",
    "chars": 28779,
    "preview": "// This file contains the definition of the completers used for the Vomnibar's suggestion UI. A\n// completer will take a"
  },
  {
    "path": "background_scripts/completion/ranking.js",
    "chars": 6204,
    "preview": "// Utilities which help us compute a relevancy score for a given item.\n\n// Whether the given things (usually URLs or tit"
  },
  {
    "path": "background_scripts/completion/search_engines.js",
    "chars": 7562,
    "preview": "// An engine provides search suggestions for a online search engine.\n//\n// An \"engineUrl\" is used for fetching suggestio"
  },
  {
    "path": "background_scripts/completion/search_wrapper.js",
    "chars": 5872,
    "preview": "import * as searchEngines from \"./search_engines.js\";\n\n// This is a wrapper class for completion engines. It handles the"
  },
  {
    "path": "background_scripts/exclusions.js",
    "chars": 2787,
    "preview": "// This module manages manages the exclusion rule setting. An exclusion is an object with two\n// attributes: pattern and"
  },
  {
    "path": "background_scripts/main.js",
    "chars": 34452,
    "preview": "import \"../lib/utils.js\";\nimport \"../lib/settings.js\";\nimport \"../lib/url_utils.js\";\nimport \"../background_scripts/tab_r"
  },
  {
    "path": "background_scripts/marks.js",
    "chars": 5651,
    "preview": "import * as TabOperations from \"./tab_operations.js\";\n\n// This returns the key which is used for storing mark locations "
  },
  {
    "path": "background_scripts/reload.js",
    "chars": 1193,
    "preview": "// Used as part of a debugging workflow when developing the extension.\n\nconst tabs = await chrome.tabs.query({});\n// Cle"
  },
  {
    "path": "background_scripts/tab_operations.js",
    "chars": 4602,
    "preview": "//\n// Functions for opening URLs in tabs.\n//\n\nimport * as bgUtils from \"../background_scripts/bg_utils.js\";\nimport \"../l"
  },
  {
    "path": "background_scripts/tab_recency.js",
    "chars": 4435,
    "preview": "// TabRecency associates an integer with each tab id representing how recently it has been accessed.\n// The order of tab"
  },
  {
    "path": "background_scripts/user_search_engines.js",
    "chars": 1824,
    "preview": "import \"../lib/url_utils.js\";\nimport * as commands from \"./commands.js\";\n\n// A struct representing a search engine entry"
  },
  {
    "path": "build_scripts/write_command_listing_page.js",
    "chars": 2157,
    "preview": "#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env\n// Write a static version of the command_listing.html "
  },
  {
    "path": "content_scripts/file_urls.css",
    "chars": 343,
    "preview": "/* Chrome file:// URLs set draggable=true for links to files (CSS selector .icon.file). This\n * automatically sets -webk"
  },
  {
    "path": "content_scripts/hud.js",
    "chars": 9202,
    "preview": "//\n// A heads-up-display (HUD) for showing Vimium page operations.\n// Note: you cannot interact with the HUD until docum"
  },
  {
    "path": "content_scripts/link_hints.js",
    "chars": 56319,
    "preview": "//\n// This implements link hinting. Typing \"F\" will enter link-hinting mode, where all clickable items\n// on the page ha"
  },
  {
    "path": "content_scripts/marks.js",
    "chars": 4903,
    "preview": "const Marks = {\n  previousPositionRegisters: [\"`\", \"'\"],\n  localRegisters: {},\n  currentRegistryEntry: null,\n  mode: nul"
  },
  {
    "path": "content_scripts/mode.js",
    "chars": 10157,
    "preview": "//\n// A mode implements a number of keyboard (and possibly other) event handlers which are pushed onto\n// the handler st"
  },
  {
    "path": "content_scripts/mode_find.js",
    "chars": 16308,
    "preview": "// NOTE(smblott). Ultimately, all of the FindMode-related code should be moved here.\n\n// This prevents unmapped printabl"
  },
  {
    "path": "content_scripts/mode_insert.js",
    "chars": 3979,
    "preview": "class InsertMode extends Mode {\n  constructor(options) {\n    super();\n    if (options == null) {\n      options = {};\n   "
  },
  {
    "path": "content_scripts/mode_key_handler.js",
    "chars": 5309,
    "preview": "// Example key mapping (@keyMapping):\n//   i:\n//     command: \"enterInsertMode\", ... # This is a registryEntry object (a"
  },
  {
    "path": "content_scripts/mode_normal.js",
    "chars": 18825,
    "preview": "class NormalMode extends KeyHandlerMode {\n  init(options) {\n    if (options == null) {\n      options = {};\n    }\n\n    co"
  },
  {
    "path": "content_scripts/mode_visual.js",
    "chars": 25642,
    "preview": "// Symbolic names for some common strings.\nconst forward = \"forward\";\nconst backward = \"backward\";\nconst character = \"ch"
  },
  {
    "path": "content_scripts/scroller.js",
    "chars": 18826,
    "preview": "// activatedElement is different from document.activeElement -- the latter seems to be reserved\n// mostly for input elem"
  },
  {
    "path": "content_scripts/ui_component.js",
    "chars": 8683,
    "preview": "// A UIComponent is an iframe containing a Vimium extension page, like the Vomnibar. This class\n// provides methods that"
  },
  {
    "path": "content_scripts/vimium.css",
    "chars": 7027,
    "preview": "/*\n * Many CSS class names in this file use the verbose \"vimium-\" as the class name. This is so we\n * don't use the same"
  },
  {
    "path": "content_scripts/vimium_frontend.js",
    "chars": 16730,
    "preview": "//\n// This content script must be run prior to domReady so that we perform some operations very early.\n//\n\nlet isEnabled"
  },
  {
    "path": "content_scripts/vomnibar.js",
    "chars": 2602,
    "preview": "//\n// This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar.\n//\nconst Vomnibar = {\n  vom"
  },
  {
    "path": "deno.json",
    "chars": 334,
    "preview": "{\n  \"fmt\": {\n    \"exclude\": [\"icons/*.svg\"],\n    \"lineWidth\": 100\n  },\n  \"imports\": {\n    \"@b-fuze/deno-dom\": \"jsr:@b-fu"
  },
  {
    "path": "lib/chrome_api_stubs.js",
    "chars": 1899,
    "preview": "//\n// Mock the Chrome extension API for our tests. In Deno and Pupeteer, the Chrome extension APIs are\n// not available."
  },
  {
    "path": "lib/dom_utils.js",
    "chars": 20848,
    "preview": "const DomUtils = {\n  //\n  // Runs :callback if the DOM has loaded, otherwise runs it on load\n  //\n  isReady() {\n    retu"
  },
  {
    "path": "lib/find_mode_history.js",
    "chars": 2176,
    "preview": "// This // implements find-mode query history as a list of raw queries, most recent first.\n// This is under lib/ since i"
  },
  {
    "path": "lib/handler_stack.js",
    "chars": 6011,
    "preview": "class HandlerStack {\n  constructor() {\n    this.debug = false;\n    this.eventNumber = 0;\n    this.stack = [];\n    this.c"
  },
  {
    "path": "lib/keyboard_utils.js",
    "chars": 5103,
    "preview": "let mapKeyRegistry = {};\nUtils.monitorChromeSessionStorage(\"mapKeyRegistry\", (value) => {\n  return mapKeyRegistry = valu"
  },
  {
    "path": "lib/rect.js",
    "chars": 3272,
    "preview": "// Commands for manipulating rects.\nconst Rect = {\n  // Create a rect given the top left and bottom right corners.\n  cre"
  },
  {
    "path": "lib/settings.js",
    "chars": 10718,
    "preview": "// The possible destinations for new tabs opened using Vimium's `createTab` command.\nconst newTabDestinations = {\n  brow"
  },
  {
    "path": "lib/types.js",
    "chars": 492,
    "preview": "// A centralized file of types which can be shared by both content scripts and background pages.\n\nglobalThis.VomnibarSho"
  },
  {
    "path": "lib/url_utils.js",
    "chars": 4704,
    "preview": "const UrlUtils = {\n  chromeNewTabUrl: \"about:newtab\",\n\n  // A set of top-level domains, e.g. [\"com\"] recognized by https"
  },
  {
    "path": "lib/utils.js",
    "chars": 14497,
    "preview": "// Only pass events to the handler if they are marked as trusted by the browser.\n// This is kept in the global namespace"
  },
  {
    "path": "make.js",
    "chars": 15101,
    "preview": "#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env --allow-net --allow-run --allow-sys\n// Usage: ./make.j"
  },
  {
    "path": "manifest.json",
    "chars": 3595,
    "preview": "{\n  \"manifest_version\": 3,\n  \"name\": \"Vimium\",\n  \"version\": \"2.4.2\",\n  \"description\": \"The Hacker's Browser. Vimium prov"
  },
  {
    "path": "pages/action.css",
    "chars": 871,
    "preview": ":root {\n  --padding: 15px;\n}\n\nbody {\n  /* This will be the size of the toolbar action popup. */\n  width: 600px;\n  height"
  },
  {
    "path": "pages/action.html",
    "chars": 3038,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"options.css\""
  },
  {
    "path": "pages/action.js",
    "chars": 6489,
    "preview": "import \"../lib/utils.js\";\nimport \"../lib/dom_utils.js\";\nimport \"../lib/settings.js\";\n\nimport * as bgUtils from \"../backg"
  },
  {
    "path": "pages/all_content_scripts.js",
    "chars": 1118,
    "preview": "// This is the set of all content scripts required to make Vimium's functionality work. This file is\n// imported by back"
  },
  {
    "path": "pages/command_listing.css",
    "chars": 1978,
    "preview": ":root {\n  --border-color: #666;\n}\n\nhtml, body {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-si"
  },
  {
    "path": "pages/command_listing.html",
    "chars": 1958,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"color-scheme\" content=\"light dark\">\n    <titl"
  },
  {
    "path": "pages/command_listing.js",
    "chars": 2872,
    "preview": "import \"./all_content_scripts.js\";\nimport { allCommands } from \"../background_scripts/all_commands.js\";\n\n// The ordering"
  },
  {
    "path": "pages/doc.css",
    "chars": 144,
    "preview": "body.documentation {\n  max-width: 730px;\n  margin: 0 25px;\n}\n\nul {\n  padding-left: 20px;\n}\n\nli {\n  margin: 10px;\n}\n\np {\n"
  },
  {
    "path": "pages/doc_search_completion.html",
    "chars": 1940,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"color-scheme\" content=\"light dark\">\n    <titl"
  },
  {
    "path": "pages/doc_search_completion.js",
    "chars": 1740,
    "preview": "import \"./all_content_scripts.js\";\nimport * as completionEngines from \"../background_scripts/completion/search_engines.j"
  },
  {
    "path": "pages/exclusion_rules_editor.js",
    "chars": 2467,
    "preview": "// The table-editor used for exclusion rules.\nconst ExclusionRulesEditor = {\n  // When the Add rule button is clicked, u"
  },
  {
    "path": "pages/help_dialog_page.css",
    "chars": 2546,
    "preview": "body {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n\n#container {\n  background-color: white;\n  borde"
  },
  {
    "path": "pages/help_dialog_page.html",
    "chars": 3196,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Vimium Help</title>\n    <meta name=\"color-scheme\" content=\"light dark\">\n    <"
  },
  {
    "path": "pages/help_dialog_page.js",
    "chars": 7369,
    "preview": "import \"./all_content_scripts.js\";\nimport * as UIComponentMessenger from \"./ui_component_messenger.js\";\nimport { allComm"
  },
  {
    "path": "pages/hud_page.css",
    "chars": 1433,
    "preview": "#hud-container {\n  display: block;\n  position: fixed;\n  width: calc(100% - 20px);\n  bottom: 8px;\n  left: 8px;\n  backgrou"
  },
  {
    "path": "pages/hud_page.html",
    "chars": 485,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>HUD</title>\n    <meta name=\"color-scheme\" content=\"light dark\">\n    <meta cha"
  },
  {
    "path": "pages/hud_page.js",
    "chars": 8287,
    "preview": "import \"../lib/chrome_api_stubs.js\";\nimport \"../lib/utils.js\";\nimport \"../lib/dom_utils.js\";\nimport \"../lib/settings.js\""
  },
  {
    "path": "pages/key_mappings.css",
    "chars": 1119,
    "preview": "/*\n * Styles for showing key bindings. Shared by help_dialog_page.html and command_listing.html.\n */\n\n.key-bindings {\n  "
  },
  {
    "path": "pages/options.css",
    "chars": 8293,
    "preview": "/* This stylesheet is included in both options.html and action.html, so changes affect both. */\n:root {\n  --closeButtonW"
  },
  {
    "path": "pages/options.html",
    "chars": 10094,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Vimium Options</title>\n    <meta charset=\"UTF-8\">\n    <meta name=\"color-schem"
  },
  {
    "path": "pages/options.js",
    "chars": 11869,
    "preview": "import \"./all_content_scripts.js\";\nimport { ExclusionRulesEditor } from \"./exclusion_rules_editor.js\";\nimport { allComma"
  },
  {
    "path": "pages/reload.html",
    "chars": 268,
    "preview": "<!--\n    This page can be used during development to reload the Vimium extension, and all open tabs.\n  -->\n<!DOCTYPE htm"
  },
  {
    "path": "pages/ui_component_messenger.js",
    "chars": 2263,
    "preview": "//\n// These are functions for a page in a UIComponent iframe to communicate to its parent frame.\n//\n\nlet ownerPagePort ="
  },
  {
    "path": "pages/vomnibar_page.css",
    "chars": 3665,
    "preview": "ul {\n  list-style: none;\n  display: none;\n  margin: 0;\n  padding: 0;\n}\n\n#vomnibar {\n  display: block;\n  position: fixed;"
  },
  {
    "path": "pages/vomnibar_page.html",
    "chars": 538,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Vomnibar</title>\n    <meta name=\"color-scheme\" content=\"light dark\">\n    <met"
  },
  {
    "path": "pages/vomnibar_page.js",
    "chars": 17134,
    "preview": "//\n// This controls the contents of the Vomnibar iframe. We use an iframe to avoid changing the\n// selection on the page"
  },
  {
    "path": "resources/tlds.txt",
    "chars": 9445,
    "preview": "aaa\naarp\nabarth\nabb\nabbott\nabbvie\nabc\nable\nabogado\nabudhabi\nac\nacademy\naccenture\naccountant\naccountants\naco\nactive\nactor"
  },
  {
    "path": "test_harnesses/cross_origin_iframe.html",
    "chars": 543,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"data:;base64,iVBORw0KGgo=\">\n    <t"
  },
  {
    "path": "test_harnesses/event_capture.html",
    "chars": 671,
    "preview": "<!--\n  This is a test harness to determine whether a page can install event listeners before Vimium's\n  content scripts "
  },
  {
    "path": "test_harnesses/form.html",
    "chars": 1118,
    "preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n  \"http://www.w3.org/TR/html4/strict.dtd\">\n<html>\n  <head>\n    <title>"
  },
  {
    "path": "test_harnesses/has_popup_and_link_hud.html",
    "chars": 2108,
    "preview": "<!--\n  This page when loaded causes Chrome to show both its \"link\" and \"popup blocked\" HUDs.\n  (mouse over the giant lin"
  },
  {
    "path": "test_harnesses/iframe.html",
    "chars": 746,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>IFrame test harness</title>\n    <link rel=\"icon\" href=\"data:;base64,iVBORw0KG"
  },
  {
    "path": "test_harnesses/page_with_links.html",
    "chars": 1716,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Page with many links</title>\n    <meta http-equiv=\"Content-type\" content=\"tex"
  },
  {
    "path": "test_harnesses/visibility_test.html",
    "chars": 6653,
    "preview": "<!--\n  This demonstrates the effectiveness a method for testing for visibility elements on various\n  conditions.\n-->\n<!D"
  },
  {
    "path": "test_harnesses/vomnibar_harness.html",
    "chars": 1497,
    "preview": "<!--\n  This harness is used to show our vomnibar with some sample suggestions. It's a convenient way to\n  restyle the Vo"
  },
  {
    "path": "test_harnesses/vomnibar_harness.js",
    "chars": 190,
    "preview": "import \"../pages/all_content_scripts.js\";\nimport \"../pages/vomnibar_page.js\";\n\nfunction setup() {\n  Vomnibar.activate(0,"
  },
  {
    "path": "tests/dom_tests/dom_test_setup.js",
    "chars": 390,
    "preview": "globalThis.vimiumDomTestsAreRunning = true;\n\nimport * as shoulda from \"../vendor/shoulda.js\";\n\n// Attach shoulda's funct"
  },
  {
    "path": "tests/dom_tests/dom_tests.html",
    "chars": 1808,
    "preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n  \"http://www.w3.org/TR/html4/strict.dtd\">\n<html>\n  <head>\n    <link r"
  },
  {
    "path": "tests/dom_tests/dom_tests.js",
    "chars": 42054,
    "preview": "let commandCount = null;\nlet commandName = null;\n\n// Some tests have side effects on the handler stack and the active mo"
  },
  {
    "path": "tests/dom_tests/dom_utils_test.js",
    "chars": 5412,
    "preview": "context(\"Check visibility\", () => {\n  should(\"detect visible elements as visible\", () => {\n    document.getElementById(\""
  },
  {
    "path": "tests/unit_tests/bg_utils_test.js",
    "chars": 157,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/url_utils.js\";\nimport \"../../background_scripts/tab_recency.js\";\nimport \".."
  },
  {
    "path": "tests/unit_tests/command_listing_test.js",
    "chars": 2065,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../tests/unit_tests/test_chrome_stubs.js\";\nimport \"../../lib/"
  },
  {
    "path": "tests/unit_tests/commands_test.js",
    "chars": 8738,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\nimport \"../../lib/keyboard_utils.js\";\nimport { allCommands } "
  },
  {
    "path": "tests/unit_tests/completion/completers_test.js",
    "chars": 18738,
    "preview": "import \"../test_helper.js\";\nimport \"../../../background_scripts/tab_recency.js\";\nimport \"../../../background_scripts/bg_"
  },
  {
    "path": "tests/unit_tests/completion/ranking_test.js",
    "chars": 5187,
    "preview": "import \"../test_helper.js\";\nimport * as ranking from \"../../../background_scripts/completion/ranking.js\";\nimport { Regex"
  },
  {
    "path": "tests/unit_tests/completion/search_engines_test.js",
    "chars": 2055,
    "preview": "import \"../test_helper.js\";\nimport \"../../../background_scripts/bg_utils.js\";\nimport * as Engines from \"../../../backgro"
  },
  {
    "path": "tests/unit_tests/doc_search_completion_test.js",
    "chars": 945,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../tests/unit_tests/test_chrome_stubs.js\";\nimport \"../../lib/"
  },
  {
    "path": "tests/unit_tests/exclusion_test.js",
    "chars": 2749,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\nimport \"../../background_scripts/bg_utils.js\";\nimport * as ex"
  },
  {
    "path": "tests/unit_tests/handler_stack_test.js",
    "chars": 3954,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/handler_stack.js\";\n\ncontext(\"handlerStack\", () => {\n  let handlerStack, han"
  },
  {
    "path": "tests/unit_tests/help_dialog_test.js",
    "chars": 1865,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../tests/unit_tests/test_chrome_stubs.js\";\nimport \"../../back"
  },
  {
    "path": "tests/unit_tests/hud_page_test.js",
    "chars": 1426,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../tests/unit_tests/test_chrome_stubs.js\";\nimport * as hudPag"
  },
  {
    "path": "tests/unit_tests/link_hints_test.js",
    "chars": 848,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/keyboard_utils.js\";\nimport \"../../lib/settings.js\";\nimport \"../../content_s"
  },
  {
    "path": "tests/unit_tests/main_test.js",
    "chars": 6930,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\nimport \"../../background_scripts/main.js\";\nimport { RegistryE"
  },
  {
    "path": "tests/unit_tests/marks_test.js",
    "chars": 1913,
    "preview": "import \"./test_helper.js\";\nimport * as marks from \"../../background_scripts/marks.js\";\n\ncontext(\"marks\", () => {\n  const"
  },
  {
    "path": "tests/unit_tests/options_page_test.js",
    "chars": 3096,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../tests/unit_tests/test_chrome_stubs.js\";\nimport * as option"
  },
  {
    "path": "tests/unit_tests/rect_test.js",
    "chars": 11189,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/rect.js\";\n\ncontext(\"Rect\", () => {\n  should(\"set rect properties correctly\""
  },
  {
    "path": "tests/unit_tests/settings_test.js",
    "chars": 3503,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\n\ncontext(\"settings\", () => {\n  context(\"v2.0 migration\", () ="
  },
  {
    "path": "tests/unit_tests/tab_operations_test.js",
    "chars": 2319,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\nimport * as to from \"../../background_scripts/tab_operations."
  },
  {
    "path": "tests/unit_tests/tab_recency_test.js",
    "chars": 3301,
    "preview": "import \"./test_helper.js\";\nimport { TabRecency } from \"../../background_scripts/tab_recency.js\";\n\ncontext(\"TabRecency\", "
  },
  {
    "path": "tests/unit_tests/test_chrome_stubs.js",
    "chars": 4620,
    "preview": "// This file contains stubs for a number of browser and chrome APIs which are missing in Deno.\nimport JSON5 from \"npm:js"
  },
  {
    "path": "tests/unit_tests/test_helper.js",
    "chars": 1041,
    "preview": "import * as shoulda from \"../vendor/shoulda.js\";\nimport * as jsdom from \"jsdom\";\nimport \"./test_chrome_stubs.js\";\nimport"
  },
  {
    "path": "tests/unit_tests/ui_component_test.js",
    "chars": 1409,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../lib/utils.js\";\nimport \"../../lib/dom_utils.js\";\nimport \".."
  },
  {
    "path": "tests/unit_tests/url_utils_test.js",
    "chars": 3878,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\nimport \"../../lib/url_utils.js\";\n\ncontext(\"isUrl\", () => {\n  "
  },
  {
    "path": "tests/unit_tests/user_search_engines_test.js",
    "chars": 1341,
    "preview": "import \"./test_helper.js\";\n\nimport * as userSearchEngines from \"../../background_scripts/user_search_engines.js\";\nimport"
  },
  {
    "path": "tests/unit_tests/utils_test.js",
    "chars": 5828,
    "preview": "import \"./test_helper.js\";\nimport \"../../lib/settings.js\";\nimport \"../../lib/url_utils.js\";\n\ncontext(\"forTrusted\", () =>"
  },
  {
    "path": "tests/unit_tests/vomnibar_page_test.js",
    "chars": 3249,
    "preview": "import * as testHelper from \"./test_helper.js\";\nimport \"../../tests/unit_tests/test_chrome_stubs.js\";\nimport { Suggestio"
  },
  {
    "path": "tests/vendor/shoulda.js",
    "chars": 10415,
    "preview": "/*\n * A unit testing micro framework. Tests are grouped into \"contexts\", each of which can share common\n * setup and tea"
  }
]

About this extraction

This page contains the full source code of the philc/vimium GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 118 files (749.5 KB), approximately 197.4k tokens, and a symbol index with 826 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!