LSP Feature Tracking

Single source of truth for which LSP 3.17 capabilities lattice implements and which are planned. Updated every commit that moves a row.

Sequencing for the in-flight polish work (the async-result render-wake fix, server lifecycle state, status surfaces, diagnostics inline/popup) lives in the LSP slice plan (complete — archived); the design contracts are in lsp-architecture.md §12–§15. Note: rows below marked ✅ for $/progress, semanticTokens, and inlayHint cover wire + cache; they only repaint off-keystroke once slice L1 lands.

Status legend:

StatusMeaning
Done; tested; benchmarked where relevant.
🚧In progress; partial implementation landed.
⏹️Planned for the indicated phase.
Deferred past v1 (rationale in the row).
n/aNot applicable (server-side, not client-side).

Phases:

PhaseScope
4.1Foundation: wire layer + actor + handshake + sync + diagnostics routing.
4.2Navigation: hover, definition family, references, symbols, completion.
4.3Edits: code actions, rename, formatting, signature help, will-save hooks.
4.4Polish: semantic tokens, inlay hints, folding, document highlight, dynamic registration, file watchers, workspace configuration, progress / messages routing.
4.5Expansion: call/type hierarchy, code lens, document links, color, moniker, linked editing, selection ranges, inline values, inline completion.
post-1.0Notebooks, multi-root workspace folders, server-side LSP, etc.

Capability columns:

  • Spec § / method — LSP 3.17 spec section + method name.
  • Client cap — what we advertise in initialize.capabilities.
  • Server cap gating — which ServerCapabilities field gates the feature.
  • Phase — when it lands.
  • Status — current implementation state.
  • Notes — implementation pointers + caveats.

Lifecycle

MethodDirectionPhaseStatusNotes
initializeC → S4.1actor::actor_main. Runs before ServerHandle is returned to the caller; failure surfaces as LspError::HandshakeFailed.
initializedC → S4.1Sent immediately after initialize response is decoded.
shutdownC → S4.1ServerHandle::shutdown() runs the protocol-mandated shutdown request before exit.
exitC → S4.1Sent after shutdown response (or 5s timeout).
$/setTraceC → S4.4.bServerHandle::set_trace(value); :lsp-trace toggle drives Verbose / Off over the wire across every actor with the matching id. Failures log + skip (the local flag still flips so the trace ring works for wire-frame records).
$/logTraceS → C4.4.bRouted via parse_log_trace -> LogSource::Trace at LogLevel::Trace. Verbose tail (when present) appended as a second indented record so the *lsp:<server>:trace* ring preserves both lines.
$/cancelRequestboth4.1ServerHandle::cancel(id) emits the notification + resolves the matching Pending with LspError::Cancelled.
$/progressS → C4.4.cActor parses the {token, value{kind,...}} envelope and publishes LspProgressUpdate on the typed bus; the App accumulates by (server_id, token) and the modeline LSP segment surfaces the most-recent active entry. lsp-progress-mode per-buffer minor gates the modeline render.

Document synchronisation

MethodDirectionPhaseStatusNotes
textDocument/didOpenC → S4.1DocSync::open -- carries languageId, version=1, full text.
textDocument/didChange (Incremental)C → S4.1DocSync::record_edit queues; DocSync::flush sends. utf-8 / utf-16 / utf-32 conversion via position module.
textDocument/didChange (Full)C → S4.1Auto-selected when server advertises Full sync mode.
textDocument/didChange (None)C → S4.1No-op when server advertises None.
textDocument/willSaveC → S4.3App::save_blocking fan-out via fire_will_save_notifications; only servers advertising wants_will_save receive it. reason: Manual for now -- AfterDelay / FocusOut land when those triggers exist.
textDocument/willSaveWaitUntilC → S4.3App::save_blocking runs run_will_save_wait_until_blocking between willSave and the disk write; per-server Pending::await with a 500ms timeout; collected TextEdits apply pre-save as one undo unit. Format-on-save flows through this path.
textDocument/didSaveC → S4.3Post-save fan-out via fire_did_save_notifications; attaches the rope's text when the server's did_save_include_text says so.
textDocument/didCloseC → S4.1DocSync::close flushes pending then sends.

Server → client notifications

MethodPhaseStatusNotes
textDocument/publishDiagnostics4.1DiagnosticsBus broadcast (4.1.d.i) + DiagnosticsLayer per-URI state with version gating + multi-server merge (4.1.d.ii). Renderer overlay + :diagnostics buffer in 4.1.d.iii–iv.
window/showMessage4.44.4.a. LspLogPushed { source: "show" } records flow through App::drain_lsp_log_events; the most recent show-message in a tick lands on the minibuffer via set_message with severity from the LSP level. Multiple shows in one tick coalesce to the last (consistent with successive :echo calls). Server-id rides in the message prefix.
window/showMessageRequest4.4.bShowMessageRequestBus + App-side drain ship the full UX. Actionless prompts auto-reply null and surface to the minibuffer + LSP log. Actionful prompts open a one-at-a-time action picker (PickerSource::LspShowMessageRequest); accept replies with the chosen MessageActionItem, dismiss replies null. Multiple in-flight requests queue (lsp_show_message_request_queue); the next opens automatically when the active one resolves. Every request also logs to *lsp:<server>* with its assigned id so the user can audit what the server asked.
window/showDocument4.4.bShowDocumentBus + App-side drain. file:// URIs open via the standard :e path with optional selection range applied. external == true delegates to xdg-open / open / explorer per platform. Non-file URIs without external are refused with success: false.
window/logMessage4.44.4.a. Routed through LspLogger::log with LogSource::LspMessage; surfaces in the per-server LSP log buffer. :messages editor-buffer surface (distinct from the LSP log) stays a post-1.0 feature -- LSP log was always the right home for server-emitted log records anyway.
telemetry/event4.44.4.a. New LogSource::Telemetry tag so plugin subscribers can filter LspLogPushed.source == "telemetry" without parsing message text. Payload rides as the compacted-JSON message suffix; subscribers needing structured access parse the tail.

Server-initiated requests

