feat: add live-cycling LSP completion

This commit is contained in:
2026-10-10 18:32:39 +02:00
parent 9d3b25e9d3
commit b6059d5ed9
11 changed files with 1916 additions and 55 deletions
+7 -4
View File
@@ -13,9 +13,9 @@ One crate; no workspace or terminal-coordinate types in the editing backend:
- `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.
- `plugin`: owns generic document snapshots/events, zero-based grapheme positions, diagnostics, frontend-independent semantic token kinds/modifiers and completion edit/item DTOs, and versioned asynchronous update contracts. Semantic token and completion 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, completion decoding, 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/completion, 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.
@@ -36,6 +36,7 @@ Frontends inspect `file_picker()` for query, presentation, view, explorer-local
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. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI only places a windowed list near the current caret inside the document text workspace, sanitizes/clips labels and detail by grapheme, and preserves gutter, explorer, and hardware cursor ownership; wheel input over the list is consumed without dispatching edits.
## Terminal lifecycle and layout
@@ -65,10 +66,12 @@ macOS uses fixed JXA/AppKit `NSPasteboardTypeString` access through `osascript`,
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.
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/completion. 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.
Manual completion dispatches the Insert-only `core.lsp.completion` action directly to the session, without the Ex execution path's mode/history transitions. The native worker sends the primary caret as UTF-16 and normalizes results into `CompletionItem { label, detail, primary_edit, edits }`; each `CompletionEdit` uses original-document, end-exclusive grapheme coordinates. Primary and additional edits form one atomic candidate. Session freshness includes the latest request, buffer/revision, primary caret, Insert source mode, and absence of modal overlays; any intervening input invalidates a pending reply. Decoding/validation errors remain completion-local and preserve healthy diagnostics/semantic tokens. Snippets, server commands, indentation adjustment, resolve requests, and automatic triggers are not supported; both frames and aggregate decoded completion text remain bounded.
## 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.