// ZPWR-HOOKS-EDITOR — SHARED STRYKE HOOKS CODE EDITOR

private frontend submodule · Monaco + monaco-vim + monaco-emacs + thin stryke-LSP adapter · esbuild IIFE bundle · build-on-each-consumer · one window.HooksEditor facade

Report GitHub
// Color scheme

>_ZPWR-HOOKS-EDITOR

One editor, built per app, single-sourced. The shared stryke Hooks code editor behind the MenkeTechnologies app stack — a Monaco editor wired with monaco-vim, monaco-emacs and a thin stryke-LSP adapter, bundled with esbuild into vendored IIFE artifacts. Extracted from Audio-Haxor so the editor is no longer duplicated across apps; the source lives here in src/ and each app builds it with its own monaco-* dependencies.

Four consumers, one source

All four apps load the same editor source. The three Tauri apps serve frontend/ statically and vendor this repo at crates/zpwr-hooks-editor; zpwr-daw is JUCE and loads the bundle in its WebView, vendoring at libs/zpwr-hooks-editor.

Audio-Haxor (Tauri)serves frontend/; vendors at crates/zpwr-hooks-editor
traderview (Tauri)serves frontend/; vendors at crates/zpwr-hooks-editor
ztranslator (Tauri)serves frontend/; vendors at crates/zpwr-hooks-editor
zpwr-daw (JUCE)loads the bundle in its WebView; vendors at libs/zpwr-hooks-editor

What it is

EditorMonaco's edcore.main bundle — all editor contributions (suggest, hover, find, folding, multi-cursor, minimap, command palette) but no bundled languages
stryke languagestryke is a Perl-5 superset, so it reuses Monaco's Perl Monarch grammar (basic-languages/perl) for tokenization + the bracket/comment language config, registered under the stryke language id
latex languagea second grammar in the same bundle, selected with create({ language: 'latex' }) — control words/symbols, math shifts, % comments, brace matching, $/brace auto-closing, snippet completion for the structural constructs, \ref/\cite completion driven by the document's own \labels and \bibitem keys, and a \part\subparagraph outline. No language server is attached (see below)
Modal editingDefault / Vim / Emacs via monaco-vim and monaco-emacs, switchable at runtime through setMode; the vim status/command line renders into a caller-supplied element
Language intelligencecompletion / hover / diagnostics / definitions / references / rename / signature help / document symbols / code actions / semantic tokens come from the stryke language server (stryke --lsp) over a caller-supplied transport — the editor speaks LSP JSON-RPC directly via a thin adapter, not monaco-languageclient
Themestryke-cyberpunk — neon on near-black, matching the apps' aesthetic, with the suggest/hover widgets styled explicitly

Load order (consuming app)

The consumer's index.html loads the CSS link + the JS bundle; the editor exposes a single global facade, window.HooksEditor. The Monaco base worker is fetched at runtime via MonacoEnvironment.getWorker (no TS/JSON/CSS language workers — all language intelligence comes from the stryke LSP).

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

Mounting an editor

HooksEditor.create(parent, opts) mounts an editor into parent and returns a handle. With uri set and an LSP transport wired (see below), language features come online once the stryke handshake completes; without a uri the LSP layer stays off and it is a plain stryke-highlighted editor.

const handle = window.HooksEditor.create(parentEl, {
  uri: 'file:///project/hook.stk',  // identifies the doc to the LSP (omit to disable LSP)
  doc: '# my stryke hook\n',         // initial text
  onChange: (text) => save(text),    // fires on every edit with full text
  mode: 'vim',                       // 'default' | 'vim' | 'emacs'
  statusBar: vimStatusEl,            // optional element for the vim status/command line
  language: 'stryke',                // 'stryke' (default) | 'latex'
});

handle.getValue();        // current text
handle.setValue(text);    // replace text
handle.focus();
handle.setMode('emacs');  // switch modal mode at runtime
handle.language();        // which grammar this editor was created with
handle.insert(text);      // undoable insert at the caret (palette-style)
handle.selectRange(a, b); // select + reveal by document offset
handle.onSelection(fn);   // fires with (from, to) offsets as the caret moves
handle.bindKey(spec, fn); // take a key back from Monaco, this editor only
handle.destroy();         // dispose editor, model, markers, LSP didClose

Editing LaTeX in the same editor

Pass language: 'latex' and the editor mounts the LaTeX Monarch grammar instead of the stryke one — same Monaco instance, same stryke-cyberpunk theme, same vim/emacs keymaps. The stryke language server speaks stryke, so a LaTeX buffer is never opened against it: omit uri and the LSP layer stays off entirely.