MethodPhaseStatusNotes
client/registerCapability4.4.nParses RegistrationParams, folds each entry into Capabilities.dynamic (a DynamicRegistry { by_id, by_method } two-way index), republishes via ArcSwap<Capabilities> on the ServerHandle. The next caps.supports_*() call observes the union (static ServerCapabilities OR dynamic). Replies null; malformed params log + still reply null so the wire stays unblocked. Client advertises workspace.didChangeWatchedFiles.dynamic_registration = true so servers register watcher patterns lazily (4.4.l consumes these).
client/unregisterCapability4.4.nParses UnregistrationParams, evicts each entry by id from Capabilities.dynamic, republishes the snapshot. Unknown ids are silent no-ops (servers occasionally unregister speculatively after restart). Probes fall back to consulting only the static field once the dynamic entry is gone.
workspace/configuration4.1Inbound channel (lattice-lsp::ConfigurationBus mpsc + per-request oneshot) ferries each request to the App. The loader caches the merged user + project TOML tree (deep-merge; project wins per scalar; sibling keys preserved); App::drain_inbound_configuration_requests looks each requested section up at lsp.<section> and replies with one serde_json::Value per item. Empty section returns the whole lsp.* sub-tree; missing sections come back as null. User puts server-namespaced settings under [lsp.<server-id>] (e.g. [lsp.rust-analyzer.cargo] features = ["foo"]).
workspace/applyEdit4.3Inbound channel (lattice-lsp::ApplyEditBus mpsc + per-request oneshot) ferries each request to the App. App::drain_inbound_apply_edits runs once per main-loop iteration; reuses flatten_workspace_edit + apply_lsp_text_edits (the rename path) per file; replies via the oneshot with applied + optional failure_reason. Empty edits reply applied: true. v1 is non-atomic (per-file batches); failed_change is queued for the future apply_workspace_edit_atomic.
workspace/codeLens/refresh4.5.dServer-initiated cache invalidation. Actor handles inline (replies null) and publishes LspCodeLensRefresh; App's drain (drain_code_lens_refresh) evicts every cached code-lens entry that came from the requesting server. Workspace-side advertisement (workspace.code_lens.refresh_support = true).
workspace/inlayHint/refresh4.4.gServer-initiated cache invalidation. Actor handles inline (replies null) and publishes LspInlayHintRefresh on the typed bus; App's drain (drain_inlay_hint_refresh) clears lsp_inlay_hints_cache for buffers attached to the requesting server; the next render tick's pump re-issues inlayHint.
workspace/inlineValue/refresh4.5.hActor handles inline (replies null per spec + logs an info breadcrumb). The host-side cache eviction is a no-op since the inline-value renderer trigger itself is deferred; once DAP lands, the actor publishes a typed LspInlineValueRefresh and the App's drain evicts the cache. Workspace-side advertisement (workspace.inline_value.refresh_support = true) ships so servers know we honour the request when the trigger is wired.
workspace/semanticTokens/refresh4.4.iServer-initiated cache invalidation. Actor handles inline (replies null) and publishes LspSemanticTokensRefresh on the typed bus; App's drain (drain_semantic_tokens_refresh) drops lsp_semantic_tokens_cache for every buffer attached to the requesting server. The next render tick's pump issues a fresh semanticTokens/full baseline (since the cached result_id is gone). Workspace-side advertisement (semantic_tokens.refresh_support = true) added to client_capabilities so servers know to send the request.
workspace/diagnostic/refresh4.4.jServer-initiated cache invalidation. Actor handles inline (replies null) and publishes LspDiagnosticRefresh on the typed bus; App's drain (drain_diagnostic_refresh) evicts lsp_pull_diagnostics_cache entries for every buffer attached to the requesting server, forcing the next per-document pull to drop the previous_result_id and the server to emit a Full report. Workspace-side advertisement (workspace.diagnostic.refresh_support = true) added to client_capabilities so servers know we honour the request.
window/workDoneProgress/create4.1Accepted with null result.
window/workDoneProgress/cancel4.4.cServerHandle::cancel_progress(token) notifies the server; :lsp-progress-cancel [server] cancels cancellable active entries (best-effort -- entries stay in the accumulator until end).

Workspace operations

