Problem
Chrome blocks content script injection into chrome-extension:// pages. Vimium can't provide any functionality inside other extensions' UIs (e.g., PDF readers). My personal motivation is to get Google Scholar PDF Reader to work with Vimium.
Proposed solution
Vimium's content scripts do local operations (scrolling, link hints, modes, key handling, which seem to be pure DOM) and remote operations (settings, tab commands, completions, which talk to the service worker via chrome.runtime.sendMessage).
Local operations work anywhere, but remote operations fail in a foreign extension because chrome.runtime.sendMessage goes to that extension's service worker, not Vimium's.
Chrome's cross-extension messaging (chrome.runtime.sendMessage(extensionId, msg)) lets any extension send messages to a specific extension's service worker. If we make the content scripts' service worker calls configurable, and make Vimium's service worker accept external messages, then Vimium's content scripts can run inside any extension (i.e., using Vimium's real service worker, settings, key mappings, and UI pages) without reimplementing anything.
The Bridge
I currently plan to maintain this as a separate repo, but putting it within Vimium (if the maintainers are fine with it) also an option.
The bridge will be a JS bundle that concatenates Vimium's content scripts and configures them to talk to Vimium's service worker via cross-extension messaging. It'll be injected into a target extension via a <script> tag, and it provides Vimium keybindings using Vimium's real settings, key mappings, and UI pages without reimplementing any Vimium functionality.
The bridge will be made easy to integrate into extensions, so it'll be built aware of that integration flow, but in most cases I expect users to be instrumenting a Chrome extension they have already installed locally and using it in dev mode.
Vimium Changes
1. Service worker routing (Backend)
Replace direct chrome.* calls in content scripts with calls to a Backend object:
chrome.runtime.sendMessage(msg) -> Backend.runtime.sendMessage(msg)
chrome.storage.sync.get(key) -> Backend.storage.sync.get(key)
chrome.runtime.getURL(path) -> Backend.runtime.getURL(path)
In the Vimium side, Backend wraps the real chrome (zero behavior change). In a bridge (which is inside the injected extension), Backend will route method invocations to Vimium's service worker via cross-extension messaging. This should be just mechanical refactoring of chrome call sites.
Now to be fair, this one can also be achieved with static source code transformation at the time of injection. Basically searching for calls to chrome and replacing them with calls that will be directed to the Vimium service worker. But this is harder to keep robust because Vimium would evolve over time and add other patterns of calling chrome methods, introducing edge cases for static transformation. Instead, what is being proposed here is a compromise between the two, where hopefully the addition of Backend does not make the maintenance and development of Vimium significantly harder, while making injection very simple and error-free. I'll follow the decision of the project owner.
2. Cross-extension message handler
Add onMessageExternal/onConnectExternal listeners to the service worker. The bridge will:
- Attach: Get settings, key mappings, CSS, session secret
- Send commands: Route through existing
sendRequestHandlers
- Receive push updates: Settings/key mapping changes via long-lived port. Needed since
chrome.storage.onChanged won't work cross-extension, and the service worker needs to have a list of registered extension IDs and explicitly fire updates to them with cross-extension messages.
Some might be concerned that malicious extensions might mess around with Vimium's service worker (though I don't immediately see why and how that might be harmful). This can be gated by a new allowedBridgeExtensions setting which is a list of extension IDs (empty = allow all, non-empty = whitelist).
I did already get this working in a local PoC with the Google Scholar PDF Reader. The screenshot shows VomniBar properly showing up (and it works well, too).
Please let me know what you think. Thanks!
Problem
Chrome blocks content script injection into
chrome-extension://pages. Vimium can't provide any functionality inside other extensions' UIs (e.g., PDF readers). My personal motivation is to get Google Scholar PDF Reader to work with Vimium.Proposed solution
Vimium's content scripts do local operations (scrolling, link hints, modes, key handling, which seem to be pure DOM) and remote operations (settings, tab commands, completions, which talk to the service worker via
chrome.runtime.sendMessage).Local operations work anywhere, but remote operations fail in a foreign extension because
chrome.runtime.sendMessagegoes to that extension's service worker, not Vimium's.Chrome's cross-extension messaging (
chrome.runtime.sendMessage(extensionId, msg)) lets any extension send messages to a specific extension's service worker. If we make the content scripts' service worker calls configurable, and make Vimium's service worker accept external messages, then Vimium's content scripts can run inside any extension (i.e., using Vimium's real service worker, settings, key mappings, and UI pages) without reimplementing anything.The Bridge
I currently plan to maintain this as a separate repo, but putting it within Vimium (if the maintainers are fine with it) also an option.
The bridge will be a JS bundle that concatenates Vimium's content scripts and configures them to talk to Vimium's service worker via cross-extension messaging. It'll be injected into a target extension via a
<script>tag, and it provides Vimium keybindings using Vimium's real settings, key mappings, and UI pages without reimplementing any Vimium functionality.The bridge will be made easy to integrate into extensions, so it'll be built aware of that integration flow, but in most cases I expect users to be instrumenting a Chrome extension they have already installed locally and using it in dev mode.
Vimium Changes
1. Service worker routing (Backend)
Replace direct
chrome.*calls in content scripts with calls to aBackendobject:In the Vimium side,
Backendwraps the realchrome(zero behavior change). In a bridge (which is inside the injected extension),Backendwill route method invocations to Vimium's service worker via cross-extension messaging. This should be just mechanical refactoring of chrome call sites.Now to be fair, this one can also be achieved with static source code transformation at the time of injection. Basically searching for calls to
chromeand replacing them with calls that will be directed to the Vimium service worker. But this is harder to keep robust because Vimium would evolve over time and add other patterns of callingchromemethods, introducing edge cases for static transformation. Instead, what is being proposed here is a compromise between the two, where hopefully the addition ofBackenddoes not make the maintenance and development of Vimium significantly harder, while making injection very simple and error-free. I'll follow the decision of the project owner.2. Cross-extension message handler
Add
onMessageExternal/onConnectExternallisteners to the service worker. The bridge will:sendRequestHandlerschrome.storage.onChangedwon't work cross-extension, and the service worker needs to have a list of registered extension IDs and explicitly fire updates to them with cross-extension messages.Some might be concerned that malicious extensions might mess around with Vimium's service worker (though I don't immediately see why and how that might be harmful). This can be gated by a new
allowedBridgeExtensionssetting which is a list of extension IDs (empty = allow all, non-empty = whitelist).I did already get this working in a local PoC with the Google Scholar PDF Reader. The screenshot shows VomniBar properly showing up (and it works well, too).
Please let me know what you think. Thanks!