73 lines
15 KiB
Markdown
73 lines
15 KiB
Markdown
# Architecture
|
|
|
|
[README](../README.md)
|
|
|
|
Contributor-facing module boundaries and implementation contracts. Project terminology and interface conventions live in [CONTEXT.md](../CONTEXT.md); verification is in [development](development.md).
|
|
|
|
## Module ownership
|
|
|
|
One crate; no workspace or terminal-coordinate types in the editing backend:
|
|
|
|
- `editor`: owns buffer lifecycle and per-buffer text, saved state, primary/selection set, modes, search, and undo/redo; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, leader/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
|
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
|
- `clipboard`: owns native plain-text provider selection and bounded subprocess I/O; it never computes selections or editing offsets.
|
|
- `syntax`: owns Tree-sitter grammar detection, per-buffer incremental trees, bundled highlight/local/injection queries, and syntax byte spans; it has no terminal color/geometry or editing dependency.
|
|
- `markdown`: owns native Markdown link/reference resolution, local destination decoding, and heading fragment lookup through the installed block/inline grammars. It has no editor lifecycle, terminal, LSP, or external-process dependency.
|
|
- `plugin`: owns generic document snapshots/events, zero-based grapheme positions, diagnostics, frontend-independent semantic token kinds/modifiers, and versioned asynchronous update contracts. Semantic token updates are native-only, not part of the Ruby wire protocol; it has no LSP protocol types.
|
|
- `lsp`: owns opt-in native server workers, bounded framed JSON-RPC, synchronization/capability negotiation, settings, UTF-16/file-URI conversion, definition/hover results, and semantic token requests/legend decoding. Its private configuration resolver supplies presets without restricting language IDs.
|
|
- `session`: `EditorSession` owns the editor, native LSP, and Ruby hosts together. It loads configuration once, registers eligible plugins before installing shared bindings, tracks document open/change/save/close events, polls native and plugin results, validates versioned diagnostics/navigation, and retires failed plugin ownership.
|
|
- `tui`: translates terminal events to session input, renders read-only active-buffer state, keeps frontend-owned viewports and syntax caches by stable buffer ID, and advertises `text`, `styled_text`, and `diagnostics` capabilities. It maps semantic tokens and diagnostics to terminal styling and has no server/protocol logic or paste acceptance policy.
|
|
- `ruby`: evaluates configuration and runs isolated plugin hosts. Protocol, deadlines, and process cleanup stay inside this module.
|
|
- The executable selects paths and frontend capabilities, opens an editor session, and launches the TUI.
|
|
|
|
## Frontend interface
|
|
|
|
Frontends drive `EditorSession::handle` or `execute`, periodically call `poll()` even without input, and inspect `editor()`; there is no mutable editor getter that could independently alter plugin registration. `poll()` reports whether background activity requires redrawing. Pasted text uses `Input::Paste(String)` through the same input seam as keys. Plugin effects are applied internally so a rejected result also retires its owner; there is no generic frontend effects entry point.
|
|
Frontends inspect `selections()` for document-ordered, end-exclusive Visual ranges of grapheme coordinates (empty outside Visual), `carets()` for all active cursors in document order, `cursor()` for the primary, and `selection_count()` for the total target count. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. Selection normalization, clipboard contents, edit planning, and history remain inside the editor module.
|
|
The TUI also observes the editor's logical caret-reveal generation. Core motion/navigation dispatch advances it even when the resulting position is unchanged, so a detached viewport can resume following without duplicating key-binding policy or changing text revisions and LSP freshness.
|
|
Frontends inspect `palette()` for the optional read-only query, filtered command entries, selected index, effective bindings, and eligibility. Palette input handling and acceptance stay in the editor; registered command execution stays in the session.
|
|
Frontends use cached `selection_navigation_hint()` for command lists and `file_navigation_hint()` for the active file view/context, without reconstructing input policy.
|
|
Frontends inspect `count()` for the optional pending motion prefix; parsing and counted motion dispatch remain editor-owned.
|
|
Frontends inspect `leader_pending()` for the cached display of a pending leader sequence; prefix recognition, cancellation, and dispatch remain editor-owned.
|
|
Frontends inspect `g_pending()` for the built-in Normal `gf` prefix. Markdown following preflights links and headings before buffer/caret mutation; an unloaded target is validated/prepared once, while loaded targets use their current unsaved text. File identity and activation stay editor-owned.
|
|
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
|
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
|
|
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
|
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
|
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text/path changes discard overlays, and native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
|
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the document workspace without changing the primary cursor or viewport.
|
|
|
|
## Terminal lifecycle and layout
|
|
|
|
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, mouse capture, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
|
|
|
|
Redraws bracket Ratatui's buffer diff and final cursor placement with DEC synchronized updates (mode 2026), presenting complete frames on supporting terminals rather than exposing intermediate rows. The hardware cursor is hidden before painting even when that extension is unsupported. Each frame releases synchronization; terminal cleanup also releases it before leaving the alternate screen or invoking caller panic diagnostics.
|
|
|
|
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row status bar, and a one-row prompt/message area. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three rows the status bar is hidden; at one row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
|
|
|
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
|
|
|
|
Ratatui's bounded layout cache is enabled: unchanged geometry and constraints reuse solved rectangles during scrolling; resizes and panel-layout changes use their own cache keys.
|
|
|
|
The buffer viewport includes a fixed left gutter with right-aligned hybrid line numbers: the highlighted primary cursor line shows its one-based absolute number; every other line shows its distance from the primary. Secondary carets do not establish separate numbering origins. Gutter width follows the total line count plus one separator cell, including an empty final row after a trailing newline. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Rows beyond the buffer have no number. The gutter is hidden when it would leave no text column.
|
|
|
|
## Syntax caches
|
|
|
|
Each buffer has a frontend-owned parser/tree cache. Unchanged revision/path skips comparison and parsing; unchanged text at a later revision also avoids parsing. Actual text changes produce a minimal UTF-8-safe byte edit with newline-aware Tree-sitter Points, edit the prior tree, then parse using that tree for incremental reuse. Grammar changes reset the tree, including TypeScript versus TSX despite their shared display name. Native highlight/local/injection queries resolve capture precedence and scoped definitions into nonoverlapping original-line spans; unchanged injected trees are reused. Buffer switching restores its cache, and closing retires it. Incomplete text and grammar ERROR nodes remain highlightable; genuine language/query/parser failures expose a status error and readable plain text. Full-tree highlight queries still run after incremental parsing—a deliberate ceiling to revisit if measured costly. There is no local semantic inference engine or runtime grammar download.
|
|
|
|
## Clipboard providers
|
|
|
|
macOS uses fixed JXA/AppKit `NSPasteboardTypeString` access through `osascript`, preserving literal RTF/EPS text rather than treating it as rich clipboard data. Linux supports Wayland with `wl-paste`/`wl-copy`, or X11 with `xclip`, on PATH with the corresponding display environment. Other platforms report unsupported access. Commands have a two-second deadline and bounded output; failures terminate/reap their process group.
|
|
|
|
## Native LSP
|
|
|
|
The native module initializes each server, sends `initialization_options` as `initializationOptions`, answers `workspace/configuration` from `settings` (including dotted sections; missing sections return `null`), honors open/change/save capabilities, synchronizes in-memory text including hidden dirty buffers, and sends close events. Incremental servers receive whole-document range replacements. Process/pipe I/O and requests stay on bounded background workers; input and idle diagnostic redraw do not wait for server responses.
|
|
|
|
Language configurations can own an ordered server stack. The first matching language wins extension overlap; all its matching servers receive document events, while the first handles hover/definition. The Python preset uses ty then Ruff. LSP merges each worker's latest diagnostic snapshot in assignment order before emitting the unchanged core-owned update. Empty publications/retirement remove only their source; revision/path/root changes and close discard all contributions. Worker snapshots retain identity so queued pre-rename diagnostics cannot attach to a new path with the same text revision. Combined diagnostic message bytes remain bounded to 4 MiB; an overflowing publication retires its worker instead of discarding healthy sibling annotations. Workers are stored in config/root order, avoiding per-poll key cloning/sorting. Shutdown flushes the exit notification and waits for peer termination within the existing bounded grace period before process cleanup.
|
|
|
|
Semantic tokens belong only to the first assigned server. Initialization advertises standard token types/modifiers, relative encoding, full/range requests, and server refresh support; deltas, overlapping/multiline tokens, and dynamic registration are not advertised. Each buffer has at most one request in flight; changes/refreshes coalesce into the latest snapshot. Full requests are preferred, with whole-document range fallback. A linear UTF-16 decoder validates raw coordinates and legend indices before outward grapheme rounding; overlong ends truncate at EOL, unknown types and invisible spans fall back to syntax. Both wire frames and decoded spans are bounded to 4 MiB. Snapshot identity rejects stale/renamed/closed replies before decoding. Invalid semantic replies clear the overlay and report an error without retiring diagnostics/navigation; primary retirement clears its overlays, and sibling retirement leaves them intact.
|
|
|
|
## Ruby action catalog
|
|
|
|
Ruby builds `config.core` from the Rust action catalog before loading configuration or plugin scripts. Dotted identifiers create nested namespaces automatically; each namespace and Action value is frozen. Action names are not duplicated in Ruby, so Rust catalog additions/renames immediately change the Ruby interface without code generation or handwritten exports.
|