中文文档 · English
A CodeMirror-like code editor written entirely in MoonBit,
compiled to wasm-gc and driven from JavaScript through a thin DOM FFI layer.
The editor logic — document model, transactions, extension system, syntax highlighting, search/replace, folding, keymaps, multi-cursor editing, undo/redo and rendering — lives in MoonBit. JavaScript only provides DOM primitives, measurement and event plumbing.
- Document model: immutable line-based text, UTF-16 code-unit offsets,
position/line mapping,
Range/RangeSet,Change/ChangeSetwith inverted changes and position mapping. - State and transactions: CodeMirror-style
Facet,StateField,StateEffect,Extension,Transaction,EditorStatewith typed, closure-based heterogeneous state (noAnyneeded). - Editing: insert/delete, auto-indent on Enter, tab stops, line/word
deletion, word-wise movement and selection (
Ctrl/⌘+Arrow,Shift+Ctrl/⌘+Arrow), indentation, line comment toggling, multi-cursor (Alt-ArrowUp/Down,Mod-d), selections, word/line double/triple click. - History: grouped typing/delete undos with redo, bounded by a group budget; non-history and remote edits safely invalidate stale undo steps.
- Syntax highlighting: incremental, line-state based tokenizers for about 50 languages (C-like family, scripting, shell/build, data/config, web, MoonBit, JSON, Markdown, Typst), driven by a configurable generic tokenizer with a per-line cache and invalidation on edits.
- Search and replace: literal and a small regex subset (
^ $ . * + ? [] |\d \w \s \b), match highlighting, next/previous, replace and replace all, a current/total match counter and keyboard hints in the panel. - Code folding: bracket and indentation based folds, foldable-line gutter
markers (
▾unfolded,▸folded with an inline…ellipsis), clicking the gutter to fold/unfold, fold all/unfold all. - Rendering: viewport-based virtual rendering with full-document scroll height, gutter line numbers, active line plus active gutter marker, focus ring and focus-aware cursor/selection colors, selections, cursors, bracket matching, themed thin scrollbars, optional soft wrapping, light/dark themes, read-only mode.
- Performance: per-line HTML cache (only edited lines are rebuilt; unchanged lines are reused as prebuilt strings) and a persistent caret overlay that is updated only when it moves, so re-renders never restart the blink animation.
- Input: extensible keymap facet with a CodeMirror-like default keymap,
platform-aware
Modbindings, configurable indentation, full IME composition sessions (one undo per composition), clipboard copy/cut/paste, mouse selection and dragging, scroll forwarding. - Diagnostics: host-provided diagnostics (
setDiagnostics) render as wavy underline marks with light/dark colors; unchanged diagnostic sets do not trigger a redraw. - Host API:
onUpdatesubscriptions, structuredgetSelection, explicitsetSelection, and a distributablejs/codemoonbit.cssscoped to.cm-editor.
| directory | contents |
|---|---|
core/ |
text, ranges, changes, selections (pure, unit tested) |
state/ |
facets, fields, effects, transactions, history (pure) |
highlight/ |
incremental tokenizers and line cache (pure) |
search/ |
search/replace, folding, bracket matching (pure) |
input/ |
keymap, commands, multi-cursor (pure) |
ffi/ |
wasm DOM imports and code-unit string marshalling |
view/ |
layout, geometry, rendering, mouse/scroll dispatch |
editor/ |
assembled editor: extensions, options, event entry points |
main/ |
wasm exports (cm_*) and the editor registry |
js/ |
DOM runtime, stylesheet, Node DOM shim, e2e tests, loader |
demo/ |
static demo page |
The project requires moonc 0.10.15 or newer (moon version --all). The
stable channel is older, so install the development channel first:
export MOONBIT_INSTALL_DEV=1
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
moon version --all # moonc v0.10.15+...moon check --target wasm-gc # type check
moon test --target wasm-gc # 137 unit tests (pure packages)
moon build --target wasm-gc # _build/wasm-gc/debug/build/main/main.wasm
node js/e2e.mjs # 39 end-to-end tests through a DOM shim
node js/browser_e2e.mjs # 84 real-browser tests (Chromium over CDP)
moon fmt && moon infopython3 -m http.server 8000
# open http://localhost:8000/demo/<link rel="stylesheet" href="./js/codemoonbit.css" />
<div id="editor" style="height: 400px"></div>
<script type="module">
import { createEditor } from "./js/browser.js";
const editor = await createEditor(document.getElementById("editor"), {
value: "fn main {\n println(\"hello\")\n}\n",
language: "moonbit",
lineNumbers: true,
theme: "light",
});
editor.focus();
editor.onUpdate(() => console.log("document changed"));
</script>The returned handle exposes getDoc, setDoc, setSelection, getHTML,
getSelection, getState, onUpdate, focus, destroy, openSearch,
closeSearch, searchNext, searchPrev, replace, replaceAll, undo,
redo, foldAll, unfoldAll, foldClick, key, mouse, paste,
selectedText, setDiagnostics, setOption.
- Typst.bit uses CodeMoonBit as its only editor (patched in-tree for the Typst language, diagnostics, IME cursor mapping and the render optimizations above).
- JS calls exported wasm functions declared in
main/moon.pkg; they use onlyInt,Bool,Doubleand#external type JsAny(externref) parameters. - MoonBit builds JS strings with
sb_new/sb_push/sb_push_js/sb_finish(sb_push_jsreuses an already-built JS string, which powers the per-line HTML cache); JS passes strings to MoonBit as externref and MoonBit reads them withjs_len/js_char. All offsets are UTF-16 code units. - Events are wired in
js/dom_runtime.js: DOM listeners forward tocm_key,cm_mouse,cm_scroll,cm_paste,cm_composition, and so on. MoonBit renders by computing an HTML string for the visible viewport and assigning it throughset_html.
- Rendering rebuilds the visible viewport's HTML string each transaction, but unchanged lines are served from the per-line HTML cache, so only edited lines are re-generated. There is no per-line DOM patching yet.
- Soft wrapping splits lines into visual segments using measured character widths; split points may differ from native browser wrapping for mixed proportional/CJK content.
- Shift-click selection extension is not wired (mousedown carries no modifier).
- The regex engine used by search is a small subset, not a full regex engine.
- The document structure is a line array with persistent unchanged strings, not a rope; very large documents pay O(lines) per edit.
- The public API is inspired by CodeMirror 6; no CodeMirror source code is included in this repository. The implementation is original MoonBit code.
- MoonBit core packages (json, string, encoding) and the wasm-gc toolchain are used as dependencies; no third-party editor source is vendored.
- Used by Typst.bit as its only
editor, and published on mooncakes.io as
V1GreenSummer/codemoonbit.
MIT. See LICENSE.