MethodPhaseStatusNotes
workspace/didChangeConfiguration4.4.kServerHandle::did_change_configuration notification + apply_option_cascade fan-out. Any typed-option change at lsp.<server_id>.<...> triggers App::fan_out_did_change_configuration(server_id), which serialises lsp.<server_id> from the merged TOML tree as the settings payload and notifies every running actor matching that server_id (cross-workspace included). lsp.<host-knob> keys (lsp.log_level, lsp.log_capacity) keep their single-dot shape and never page servers -- they're host-side. Notification-only; errors log + skip. Pure helper lsp_server_scope(canonical_name) -> Option<&str> keeps the parsing testable.
workspace/didChangeWatchedFiles4.4.lEnd-to-end: pure parse + match layer (lattice-lsp::file_watcher) compiles every dynamic-registered FileSystemWatcher glob into one shared globset::GlobSet per server, honouring per-watcher WatchKind (default 7 = Create
workspace/didChangeWorkspaceFolderspost⏹️Multi-root workspaces -- post-1.0 feature; v1 is single-root.
workspace/willCreateFiles4.4.m🚧Wire wrapper shipped (ServerHandle::will_create_files + Capabilities::supports_will_create_files); client advertises interest. Strong-reason defer on the trigger: willCreate is a pre-create blocking request -- the server may return a WorkspaceEdit the client must apply BEFORE the file lands on disk, atomically with the save. The host's save path (save_blocking) already runs willSave + willSaveWaitUntil as bounded blocking phases; willCreate adds a fourth blocking phase that interacts with willSaveWaitUntil's existing 500ms budget. Wiring cleanly needs a unified "pre-save blocking edits" pipeline that today's code doesn't have. The wrapper ships so the future trigger plugs into a stable surface.
workspace/didCreateFiles4.4.mServerHandle::did_create_files wrapper + capability probe. Trigger in App::save_blocking: detects whether the resolved path existed before block_on(self.document.save()) and, when it didn't, fans out workspace/didCreateFiles to every attached server advertising supports_did_create_files. Single-file batch today; multi-file create UX (e.g. plugin scaffolding) plugs into the same wrapper.
workspace/willRenameFiles4.4.m🚧Wire wrapper + probe shipped. Strong-reason defer on the trigger: Lattice has no in-place file-rename UX today -- there is no :saveas variant that moves the buffer's prior path (the existing save_as is a copy, leaving the original on disk). Wiring willRename means designing the rename UX first; the wrapper is ready for it.
workspace/didRenameFiles4.4.m🚧Wire wrapper + probe shipped. Trigger blocked on the same missing rename UX as willRenameFiles.
workspace/willDeleteFiles4.4.m🚧Wire wrapper + probe shipped. Strong-reason defer on the trigger: the editor's :bd/:bdelete is buffer-list eviction, not on-disk deletion. There's no editor-driven path that removes a file from disk; the file-watcher (4.4.l) picks up external deletes and surfaces them as workspace/didChangeWatchedFiles. willDelete fires when the editor drives the delete, which it currently doesn't.
workspace/didDeleteFiles4.4.m🚧Wire wrapper + probe shipped. Trigger blocked on the same missing editor-driven delete path as willDeleteFiles.
workspace/executeCommand4.3Wired through the codeAction Command-payload arm. :code-actions accept fires it via the originating server; capability-gated on executeCommandProvider.
workspace/symbol4.2:lsp-workspace-symbol [query] (Phase 4.2.f). Fans out across every running server (workspace-scoped); dedups by (path, line, col, name); opens the merged list as a vertico PickerSource::LspLocations picker. Empty query → server's idea of "every workspace symbol".
workspaceSymbol/resolve4.2LSP 3.17+ lazy-location path. workspace/symbol response upgrades from the legacy Vec<SymbolInformation> to WorkspaceSymbolResponse (Flat | Nested union); Nested symbols whose location is WorkspaceLocation (URI only) eager-resolve at fan-out via workspace_symbol_to_row so the picker's row shape stays uniform. Falls back to (path, 0, 0) when the server doesn't advertise resolveProvider or resolve fails -- the user can still navigate to the file.

Language features (text document)

Hover / signatures / completion

MethodPhaseStatusNotes
textDocument/hover4.2K keystroke (Phase 4.2.b). Spawns the request on the LSP runtime; first non-empty body across attached servers wins; relay's cancellation token flips on a follow-up K so a stale response can't drop a popup over a moved cursor. Markdown body feeds the existing HoverPopup pipeline. Multi-server merge with --- {name} --- separators is a polish item.
textDocument/signatureHelp4.3:signature-help (alias :sighelp). Fan-out across attached servers; first non-empty wins. signature_help_to_markdown renders the active signature + active parameter (fenced-code label + **param:** highlight + parameter docs); body feeds do_open_hover so the popup pipeline (markdown highlight, anchored placement, State A/B focus model) is shared with K hover. Trigger-character autopilot (( / , etc.) in Insert mode is queued.
textDocument/completion4.2Insert-mode popup via <C-x><C-o> / <C-Space> / smart-tab (Phase 4.2.g full surface: shell + buffer-words + LSP source + docs popup + lazy completionItem/resolve + snippets + frequency ranking + per-source priority + per-language overrides + tree-sitter symbols + path source + commit chars + ghost text + cross-source visual dedup + typed picker routing). Item adaptation: filterText / sortText / detail / documentation / commitCharacters / additionalTextEdits / textEdit.range / insertTextFormat; isIncomplete: true re-fires per keystroke. Multi-server fan-out + dedup by (label, kind). Sidecar metadata via App.insert_completion_lsp_meta keyed by CandidateData::Extension { kind_id: LSP_COMPLETION_KIND_ID, payload }. Picker bridge :complete survives as the cmdline-driven peer. Behavioural spec at docs/../architecture/insert-completion.md.
completionItem/resolve4.2completionItem/resolve fires lazily when the user opens the docs popup for a candidate that arrived without documentation. Round-trips the original CompletionItem JSON to the originating server (preserves the opaque data blob); response fills in documentation / detail / additionalTextEdits / command on the host's LspCompletionMeta sidecar. Cancellation token rides on selection changes so a slow server's stale resolve never overwrites the popup body. Phase 4.2.g.3.
textDocument/inlineCompletion4.5.i🚧Strong-reason defer at the wire layer: lsp-types 0.97 doesn't export InlineCompletionParams / InlineCompletionResponse (LSP 3.18 / pre-spec, the request types live behind a proposed feature flag the workspace doesn't opt into). The server-cap field inline_completion_provider is visible but no client-facing types are accessible, so a wrapper would not compile. Revisit when lsp-types promotes inlineCompletion out of proposed -- the Insert-mode autopilot trigger + ghost-text overlay land in the same slice.
MethodPhaseStatusNotes
textDocument/declaration4.2gD keystroke (Phase 4.2.c follow-up). Routes through the unified do_lsp_nav_request(LspNavKind::Declaration) -- same per-server merge + jump-or-picker dispatch as definition.
textDocument/definition4.2gd keystroke (Phase 4.2.c). Spawns the request on the LSP runtime; merged + deduped (by uri+range.start) across attached servers. Single result jumps in-place (or via :e <path> if cross-file); multiple results open a vertico PickerSource::LspLocations picker (Phase 4.2.d.picker). Pre-jump cursor pushed onto position history with PluginPush source so <C-o> walks back. Cancellation token rides on follow-up gd so a slow server can't drop a popup over a moved cursor.
textDocument/typeDefinition4.2gy keystroke. Same dispatch as definition.
textDocument/implementation4.2gI keystroke (capital I; lowercase gi reserved for vim's "go to last insert position"). Same dispatch as definition.
textDocument/references4.2gr keystroke (Phase 4.2.d). Fans out per attached server with include_declaration: true; sort+dedup by (uri, range.start); opens the merged result as a vertico picker. Empty result echoes "no references for X".
textDocument/documentHighlight4.4.ePer-tick pump (maybe_request_document_highlight) fires the request when the cursor moves to a different (line, byte); CancellationToken invalidates stale in-flight requests so a /word search through 100 matches only paints the last one. Result lands in App::lsp_document_highlights; renderer paints a soft background tint per row (READ = dim green, WRITE = dim red, TEXT = dim blue). lsp-document-highlight-mode gates both the request issuance and the paint.
textDocument/documentSymbol4.2:lsp-symbols (Phase 4.2.e). Walks both Flat and Nested response shapes; Nested DFS preserves outline order and assigns indent depth in the picker display. Multi-server merge dedups by (path, line, col, name).
textDocument/prepareCallHierarchy4.5.aServerHandle::prepare_call_hierarchy + Capabilities::supports_call_hierarchy probe (consults static + dynamic registry). Client advertises text_document.call_hierarchy.dynamic_registration = false. Used by :lsp-incoming-calls / :lsp-outgoing-calls to identify the callable at the cursor before fanning out the directional request.
callHierarchy/incomingCalls4.5.aServerHandle::call_hierarchy_incoming_calls. :lsp-incoming-calls ex-command fires prepareCallHierarchy at the cursor, takes the first item, then issues callHierarchy/incomingCalls; results render through the existing PickerSource::LspLocations + RoutingPayload::JumpToLspLocation pipeline. Single-server strategy (highest-priority server with callHierarchyProvider); merging across servers would produce duplicate rows on mixed-language buffers. Tag stack pushes the pre-jump cursor for <C-t>.
callHierarchy/outgoingCalls4.5.aServerHandle::call_hierarchy_outgoing_calls. Symmetric peer; :lsp-outgoing-calls ex-command. Same picker pipeline + tag-stack handling as incomingCalls.
textDocument/prepareTypeHierarchy4.5.bServerHandle::prepare_type_hierarchy. Capabilities::supports_type_hierarchy consults the dynamic registry only -- lsp-types 0.97 doesn't model a static type_hierarchy_provider field on ServerCapabilities, so the static path isn't reachable. Most servers register type-hierarchy dynamically anyway (rust-analyzer, pyright). Client advertises text_document.type_hierarchy.dynamic_registration = false.
typeHierarchy/supertypes4.5.bServerHandle::type_hierarchy_supertypes. :lsp-supertypes ex-command fires prepareTypeHierarchy at the cursor, takes the first item, then typeHierarchy/supertypes; results render through the existing PickerSource::LspLocations pipeline. Container hint reads "of <type-name>". Same single-server strategy + tag-stack handling as 4.5.a.
typeHierarchy/subtypes4.5.bServerHandle::type_hierarchy_subtypes. :lsp-subtypes ex-command; symmetric peer of supertypes with the same picker pipeline.
textDocument/moniker4.5.gServerHandle::moniker + Capabilities::supports_moniker probe (consults static + dynamic registry). :lsp-moniker ex-command fires the request on the first server with monikerProvider; the response folds into a comma-separated scheme:identifier (kind) summary echoed to the minibuffer. Async drain (drain_pending_moniker) wired into the runtime tick so the request resolves without blocking the UI. Niche feature aimed at indexers / cross-repo workflows; no picker UX since each invocation typically yields a single moniker.
textDocument/selectionRange4.4.eServerHandle::selection_range + supports_selection_range + lsp-selection-range-mode sub-mode + :lsp-expand-region / :lsp-shrink-region ex-commands. First invocation fires the request; the response is flattened innermost-first into App::lsp_selection_chain. Subsequent invocations step lsp_selection_chain_index outward / inward and re-seat the Visual selection. Cache invalidates when the cursor moves outside the innermost range or the buffer changes.

Edits

MethodPhaseStatusNotes
textDocument/codeAction4.3:code-actions (alias :ca). Picks first server with codeActionProvider; context carries overlapping diagnostics + InvokedTriggerKind. Visual selection / point cursor range. Vertico picker; accept routes Command payloads through executeCommand and WorkspaceEdit payloads through the rename apply path (per-file one-undo-unit).
codeAction/resolve4.3Lazy-resolve fires when the chosen action arrived without both edit and command. Resolve response feeds the same apply path; the Resolved arm of CodeActionOutcome distinguishes from fresh-fetch.
textDocument/rename4.3:rename <new-name> (alias :rn). Single highest-priority server with renameProvider. WorkspaceEdit flattens both legacy changes map and modern document_changes Edits arm; AnnotatedTextEdit unwraps to plain TextEdit. Active buffer applies as one undo unit; cross-file edits open via :e then apply (cross-file atomic rollback is a follow-up).
textDocument/prepareRename4.3Runs before rename when the server advertises prepare_provider. RangeWithPlaceholder-shape responses pre-populate the new-name when the user types :rename with no arg; Range / DefaultBehavior responses fall through. NotRenameable echoes the server's reason.
textDocument/formatting4.3:format (alias :fmt). Highest-priority server with documentFormattingProvider; returned Vec<TextEdit> applies as one undo unit. TextEdits sorted in REVERSE by start position before apply (LSP convention: non-overlapping edits relative to original document). Single-server strategy per architecture doc.
textDocument/rangeFormatting4.3:format-range. Active Visual selection (when in Visual) or whole buffer; same dispatch as formatting. = operator on motions / objects -- queued.
textDocument/onTypeFormatting4.3Insert-mode trigger-character autopilot. do_insert_text fires the request when the typed char matches the server's documentOnTypeFormattingProvider.first_trigger_character / more_trigger_character. Edits land via the format-channel apply path (one undo unit).
textDocument/linkedEditingRange4.5.f🚧Wire wrapper shipped (ServerHandle::linked_editing_range + Capabilities::supports_linked_editing_range). Strong-reason defer on the trigger UX: the feature needs a multi-cursor shadow-edit machinery (typing inside one range mirrors edits to its paired ranges in the same gesture). Lattice's edit pipeline today applies one Edit per keystroke; layering the mirror requires either a new "tracked-region" mode that intercepts insert/delete in Insert mode + replays them across the paired ranges, or a per-edit fan-out hook. Both are non-trivial design work that should land alongside structural editing (M.7.x). The wrapper ships so the future trigger plugs into a stable surface.

Decorations / inline information

MethodPhaseStatusNotes
textDocument/publishDiagnostics4.1Routing (4.1.d.i) ✅, per-URI state w/ version gating + multi-server merge (4.1.d.ii) ✅, renderer integration: gutter severity column + inline underline overlay (4.1.d.iii) ✅, :diagnostics help-style buffer w/ clickable Source links + :diag-next / :diag-prev / :cnext / :cprev cursor navigation (4.1.d.iv) ✅.
textDocument/diagnostic4.4.jPull-based diagnostics (LSP 3.17). ServerHandle::document_diagnostic + Capabilities::supports_pull_diagnostics + diagnostic_identifier. Per-tick pump (maybe_request_pull_diagnostics) fires when lsp-diagnostics-mode is on AND the active server advertises pull AND the buffer's document version changed; threads the cached previous_result_id so the server can answer Unchanged cheaply. Drain handles both Full (apply via DiagnosticEvent into DiagnosticsLayer -- same shape push uses) and Unchanged (no-op on the layer, just refresh the cached result_id). Single-flight: each request cancels its predecessor. Client advertises text_document.diagnostic = { related_document_support: true } so servers can attach related-document reports.
workspace/diagnostic4.4.j🚧Wire wrapper shipped (ServerHandle::workspace_diagnostic + Capabilities::supports_workspace_diagnostic_pull). Strong-reason defer on the host pump: the per-document textDocument/diagnostic pump already covers every open buffer's diagnostics; workspace/diagnostic is for closed-file workspace-wide pulls (file path's open status irrelevant). The surface that would consume those is the :diagnostics workspace view, which today is fed from the broadcast DiagnosticsBus and assumes open-file scope. Wiring workspace pull cleanly requires reworking :diagnostics to merge a one-off pull batch with the bus stream + handle workspace-wide cancellation -- its own slice. Niche use case (servers that ONLY emit diagnostics on request AND never for open buffers via push are rare today); revisit if a real corpus needs it.
textDocument/inlayHint4.4.gServerHandle::inlay_hint + supports_inlay_hint capability probe + lsp-inlay-hint-mode sub-mode. Per-tick pump (maybe_request_inlay_hint) fires on (a) document-version change OR (b) viewport scrolled outside the cached requested_first_line..=requested_last_line window. Each request fetches the viewport ± 100 lines overscan (clamped to the buffer's last addressable line); cache stores the requested range alongside the hints so small scrolls stay cached and only large jumps trigger a refetch. Small buffers fit entirely inside one overscanned window and only fetch on edits. Renderer's splice_virtual_text_into_spans helper inserts each hint label at its utf-16→utf-8-converted byte offset, styled as italic + dim gray. paddingLeft / paddingRight honored.
inlayHint/resolve4.4.g🚧Wire wrapper shipped: ServerHandle::inlay_hint_resolve + Capabilities::supports_inlay_hint_resolve (reads InlayHintOptions.resolve_provider from both flavours of InlayHintServerCapabilities). Strong-reason defer on the trigger: there's no inlay-hint interaction UX today -- no mouse target, no cursor-on-inlay gesture, no :lsp-inlay-hint-apply command. The two consumers (tooltip + textEdits) want different UX paths: tooltip overlaps with K (the user gets equivalent info via hover on the underlying symbol, so tooltip-only is duplicative), and text_edits ("apply this inlay as real text, e.g. inline the type") needs a dedicated apply gesture that we haven't designed. Shipping tooltip-only without textEdits is half a feature; designing the full interaction is its own task. The wrapper ships ahead so the future trigger plugs into a stable surface. Revisit when an inlay-hint interaction UX lands (likely paired with mouse / click-target work post-1.0).
textDocument/inlineValue4.5.h🚧Wire wrapper shipped (ServerHandle::inline_value + Capabilities::supports_inline_value). Strong-reason defer on the trigger: inlineValue is a debug-flow feature -- the server returns "the values to render at each line right now" given a debugger's stopped location + frame id. Without a debug-adapter (DAP) integration there's nothing to drive the trigger, and no renderer surface for the values. Both land together once DAP arrives (post-1.0). The wrapper ships so the future trigger plugs into a stable surface.
textDocument/codeLens4.5.dServerHandle::code_lens + Capabilities::supports_code_lens probe (static + dynamic). Per-tick pump on doc-version change caches Vec<CodeLens> per buffer (LspCodeLensCache { document_version, lenses, server_id }); :lsp-code-lens opens a picker over the cached lenses with one row per entry (title or (unresolved lens at line N) placeholder). Renderer overlay (above-line virtual text) queued -- today the picker is the only surface. Single-server strategy (cache records the originating server so accept can route the command back).
codeLens/resolve4.5.dServerHandle::code_lens_resolve + Capabilities::code_lens_resolve_provider. Picker accept fires the resolve lazily when the chosen lens lacks command AND the server advertises resolveProvider. Blocking via lattice_runtime::block_on -- the user accepted the picker so a few-ms wait is acceptable. The resolved command routes through workspace/executeCommand on the originating server (App::execute_lsp_command).
textDocument/documentLink4.5.cServerHandle::document_link + Capabilities::supports_document_link (static + dynamic). Per-tick pump on doc-version change caches Vec<DocumentLink> per buffer; gx (Normal-mode chord, AfterG layer) walks the cache for the first link whose range covers the cursor and follows it. file:// URIs route through do_edit; external URIs delegate to the OS handler (xdg-open / open / explorer). Renderer underline overlay queued -- the cache only drives navigation today. Client advertises tooltip_support = true for future hover-on-link UX.
documentLink/resolve4.5.cServerHandle::document_link_resolve + Capabilities::document_link_resolve_provider. gx fires the resolve lazily when the cached link at the cursor lacks target AND the server advertises resolveProvider. Blocking via lattice_runtime::block_on -- the user typed gx so a few-ms wait is acceptable; the cancellation token bounds long resolves. Skips the request (echoes "link has no target") when the server doesn't advertise resolve.
textDocument/documentColor4.5.eServerHandle::document_color + Capabilities::supports_color probe. Per-tick pump caches Vec<ColorInformation> per buffer (LspDocumentColorCache { document_version, colors, server_id }); the cache feeds :lsp-color-presentation (renderer swatch overlay queued). Single-server strategy.
textDocument/colorPresentation4.5.eServerHandle::color_presentation. :lsp-color-presentation reads the cached color literal at the cursor, fires the request blockingly, opens a vertico picker of alternatives. Accept splices the chosen text_edit (or label as a plain range-replace) via the existing apply_lsp_text_edits path. New PickerAction::AcceptColorPresentation + RoutingPayload::ColorPresentation { index }.
textDocument/foldingRange4.4.fNew FoldMethod::Lsp variant in lattice-core::folding. Per-tick pump (maybe_request_folding_range) fires when the buffer's document version changes; the drain seats LspFoldsCache and triggers recompute_folds. Identity hash combines start_line + end_line + kind so closed-state survives across re-fetches. Cascades to Syntax when no server advertises the capability or the cache is empty. lsp-folding-mode is bidirectionally coupled to the foldmethod option via its hand-written Mode::on_activate / on_deactivate hooks (mode-architecture.md §5.2 Phase 1): activating swaps foldmethod=lsp (stashing the prior value in BufferLocal<PriorFoldmethod>); deactivating restores. The mode owns the work end-to-end — any activation path (:lsp-folding-mode toggle, lsp-mode cascade, plugin API, programmatic) gets the sync for free.
textDocument/semanticTokens/full4.4.hServerHandle::semantic_tokens_full + supports_semantic_tokens + legend accessors (semantic_token_types, semantic_token_modifiers). Per-tick pump fires on document-version change; decoder turns the relative-position varint stream into absolute DecodedSemanticToken { line, start_char, length, token_type, modifiers }. Renderer's apply_semantic_token_overlay repaints the foreground (kind-driven hue) within each token's byte range while preserving existing bg / underline so visual + diagnostics layer cleanly on top. lsp-semantic-tokens-mode sub-mode gates both request issuance and paint.
textDocument/semanticTokens/full/delta4.4.iServerHandle::semantic_tokens_full_delta + Capabilities::supports_semantic_tokens_delta. Pump prefers full/delta when (a) the cache has a result_id from the previous response and (b) the server advertises delta support; falls back to plain full otherwise. LspSemanticTokensCache carries raw_data: Vec<lsp_types::SemanticToken> alongside the decoded view so the drain can splice the server's SemanticTokensEdit script ({ start, delete_count, data }) in place via apply_semantic_token_edits, then re-decode using the legend captured at request time. Stale baselines (cache's result_id no longer matches the server's previous_result_id) or out-of-bounds splices evict the cache so the next pump issues a clean full request.
textDocument/semanticTokens/range4.4.i🚧Wrapper shipped (ServerHandle::semantic_tokens_range + Capabilities::supports_semantic_tokens_range); the v1 pump uses full + full/delta and ignores range. Strong-reason defer: range mode and delta mode are mutually exclusive in the LSP spec -- range responses carry no result_id to anchor a future delta, so adopting range means giving up incremental update across edits. Full+delta already keeps the steady-state cost at "transmit only the changed tokens"; range only wins on first paint for very large buffers (50k+ lines), and even there the cost is one full decode versus none. The pump complexity to reconcile full / delta / range with mode-flip cache eviction is real, the speedup is narrow, and no benchmark exists today that demonstrates user-visible lag on the common case. Revisit when an actual large-file corpus shows >16ms first-paint with full+delta. Until then the wrapper stays callable for hand-driven experiments.

Logging & introspection (lattice-side)

These aren't LSP methods -- they're lattice's debugging surface around LSP traffic. Tracked here so the matrix is the single source of truth.

FeaturePhaseStatusNotes
LspLogger (rings + level gating + trace toggle)4.1.fcrate::logging. ~9ns trace-off, ~100ns per record.
tracing crate fan-out4.1.fEvery LspLogger::log also fires tracing::*. RUST_LOG=lattice_lsp=debug works.
LspSupervisor (per-buffer attachment + per-(workspace, server-id) actor reuse)4.1.hcrate::supervisor. open_buffer / close_buffer / record_edit / flush / flush_all / servers_for / shutdown. attach_handle for tests + custom transports. Supports multi-server-per-buffer + multi-buffer reuse of one actor.
App-side wiring: App holds LspSupervisorHandle, event-driven attach via Event::DocumentOpened, lsp_flush / lsp_close_buffer sync hooks, BufferId ↔ Uri map4.1.ilattice-ui-tui::app. App::new builds the supervisor with builtin_servers, spawns its task on the LSP runtime, and publishes Event::DocumentOpened for the initial doc. The attach driver (lattice_lsp::attach_driver) consumes the event off the LSP runtime and submits the open to the supervisor's mailbox -- UI thread never parks. do_buffer_delete fires lsp_close_buffer. (4.x audit slice 9.)
Edit dispatch: apply_edit_blocking → Event::DocumentChangedlattice_lsp::fan_in → actor RecordEdit + debounced flush4.1.i.2Per-actor edit fan-in (audit slice 1b–3) subscribes to Event::DocumentChanged on the bus; each actor pulls AppliedEdit and routes to its own DocSync mirror via mailbox. Debounced flush wakes 50ms after the last edit, sends one didChange. :e <path> publishes Event::DocumentOpened; the attach driver picks it up async (audit slice 9) -- no UI-thread parking on handshake.
*lsp* subsystem buffer4.1.g:lsp-log (no arg). Help-style buffer; one row per record from LspLogger::snapshot_global(). Format: HH:MM:SS.mmm <level> <source>: <message>.
*lsp:<server>* per-server buffer4.1.g:lsp-log <server>. Per-server records (stderr + window/logMessage + window/showMessage + lifecycle), trace records filtered out.
*lsp:<server>:trace* JSON-RPC trace buffer4.1.g:lsp-trace <server>. Filtered to LogSource::Trace; toggle inbound/outbound recording on/off; / markers in body.
:lsp-log [server] ex-command4.1.gDashed-canonical name; no collapsed alias.
:lsp-trace <server> ex-command4.1.gFlips toggle + opens trace buffer.
:lsp-status ex-command4.1.gOne row per running server with workspace root + capability summary + diagnostic-subscriber count.
:lsp-restart <server> ex-command4.4.dWired end-to-end: dispatcher posts a Restart cmd onto the supervisor mailbox, echoes queued synchronously, and reports the respawn outcome (actor count + replayed didOpen URIs) into the *lsp* log when the async work lands.
:lsp-log-level / :lsp-log-clear4.1.gRuntime level: :lsp-log-level <level> (subsystem) or :lsp-log-level <server> <level> (per-server). Clear: :lsp-log-clear [server].
lsp.toml log keys (log_level, log_capacity, trace_io)4.4.olsp.log_level + lsp.log_capacity typed options seed the logger at boot (4.4.o). Strong-reason defer on lsp.trace_io: trace is meaningful per-server (you debug one server while leaving the rest quiet), so the right shape is lsp.<server>.trace_io = true, which would introduce a per-server typed-option dimension no other lsp.* option uses. The runtime knob :lsp-trace <server> covers the practical case; a global boot-time lsp.trace_io = true would be confusing UX (every attached server suddenly verbose), and the per-server typed-option work is its own slice. Revisit if/when per-server typed-option keys are introduced for any other reason.

Notebook documents

MethodPhaseStatusNotes
notebookDocument/didOpenpostLattice's notebook story is post-1.0 (rich-buffer rendering required).
notebookDocument/didChangepost--
notebookDocument/didSavepost--
notebookDocument/didClosepost--

Tracking summary

Counts as of 2026-06-30:

StateCount
✅ Done91
🚧 In progress11
⏹️ Planned1
⛔ Deferred4

✅ breaks down as 77 LSP protocol methods + 14 lattice-side logging / introspection features. The 11 🚧 rows all ship the wire wrapper + capability probe; only the host-side trigger is deferred (file-op UX, multi-cursor shadow edits, DAP, inlay-interaction UX, or the lsp-types proposed flag — see each row's note). ⏹️ = workspace/didChangeWorkspaceFolders (multi-root, post-1.0); ⛔ = the four notebookDocument/* methods.

Phase rollup:

  • 4.1 Foundation: complete. Wire layer + actor/handshake + sync + diagnostics (broadcast → layer → renderer → buffer view + nav) + logging (rings + tracing fan-out + buffer views + commands) + supervisor + App-side wiring + edit-dispatch + open-on-:e all shipped. workspace/configuration surfaces real values from the merged user + project TOML tree via the ConfigurationBus mpsc + oneshot channel and the App::drain_inbound_configuration_requests drain; users place server-namespaced settings under [lsp.<server-id>].
  • 4.2 Navigation: complete. ✅ hover (K), definition (gd), declaration (gD), typeDefinition (gy), implementation (gI), references (gr), documentSymbol (:lsp-symbols), workspaceSymbol (:lsp-workspace-symbol), workspaceSymbol/resolve (eager-resolve at fan-out for LSP 3.17+ Nested-WorkspaceLocation symbols), Insert-mode completion (the full 4.2.g surface: shell + buffer-words + LSP source + docs popup + lazy completionItem/resolve + snippets + frequency / per-source priority / per-language overrides + tree-sitter + path source + commit chars + ghost text + cross-source dedup + typed picker routing). All multi-result lookups + :diagnostics route through the unified vertico picker (PickerSource::LspLocations + PickerAction::JumpToLspLocation); picker routing payload is now typed (RoutingPayload enum) so accept dispatch reads variants instead of parsing tab-encoded strings. Tag stack <C-t> pops gd-family drill-downs LIFO; jump list <C-o>/<C-i> walks every cursor jump chronologically. The :complete picker bridge stays as the cmdline-driven peer of the inline popup.
  • 4.3 Edits: complete. formatting + rangeFormatting; signatureHelp + Insert-mode autopilot; rename + prepareRename; willSave / didSave notifications; willSaveWaitUntil format-on-save (500ms per-server bound); codeAction + resolve + executeCommand; onTypeFormatting Insert-mode autopilot; workspace/applyEdit (server-initiated) ferries through the ApplyEditBus mpsc + per-request oneshot, drained per-frame by App::drain_inbound_apply_edits. codeAction Commands keep routing through executeCommand (fire-and-forget); applyEdit is the inbound complement servers use after executeCommand callbacks.
  • 4.4 Polish: complete. All slices shipped end-to-end where the trigger UX exists today; wire wrappers + capability probes ship for the rest with strong-reason defers on missing host plumbing. 4.4.k (workspace/didChangeConfiguration fan-out from the typed-option cascade) + 4.4.l (file-watcher pipeline: pure parse + match in lattice-lsp::file_watcher + App-side notify::RecommendedWatcher in lattice-ui-tui::app::lsp_watcher fanning out workspace/didChangeWatchedFiles per matching server) + 4.4.m (six wire wrappers for the file-lifecycle hooks + FileOpKind capability discriminator + didCreateFiles trigger on first-save; willCreate / will-Rename / did-Rename / will-Delete / did-Delete triggers strong-reason deferred on missing editor UX) + 4.4.n (client/registerCapability + client/unregisterCapability parse + index into Capabilities.dynamic, republished via ArcSwap<Capabilities>). 4.4.a (window/showMessage + window/logMessage + telemetry/event) + 4.4.b ($/setTrace + $/logTrace + window/showDocument + window/showMessageRequest full picker UX: actionless prompts auto-reply, actionful prompts open a queued one-at-a-time picker with accept/dismiss arms wired through RoutingPayload::AcceptShowMessageAction) + 4.4.c ($/progress accumulator + modeline slot + window/workDoneProgress/cancel + lsp-progress-mode sub-mode + :lsp-progress-cancel) + 4.4.d (supervisor restart_server + per-server backoff window + crash-detection auto-restart via LspActorExited typed event + :lsp-restart wired end-to-end) + 4.4.e (documentHighlight cursor-driven pump + renderer overlay + selectionRange smart-expansion via :lsp-expand-region/:lsp-shrink-region; both sub-modes gate request issuance and paint) + 4.4.f (foldingRange -> FoldMethod::Lsp feeding the existing fold infra; per-tick pump on document-version change; cascades to Syntax when cache is empty; lsp-folding-mode sub-mode gate) + 4.4.g (textDocument/inlayHint viewport-bounded pump with viewport±100 overscan + workspace/inlayHint/refresh + lsp-inlay-hint-mode + renderer's reusable splice_virtual_text_into_spans virtual-text helper; inlayHint/resolve wrapper + capability probe shipped, trigger UX strong-reason deferred) + 4.4.h (textDocument/semanticTokens/full with relative-position decoder + per-kind fg overlay preserving prior bg/underline; lsp-semantic-tokens-mode sub-mode) + 4.4.i (textDocument/semanticTokens/full/delta -- pump prefers delta when the cache has a result_id and the server advertises support; drain splices SemanticTokensEdit operations into the cached raw vec, re-decodes against the legend captured at request time, and falls back to a fresh full request on stale baseline / out-of-bounds splice; workspace/semanticTokens/refresh invalidates the cache for attached buffers; semanticTokens/range wrapper shipped, viewport pump strong-reason deferred) + 4.4.j (textDocument/diagnostic pull pump with previous_result_id threading + Unchanged/Full drain into the shared DiagnosticsLayer + workspace/diagnostic/refresh cache eviction; workspace/diagnostic wrapper shipped, host pump strong-reason deferred) + 4.4.o (lsp.log_level + lsp.log_capacity typed options seed the logger at boot; lsp.trace_io strong-reason deferred -- per-server typed-option shape unwarranted, runtime :lsp-trace covers the case) shipped.
  • 4.5 Expansion: complete where the host UX exists; the rest is wire-only with documented strong-reason defers. 4.5.a (callHierarchy) + 4.5.b (typeHierarchy) + 4.5.c (documentLink + gx) + 4.5.d (codeLens + resolve + refresh) + 4.5.e (documentColor + colorPresentation) + 4.5.g (moniker) + 4.5.h workspace/inlineValue/refresh shipped end-to-end. 4.5.f (linkedEditingRange), 4.5.h (inlineValue trigger), and 4.5.i (inlineCompletion) ship wire wrappers + capability probes where lsp-types exposes them; deferred on missing host plumbing (multi-cursor shadow-edit machinery, DAP integration, lsp-types proposed feature flag respectively). 4.5.a (textDocument/prepareCallHierarchy + callHierarchy/incomingCalls + callHierarchy/outgoingCalls; :lsp-incoming-calls / :lsp-outgoing-calls ex-commands) + 4.5.b (textDocument/prepareTypeHierarchy + typeHierarchy/supertypes + typeHierarchy/subtypes; :lsp-supertypes / :lsp-subtypes ex-commands) + 4.5.c (textDocument/documentLink + documentLink/resolve; per-tick cache + gx Normal-mode chord follows the link covering the cursor; file:// -> :e, external -> OS handler) + 4.5.g (textDocument/moniker; :lsp-moniker ex-command).
  • post-1.0: notebooks + multi-root workspaces.

When you finish a feature, flip its Status column and update the phase rollup. The matrix is the single source of truth: every LSP feature in lattice has a row here, and every row reflects reality.

4.4 slicing plan

Phase 4.4's items split into six affinity groups. Recommended order is depth-first by group; rationale beside each. Slice ids (4.4.a, 4.4.b, ...) follow the same scheme as 4.2.g.

Group 1 — server → editor messaging

window/showMessage, window/showMessageRequest, window/showDocument, window/logMessage, telemetry/event, $/setTrace (C → S), $/logTrace (S → C). All map onto primitives that already exist (minibuffer notifications, modal pickers, buffer-open, the *lsp:<server>:trace* buffer). Several are partially logged today; flipping them to first-class UI is the highest UX-per-line ratio of the phase.

  • 4.4.a -- window/showMessage + window/logMessage + telemetry/event. The three currently-logged items get a proper UI surface (minibuffer for show, opt-in plugin subscription for telemetry).
  • 4.4.b -- ✅ shipped. $/setTrace (ServerHandle::set_trace driven by :lsp-trace) + $/logTrace (routes into the trace ring; verbose tail preserved as an indented second record) + window/showDocument (full bus -> drain -> reply pipeline; file:// opens via :e; external delegates to OS handler) + window/showMessageRequest (actionless prompts auto-reply null and surface on the minibuffer; actionful prompts open a one-at-a-time picker -- PickerSource::LspShowMessageRequest, RoutingPayload::AcceptShowMessageAction { request_id, action_index }; accept replies with the chosen MessageActionItem, dismiss replies null. Multiple in- flight requests queue in lsp_show_message_request_queue and open in arrival order). None of the slice's wire surfaces require renderer changes.

Group 2 — server → editor live state

$/progress → modeline progress slot, window/workDoneProgress/cancel, plus the supervisor work needed for :lsp-restart to actually shut down + respawn an actor and replay didOpen to every attached buffer. Stays close to the per-actor DocSync work that's fresh in mind.

  • 4.4.c -- ✅ shipped. $/progress accumulator (App-side HashMap<(server_id, token), LspProgressUpdate> fed by LspProgressUpdate typed events the actor emits) + modeline slot (the [lsp:rust] segment grows a [title: message NN%] suffix when active progress matches an attached server) + L ServerHandle::cancel_progress + :lsp-progress-cancel [server] ex-command. lsp-progress-mode minor gates the modeline render per buffer.
  • 4.4.d -- ✅ shipped. Supervisor restart_server walks every (workspace, server_id) actor, shuts the old handle, drops the fan-in, re-spawns through actor::spawn, restarts the diagnostics pump, and replays didOpen (empty text -- next edit refills the actor mirror) for every URI bound to the recovered key. Per-server-id rolling-window backoff (MAX_RESTARTS=3 inside a 60s window, `RESTART_BACKOFF_BASE
    • 2^ndelay capped at 30s) gates both:lsp-restartand the auto path. Crash-detection auto-restart subscribes the supervisor task toLspActorExited(emitted by the actor on exit;Clean/Unexpectedreason discriminator) and reuses the same gate + spawn path.RestartReport` returns the re-spawned keys + replayed URIs for the user-facing echo.

Group 3 — new renderer overlays

textDocument/documentHighlight, textDocument/selectionRange, textDocument/inlayHint + inlayHint/resolve + workspace refresh, textDocument/foldingRange. Each adds a renderer layer; can land in any order. Suggested grouping is by infra reuse:

  • 4.4.e -- ✅ shipped. documentHighlight: per-tick pump (maybe_request_document_highlight) compares the live cursor to the last-issue cursor; on change, cancels any in-flight request and fires a fresh one. The drain coalesces multiple responses to the latest so a /word search-storm only paints the final result. Renderer overlay paints a soft background tint per DocumentHighlightKind (READ = dim green, WRITE = dim red, TEXT/None = dim blue); composes cleanly with diagnostics underline + visual selection + hlsearch. lsp-document-highlight-mode gates both the request issuance and the renderer paint (mode-off clears stale state so the overlay disappears immediately). selectionRange: :lsp-expand-region / :lsp-shrink-region ex-commands. First expand fires the request; the linked-list response is flattened innermost-first into App::lsp_selection_chain. Subsequent invocations step lsp_selection_chain_index outward (expand) / inward (shrink) and reseat the Visual selection. Cache invalidates when the cursor moves outside the innermost range or the buffer changes. Shrink at index 0 exits Visual; expand past outermost echoes "outermost". lsp-selection-range-mode gates the request issuance.
  • 4.4.f -- ✅ shipped. FoldMethod::Lsp added to lattice-core::folding (label lsp, appended to :set foldmethod=<Tab> enumeration). ServerHandle::folding_range
    • supports_folding_range capability probe. Per-tick pump (maybe_request_folding_range) fires on document-version change; drain seats LspFoldsCache and triggers recompute_folds. Identity hash combines start_line + end_line + kind discriminant so closed-state carries across re-fetches that return the same logical fold. Cascade to Syntax (which itself cascades to markdown / indent) keeps the fold list non-empty while the server is slow or doesn't advertise the capability. lsp-folding-mode is coupled to the foldmethod option: activating the mode (direct :lsp-folding-mode toggle or the lsp-mode cascade) stashes the buffer's prior foldmethod and swaps it to lsp; deactivating restores the stash. So :lsp-folding-mode is the one-touch on/off for the LSP fold provider -- the mode and the option stay coherent. Cascade list in (de)activate_lsp_sub_modes_for updated to include the modes added in 4.4.c (lsp-progress-mode), 4.4.e (lsp-document-highlight-mode, lsp-selection-range-mode), and 4.4.f (lsp-folding-mode) -- the umbrella now cascades all 13 sub-modes.
  • 4.4.g -- ✅ shipped (inlayHint/resolve deferred). textDocument/inlayHint + workspace/inlayHint/refresh
    • new lsp-inlay-hint-mode sub-mode cascaded by LspMode::implies(). Renderer infrastructure: a generic splice_virtual_text_into_spans(spans, byte_offset, text, style) helper splits the span at the boundary (or inserts cleanly between spans). Used by the inlay-hint pass; reusable by future virtual-text overlays (inline- value, inline-completion). Cache keyed by (BufferId, doc_version) with refresh-event-driven invalidation. The lazy inlayHint/resolve path is deferred until the hint-tooltip UX lands (servers must tolerate clients that don't resolve, per spec).

Group 4 — semantic tokens

textDocument/semanticTokens/full + /delta + /range, workspace/semanticTokens/refresh. Most complex single feature in 4.4: relative-position varint encoding, modifier bitmask, layered highlight resolution against tree-sitter. Stays standalone so the tree-sitter highlight fast paths don't churn under unrelated changes.

  • 4.4.h -- ✅ shipped. textDocument/semanticTokens/full end-to-end: ServerHandle::semantic_tokens_full request wrapper + capability probe + legend accessors (the per-server token-type / token-modifier names captured at handshake). Per-tick pump on doc-version change; decoder walks the relative-position stream into absolute DecodedSemanticToken { line, start_char, length, token_type, modifiers }. Renderer's apply_semantic_token_overlay repaints the foreground within each token's byte range while preserving the prior span's bg / underline / reverse so the visual selection, hlsearch, and diagnostic-underline overlays layer cleanly on top of LSP-colored text. Kind → hue mapping covers the LSP-standard set (keyword, type, function, string, number, comment, operator, variable, namespace); modifiers fold into the style (deprecated → strike-through; readonly/static → italic; defaultLibrary → dim). lsp-semantic-tokens-mode sub-mode gates request issuance and paint.
  • 4.4.i -- ✅ shipped (range pump strong-reason deferred -- see the matrix row above). semanticTokens/full/delta: pump prefers delta when the cache has a result_id and the server advertises delta = true in its options; falls back to full otherwise. LspSemanticTokensCache grew a raw_data: Vec<lsp_types::SemanticToken> field alongside the decoded view so the drain can splice SemanticTokensEdit { start, delete_count, data } operations in place (via the apply_semantic_token_edits helper) before re-decoding through the legend captured at request time. Stale baselines (the cache's result_id no longer matches the delta's previous_result_id) and out-of-bounds splices evict the cache and let the next pump issue a clean semanticTokens/full request. workspace/semanticTokens/refresh lands the same shape as workspace/inlayHint/refresh: actor replies null inline + publishes LspSemanticTokensRefresh; the App's drain_semantic_tokens_refresh drops the cache (including the cached result_id) for every buffer attached to the requesting server. Workspace-side advertisement (semantic_tokens.refresh_support = true) is in client_capabilities so servers know we honour the request. semanticTokens/range ships only as a wrapper + capability probe; the v1 pump uses full + delta and ignores range. Viewport- aware fetching is a follow-up that swaps the pump strategy without re-touching the wire path.

Group 5 — pull-based + workspace integration

Pull diagnostics, configuration push, file watchers. The largest single dependency surface in 4.4 (the notify crate) lands here.

  • 4.4.j -- ✅ shipped (workspace pull strong-reason deferred -- see the matrix row). textDocument/diagnostic per-tick pump fires when lsp-diagnostics-mode is on AND the active server advertises pull AND doc-version changed; threads previous_result_id so Unchanged responses are cheap. LspPullDiagnosticsCache { document_version, result_id } keys the staleness bookkeeping per buffer; Full reports flow into DiagnosticsLayer via the same DiagnosticEvent shape push uses (so the layer / :diagnostics view / modeline summary don't need to know whether the diagnostics arrived push or pull). workspace/diagnostic/refresh evicts the cache for every attached buffer so the next tick re-pulls with no previous_result_id and forces a Full. Client advertises text_document.diagnostic = { related_document_support = true } + workspace.diagnostic = { refresh_support = true }. Wire wrapper workspace_diagnostic ships callable; the host pump is deferred because the :diagnostics view fed off the bus assumes open-file scope, and the rework to merge a one- off workspace pull is its own slice. Niche today (servers that only emit on workspace pull and never push are rare); revisit if a real corpus needs it.
  • 4.4.k -- ✅ shipped. workspace/didChangeConfiguration fan-out from the §5.12 OptionChanged cascade. The cascade (apply_option_cascade) calls a pure lsp_server_scope(name) -> Option<&str> helper to discriminate server-scoped lsp.<server>.<key> writes from host-side lsp.<host-knob> writes; only server-scoped writes fan out. fan_out_did_change_configuration(server_id) serialises the lsp.<server> JSON subtree from the merged TOML and notifies every matching running actor (cross-workspace too -- config is global). Pairs cleanly with the existing inbound workspace/configuration path; servers that pull see the same subtree the push delivered.
  • 4.4.l -- ✅ shipped. File watchers + workspace/didChangeWatchedFiles. Pure parse + match in lattice-lsp::file_watcher (compiles each FileSystemWatcher glob into a shared globset::GlobSet per server, honours WatchKind defaults + RelativePattern base anchoring); App-side service in lattice-ui-tui::app::lsp_watcher owns a notify::RecommendedWatcher lazily spawned when at least one server has registered watchers. Per-tick: refresh the watcher's subscribed paths to the union of running-actor workspace roots (per-server fingerprint short-circuit skips recompile) + drain queued fs events, classify them into LSP FileChangeType, match per server, and batch into one workspace/didChangeWatchedFiles notification per matching server. Debounce + target//node_modules/ ignore globs are follow-up work.
  • 4.4.m -- ✅ wire layer + didCreateFiles trigger shipped; remaining triggers strong-reason deferred. All six wire wrappers (will_create_files, did_create_files, will_rename_files, did_rename_files, will_delete_files, did_delete_files) on ServerHandle + matching supports_*_files probes via new FileOpKind discriminator
    • Capabilities::file_operations_options. Client advertises interest in all six hooks under workspace.file_operations. Trigger: App::save_blocking detects first-save (path didn't exist pre-save) and fans out workspace/didCreateFiles to every attached server advertising the capability. The other five triggers are deferred on missing editor UX (in-place file rename, editor-driven delete) or missing pre-save blocking-edits infrastructure (willCreate's atomic WorkspaceEdit application). The wrappers ship ahead so future trigger work plugs into a stable surface.

Group 6 — capability + config polish

  • 4.4.n -- ✅ shipped. Dynamic registration tracking. New lattice-lsp::dynamic_registration module owns the DynamicRegistration { id, method, register_options } record
    • the DynamicRegistry { by_id, by_method } two-way index (HashMap<id, registration> + HashMap<method, Vec>) so register is O(1), unregister-by-id is O(1), and the "is method X registered?" probe is O(1). Capabilities grew a pub dynamic: DynamicRegistry field; with_dynamic_mut clones the snapshot and mutates the registry through a closure so the actor can publish without touching the rest of the struct. HandleInner.capabilities flipped from Arc<Capabilities> to Arc<ArcSwap<Capabilities>> so reads are lock-free and writes are a single atomic swap; ServerHandle::capabilities() loads a fresh Arc per call. The actor parses RegistrationParams / UnregistrationParams inline (before falling through to handle_server_request), mutates the local snapshot, and stores into the cell -- malformed params log + still reply null so the wire stays unblocked. Every supports_* probe that gates a likely-dynamic feature (hover, definition, references, document_symbol, workspace_symbol, completion, formatting / range / signatureHelp, rename, code_action, document_highlight, selection_range, folding_range, inlay_hint) ORs in dynamic.has(method). Client advertises workspace.didChangeWatchedFiles.dynamic_registration = true (+ relative_pattern_support) so servers register file- watcher patterns lazily after handshake -- 4.4.l consumes those DidChangeWatchedFilesRegistrationOptions blobs from the registry.
    • 4.4.o -- lsp.toml log keys (log_level, log_capacity, trace_io) wired through the §5.12 typed-options layer at startup; today only the runtime :lsp-log-level / :lsp-trace paths exist.
    • Trade-offs flagged

      • Semantic tokens (Group 4) ahead of overlays (Group 3) is defensible if you'd rather get the highest-impact polish in first, but it's also the highest churn risk -- highlight layering interacts with tree-sitter and the renderer's fast paths. Default order keeps it after the overlays so the renderer changes that group introduces are stable before semantic tokens layer on top.
      • File watchers (Group 5) earlier as standalone infra would unlock non-LSP features (markdown live-preview, etc.) sooner, but they're standalone enough to stay where they are without blocking anything.
      • :lsp-restart (4.4.d) could fold into 4.4.c as a single "supervisor lifecycle" slice, but the two touch different surfaces (modeline rendering vs. actor lifecycle) and read cleaner as separate commits.