High-performance, cross-platform code editor surface published as @honeide/editor. Designed to be embeddable by other developers for markdown editors, config editors, query consoles, etc. Compiled to native binaries via Perry (TypeScript-to-native compiler, v0.2.x).
Key constraint: Perry's Canvas widget has no text-on-canvas capability. The editor uses custom Rust FFI crates per platform for native text rendering (Core Text on macOS/iOS, DirectWrite on Windows, Pango/Cairo on Linux, Skia on Android, DOM on Web).
- Core logic: TypeScript (platform-independent, shared across all 6 targets)
- Native rendering: Rust FFI crates (one per platform)
- Syntax highlighting: Lezer parser ecosystem (@lezer/*)
- Build: Perry compiler (
perry compile) - Test runner:
bun test - Package manager: Bun
core/ Platform-independent TypeScript
buffer/ Piece table + B-tree rope text buffer (O(log n) ops)
document/ EditorDocument, EditBuilder, encoding detection
cursor/ Multi-cursor management, selections, word boundaries
commands/ Command registry + editing/navigation/selection/clipboard/multicursor
history/ Undo/redo with time-based coalescing
viewport/ Virtual scrolling, line height cache, scroll controller
tokenizer/ Lezer syntax highlighting for 10 languages
search/ Literal + regex search/replace, incremental search
folding/ Indent-based + syntax-based code folding
diff/ Myers diff algorithm, inline char-level diff, hunk operations
lsp-client/ LSP client: JSON-RPC transport, protocol types, capability negotiation
dap-client/ DAP client: debug session lifecycle, breakpoints, stepping, variables
view-model/ Reactive state bridging core → rendering
editor-view-model.ts Central orchestrator, key bindings, mouse handling
theme.ts Dark + light themes, token color mappings
line-layout.ts RenderedLine computation
cursor-state.ts Blink controller, IME composition
gutter.ts Line numbers, fold indicators, breakpoints, diff markers
find-widget.ts Find/replace widget controller
ghost-text.ts AI ghost text inline completion
minimap.ts Minimap data generation
overlays.ts Autocomplete, hover, parameter hints, diagnostics overlays
decorations.ts Search highlights, selection, diagnostic underlines
diff-view-model.ts Side-by-side/inline diff view state
native/ Platform-specific rendering bridge + Rust FFI crates
ffi-bridge.ts NativeEditorFFI interface, NoOpFFI test impl
render-coordinator.ts Bridges EditorViewModel → FFI calls with dirty tracking
touch-input.ts Touch gesture handler (tap, pan, pinch, long press)
word-wrap.ts Word wrap computation + WrapCache
index.ts Barrel export
macos/ Core Text + Core Animation + Metal (Rust)
ios/ Core Text + UIKit (Rust, shares rendering with macOS)
windows/ DirectWrite + Direct2D + DirectComposition (Rust)
linux/ Pango + Cairo + X11/Wayland (Rust)
android/ Canvas + Skia via JNI (Rust)
web/ DOM spans + CSS + WASM (Rust)
tests/ Unit and integration tests
- Text buffer: Piece table with B-tree rope indexing — O(log n) for all offset/line operations
- Line endings: Always normalized to
\ninternally; original line ending style preserved in EditorDocument for saving - No external editor dependencies: No CodeMirror, Monaco, prosemirror. Self-contained.
- Edits are atomic: EditBuilder collects edits, applies via
buffer.applyEdits()in reverse offset order - Undo coalescing: Single-character inserts/deletes within 500ms are grouped; newlines always start new groups
- Multi-cursor: Cursors sorted by position, merged when overlapping, desired column preserved across vertical movement
- Virtual scrolling: Only visible lines + 10-line buffer zone are rendered
core/andview-model/are 100% shared across all platforms- macOS and iOS share Core Text rendering code
- Only
native/Rust FFI crates are platform-specific - FFI contract is identical across all platforms (same function signatures)
Perry v0.4.14+ compiles standard TypeScript patterns correctly, including: ?., ??, for...of,
ES6 shorthand { key }, array.push/indexOf/map on class fields, obj[variable] dynamic keys,
/regex/.test(), character range comparisons, and Array.sort() with comparators (TimSort).
Threading: parallelMap, parallelFilter, spawn from perry/thread for data-parallel work.
The core/utils/parallel.ts shim provides sequential fallback for Bun tests.
Module-level state: Cursor position, undo stacks, and tokenization state use module-level variables (not class fields) as a proven pattern for cross-function mutable state in Perry.
Perry's AOT runtime does NOT fire setInterval or requestAnimationFrame after startup. C function
pointer callbacks into Perry-compiled code crash on ARM64 (W^X: Perry functions may be in
non-executable heap memory). This means:
- Rust NSView buffers all input events in
EditorView.pending_events - TypeScript's
_pollEvents()RAF loop is registered but never fires after startup - Do NOT call Perry-compiled functions via
event_callbackfrom Rust — it crashes - The
hone_editor_set_event_callbackFFI exists but must remain unused in Perry mode - All new FFI polling functions must be listed in
package.jsonperry.nativeLibrary.functions
Because Perry's RAF/timer loop never fires, TypeScript only renders twice at startup
(from coordinator.attach() and vm.onResize()). After that, the Rust FFI layer handles
ALL interaction directly by mutating frame_lines in place and calling setNeedsDisplay.
What every platform's Rust crate must implement:
-
Rust-side tokenizer —
native/macos/src/tokenizer.rsis the reference implementation. After everyon_text_inputandon_action deleteBackward, call:self.frame_lines[idx].tokens = crate::tokenizer::tokenize_line(&self.frame_lines[idx].text);
Do NOT call
tokens.clear()without retokenizing — that causes the whole line to go gray permanently (default color with no spans). The tokenizer portskeyword-syntax-engine.tslogic to Rust: same keyword lists, same VS Code dark theme colors. -
Cursor positioning —
on_mouse_downmust usemeasure_text()per-char prefix scan (notchar_widthdivision) to find the closest byte column to the click x position. After positioning, setrust_sel_anchor = Some((line, col))so drag immediately extends the selection. A plain click with no drag produces anchor == cursor (no visible selection). -
Mouse drag selection — Register
mouseDragged:(or platform equivalent) and callon_mouse_drag(x, y). It repeats the hit-test logic fromon_mouse_down, updatesrust_cursor_line/rust_col/cursor, then callssync_selection_rects().sync_selection_rects()builds oneSelectionRegionrect per visible line in the selection range, measured withmeasure_text()for pixel-accurate start/end x. -
Scroll clamping —
on_scrollusesinitial_top_y(set on firstend_frame) to boundframe_lines[0].y_offset. Without clamping, content drifts off-screen permanently. -
Line height inference — TypeScript sends
lineHeightPx = fontSize * 1.5. Rust's native font metrics (CTFont.line_height,Pango.line_height, etc.) differ. Infer the TypeScript line height fromframe_lines[1].y_offset - frame_lines[0].y_offsetafter the first frame. Store this in a helperts_line_height()— it's used by newline insert, backspace line-join, and multi-line selection delete. -
sync_cursor_xmust also sync Y — After any action that changesrust_cursor_line(newline, backspace line-join, up/down movement), the cursor's pixel y must be updated toframe_lines[cursor_line_idx].y_offset. Syncing only X leaves the cursor visually stuck on the wrong line. The macOS implementation handles this insync_cursor_x(). -
Newline / backspace / selection delete —
on_actionhandlesinsertNewline:,deleteBackward:,copy:,cut:,paste:,selectAll:, and all movement selectors (includingmoveXxxAndModifySelection:variants). Seeeditor_view.rsfor the full reference implementation withdelete_selection_if_any(),get_selected_text(),write_to_clipboard(),read_from_clipboard(), andsync_selection_rects(). -
user_has_clickedflag — After a user click, set this flag to prevent TypeScript's two startup re-renders from overriding the Rust-managed cursor position. -
Event queue — Implement
pending_events: Vec<PendingEvent>with the same event type constants (TEXT=1, ACTION=2, SCROLL=3, MOUSE_DOWN=4) and FFI polling functions (hone_editor_pending_event_count,hone_editor_get_event_*,hone_editor_clear_events). These are polled by TypeScript's RAF loop on platforms where it does fire (web, potentially iOS).
Platform status:
- macOS: ✅ Complete —
native/macos/src/editor_view.rs+tokenizer.rs - iOS: ⬜ Shares Core Text with macOS — needs
tokenizer.rsport + same EditorView patterns - Windows: ⬜ DirectWrite — needs
tokenizer.rsport + same EditorView patterns - Linux: ⬜ Pango/Cairo — needs
tokenizer.rsport + same EditorView patterns - Android: ⬜ Canvas/Skia JNI — needs
tokenizer.rsport + same EditorView patterns - Web: ⬜ DOM spans — RAF likely fires here; TypeScript re-render may work without Rust tokenizer
Run tests: bun test
Run single test file: bun test tests/buffer.test.ts
Run benchmarks: bun run tests/benchmarks/keystroke-latency.ts
Install deps: bun install
Build (Perry): perry compile core/index.ts --target macos --bundle-ffi native/macos/
Build macOS crate: cd native/macos && cargo build
Run macOS interactive demo: cd native/macos && cargo run --example demo_editor
Build Windows crate: cd native/windows && cargo build
Run Windows interactive demo: cd native/windows && cargo run --example demo_editor
Run iOS interactive demo: cd native/ios && cargo run --example demo_editor_ios
Run Android interactive demo: cd native/android && bash run-demo.sh
Run Web interactive demo: cd native/web && bash run-demo.sh
Open a file, see monospace text, type, move cursor, undo/redo.
- Piece table + rope B-tree (
core/buffer/) - Line index with incremental updates
- TextBuffer API (insert, delete, getText, getLine, applyEdits, snapshot/restore)
- EditorDocument + EditBuilder + encoding detection (
core/document/) - CursorManager with multi-cursor, word boundaries, selections (
core/cursor/) - UndoManager with coalescing and inverse edits (
core/history/) - ViewportManager with virtual scrolling (
core/viewport/) - Command registry + all basic commands (
core/commands/) - EditorViewModel central orchestrator (
view-model/) - Theme system with dark + light themes
- 128 tests passing across 5 test files
Full-featured editor, syntax highlighting, search, folding, diff.
- Lezer parser integration for 10 languages (
core/tokenizer/) - Search/replace engine with literal + regex + incremental search (
core/search/) - Code folding — indent-based + syntax-based (
core/folding/) - Diff engine — Myers algorithm, inline char diff, hunk merge/split/navigate (
core/diff/) - Find/replace widget, ghost text, minimap, overlays, decorations (
view-model/) - All Phase 1 subsystems wired into EditorViewModel
- 210 tests passing across 9 test files
LSP and DAP integration for smart editor features.
- JSON-RPC transport layer with Content-Length framing, request correlation, cancellation
- LSP protocol types (positions, ranges, completion, hover, diagnostics, signature help, code actions, formatting)
- LSP client with initialize lifecycle, document sync, completion, hover, definition, references, signature help, code actions, formatting
- Capability negotiation and feature detection
- DAP protocol types (breakpoints, stack frames, scopes, variables, threads)
- DAP client with launch/attach, breakpoint management, execution control, stack inspection, variable inspection, evaluate
- 249 tests passing across 11 test files
Native FFI bridge, render coordinator, touch input, word wrap, all 6 platform Rust crates.
- FFI bridge TypeScript abstraction (
native/ffi-bridge.ts) with NativeEditorFFI interface and NoOpFFI test implementation - NativeRenderCoordinator: converts EditorViewModel state to FFI calls with dirty tracking, frame batching
- Touch input handler: single/double/triple tap, long press, pan scroll with momentum, pinch zoom
- Word wrap engine: none/word/bounded modes, WrapCache with binary-search break finding, CJK support
- macOS Rust FFI crate: Core Text text renderer, NSView input handling (keyboard/mouse/scroll/context menu), EditorView with callbacks, interactive demo
- iOS Rust FFI crate: Core Text rendering (shared with macOS), UIKit integration, touch handler, interactive demo
- Windows Rust FFI crate: DirectWrite + Direct2D rendering, Win32 input handling, interactive demo
- Linux Rust FFI crate: Pango + Cairo scaffolding
- Android Rust FFI crate: Canvas/Skia via JNI, Kotlin demo app, interactive demo
- Web Rust FFI crate: Canvas-based rendering, pure HTML/JS interactive demo
- 293 tests passing across 12 test files
Examples, benchmarks, Perry config, package v0.2.0.
- Perry configuration (
perry.config.ts): all 6 targets with FFI crate paths, arch, frameworks - Example: minimal editor (
examples/minimal/) - Example: markdown editor with preview (
examples/markdown-editor/) - Example: side-by-side diff viewer (
examples/diff-viewer/) - Benchmark: keystroke-to-render latency (
tests/benchmarks/keystroke-latency.ts) - Benchmark: large file open — 10K/50K/100K lines (
tests/benchmarks/large-file-open.ts) - Benchmark: scroll throughput FPS (
tests/benchmarks/scroll-perf.ts) - Missing Rust modules: metal_blitter.rs (macOS), compositor.rs (Windows/Linux), input_handler.rs (Android), selection_overlay.rs (Web)
- Package version bumped to v0.2.0