Headless editor engine + integrations for building UI-agnostic text editors.
editor-core focuses on:
- State management (commands, undo/redo, selection state, change notifications)
- Unicode-aware measurement (cell widths for CJK/emoji)
- Coordinate conversions (char offsets ⇄ line/column ⇄ wrapped “visual” rows; plus UTF-16 for LSP)
The project is intentionally UI-agnostic: frontends render from snapshots (HeadlessGrid) and
drive edits through the command/state APIs.
Note: this section lists the crates in this repository’s Cargo workspace. The editor’s multi-buffer model is the
editor_core::Workspacetype (see “Workspace model” below).
crates/editor-core/— core headless editor engine (PieceTable,LineIndex,LayoutEngine, snapshots, commands/state).- See
crates/editor-core/README.md
- See
crates/editor-core-diff/— line-based diff + hunk primitives (useful for “modified lines” gutters and diff views).crates/editor-core-lang/— lightweight language configs (e.g. comment tokens) for kernel features.crates/editor-core-lsp/— LSP integration (UTF-16 conversions, semantic tokens decoding, stdio JSON-RPC client/session).- See
crates/editor-core-lsp/README.md
- See
crates/editor-core-app/— UI-agnostic “editor app shell” helpers (workspace file index, explorer helpers, sessions/recents, settings, find-in-files).- See
crates/editor-core-app/README.md
- See
crates/editor-core-ffi/— C ABI bridge for Swift/C++/C# and other native hosts (typed hot-path + binary viewport blobs + JSON control plane).- See
crates/editor-core-ffi/README.mdanddocs/abi-v1-draft.md
- See
crates/editor-core-sublime/—.sublime-syntaxhighlighting + folding engine (headless output as style intervals + fold regions).- See
crates/editor-core-sublime/README.md
- See
crates/editor-core-treesitter/— Tree-sitter integration (incremental parsing → highlighting + folding).- See
crates/editor-core-treesitter/README.md
- See
crates/editor-core-highlight-simple/— lightweight regex-based highlighting helpers (JSON/INI/etc).crates/tui-editor/— runnable TUI demo app (ratatui + crossterm) that wires everything together.
The editor consistently uses character offsets (Rust char indices) at API boundaries:
- Character offset: index into the whole document in Unicode scalar values (not bytes).
- Logical position:
(line, column)wherecolumnis also counted inchars. - Visual position: after soft wrapping (and optional folding), a single logical line can map to multiple visual rows.
- LSP positions:
(line, character)wherecharacteris UTF-16 code units (seeeditor-core-lsp).
The canonical coordinate model is still char-indexed (Unicode scalar values), but the kernel
also includes grapheme/word-aware cursor and delete commands (UAX #29). This means host UIs can
opt into “move by grapheme/word” behavior without introducing a separate coordinate space.
Frontends render from HeadlessGrid:
- A snapshot contains a list of visual lines.
- Each line is a list of cells where
Cell.widthis typically1or2(Unicode-aware). - Each cell carries a list of
StyleIds; the UI/theme layer mapsStyleId→ colors/fonts.
Derived metadata (semantic tokens, syntax highlighting, folding ranges, diagnostics overlays, …) is represented as editor edits of derived state:
DocumentProcessorcomputes a list ofProcessingEdits.EditorStateManager::apply_processing_editsapplies them (replacing style layers, folding regions, …).
This makes high-level integrations composable and keeps the core engine UI-agnostic.
Full editors typically need more than “one document, one viewport”. editor-core provides an
optional Workspace model with two core concepts:
- Buffer: document text + undo/redo + derived metadata tied to the text (styles, folding, diagnostics, decorations, symbols…).
- View: per-viewport state like selections/cursors, wrap width/mode, and scroll position.
In editor_core::Workspace, commands are executed against a ViewId. Text edits are applied
to the underlying buffer, and any resulting TextDelta is broadcast to all views of that buffer
(so split panes stay consistent).
- Rust 1.91+ (see
rust-versionin the workspaceCargo.toml)
cargo build
cargo testRun the main editor-core integration test only:
cargo test -p editor-core --test integration_testcargo run -p tui-editor -- crates/editor-core/tests/fixtures/demo_file.txtThe TUI demo also exports a convenience editor binary name (useful for $EDITOR workflows):
cargo run -p tui-editor --bin editor -- crates/editor-core/tests/fixtures/demo_file.txtOptional open-at-location helpers:
cargo run -p tui-editor --bin editor -- --line 10 --column 5 foo.rs
cargo run -p tui-editor --bin editor -- foo.rs:10:5The TUI demo supports:
- soft wrapping + Unicode width
- selection, multi-cursor, rectangular selection
- find/replace
- optional highlighting/folding via Sublime syntax or LSP
If the current directory contains a matching .sublime-syntax file (example: Rust.sublime-syntax
or TOML.sublime-syntax), tui-editor will auto-enable editor-core-sublime highlighting and folding.
Otherwise it falls back to the built-in regex highlighter for simple formats (JSON/INI).
The demo can connect to any stdio LSP server.
- Default behavior: when opening a
.rsfile, it will try to startrust-analyzer(if installed). - Override via env vars (applies to all file types):
# Example: Python
EDITOR_CORE_LSP_CMD=pylsp \
EDITOR_CORE_LSP_LANGUAGE_ID=python \
cargo run -p tui-editor -- foo.pyAdditional env vars:
EDITOR_CORE_LSP_ARGS— whitespace-separated args passed to the LSP serverEDITOR_CORE_LSP_ROOT— override workspace root for LSP initialization
There are two primary entry points, depending on whether you need multi-buffer/multi-view:
- Single-buffer / single-view:
EditorStateManager- ergonomic wrapper around a
CommandExecutor - adds
version,is_modified, and change notifications - convenient for simple apps, tests, and “one file open” tools
- ergonomic wrapper around a
- Multi-buffer / multi-view (split panes):
Workspace- owns multiple buffers and multiple views per buffer
- routes commands via
Workspace::execute(view_id, Command) - exposes buffer-wide utilities like search and applying multi-buffer edits (useful for LSP)
If you’re building a “single document” editor (or a code editor widget embedded in a larger app),
start with EditorStateManager. If you need tabs/split panes/multi-file operations, use
Workspace and treat each ViewId as a UI viewport.
use editor_core::{Command, EditCommand, EditorStateManager};
let mut state = EditorStateManager::new("Hello\nWorld\n", 80);
// Apply an edit via the command interface.
state.execute(Command::Edit(EditCommand::Insert {
offset: 0,
text: "Title: ".to_string(),
})).unwrap();
// Render a viewport snapshot (visual lines).
let grid = state.get_viewport_content_styled(0, 20);
assert!(grid.actual_line_count() > 0);use editor_core::{Command, CursorCommand, EditCommand, Workspace};
let mut ws = Workspace::new();
let opened = ws
.open_buffer(Some("file:///demo.txt".to_string()), "Hello\nWorld\n", 80)
.unwrap();
let view = opened.view_id;
ws.execute(view, Command::Cursor(CursorCommand::MoveTo { line: 1, column: 0 }))
.unwrap();
ws.execute(view, Command::Edit(EditCommand::InsertText { text: ">> ".into() }))
.unwrap();
let grid = ws.get_viewport_content_styled(view, 0, 20).unwrap();
assert!(grid.actual_line_count() > 0);use editor_core::EditorStateManager;
use editor_core_highlight_simple::{RegexHighlightProcessor, SimpleJsonStyles};
let mut state = EditorStateManager::new(r#"{ "k": 1, "ok": true }"#, 80);
let mut processor =
RegexHighlightProcessor::json_default(SimpleJsonStyles::default()).unwrap();
state.apply_processor(&mut processor).unwrap();
let grid = state.get_viewport_content_styled(0, 10);
assert!(grid.lines[0].cells.iter().any(|c| !c.styles.is_empty()));For richer syntax highlighting and folding, use:
editor-core-sublime(SublimeProcessor)editor-core-treesitter(TreeSitterProcessor)editor-core-lsp(LspSession)
- Design:
docs/DESIGN.md - API docs:
cargo doc --no-deps --open - Examples:
cargo run -p editor-core --example command_interfacecargo run -p editor-core --example multiview_workspacecargo run -p editor-core --example workspace_search_applycargo run -p editor-core --example state_managementcargo run -p editor-core --example performance_milestones
editor-core includes a small criterion benchmark suite for large-file/open/typing/viewport paths:
cargo bench -p editor-core --bench performanceCommon commands:
cargo fmt
cargo clippy --all-targets --all-featuresRepo layout highlights:
crates/editor-core/src/— storage/index/layout/intervals/snapshot + command/state layerscrates/*/tests/— stage validations and integration tests
Licensed under either of
- Apache License, Version 2.0,(LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.