The brief allows TypeScript packages (CodeMirror, Milkdown) but wants them modular and replaceable by Rust. This note is the mechanism; the principle is ADR-0002 Rust first, TypeScript behind traits and ADR-0008 Rust owns the document, JS is a view.
The shape
flowchart LR subgraph Rust D[Document: Rope + version] T["trait CodeEditorBackend"] CM[backend::codemirror] NA[backend::native — future] D <--> T T --- CM T --- NA end subgraph JS["packages/js/codemirror (one bundle)"] W[window.moonkale.codemirror: mount/apply/destroy] end CM -- "eval.send / dioxus.recv" --> W W -- "dioxus.send → eval.recv" --> CM
Same shape for milkdown ↔ RichTextBackend and xterm ↔ TerminalBackend.
Rules (also in packages/js/README.md)
- One TS dependency per folder; no folder imports another; each builds to one self-contained ESM file loaded via
asset!(). - JS holds no application state. The rope/AST/session lives in Rust; JS receives text + decorations and emits edits.
- JS contains no language knowledge. Highlighting, folding, link resolution, LSP data arrive from Rust as decorations. This keeps the bundles small and means a Rust backend needs nothing from the JS one.
- Every message is documented in the package’s
PROTOCOL.md. That file is the acceptance test for a Rust replacement. - Replacement = flip the crate feature (
backend-codemirror→backend-native), delete the folder.
Transport
document::eval (Dioxus, renderer-agnostic — works in browser, WebView2, WKWebView, WebKitGTK, and the mobile webviews). As built in Milestone 1: one eval per mounted editor; JS → Rust with dioxus.send(value), Rust → JS with eval.send(value) / await dioxus.recv() — Dioxus’s own channel, no CustomEvents and no global listeners. The initial text is sent over the channel, never formatted into the script. See packages/js/codemirror/PROTOCOL.md and packages/editors/code/editor-code.md. Binary (terminal output) will use base64 initially; a SharedArrayBuffer path is an optimisation.
Cost we accept
Latency of a JSON hop per keystroke. CodeMirror applies the edit locally first (optimistic) and Rust reconciles; conflicts are rare (single user) and resolved by Rust re-sending the canonical text. This is the same trade Zed’s remote mode and VS Code’s extension host make.
Inventory
What is still JavaScript, sizes, and the distance to zero: JavaScript Inventory.
The long-term direction
Once all three backends have Rust replacements (Rust-native Editor Candidates), the desktop app no longer needs a JS engine at all and could move from a webview to dioxus-native (Blitz, wgpu-rendered). That would also dissolve the Graph View’s hardest platform problem (the surface inside a webview). The JS dependencies are, literally, what keeps us in a webview — which is the strongest argument for keeping them replaceable.