>_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.ts | the vim engine (vendored), ~7.1k lines; imports its "CodeMirror" from an adapter |
| adapters/monaco_adapter.ts | implements the CodeMirror API over a Monaco editor (ships today) |
| adapters/dom_adapter.ts | the 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 engine | the full CodeMirror vim keymap (motions, operators, registers, marks, ex commands, search) vendored via monaco-vim, bolted onto whichever adapter class it imports |
| Emacs engine | monaco-emacs' extension — keybindings, kill-ring, mark, incremental search — over the Monaco editor API |
| Ownership | vendored 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 bar | vim mode / key-buffer / ex command line render into a caller-supplied element (src/statusbar.ts) |
| Theme | zmodal-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 Monaco | the 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 Monaco | use 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 / StatusBar | the 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.ts | the IIFE entry — Monaco theme + worker wiring, the create / attachVim / attachEmacs facade on window.ZModal |
src/engine/vim/keymap_vim.ts | the vim engine (vendored from monaco-vim); imports its "CodeMirror" from the surface adapter |
src/adapters/monaco_adapter.ts | the 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.ts | the vim mode / key-buffer / ex command-line status bar |
src/worker-entry.ts | Monaco base editor web-worker entry |
scripts/build-modal-editor.mjs | esbuild 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)