Firenvim: How It Works and Why It Sucks

Firenvim logo

Who am I?

  • Github logo glacambre
  • Neovim logo Neovim
  • Tridactyl logo Tridactyl: Modal internet browsing
  • Ileum, Syncwd, Shelley: ZSH integrations
  • Nwin: Neovim GUI for Sway/i3
  • Firenvim logo Firenvim: Integrating Neovim in Chrome, Firefox, Thunderbird, Qutebrowser...

What's Firenvim?

Neovim GUIs, how do they work?

Two kinds of GUIs:
GUI+Libnvim
Two kinds of GUIs:
GUI
Neovim
Two kinds of GUIs:
Libnvim:
Direct function calls
Performance
Access to neovim internals
RPC:
MessagePack
More documented
Easier to get started
Different contexts
Browser extensions are distributed systems:
BackgroundContent
Open tabsRead page
Run programsAdd elements
Much more...Execute js
And communicate through message passing:

						browser.runtime.onMessage.addListener((message) => { ... })
						browser.runtime.sendMessage(message)
					
Biggest issue (#322, #625, #672, #779, #784, #970, #978):
  • Reading from textarea:
    
    						let content = document.activeElement.textContent;
    					
  • Reading from CodeMirror:
    
    					export function executeInPage(code: string): Promise<any> {
    							return new Promise((resolve, reject) => {
    									const script = document.createElement("script");
    									const eventId = (new URL(browser.runtime.getURL(""))).hostname + Math.random();
    									script.innerHTML = `(async (evId) => {
    											try {
    													let result;
    													result = await ${code};
    													window.dispatchEvent(new CustomEvent(evId, {
    															detail: {
    																	success: true,
    																	result,
    															}
    													}));
    											} catch (e) {
    													window.dispatchEvent(new CustomEvent(evId, {
    															detail: { success: false, reason: e },
    													}));
    											}
    									})(${JSON.stringify(eventId)})`;
    									window.addEventListener(eventId, ({ detail }: any) => {
    											script.parentNode.removeChild(script);
    											if (detail.success) {
    													return resolve(detail.result);
    											}
    											return reject(detail.reason);
    									}, { once: true });
    									document.head.appendChild(script);
    							});
    					}
    					executeInPage(`(()=>{return document.getElementById(Id).CodeMirror.getValue()})`);
    					
Side effect of biggest issue:
Running arbitrary JS in page context runs afoul of CSP (#811).
Workarounds:
  • Firefox:
    • Patched Firefox: not viable, requires recompiling
    • Obscure APIs (wrappedJSObject) not part of WebExtension standard
  • Chrome
    • No solution. Especially with newer webextension standard...
Second biggest issue: keybindings (#544, #202, #640)
  • Keyboard shortcut events are mangled (OSX)
  • Some keyboard shortcuts (e.g.
    <C-w>
    are reserved). Workarounds:
    • Chrome: Use separate webextension API for reserved shortcuts
    • Firefox: Hotpatch firefox (thanks @alerque!)
Third issue: installation (#1196, too many to list!)
  • Webextension API requires manifest/registry to exist
  • Can't choose args used to run neovim
  • Can't choose protocol to communicate with neovim

Thanks for listening!