Developer Guide
RoyiTextEditor is a Windows-first Rust application. Its core is split into small crates so text storage, editing, layout, rendering, and operating-system integration have clear owners.
Architecture
flowchart LR
Win[Win32 input and window] --> Platform[rte_platform]
Platform -->|events| App[rte_app: Editor and Document]
App -->|edit commands| Edit[rte_edit: selections and history]
Edit -->|transactions| Text[rte_text: persistent text tree]
Text -->|snapshots| App
App -->|snapshot and view state| View[rte_view: layout and display list]
View -->|display list| Render[rte_render: CPU rasterizer]
Render -->|pixels| Platform
App -->|snapshots and edits| Syntax[rte_syntax on the syntax thread]
Syntax -->|trees and spans| App
App -->|queries| Search[rte_search on the search thread]
Search -->|matches and counts| App
App -->|snapshots and ranges| Spell[rte_spell on the spelling thread]
Spell -->|misspelled words| App
Workers[Loader and save threads] -->|results and wake events| App
The window thread owns the editor, document, selections, popups, and frame construction. Background threads (loading, saving, search, syntax, spelling) receive immutable snapshots, return results through channels and wake the window; they do not mutate UI state directly. The spelling thread asks the syntax thread which Markdown text is not prose.
| Crate | Responsibility |
|---|---|
rte_text | Persistent piece tree, immutable snapshots, line queries, transactions, position mapping, and UTF-8/UTF-16 conversion. |
rte_edit | Byte-offset selections, caret motion, editing commands (including comment, auto-closing and indentation edits), clipboard data, and undo history. |
rte_search | Literal, Glob and regular-expression search over snapshots, and replacements. |
rte_spell | Words of prose, the spelling front end (user words, ignored words, cache), the built-in word list (expanded at build time from the shipped Hunspell dictionary), the Hunspell engine (spellbook) and suggestions. |
rte_syntax | Language registry and detection, built-in tree-sitter grammars with trimmed queries (queries/, regenerated by cargo xtask syntax-queries), parsing sessions, highlight spans, injections, brackets, syntax-node selection, symbols, and the tree-sitter allocator. |
rte_view | Integer-cell layout, hit-test geometry, and display lists for text and popups. |
rte_render | CPU glyph rasterization, caches, clipping, and framebuffer drawing. |
rte_platform | Raw Win32 window, input events, DIB presentation, clipboard, file operations, and the Windows spell checker (COM loaded at run time). |
rte_app | Startup, command dispatch, document lifecycle, loading, saving, palette, search, syntax and spelling threads, and UI coordination. |
rte_assets | Bundled default font and build-time font acquisition. |
xtask | Startup and performance benchmarks, import checks, release packaging, and query generation. |
Recovery journals and external-change merging are not shipped features.
Data Flow
The loader builds immutable text snapshots off the UI thread. Editing commands produce transactions and updated snapshots; the application then rebuilds the visible display list and rasterizes it into the window buffer. Each edit is also sent to the syntax thread, which re-parses incrementally. Saving writes a temporary sibling file and uses the platform replacement path, with conflict checks and backup handling.
Workers should receive owned or immutable data. Keep window and view state on the UI thread, reject stale asynchronous results, and wake the window only when there is new work to present. These boundaries are also described in the architecture plan.
One File per Session
A session (process) handles one file at a time and never starts, closes or
contacts another session. editor/lifecycle.rs owns the transitions:
stateDiagram-v2
[*] --> Empty: no file argument
[*] --> Open: file argument
Empty --> Pending: file dropped
Open --> Pending: file dropped, no unsaved changes
Pending --> Open: loader confirms a file
Pending --> Empty: folder or missing (from the empty state)
Pending --> Open: folder, or unsaved changes meanwhile (old file kept)
Open --> Empty: Ctrl+W (after Save or Don't Save)
Empty --> [*]: Alt+F4
Open --> [*]: Alt+F4 (after Save or Don't Save)
- A dropped file loads while the current file stays; it replaces it only when its loader’s first answer is a file and the current file still has no unsaved changes.
- Replacing or closing a file resets every per-file state: the loader is stopped (it checks a stop flag between blocks and before publishing), the syntax thread is dropped (its trees are freed on that thread) and the search box is closed. Search generations only grow, and the count checkpoints holding the old text are freed.
- The empty state is the
no_fileedit lock: edits and saves are refused, and only the empty-state commands run (works_without_file). - Drops arrive as
WM_DROPFILES(theWS_EX_ACCEPTFILESwindow style);shell32is loaded on the first drop, so startup is unchanged.
Syntax Threads
Highlighting starts while the window is still being created. The work runs in parallel on short-lived threads: in the benchmarks, files of 10,000 lines of TypeScript, Python and C are fully highlighted 36, 43 and 70 ms after the process starts.
sequenceDiagram
participant UI as UI thread
participant Loader as Loader thread
participant Query as Query thread
participant Worker as Syntax worker
participant Full as Whole-file parse thread
UI->>Query: compile the highlight query of the file name's language
UI->>Loader: read the file
par Window
UI->>UI: create the window, draw the first frame as plain text
and Syntax
Loader->>Worker: start with the complete text
Worker->>Full: above 128 KiB, parse the whole file
Worker->>Worker: parse the file, or above 128 KiB a 32 KiB window
Query-->>Worker: compiled query
Worker-->>UI: first trees, the next frame is highlighted
Full-->>Worker: whole-file tree
Worker-->>UI: whole-file trees, fully highlighted
end
UI->>Worker: edits, visible range, span and symbol requests
Worker-->>UI: trees and spans
- The loader starts the syntax worker as soon as the text is read. The editor adopts that worker when the language and text match; otherwise it starts its own.
- The highlight query compiles on its own thread while the text parses; the first view waits for it.
- Files above 128 KiB parse a 32 KiB window around the view first. The whole file parses at the same time on its own thread; edits made meanwhile are applied to its tree when it arrives, and it is adopted before any further window parse.
- The UI thread never waits for these threads. It draws the visible rows from the latest trees within 1 ms per frame and asks the worker for the rest. Trees it replaces go back to the worker to be freed.
Memory Allocation
Tree-sitter allocates its parse trees through
mimalloc
(rte_syntax::install_allocator, an unsafe function called first in
main, before any tree-sitter object exists; a failed allocation aborts the
process). Parsing is 6-16%
faster than with the Windows heap, and Go to Symbol about 25% faster. The
rest of the application keeps the Windows heap: mimalloc there gave no
measurable gain and doubled the memory of small files.
Fast rendering with a higher memory footprint is the project owner’s
design choice: when the two conflict, speed wins, within explicit limits
such as syntax.maxFileMiB. The parallel
threads and mimalloc’s per-thread pages add 2-3 MiB to the peak memory of
small and 10,000-line files, and editing an 8 MiB file while it is still
being parsed can peak 13-20 MiB higher, because the whole-file parse no
longer pauses for the edits. In return highlighting is complete sooner, and
8 MiB files peak about 24 MiB lower. Startup memory is unchanged (5.1 MiB
for an empty file), and --syntax none restores the memory footprint of
0.3.0 (a 64 KiB JSON file peaks at 5.7 MiB instead of 10.7 MiB). The
design and measurements are recorded in
Done/SyntaxPerformance.md.
Build and Validation
The repository targets Windows x64 with the GNU Rust toolchain and
x86-64-v3 CPU features. windres is required to embed the application
manifest and icon. Missing font assets are fetched from pinned releases and
SHA-256 verified by the rte_assets build script.
Run focused tests for the crate being changed. Do not run the whole workspace suite or performance suite unless requested; see AGENTS.md.
| Task | Command |
|---|---|
| Run the editor | cargo run --release -p rte_app -- <file> |
| Test a crate | cargo test -p <crate> |
| Check formatting | cargo fmt --all --check |
| Run the documentation build | ./tools/Build-Docs.ps1 |
| Measure startup | cargo xtask bench-startup |
| Run the short performance report | cargo xtask bench-perf --short |
| Run selected performance tests | cargo xtask bench-perf --only L01,F01 |
| Package a release ZIP | cargo xtask package |
The workspace and shipped behavior are documented in Done/Architecture001.md, DEVELOPMENT.md, and AGENTS.md.