// ZPWR-MODAL-EDITOR — SHARED VIM / EMACS MODAL EDITOR

shared frontend submodule · monaco-vim + monaco-emacs engines vendored & owned · surface-adapter driven · esbuild IIFE bundle · build-on-each-consumer · one window.ZModal facade

Report GitHub
// Color scheme

>_ZPWR-MODAL-EDITOR

One modal engine, owned in source, droppable anywhere. The shared Vim / Emacs modal-editing editor for the MenkeTechnologies app stack. The Vim engine and Emacs engine are vendored from monaco-vim and monaco-emacs and adapted so we own them outright — no external runtime deps. The source lives here in src/ and each app builds it with its own monaco-editor dependency.

The adapter seam — why it drops in anywhere

The Vim engine talks only to a surface adapter that implements the CodeMirror API it expects. Swap the adapter and the same engine drives a different surface — a Monaco editor today, a raw DOM surface next.

engine/vim/keymap_vim.tsthe vim engine (vendored), ~7.1k lines; imports its "CodeMirror" from an adapter
adapters/monaco_adapter.tsimplements the CodeMirror API over a Monaco editor (ships today)
adapters/dom_adapter.tsthe same API over a textarea / contenteditable, for in-place editing with no Monaco (next milestone)
engine/emacs/*the emacs extension (vendored); currently coupled to the Monaco editor API, rides the Monaco adapter only

What it is

Vim enginethe full CodeMirror vim keymap (motions, operators, registers, marks, ex commands, search) vendored via monaco-vim, bolted onto whichever adapter class it imports
Emacs enginemonaco-emacs' extension — keybindings, kill-ring, mark, incremental search — over the Monaco editor API
Ownershipvendored in src/engine/ with upstream MIT licenses retained; the two lodash helpers monaco-emacs used are replaced by engine/emacs/localutil.ts so there are no external runtime deps
Status barvim mode / key-buffer / ex command line render into a caller-supplied element (src/statusbar.ts)
Themezmodal-cyberpunk — neon on near-black, matching the apps' aesthetic

⚠ One Monaco per page

The default build bundles its own Monaco. If the host page also loads another Monaco bundle (e.g. zpwr-hooks-editor), a WebKit/WKWebView content process can crash on the second full Monaco — a blank window (Chromium tolerates two; WebKit does not). Two clean integrations:

Share one Monacothe app creates the editor and calls attachVim(editor) / attachEmacs(editor) — these need no bundled editor of their own (a monaco-editor-external build variant is the tidy way to ship this)
Be the only Monacouse create(...) (which bundles Monaco) only in apps that don't already load one

Load order (consuming app)

The consumer's index.html loads the CSS link + the JS bundle; the editor exposes a single global facade, window.ZModal. The Monaco base worker is fetched at runtime via MonacoEnvironment.getWorker.

<link rel='stylesheet' href='lib/modal-editor.bundle.css'>
<script src='lib/modal-editor.bundle.js'></script>
<!-- lib/modal-editor.worker.js is fetched at runtime, not script-tagged -->

Mounting an editor

ZModal.create(host, opts) creates a Monaco editor into host, applies the modal mode and returns a handle. setMode swaps the active modal layer live.

const handle = window.ZModal.create(hostEl, {
  doc: 'hello\n',          // initial text
  mode: 'vim',             // 'default' | 'vim' | 'emacs'
  statusBar: vimStatusEl,  // optional element for the vim status/command line
  language: 'plaintext',
  onChange: (text) => save(text),
});

handle.getValue();        // current text
handle.setValue(text);    // replace text
handle.focus();
handle.setMode('emacs');  // switch modal mode at runtime
handle.layout();          // re-layout after the host resizes / becomes visible
handle.destroy();         // dispose the modal layer, editor and model

Attaching to an existing editor

When the host already has a Monaco editor (so a second Monaco must not be bundled), attach a mode directly. These are the Monaco-agnostic primitives create is built on.

const vim = window.ZModal.attachVim(existingMonacoEditor, vimStatusEl);
vim.dispose();   // detach vim

const emacs = window.ZModal.attachEmacs(existingMonacoEditor);
emacs.dispose(); // detach emacs

window.ZModal facade

create(host, opts)create a Monaco-backed editor + apply a mode; returns a handle (getValue / setValue / focus / setMode / layout / destroy)
attachVim(editor, statusEl?)attach vim to an existing Monaco editor; returns the vim adapter
attachEmacs(editor)attach emacs to an existing Monaco editor; returns the started extension
VimMode / EmacsExtension / StatusBarthe underlying classes, re-exported for advanced use

Build (build-on-each-consumer)

The editor source lives here in src/, but the bundle is built inside the consuming app so esbuild resolves that app's monaco-editor dep and writes into its frontend/lib/. Each consumer keeps esbuild + the pinned monaco-editor in devDependencies (the vim/emacs engines are vendored here, so no monaco-vim / monaco-emacs deps are needed) and invokes the bundler from its project root:

node <path-to-submodule>/scripts/build-modal-editor.mjs

The output dir defaults to <cwd>/frontend/lib; override with the MODAL_EDITOR_OUT env var. The generated bundles are build artifacts (gitignore them in the consumer). The bundler emits IIFE, minified, ES2020, with Monaco's codicon .ttf inlined as a data URL so the single .css artifact is self-contained.

Source layout

src/index.tsthe IIFE entry — Monaco theme + worker wiring, the create / attachVim / attachEmacs facade on window.ZModal
src/engine/vim/keymap_vim.tsthe vim engine (vendored from monaco-vim); imports its "CodeMirror" from the surface adapter
src/adapters/monaco_adapter.tsthe CodeMirror-API adapter over a Monaco editor
src/engine/emacs/*the emacs extension (vendored from monaco-emacs) + localutil.ts (lodash-free throttle / kebab-case)
src/statusbar.tsthe vim mode / key-buffer / ex command-line status bar
src/worker-entry.tsMonaco base editor web-worker entry
scripts/build-modal-editor.mjsesbuild bundler — reads src/ from this repo, writes the bundle into the consuming app

The app stack

zpwr-modal-editor is a shared editor component across the MenkeTechnologies apps. Browse the rest via the MenkeTechnologiesMeta umbrella repo:

  • zpwr-hooks-editor — the shared Monaco code editor (stryke-LSP); the sibling this repo mirrors
  • zgui-core — the shared UI widget toolkit
  • zoffice — the desktop office suite (a consumer)