What a LaTeX buffer gets instead, all computed locally from the model:

Structural snippetstyping \ offers \frac, \dfrac, \binom, \sqrt, \left…\right, \sum, \prod, \int, \lim, pmatrix, cases, aligned, accents … as tab-stop snippets
Cross-referencesinside \ref{} / \eqref / \pageref / \autoref / \cref / \vref the document's own \labels complete (with the line each is defined on); inside \cite{} / \citep / \citet / \citeauthor / \citeyear / \nocite its \bibitem keys do. No project index — the open model is the source of truth
Outline\part, \chapter, \section, \subsection, \subsubsection, \paragraph, \subparagraph (starred forms included) become document symbols, so Go to Symbol navigates by sectioning rather than by line

Taking a key back from Monaco

Monaco binds a lot of keys, and a host accelerator can collide with one — ⇧⌘O is Monaco's Go to Symbol, which opens its @ quick access. bindKey overrides the binding inside one editor, for that editor's lifetime, without changing it for every other app on the bundle.

handle.bindKey('CmdOrCtrl+Shift+O', () => app.openOwnSymbolPalette());

The spec is an Electron-style string: Cmd / Ctrl / CmdOrCtrl / Meta all map to Monaco's CtrlCmd, plus Shift and Alt / Option; the last segment is the key (a single letter or digit, or a Monaco KeyCode name such as Enter or Escape). Returns false when the key cannot be parsed.

Wiring the stryke LSP transport

The editor speaks LSP JSON-RPC but does not own the transport. The host (e.g. Rust) connects stryke --lsp, adds Content-Length framing on the server's stdin, and bridges messages. The app calls initClient with a send function and feeds inbound server messages back via receive.

window.HooksEditor.initClient({
  send: (message) => host.sendToServer(message),   // client → server (raw JSON-RPC)
});
host.onServerMessage((msg) => window.HooksEditor.receive(msg)); // server → client

window.HooksEditor.clientReady();   // transport connected?
await window.HooksEditor.whenReady(); // true once the stryke initialize handshake completed

whenReady() resolves true only after the server answered initialize — suitable for a status indicator, unlike a transport "connected" flag. Call it after initClient.

window.HooksEditor facade

initClient(transport)register Monaco providers, then start the LSP client over transport.send
receive(message)feed one raw JSON-RPC message from the server to the client
clientReady()transport connected (a send function is installed)
whenReady()promise resolving true once the stryke initialize handshake completed
create(parent, opts)mount an editor; returns a handle with getValue / setValue / focus / setMode / language / insert / selectRange / bindKey / onSelection / destroy

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-* deps and writes into its frontend/lib/. Each consumer keeps esbuild + the pinned monaco-editor / monaco-vim / monaco-emacs in devDependencies and invokes the bundler from its project root (e.g. via tauri.conf.json beforeDevCommand):

node crates/zpwr-hooks-editor/scripts/build-hooks-editor.mjs

The output dir defaults to <cwd>/frontend/lib; override with the HOOKS_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.

Bundler resolve fixes

scripts/build-hooks-editor.mjs carries esbuild resolve overrides because the default browser/CommonJS resolution pulls in AMD define() builds that throw in the WebView and silently degrade the editor to a textarea:

monaco-vimforced to its ESM build (dist/index.mjs); the browser condition's UMD bundle carries an AMD define() from the embedded CodeMirror vim keymap
monaco-editorbare imports pinned to esm/vs/editor/edcore.main.js — the same lean ESM instance the entry uses, instead of the AMD min build monaco-emacs' require() would pull
monaco-editor/esm/*deep subpaths without a file extension get .js appended and resolve to the package dir directly (the package exports map only resolves extensioned paths)

Source layout

src/hooks-editor-entry.mjsthe editor entry — Monaco instance, stryke language registration, cyberpunk theme, vim/emacs keymaps, LSP↔Monaco provider/marker mapping, the window.HooksEditor facade
src/lsp-client-core.mjsthin stryke-LSP JSON-RPC client (createLspCore): init / notify / request / receive / connected / isInitialized / onDiagnostics / ready
src/hooks-editor-worker-entry.mjsMonaco base editor web-worker entry (no language workers)
scripts/build-hooks-editor.mjsesbuild bundler — reads src/ from this repo, writes the bundle into the consuming app

The app stack

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

  • Audio-Haxor — Tauri v2 desktop app; original home of the editor
  • traderview — Tauri trading/market app
  • ztranslator — Tauri MIDI/OSC/DMX/Link translation app
  • zpwr-daw — JUCE DAW; loads the bundle in its WebView
  • strykelang — the stryke language + stryke --lsp server the editor talks to