Skip to main content

lattice_host/
editor.rs

1// The renderer-agnostic editor state.
2//
3// Phase 5.B.3 introduces [`Editor`] as the destination for
4// the per-cluster field migration from
5// `lattice-ui-tui::App`. See
6// [`docs/dev/architecture/phase-5b-app-design.md`] for the
7// Option-D → Option-E pivot that this struct realises:
8//
9// - The host owns the editor's state and logic in `Editor`.
10// - Each renderer crate composes `Editor` into its own
11//   concrete `App` wrapper alongside its renderer-specific
12//   caches (`theme`, `pane_render_registry`, ...).
13//
14// Subsequent slices (5.B.4 onwards) relocate field clusters
15// one at a time from `App` into `Editor`, moving the methods
16// that touch only those fields into `impl Editor` here. Each
17// per-cluster commit ships green: methods that still live in
18// `impl App` access migrated fields via `self.editor.foo`;
19// methods that have moved access them via `self.foo` (now an
20// inherent method on `Editor`).
21//
22// The empty-now/grows-later shape is intentional: it lets
23// the wrapper field `editor: Editor` get added to `App`
24// before any field actually moves, giving every subsequent
25// migration a target that already exists in the type
26// system.
27
28use std::collections::HashMap;
29use std::path::PathBuf;
30
31use lattice_grammar::{CommandInvocation, Register};
32use lattice_protocol::position::{Position, Range as ProtoRange};
33
34use std::sync::Arc;
35
36use lattice_config::{ConfigRegistry, OptionOverrideSet, ResolvedOptions};
37use lattice_core::ui::popup::PopupPlacement;
38use lattice_grammar::ModalState;
39use lattice_grammar::builtins::Builtins;
40use lattice_help::topics::HelpTopicRegistryHandle;
41use lattice_lsp::cache::{
42    CodeActionOutcome, CodeActionRow, CompletionItemRow, CompletionOutcome,
43    CompletionResolveOutcome, DocumentHighlightCache, FormatOutcome, HoverOutcome,
44    LspCodeLensCache, LspDocumentColorCache, LspDocumentLinksCache, LspFoldsCache,
45    LspInlayHintCache, LspNavKind, LspPullDiagnosticsCache, LspSelectionChain,
46    LspSemanticTokensCache, ReferencesOutcome, RenameOutcome, SelectionRangeOutcome,
47    SignatureHelpOutcome, SymbolsOutcome,
48};
49// Phase 5.8.AF.5 / Slice 3b.0–3b.5: `CodeLensOutcome`,
50// `DocumentColorOutcome`, `DocumentHighlightOutcome`,
51// `DocumentLinksOutcome`, `FoldingRangeOutcome`,
52// `InlayHintOutcome`, `PullDiagnosticsOutcome`,
53// `SemanticTokensOutcome` no longer imported -- their
54// `pending_*_rx` fields retired (spawned tasks write directly
55// via `PerBufferCache::insert_for` / `ArcSwapOption::store`).
56use lattice_lsp::{DiagnosticsLayer, LspLogger, LspSupervisorHandle};
57use lattice_mode::{
58    ActiveModes, BufferLocals, GuardStoreHandle, ServiceRegistry, TickCallbackRegistration,
59};
60use lattice_picker::{Picker, PickerMruIndex};
61use lattice_protocol::CancellationToken;
62use lattice_protocol::Event;
63use lattice_protocol::edit::EditDelta;
64use lattice_runtime::{EventBus, MessagePushed, MessagesRing, SnapshotCache};
65use lattice_syntax::{LangRegistry, SyntaxHandle};
66
67use crate::action::{Action, EchoMessage};
68use crate::actions::ActionIds;
69use crate::buffer_registry::BufferRegistry;
70use crate::buffers::BufferId;
71use crate::chord::KeyChord;
72use crate::dispatch::RendererSignal;
73use crate::keymap_registry::{KeymapHandle, LayerId};
74use crate::pane::PaneTree;
75use crate::state::{
76    CompletionState, LastFind, LastSearch, LastVisual, LivePickerQueryState, MacroRecording,
77    OptionCache, PendingBlockInsert, PendingPickerAccept, PendingPickerInit, PositionEntry,
78    PrevPaneState, ReplaceEntry, SearchLine, SubstitutePreview, TagStackEntry, UnnamedRegister,
79};
80use crate::versioned::Versioned;
81use lattice_core::BufferKind;
82use lattice_protocol::position::Position as ProtoPosition;
83
84/// Cross-thread wake signal for the overlay worker.
85///
86/// Wraps `Arc<tokio::sync::Notify>` so `Editor` can keep its
87/// `#[derive(Default)]` (Notify itself doesn't impl Default).
88/// Cloning the wrapper clones the inner Arc — same notify
89/// channel. `notify_one()` is fired at the tail of
90/// `publish_render_state` so the worker re-evaluates inputs after
91/// every state change without polling.
92///
93/// Phase 5.8.AF.5 / Slice X2; renamed from `HighlightWake` in
94/// display-line B4.2 (the worker no longer makes highlights).
95#[derive(Clone)]
96pub struct OverlayWake(pub Arc<tokio::sync::Notify>);
97
98/// 2026-05-26: invocation-runner function pointer. The host
99/// registers one per [`lattice_mode::Mode`] whose
100/// EP.6 (2026-08-11): where a references result should land.
101///
102/// One query, three surfaces. Kept as an enum rather than a bool
103/// because a third case arrived within a day of the second — and a
104/// bool that grew a "…or the error list" meaning would have been the
105/// silent-wrong-terminus bug waiting to happen.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum ReferencesTerminus {
108    /// `gr` — the locations picker. The historical behaviour.
109    #[default]
110    Picker,
111    /// `:lsp-references` — the editable multibuffer.
112    View,
113    /// `:lsp-references-to-error-list` — the core error list.
114    ErrorList,
115}
116
117/// [`lattice_mode::Mode::invocation_runner`] returns `Some(id)`.
118/// Returns `true` when the runner claimed the invocation,
119/// `false` when [`Editor::run_invocation`] should fall through
120/// to the grammar Action gate / `run_document_invocation` for
121/// central dispatch.
122pub type InvocationRunnerFn = fn(&mut Editor, lattice_grammar::CommandInvocation) -> bool;
123
124impl Default for OverlayWake {
125    fn default() -> Self {
126        Self(Arc::new(tokio::sync::Notify::new()))
127    }
128}
129
130impl std::fmt::Debug for OverlayWake {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("OverlayWake").finish_non_exhaustive()
133    }
134}
135
136/// S2.1 (2026-05-26): wake signal for the cell-builder worker
137/// (S2.2+). Same shape as [`OverlayWake`]: a `Notify` cloned
138/// into [`Editor::cells_wake`] and a sibling clone held by the
139/// worker task. `publish_render_state` fires `notify_one()`
140/// after every dispatch tick so the worker re-evaluates inputs
141/// from the latest published
142/// [`crate::render_state::CellsRenderState`].
143#[derive(Clone)]
144pub struct CellsWake(pub Arc<tokio::sync::Notify>);
145
146/// D.0a.1 (2026-05-29): wake signal for the
147/// `virtual_rows_worker`. Sibling of `CellsWake`. Fired by
148/// `publish_render_state` and by provider-state changes (e.g.,
149/// `DiffSubsystem` after publishing new hunks). The worker
150/// awaits via `notified()` and rebuilds the `VirtualRowMatrix`
151/// off the UI thread.
152#[derive(Clone)]
153pub struct VirtualRowsWake(pub Arc<tokio::sync::Notify>);
154
155impl Default for VirtualRowsWake {
156    fn default() -> Self {
157        Self(Arc::new(tokio::sync::Notify::new()))
158    }
159}
160
161impl std::fmt::Debug for VirtualRowsWake {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        f.debug_struct("VirtualRowsWake").finish_non_exhaustive()
164    }
165}
166
167impl Default for CellsWake {
168    fn default() -> Self {
169        Self(Arc::new(tokio::sync::Notify::new()))
170    }
171}
172
173impl std::fmt::Debug for CellsWake {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        f.debug_struct("CellsWake").finish_non_exhaustive()
176    }
177}
178
179/// A minor mode whose active/inactive state is driven by a
180/// predicate reading a shared, mode-owned session service —
181/// reconciled on the active buffer each `sync_keymap_overlays`
182/// cycle. Modes contribute these at boot so the generic
183/// overlay-sync carries no subsystem-specific knowledge:
184/// `active-snippet-mode` keys off the shared `SnippetSession`
185/// (`lattice_snippet::snippet_active_predicate`). See
186/// `feedback_mode_owns_its_surface`.
187#[derive(Clone)]
188pub struct SessionBackedMinor {
189    /// `true` ⇒ the mode should be active on the **given buffer**.
190    /// SN.3e: the predicate is buffer-scoped so a session live in one
191    /// buffer never activates the mode in another; `sync_keymap_overlays`
192    /// passes the buffer it is reconciling.
193    pub active: std::sync::Arc<dyn Fn(lattice_core::BufferId) -> bool + Send + Sync>,
194    /// The minor mode toggled by `active`.
195    pub mode_id: lattice_mode::ModeId,
196}
197
198impl std::fmt::Debug for SessionBackedMinor {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        // The predicate closure isn't Debug and is now buffer-scoped
201        // (no buffer to evaluate against here); report the mode it
202        // drives instead.
203        f.debug_struct("SessionBackedMinor")
204            .field("mode_id", &self.mode_id)
205            .finish_non_exhaustive()
206    }
207}
208
209/// I4 (openDiff) D-fix.1: the pane/buffer bookkeeping needed to tear a
210/// programmatic side-by-side diff down cleanly — close the two transient diff
211/// panes and return focus to the pane the `openDiff` was launched from (the
212/// `:claude` terminal). Recorded by
213/// [`Editor::open_programmatic_diff`](crate::dispatch), consumed by
214/// `Editor::finish_programmatic_diff_panes` on `:diff-accept` / `:diff-reject`.
215/// Keyed in [`Editor::programmatic_diff_panes`] by the session's primary
216/// (proposed / right) `BufferId`.
217#[derive(Debug, Clone)]
218pub struct ProgrammaticDiffPanes {
219    /// The pane that was active when `openDiff` fired (the `:claude` terminal
220    /// pane). Focus returns here on resolve — Option A: claude stays put while
221    /// the diff opens in transient panes to its right.
222    pub origin_pane: lattice_core::ui::pane::PaneId,
223    /// The transient baseline + proposed panes opened for the diff; both closed
224    /// on resolve. The `origin_pane` is never in this list.
225    pub diff_panes: Vec<lattice_core::ui::pane::PaneId>,
226    /// The throwaway in-memory baseline + proposed buffers; removed from the
227    /// registry on resolve so they don't linger in `:ls`.
228    pub diff_buffers: Vec<lattice_core::BufferId>,
229    /// D-fix.6: the IDE-peer connection that opened this diff (its
230    /// `ProgrammaticDiffRequest.origin_session`). A session-scoped close
231    /// (`close_tab` / `closeAllDiffTabs` from that same connection) tears
232    /// down only the diffs whose `origin_session` matches — so one agent
233    /// session can never close another's diffs. `0` = no originating session
234    /// (a non-IDE producer), matched by no connection's close.
235    pub origin_session: u64,
236}
237
238/// MG.17b: a transient menu parked while its argument is being typed.
239///
240/// Carries everything needed to put the menu back exactly as it was —
241/// including the parent stack, so an argument inside a submenu returns
242/// you to that submenu rather than to the root.
243#[derive(Debug, Clone)]
244pub struct PendingTransientArgument {
245    /// The menu to re-seat.
246    pub spec: std::sync::Arc<lattice_picker::TransientSpec>,
247    /// Its state, with the typed value written in on submit.
248    pub state: lattice_picker::TransientState,
249    /// Parent menus, so `BS` still walks back out after the round-trip.
250    pub stack: Vec<(
251        std::sync::Arc<lattice_picker::TransientSpec>,
252        lattice_picker::TransientState,
253        usize,
254    )>,
255    /// Which argument's value is being collected.
256    pub name: String,
257}
258
259/// IM.7a — the drawing peer's cell geometry, in pixels.
260///
261/// Two numbers rather than a pane-width channel: combined with a pane's
262/// column count (already published per pane) they give that pane's pixel
263/// width, which is what `lattice_media::block_geometry` needs alongside the
264/// line height.
265#[derive(Debug, Clone, Copy, PartialEq)]
266pub struct CellMetrics {
267    /// One display row's height in pixels — GPUI's `font_size * 1.3`.
268    pub row_px: f32,
269    /// One column's advance in pixels.
270    pub col_px: f32,
271}
272
273/// Renderer-agnostic editor state.
274///
275/// The renderer-agnostic half of every editor App. Each
276/// renderer's `App` struct composes one of these alongside
277/// its renderer-specific caches. Host-level code (mode
278/// lifecycle, dispatch, picker sources, LSP supervisor, ...)
279/// takes `&mut Editor` directly; renderer-side code takes
280/// `&mut App` and reaches the editor via `app.editor`.
281///
282/// **Field set grows per-cluster.** Each 5.B.x slice
283/// migrates a logical cluster of fields here from
284/// `lattice-ui-tui::App`. As clusters land, this struct
285/// accumulates state; in parallel, `App`'s direct field set
286/// shrinks. When the migration completes, every renderer-
287/// agnostic field on App lives here, every renderer-agnostic
288/// method on App lives in this crate's `impl Editor` blocks,
289/// and App becomes a thin wrapper holding `editor: Editor`
290/// plus renderer-specific caches only.
291///
292/// **Clusters landed so far:**
293/// - 5.B.4 -- macro recording state (`macros`,
294///   `macro_recording`, `last_played_macro`).
295/// - 5.B.5 -- marks + registers (`marks`, `registers`,
296///   `pending_register`, `unnamed_register`).
297/// - 5.B.6 -- position history + tag stack
298///   (`position_history`, `position_history_cursor`,
299///   `recent_files`, `tag_stack`, `pending_tag_origin`).
300/// - 5.B.7 -- search state (`search_line`, `last_search`,
301///   `current_match`, `all_matches`, `substitute_preview`).
302/// - 5.B.8 -- vim repeat + visual state (`pending_count`,
303///   `op_count`, `visual_anchor`, `last_change`,
304///   `last_visual`, `last_find`).
305/// - 5.B.9 -- replace + insert state (`replace_history`,
306///   `last_insert`, `recording_insert`,
307///   `pending_block_insert`).
308/// - 5.B.10 -- popup (subset) (`popup_buffer`,
309///   `prev_pane_for_popup`, `popup_placement`). Skipped:
310///   `popup_back_stack` -- holds `PopupSnapshot` which still
311///   lives in `lattice-ui-tui::app::popup`; follow-up slice
312///   moves the snapshot type to host before migrating the
313///   field.
314/// - 5.B.11 -- cmdline + echo (`command_line`,
315///   `last_message`, `messages`,
316///   `pending_message_event_rx`, `pending_redraw`,
317///   `command_history`, `command_history_cursor`,
318///   `command_history_pending`, `auto_submit_after_chord`).
319/// - 5.B.12 -- syntax (`lang_registry`, `syntax`,
320///   `last_parsed_text_version`, `pending_syntax_edits`,
321///   `last_synced_syntax_version`, `visible_highlights`,
322///   `pane_highlights`). Skipped:
323///   `visible_highlights_key` -- its type
324///   `VisibleHighlightsKey` lives in
325///   `lattice-ui-tui::app::highlights` with `pub(super)`
326///   visibility; follow-up slice promotes it to host first.
327/// - 5.B.13 -- picker (`picker`, `picker_registry`,
328///   `picker_mru`, `picker_mru_path`,
329///   `pending_picker_init`, `live_picker_query`,
330///   `previewing`). Picker support types (`PendingPickerInit`,
331///   `LivePickerQueryState`, `InFlightLiveQuery`,
332///   `LIVE_PICKER_DEBOUNCE`) also moved from
333///   `lattice-ui-tui::app` to `lattice_host::state`.
334/// - 5.B.14 -- config + modes (`config`, `option_cache`,
335///   `mode_registry`, `services`, `mode_guards`,
336///   `active_modes`, `buffer_locals`, `resolved_options`,
337///   `buffer_local_overrides`, `option_change_rx`,
338///   `help_topics`, `host_theme`).
339/// - 5.B.15 -- modal + dispatch (`modal`, `partial_chord`,
340///   `registry`, `event_bus`, `builtins`, `action_ids`,
341///   `keymap`, `completion_popup_layer`).
342/// - 5.B.16 -- active-pane state (subset) (`cursor`,
343///   `scroll`, `should_quit`, `viewport_height`,
344///   `terminal_width`, `active_buffer`,
345///   `document_buffer_id`, `buffers`). Skipped:
346///   `document`, `snapshot_cache`, `pane_tree` --
347///   their types have no natural `Default` (actor handles +
348///   tree-with-root invariants). Follow-up slice removes
349///   `#[derive(Default)]` from `Editor` in favour of an
350///   `Editor::new(...)` constructor so these can migrate.
351/// - 5.B.17 -- LSP per-buffer caches (`lsp_progress`,
352///   `lsp_selection_chain`, `lsp_selection_chain_index`,
353///   `lsp_document_highlights`,
354///   `last_document_highlight_issue_cursor`,
355///   `lsp_folds_cache`, `lsp_inlay_hints_cache`,
356///   `lsp_document_links_cache`, `lsp_code_lens_cache`,
357///   `lsp_document_color_cache`,
358///   `lsp_semantic_tokens_cache`,
359///   `lsp_pull_diagnostics_cache`).
360/// - 5.B.18 -- all remaining LSP fields: subsystem handles
361///   (`lsp`, `lsp_diagnostics`, `lsp_logger`, plus
362///   `lsp_log_event_rx`, `lsp_progress_event_rx`,
363///   `lsp_config_tree`, `buffer_uris`), server-initiated
364///   channels (`pending_apply_edit_rx`,
365///   `pending_show_message_request_rx`,
366///   `lsp_pending_show_message_requests`,
367///   `lsp_show_message_request_queue`,
368///   `lsp_next_show_message_request_id`), and all per-
369///   feature request channels (the `pending_*_rx` /
370///   `pending_*_token` pairs for hover, definition,
371///   references, symbols, format, signature-help,
372///   completion, moniker, rename, code-action,
373///   selection-range, document-highlight, folding-range,
374///   document-links, code-lens, document-color, inlay-hint,
375///   semantic-tokens, pull-diagnostics, plus the refresh
376///   channels and the lifecycle / detach channels).
377///   `LspSupervisorHandle` and `DiagnosticsLayer` gained
378///   placeholder `Default` impls (dropped-receiver
379///   channels; production overwrites in `boot.rs`).
380///   `lsp_file_watcher` stays on App for now: its inner
381///   type `LspFileWatcher` lives in `lattice-ui-tui::app::
382///   lsp_watcher` -- migrates with a follow-up that moves
383///   the watcher into a host module.
384/// - 5.B.19 -- call-site migration for all LSP per-feature
385///   request channel fields scaffolded in 5.B.18: updated
386///   all `self.pending_*` / `app.pending_*` accesses in
387///   `app/lsp.rs`, `app/boot.rs`, `app/picker.rs`,
388///   `app/mode.rs`, `app/completion.rs` to
389///   `self.editor.pending_*`; removed the now-redundant
390///   duplicate declarations from `App`. Completion cluster
391///   (`completion_registry`, `completion_state`,
392///   `insert_completion`, etc.) stays on App -- next slice.
393/// - 5.B.20 -- completion cluster tail + popup back-stack +
394///   pending config bucket. `insert_completion`,
395///   `snippet_registry`, `insert_completion_snippet_meta`,
396///   `completion_accept_freq`, `per_language_completion`,
397///   `completion_in_path_context`, `active_snippet`,
398///   `snippet_dirs`, `popup_back_stack` (popup #7 tail), and
399///   `pending_config_structural_sections` move from `App`
400///   to `Editor`. `SnippetCandidateMeta` moved from
401///   `lattice-ui-tui::app` to `lattice-host::state` so
402///   the sidecar type lives next to the field that owns it;
403///   `lattice-ui-tui::app` re-exports the type for
404///   compatibility. After this slice the only fields left on
405///   `App` are the renderer-specific caches (`theme`,
406///   `pane_render_registry`) plus the `LspFileWatcher`
407///   wrapper -- `App` becomes a thin renderer wrapper.
408#[derive(Debug, Default)]
409pub struct Editor {
410    /// Perf plan B.4: identity-preserving sub-state cache for
411    /// `build_render_state`. Cached `Arc<SubState>` slots keyed
412    /// by the `u64` version captured from the corresponding
413    /// `Versioned<T>` field. `std::sync::Mutex` (not `RefCell`)
414    /// because `Editor` is shared across threads as `Arc<Editor>`
415    /// and therefore must be `Sync`; uncontested in practice
416    /// because only `build_render_state` (called on the actor
417    /// thread) takes the lock. See
418    /// [`crate::render_state::PublishCache`] for the slot
419    /// inventory and rebuild contract.
420    pub publish_cache: std::sync::Mutex<crate::render_state::PublishCache>,
421
422    /// Completed macro recordings keyed by register name.
423    /// Replays go through the dispatch layer's `PlayMacro`
424    /// action handler. v1 records `Action` streams; insert-
425    /// mode keystrokes ARE captured (every Action::Insert is
426    /// recorded), but dot-repeat-style replay of insert content
427    /// from `c`/`i`/`a` remains a §15 follow-up.
428    pub macros: HashMap<char, Vec<Action>>,
429    /// In-flight macro recording. `Some` while between
430    /// `q<reg>` start and the matching `q` stop; pushed
431    /// Actions append to `actions`.
432    pub macro_recording: Option<MacroRecording>,
433    /// The most recently played macro register, for `@@`
434    /// repeat.
435    pub last_played_macro: Option<char>,
436    /// Unnamed register -- destination of `y` / `d` / `c`,
437    /// source of `p` / `P`. `None` until something has been
438    /// yanked.
439    pub unnamed_register: Option<UnnamedRegister>,
440    /// User-set marks. v1 stores them flat by name (a-z,
441    /// A-Z, 0-9); uppercase / numbered global marks treat
442    /// all marks as buffer-local since the v1 TUI runs
443    /// against a single document.
444    pub marks: HashMap<char, Position>,
445    /// Named registers `"a-z`, `"A-Z`, numbered `"0-"9`,
446    /// etc. Stores content + kind. `""` (the unnamed
447    /// register) is [`Self::unnamed_register`]; this map
448    /// covers everything else.
449    pub registers: HashMap<Register, UnnamedRegister>,
450    /// YR.1: every yank and every delete, newest first. Distinct from
451    /// `registers`, which is addressed by name — this is addressed by
452    /// recency, and it is what the yank picker lists and what YR.2's
453    /// `"0`–`"9` projection reads.
454    pub yank_ring: crate::state::YankRing,
455    /// Register selected for the next operator / paste
456    /// (`"a` prefix). Consumed-and-cleared by `run_invocation`
457    /// (operators) and `do_paste` (paste). `None` means use
458    /// unnamed.
459    pub pending_register: Option<Register>,
460    /// Unified position-history ring (DESIGN.md §5.1.1).
461    /// Every entry is tagged by source so different keybindings
462    /// walk filtered views of the same data (`Ctrl-O` / `Ctrl-I`
463    /// walk `AutoJump` + `PluginPush`; `g;` / `g,` walk
464    /// `NamedMark`).
465    pub position_history: Vec<PositionEntry>,
466    /// Cursor into [`Self::position_history`] -- the next entry
467    /// the navigation action would visit.
468    pub position_history_cursor: usize,
469    /// CM.2 (2026-07-22): the error list — a persistent,
470    /// cross-file list of navigable locations walked by generic
471    /// `:cnext` / `]q` dispatch. Core/host state shaped like
472    /// [`Self::position_history`], NOT owned by any mode:
473    /// compilation (CM.3), diagnostics, and search are *producers*
474    /// that populate it via [`Self::set_error_list`]. See
475    /// `docs/dev/architecture/compilation-mode.md` §3.
476    pub error_list: crate::error_list::ErrorList,
477    /// CM.3c (2026-07-22): per-buffer severity gutter index for the
478    /// `*compilation*` buffer, keyed by [`lattice_core::BufferId`].
479    /// Written by the `AppEffect::CompilationGutterSet` arm (the
480    /// off-thread compilation drain's host-state seam) and snapshotted
481    /// into `RenderState::compilation_severity` at publish. Shaped like a
482    /// producer-fed cache (compilation is the only producer today); the
483    /// outer `Arc` lets the publish clone be O(1) and the inner
484    /// per-buffer `Arc<Vec<..>>` lets the renderer read wait-free.
485    pub compilation_severity: std::sync::Arc<
486        std::collections::HashMap<
487            lattice_core::BufferId,
488            std::sync::Arc<Vec<(u32, lattice_mode::GutterSeverityLevel)>>,
489        >,
490    >,
491    /// CM.3c (2026-07-22): per-buffer compilation location-line
492    /// index for theme-based highlighting. Twin of
493    /// `compilation_severity` above. Written by the
494    /// `AppEffect::CompilationLocationLines` arm and snapshotted
495    /// into `RenderState::compilation_location_lines` at publish.
496    /// The outer `Arc` lets the publish clone be O(1) and the
497    /// inner per-buffer `Arc<Vec<(u32,u32,u32)>>` lets the renderer read
498    /// wait-free.
499    pub compilation_location_lines: std::sync::Arc<
500        std::collections::HashMap<lattice_core::BufferId, std::sync::Arc<Vec<(u32, u32, u32)>>>,
501    >,
502    /// MC.2b: per-buffer set of source lines inside a fenced/indented code
503    /// block, for the full-width `syntax.code_block` background tint. Recomputed
504    /// from the syntax tree in `recompute_folds_because` (the same reparse-driven
505    /// trigger folds ride) and snapshotted into `RenderState::code_block_lines`.
506    /// Outer `Arc` = O(1) publish clone; inner per-buffer `Arc` = wait-free read.
507    pub code_block_lines:
508        std::sync::Arc<std::collections::HashMap<lattice_core::BufferId, std::sync::Arc<Vec<u32>>>>,
509    /// MG.21a (2026-07-29): per-buffer diff sign maps published by a
510    /// *mode*, for buffers whose content is itself a unified diff.
511    /// Written by the `AppEffect::DiffLineSigns` arm and merged into
512    /// `RenderState::diff.sign_maps` by `diff_sign_maps_by_buffer`, so
513    /// the existing line-tint path in both renderers picks them up
514    /// unchanged.
515    ///
516    /// Kept as a slot separate from the session-derived maps because the
517    /// two have different lifetimes: a session map is recomputed as the
518    /// user edits, while these are rewritten only when the mode
519    /// regenerates the buffer. Sessions win on the (currently
520    /// impossible) key collision — see `diff_sign_maps_by_buffer`.
521    pub provider_diff_signs: std::sync::Arc<
522        std::collections::HashMap<
523            lattice_core::BufferId,
524            std::sync::Arc<crate::diff::overlay::DiffSignMap>,
525        >,
526    >,
527    /// CM.3d (2026-07-22): resolved `compilation.location` theme
528    /// colours — published by the mode during activation so the TUI
529    /// and GPUI renderers read from the theme rather than hardcoding
530    /// RGB. `bg` is the location-line background tint; `fg` is the
531    /// link-like file-path foreground. Defaults: surface2 bg, blue fg.
532    pub compilation_theme_colors: std::sync::Arc<(u32, u32)>,
533    /// MRU list of canonical paths the user has opened via
534    /// `:edit` (or any path flowing through `do_edit`). Newest
535    /// first; deduplicated; capped at `MAX_RECENT_FILES`. Source
536    /// for the `:recent` picker.
537    pub recent_files: Vec<PathBuf>,
538    /// Vim-style tag stack (DESIGN.md §5.1.1 follow-up).
539    /// Distinct from the jump list: each "drill-down" navigation
540    /// (`gd` / `gD` / `gy` / `gI` and their multi-result picker
541    /// accept variants) pushes one entry; `<C-t>` pops the most
542    /// recent entry. `<C-o>` walks all jumps chronologically;
543    /// `<C-t>` pops only the LIFO tag-style drill-downs.
544    pub tag_stack: Vec<TagStackEntry>,
545    /// Pre-jump origin captured when an LSP nav request fires;
546    /// transferred to [`Self::tag_stack`] on the actual jump
547    /// (single-result drain or multi-result picker accept).
548    /// Cleared on picker dismiss / nav cancellation / drain
549    /// with no results.
550    pub pending_tag_origin: Option<TagStackEntry>,
551    /// In-progress `/` or `?` search. `Some` only while
552    /// `modal == ModalState::Search(_)`.
553    pub search_line: Option<SearchLine>,
554    /// The `action:*` name to fire on `<CR>` while a generic
555    /// `Effect::OpenPrompt` prompt is focused (`modal ==
556    /// ModalState::Prompt`). Set by `open_prompt_line`, consumed
557    /// (`.take()`) by `do_prompt_line_submit`.
558    pub pending_prompt_submit_action: Option<String>,
559    /// OC.3a: the synthetic name the prompt was opened with, held so the
560    /// submit can hand it back to the action.
561    ///
562    /// `open-prompt-payload.buffer-name` is documented as the channel a caller
563    /// uses to "smuggle state through a multi-step flow", and a NATIVE handler
564    /// reads it off the prompt buffer it is handed. A plugin action cannot: it
565    /// receives a `buffer-id` over WIT and has no way to turn that into a name.
566    /// So the host carries it back in the fired invocation's args instead —
567    /// same channel, reachable from both sides.
568    pub pending_prompt_buffer_name: Option<String>,
569
570    /// MG.17b: the transient a prompt was opened *from*, held across
571    /// the surface switch.
572    ///
573    /// A transient's `TransientState` already survives flag toggles —
574    /// the `Flag` arm mutates it in place and the menu stays open, and
575    /// the preview closure re-reads it every frame. What it cannot
576    /// survive on its own is an `Argument`: the prompt is a different
577    /// surface that takes the editing buffer and the modal state, so
578    /// the picker is torn down and something has to own the menu until
579    /// it can be re-seated. That is this.
580    ///
581    /// `Some` only between opening an argument's prompt and its submit
582    /// or cancel.
583    pub pending_transient_argument: Option<PendingTransientArgument>,
584    /// Most recent submitted search; consulted by `n` / `N`.
585    pub last_search: Option<LastSearch>,
586    /// Range of the most recent search match, used to draw
587    /// the primary highlight in the buffer view. Cleared on
588    /// Esc and on cursor motion.
589    pub current_match: Option<ProtoRange>,
590    /// Every occurrence of the most recent search pattern,
591    /// used to draw the secondary "hlsearch" overlay.
592    /// Cleared on Esc; persists after submit until the next
593    /// search.
594    pub all_matches: Vec<ProtoRange>,
595    /// In-progress substitute preview. Populated as the user
596    /// types `:s/pat...`; the renderer overlays match ranges
597    /// (and the typed replacement once the second `/` has
598    /// been entered) so the user sees the substitution before
599    /// pressing Enter. Cleared when the cmdline closes or the
600    /// input no longer parses as a substitute (DESIGN.md
601    /// §5.9.10).
602    pub substitute_preview: Option<SubstitutePreview>,
603    /// MB.4: live `:` command-line decorations — syntax spans, a
604    /// validation error, and a parameter hint. Recomputed on every
605    /// command-line edit (`refresh_command_line_decorations`, on the
606    /// actor thread) and published into the modeline render state.
607    /// `None` when the command line is closed / empty.
608    pub command_line_decorations: Option<crate::excommand::CommandLineDecorations>,
609    /// In-progress count prefix being typed (`3` of `3w`,
610    /// `12` of `12dd`). 0 means "no count typed". The next
611    /// `Action::Invoke` consumes this and resets it to 0.
612    pub pending_count: u32,
613    /// Count latched when an operator key was pressed (`2`
614    /// of `2d3w`). Multiplied with the motion's count (`3`)
615    /// to give the final count the operator dispatches with
616    /// (`6`). 0 means "no operator count".
617    pub op_count: u32,
618    /// Anchor position when Visual mode was entered. `None`
619    /// outside Visual; restored on Esc. The `head` of the
620    /// selection follows the cursor; the anchor stays put so
621    /// the selection extends or contracts as the user moves.
622    pub visual_anchor: Option<Position>,
623    /// Last operator-class invocation that mutated the
624    /// buffer. `.` re-dispatches it from the current cursor.
625    /// v1 records operator + motion / operator + range /
626    /// Visual-mode operator; insert-mode text replay remains
627    /// a §5.2.4 gap.
628    pub last_change: Option<CommandInvocation>,
629    /// Last Visual-mode selection extents, captured on exit
630    /// so `gv` can re-enter Visual with the same anchor /
631    /// head / kind.
632    pub last_visual: Option<LastVisual>,
633    /// Last f/F/t/T find on this buffer, for `;` / `,`.
634    pub last_find: Option<LastFind>,
635    /// Per-Replace-session log of overwritten bytes so
636    /// backspace can restore the original (rather than
637    /// deleting). Cleared on entry, pushed on each
638    /// `OverwriteChar`, popped on `ReplaceUndoLast`.
639    pub replace_history: Vec<ReplaceEntry>,
640    /// IN.1: line that was auto-indented and has gained nothing
641    /// since. Vim strips such indent when the line is left, so
642    /// `o<Esc>` leaves no trailing whitespace on a line the user
643    /// never typed into. Set by `note_auto_indent`, cleared by the
644    /// first content typed into the line, consumed by
645    /// `strip_pending_auto_indent` on leaving Insert.
646    pub auto_indent_line: Option<u32>,
647    /// RF.5: whether the `formatprg` deprecation note has been emitted
648    /// this session.
649    ///
650    /// Once, not per format: a note repeated on every `:w` with
651    /// `formatonsave` on is noise the user learns to scroll past, which
652    /// is the opposite of what a deprecation notice is for.
653    pub formatprg_deprecation_noted: bool,
654    /// Text inserted during the most recently completed
655    /// Insert session. Captured on Esc out of Insert;
656    /// replayed by dot-repeat after the operator part. `None`
657    /// if the last change had no insert phase.
658    pub last_insert: Option<String>,
659    /// In-flight blockwise-visual `I` / `A` session. Captured
660    /// at mode-entry time (block extents + per-line insert
661    /// column); consumed when Insert exits, at which point
662    /// the recorded text is replicated to every line in the
663    /// block other than the top row (the top row's insert is
664    /// the recording itself). `None` outside a block-visual
665    /// insert.
666    pub pending_block_insert: Option<PendingBlockInsert>,
667    /// Text being captured during the *current* Insert
668    /// session. Promoted into [`Self::last_insert`] when
669    /// leaving Insert.
670    pub recording_insert: Option<String>,
671    /// Active popup buffer slot, if a popup overlay is open.
672    /// The concrete content lives in the [`crate::buffer_registry::BufferRegistry`]
673    /// keyed by this id, flagged
674    /// `BufferFlags { listed: false, hidden: true }`.
675    pub popup_buffer: Option<BufferId>,
676    /// Pane state captured before activating help -- used by
677    /// `dismiss_popup` to restore the user to whatever buffer
678    /// / cursor / scroll they came from. Set by the in-pane
679    /// help activation path; cleared by dismiss.
680    ///
681    /// **Popup lifetime only.** The bury-back address for an
682    /// in-pane synthetic buffer lives in [`Self::bury_target`]
683    /// and is deliberately a different field -- see there.
684    pub prev_pane_for_popup: Option<PrevPaneState>,
685    /// The pane [`Editor::open_help_in_split`] created for a help buffer, when
686    /// help was opened in its OWN split (not layered over an existing pane).
687    /// `dismiss_popup` reads it to decide between CLOSING that pane (help
688    /// brought it into being, so `<Esc>` removes it) and restoring the buffer
689    /// an active-pane help displaced. `None` for popup / active-pane help and
690    /// once the pane is gone. Cleared on dismiss.
691    pub help_split_pane: Option<lattice_core::ui::pane::PaneId>,
692    /// Where `Effect::BuryBuffer` (magit's `q`) returns the pane
693    /// after an in-pane synthetic buffer is closed. Written by
694    /// `open_synthetic_buffer_seeded`, consumed by
695    /// [`Editor::bury_buffer`](crate::editor::Editor::bury_buffer).
696    ///
697    /// **Its own field because the two mechanisms have different
698    /// lifetimes.** Both used to share `prev_pane_for_popup`, and
699    /// whichever teardown ran first consumed whatever was in the
700    /// slot regardless of who wrote it. Reported against
701    /// org-capture: a transient menu was still up (State A) when
702    /// the `*org-capture*` draft opened, so the draft's bury
703    /// address landed in the shared slot; dismissing the menu
704    /// then hand-restored the pane to the pre-capture buffer
705    /// *without* going through `activate_buffer` -- silently,
706    /// since only that function echoes. `C-c C-c`'s
707    /// `Effect::BufferDelete` resolves its target from
708    /// `active_pane_buffer_id()`, so it deleted the buffer the
709    /// user came from and left the draft alive, unsaved and
710    /// unreachable. MG.47 had patched one call site of this on
711    /// the reasoning that a State-A popup "leaves
712    /// `prev_pane_for_popup` as `None` -- so this guard cannot
713    /// move the pane"; that holds only until something opens a
714    /// synthetic buffer underneath the popup.
715    ///
716    /// See `crates/lattice-host/tests/a_popup_dismiss_must_not_move_the_pane.rs`.
717    pub bury_target: Option<PrevPaneState>,
718    /// Where the popup overlay sits on screen when one is
719    /// open. Lives on the editor (not on the buffer) because
720    /// the popup is a generic rectangular surface inside
721    /// which any buffer kind renders -- placement is a
722    /// property of the popup, not of whatever buffer happens
723    /// to be its content.
724    pub popup_placement: PopupPlacement,
725    /// PU refactor (2026-07-22): decoupled focus-state bool,
726    /// extracted from `active_buffer == BufferKind::Help` which
727    /// was an architectural leak — a focus flag smuggled inside
728    /// a content-identity field. `true` when a Steal popup has
729    /// keyboard focus (State B). Set in `focus_help_popup`,
730    /// `activate_help_in_pane`, `open_popup_buffer` (Steal);
731    /// cleared in `dismiss_popup`. Published in render state so
732    /// both TUI and GPUI renderers read it directly.
733    pub popup_focused: bool,
734    /// Cursor position snapshot at popup-open time, used as
735    /// the anchor for `CursorAnchored` popups. Captured in
736    /// `open_floating_popup` / `open_popup` BEFORE any cursor
737    /// mutation so the renderer paints the popup at the
738    /// symbol the user pressed K on, not the cursor's current
739    /// position. Issue 2026-05-22 (third triage round): without
740    /// this the popup follows the cursor — moving with motions
741    /// rather than staying anchored. `None` when no popup is
742    /// open OR for centered popups (anchor irrelevant).
743    pub popup_anchor: Option<lattice_protocol::Position>,
744    /// Document scroll captured at popup-open time so
745    /// CursorAnchored renderers can convert `popup_anchor.line`
746    /// to a screen row in State B (where `self.scroll` is the
747    /// POPUP's scroll, not the document's).
748    pub popup_doc_scroll_at_anchor: u32,
749    /// PU.1a: the popup's persisted view state when it is NOT the
750    /// focused buffer (State A), and the stash loaded into
751    /// `self.scroll` / `self.cursor` when focus moves into the
752    /// popup (State B). Replaces the old `HelpBuffer.{scroll,cursor}`
753    /// registry fields now that help content is an actor-backed
754    /// Document with no per-view cursor of its own. Reset to 0 /
755    /// ZERO on every popup open; updated by `snapshot_active_pane`
756    /// when an in-pane help buffer is stashed.
757    pub popup_scroll: u32,
758    pub popup_cursor: lattice_protocol::Position,
759    /// PU.1b-3: inner-rect geometry of the floating popup overlay,
760    /// fed back from the renderer each frame (mirrors the per-pane
761    /// `set_pane_viewport` hand-off). The renderer is the single
762    /// authority on the popup's inner rect — it computes it from the
763    /// buffer area + `popup_outer_size` + placement — so it pushes the
764    /// resolved `(height, width)` here. `build_cells_panes` reads them
765    /// to size the synthetic popup-pane `DisplayMatrix` (wrap width =
766    /// `popup_viewport_width`). Both 0 until the first feedback after a
767    /// popup opens; the synthetic pane is gated on
768    /// `popup_viewport_width > 0` so a zero-geometry frame is skipped
769    /// (the renderer's plain-text fallback covers that one frame).
770    pub popup_viewport_height: u32,
771    pub popup_viewport_width: u32,
772    /// PU.5c: the ephemeral registry buffer backing the Insert-mode
773    /// completion-docs side popup. `None` when no docs are shown.
774    /// Reconciled from `insert_completion.doc_popup.body` once per cycle
775    /// (`reconcile_completion_docs_buffer` in `run_tick_pending`):
776    /// created when docs appear, text-replaced when they change, and
777    /// garbage-collected when they vanish — a single chokepoint instead
778    /// of the scattered `insert_completion = None` teardown sites. It is a
779    /// help-flavoured `BufferData::Help` Document with the `ephemeral`
780    /// flag, so it reuses markdown syntax + link styling + the
781    /// `nonu`/`signcolumn=no`/`wrap` help-mode options and renders through
782    /// the shared compose seam (`PaneId::COMPLETION_DOCS`).
783    pub completion_docs_buffer: Option<BufferId>,
784    /// Picker live-preview reusable slot. A single ephemeral, read-only
785    /// buffer the find-file (and other file-opening) picker previews load
786    /// content into as the selection changes — instead of running the full
787    /// `do_edit` open path per keystroke (which synchronously parses +
788    /// attaches LSP + leaks a registry buffer per candidate, freezing the
789    /// UI thread). `do_preview` reuses this one buffer; `do_edit` is
790    /// reserved for the final accept. GC'd on picker dismiss.
791    pub preview_buffer: Option<BufferId>,
792    /// PU.5c: inner-rect geometry of the completion-docs side popup, fed
793    /// back from the renderer each frame (the second synthetic popup, peer
794    /// of `popup_viewport_*`). `build_cells_panes` reads them to size the
795    /// `PaneId::COMPLETION_DOCS` matrix; the synthetic pane is gated on
796    /// `completion_docs_viewport_width > 0`.
797    pub completion_docs_viewport_height: u32,
798    pub completion_docs_viewport_width: u32,
799    /// WK.12: the minibuffer band's buffer — a popup opened with
800    /// `PopupPlacement::MinibufferBand` lands HERE rather than in
801    /// [`Self::popup_buffer`], and the two are independent: opening the band
802    /// never dismisses a popup, which is the bug this slot exists to remove.
803    /// Never focused, so it has no anchor, scroll or focus-stack frame.
804    pub band_buffer: Option<BufferId>,
805    /// Renderer-fed inner geometry for the band, exactly as
806    /// `completion_docs_viewport_*` is fed. The synthetic
807    /// `PaneId::MINIBUFFER_BAND` pane is gated on `band_viewport_width > 0`.
808    pub band_viewport_height: u32,
809    pub band_viewport_width: u32,
810    /// VM.3g-1: vim's `curswant` — the column `j` / `k` aim for, which survives
811    /// a short line in between. `None` until something sets one.
812    pub curswant: Option<lattice_grammar::Curswant>,
813    /// VM.3g-1: set when THIS dispatch was a motion that keeps or pins the goal
814    /// column (`j` / `k` / `$`). Everything else — every other motion, every
815    /// edit, Insert exit and yank — lets the dispatch tail set the goal from
816    /// wherever the cursor ended up, which is vim's rule.
817    pub curswant_claimed: bool,
818    /// VM.3g-3: where a motion reports the goal column it AIMED at, when that
819    /// is not the column it reached. Only `gj` / `gk` do: they aim at a screen
820    /// column the landing display row may be too short to hold, and vim keeps
821    /// the aim (measured: a clamped `gj` landing at column 100 records 160).
822    ///
823    /// A long-lived slot on the Editor rather than one built per dispatch
824    /// because `dispatch_blocking` takes `&self` — it can clone this `Arc`
825    /// into the `DispatchEnv` but cannot hand anything back. The dispatch tail,
826    /// which has `&mut self`, `take()`s it.
827    ///
828    /// Taken rather than read: a motion writes it on every motion dispatch
829    /// (`None` for all but two), but an operator or action never touches it,
830    /// so leaving a value behind would let one `gj`'s aim resurface after an
831    /// unrelated command.
832    pub curswant_report: std::sync::Arc<std::sync::Mutex<Option<lattice_grammar::Curswant>>>,
833    /// FS.1: the focus **stack** — one frame per surface that has taken
834    /// focus away from the pane's own buffer, innermost last.
835    ///
836    /// MB.1 introduced this as a single `Option`, which was right while only
837    /// one surface could hold focus: the `:` line, the `/` line, or a
838    /// prompt. It is wrong the moment focus can nest, and it can — a popup
839    /// holds focus, and `/` inside it takes focus again:
840    ///
841    /// ```text
842    ///     []                       editing the file
843    ///     [popup]                  the popup has focus
844    ///     [popup, search-line]     …and `/` inside it
845    /// ```
846    ///
847    /// As an `Option` the second push recorded nothing (it was guarded on
848    /// `is_none()`) and one restore returned to the FILE, skipping the popup
849    /// — which is how `<Esc>` out of a search left the editor focused
850    /// nowhere. See `focused-surface.md` §2.
851    ///
852    /// [`Editor::focused_surface`] reads the top frame; use it rather than
853    /// indexing, so "is something focused" and "what is focused" stay one
854    /// question.
855    pub focus_stack: Vec<crate::state::MinibufferFocus>,
856    /// FS.2: the [`Self::focus_stack`] depth a focused popup sits at, so
857    /// dismissing it unwinds any surface opened INSIDE it (a `/` line, a
858    /// prompt) rather than leaving that focused over a popup that is gone.
859    /// `None` whenever no popup holds focus.
860    pub popup_focus_depth: Option<usize>,
861    /// Most recent transient status / error message,
862    /// displayed in the echo area until replaced.
863    pub last_message: Option<EchoMessage>,
864    /// Append-only chronological ring of every echo
865    /// (`set_message` call). The `:messages` ex-command
866    /// opens a `*messages*` buffer rendered from this ring
867    /// (the emacs `*Messages*` analogue). Bounded by
868    /// `MessagesRing::capacity`. Wrapped in `Arc<Mutex<>>`
869    /// so the boot-installed `MessagesLayer` (a
870    /// `tracing::Layer` running on whatever thread emitted
871    /// the event) can push into the same ring the App reads
872    /// on the main thread for backlog seeding.
873    pub messages: std::sync::Arc<std::sync::Mutex<MessagesRing>>,
874    /// Receiver for [`lattice_runtime::MessagePushed`] events
875    /// published by `set_message`. The runtime's per-tick
876    /// drain coalesces bursts and rebuilds the `*messages*`
877    /// buffer view once per frame.
878    pub pending_message_event_rx: Option<tokio::sync::mpsc::UnboundedReceiver<MessagePushed>>,
879    /// Set by `Action::RedrawScreen` (`<C-l>`); the runtime
880    /// clears this on its next frame after issuing a full
881    /// terminal-clear so any leftover ANSI / stale glyph
882    /// state gets repainted from scratch.
883    pub pending_redraw: bool,
884    /// Submitted `:` command history. Newest at the back.
885    /// Bounded.
886    pub command_history: Vec<String>,
887    /// While in Command modal: index into
888    /// [`Self::command_history`] of the entry currently
889    /// shown (`None` = the user's in-progress text).
890    pub command_history_cursor: Option<usize>,
891    /// Snapshot of the user's typed `command_line` on the
892    /// first Up so Down can return to it after walking
893    /// through history.
894    pub command_history_pending: Option<String>,
895    /// MB.5b: search-line history (peer of `command_history`).
896    /// Each submitted `/` or `?` pattern is pushed here;
897    /// `<C-p>`/`<C-n>` walk this in the search line.
898    pub search_history: Vec<String>,
899    /// MB.5b: index into `search_history` during history walk.
900    pub search_history_cursor: Option<usize>,
901    /// MB.5b: user's typed pattern saved on first `<C-p>` so
902    /// `<C-n>` can return to it.
903    pub search_history_pending: Option<String>,
904    /// Chord-capture overlay flag. Set when the user submitted
905    /// a Chord-arg-required command with no value
906    /// (`:describe-key<CR>` or the K.3.2 `<C-h>k` binding); the
907    /// cmdline pre-fills with the command word + space and
908    /// translation routes every key through
909    /// `translate_command_chord_capture` so plain letters
910    /// appear as chord tokens (`g` → `g`, `<C-c>` → `<C-c>`,
911    /// `<Up>` → `<Up>`, ...). The renderer reads this through
912    /// `auto_submit_hint` to draw the chord-capture cmdline
913    /// hint. Reset on cancel / submit.
914    ///
915    /// K.3.5.fix (2026-06-03): the field's original purpose
916    /// also included auto-submitting on the FIRST captured
917    /// chord token — that auto-submit was dropped because
918    /// chord arguments are sequences (`gg`, `<C-w>v`, `]e`,
919    /// `<leader>fz`), not single chords. The user now types
920    /// the full chord text and submits with `<CR>`. Field name
921    /// kept for backward compat across the renderer / context
922    /// boundaries; behavior is "chord-capture mode active,"
923    /// no longer "auto-submit on chord."
924    ///
925    /// DK.2 (2026-09-07): the explicit `<CR>` is gone again, but not by
926    /// reinstating auto-submit-on-first-chord. The trie decides —
927    /// see [`Self::chord_capture_seq`].
928    pub auto_submit_after_chord: bool,
929    /// DK.2: the chords captured so far in the current chord-capture session.
930    ///
931    /// Capture has to accumulate a SEQUENCE (`gg`, `<C-w>v`, `<leader>fz`), so
932    /// it cannot submit on the first keystroke; but making the user terminate
933    /// with `<CR>` meant `<CR>` — and the `<Esc>` and `<BS>` reserved beside it
934    /// — could never themselves be described. The keymap trie already
935    /// distinguishes "waiting for more" (`Partial`) from "this is the answer"
936    /// (`Bound` / `Unbound`) on every ordinary keystroke, so capture asks it
937    /// instead of asking the user, and no key needs reserving.
938    ///
939    /// Parallel to the command line's TEXT rather than derived from it: the
940    /// line also holds the command word and any earlier args, and re-parsing a
941    /// chord argument back out of it would mean re-deriving the arg span on
942    /// every keystroke to answer a question this vector answers directly.
943    /// Cleared when a prompt arms, and on submit / dismiss.
944    pub chord_capture_seq: Vec<lattice_protocol::chord::KeyChord>,
945    /// Tree-sitter language registry. Services the document
946    /// buffer's `Syntax` and every `HelpBuffer` constructed
947    /// by `:describe-*` / `:apropos` / `:keymap` (help
948    /// bodies render with markdown highlighting + fenced-
949    /// block injections sourced from this same registry).
950    pub lang_registry: Arc<LangRegistry>,
951    /// Per-document tree-sitter state. `None` when the
952    /// document's language is `Plain` (no grammar bundled).
953    /// Reparses run on a worker task; reads against the
954    /// latest snapshot are wait-free via `ArcSwap`.
955    pub syntax: Option<SyntaxHandle>,
956    /// `text_version` last sent to the syntax handle's
957    /// reparse channel. Used to skip republishing identical
958    /// state when no text mutation has happened since the
959    /// previous frame.
960    pub last_parsed_text_version: u64,
961
962    /// Tree-sitter-shaped edit deltas accumulated since the
963    /// last `maybe_reparse_syntax` call. Pushed by
964    /// `publish_document_changed` after each
965    /// `Buffer::apply_edit`; drained by
966    /// `maybe_reparse_syntax` and shipped to the syntax
967    /// worker as `Vec<EditDelta>` for incremental reparse.
968    pub pending_syntax_edits: Vec<EditDelta>,
969    /// `text_version` the syntax worker's tree is known to
970    /// be at. Sent as `from_version` on the next reparse
971    /// request so the worker can verify edits apply to the
972    /// correct tree baseline.
973    pub last_synced_syntax_version: u64,
974    /// OWC: per-buffer text version last seen after a host-issued
975    /// edit. Used to detect owner writes: when the active document's
976    /// `text_version` exceeds this, the host did not issue the edit
977    /// and should adopt the document's primary selection head.
978    pub last_seen_text_version: HashMap<BufferId, u64>,
979    /// LA.2: the `LanguagesRegistered` drain — a plugin whose load changed the
980    /// mode/language catalog lands here, and `run_tick_pending` re-resolves the
981    /// majors and languages of buffers that were opened against the old one.
982    ///
983    /// A channel rather than a flag, and drained here rather than acted on in
984    /// the subscription, because the re-resolution needs `&mut Editor` and the
985    /// bus forwarder runs on the runtime. `EventBus::publish_typed` calls the
986    /// forwarder synchronously, so a publish is visible to the very next tick —
987    /// the paired wake forwarder in `editor_boot` is what makes that tick
988    /// happen without a keypress.
989    ///
990    /// `None` when nothing subscribed it (bare `Editor::default()` in tests);
991    /// the drain is then a no-op.
992    pub pending_catalog_change_rx:
993        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_plugin_loader::LanguagesRegistered>>,
994    /// Pane tree (DESIGN.md §5.9). Always represents the
995    /// ACTIVE tab's panes — when switching tabs we
996    /// `mem::swap` between this field and `tabs[target].panes`.
997    /// Perf plan B.4: wrapped in [`Versioned`] so the panes
998    /// sub-state cache in `build_render_state` can reuse its prior
999    /// `Arc<PanesRenderState>` when the tree hasn't moved since the
1000    /// last publish. `Deref` reads (e.g. `editor.pane_tree.active()`)
1001    /// do NOT bump; `DerefMut` accesses (split/close/set_active)
1002    /// fire one `u64` increment.
1003    pub pane_tree: Versioned<PaneTree>,
1004
1005    /// Issue #29 (2026-05-22): tab pages. Each `TabSlot` carries
1006    /// one tab's pane tree + optional label. The active tab's
1007    /// `panes` field is a default placeholder while live — its
1008    /// real tree sits on `editor.pane_tree`. Inactive tabs hold
1009    /// the full stashed tree. Always non-empty; default boot
1010    /// state is one tab whose pane_tree matches `editor.pane_tree`.
1011    /// Perf plan B.4.b: wrapped in [`Versioned`] so the tabs
1012    /// sub-state cache can detect when the tab list shape changes
1013    /// (push / remove / reorder). The composite cache key for
1014    /// `tabs` also includes `active_tab`, `pane_tree.version()`,
1015    /// and `buffers.version()` because label resolution reads
1016    /// across all four inputs.
1017    pub tabs: Versioned<Vec<lattice_core::ui::tab::TabSlot>>,
1018    /// Index of the active tab in `tabs`. Always valid (clamped
1019    /// on tab close).
1020    pub active_tab: usize,
1021
1022    /// Issue #32 (2026-05-22): override for the next picker
1023    /// accept's open routing. Set by `<C-s>` / `<C-v>` / `<C-t>`
1024    /// chords on picker overlays before dispatching the accept;
1025    /// read + cleared by `apply_picker_outcome` for the file-
1026    /// targeting variants (OpenFile / SwitchBuffer / JumpInBuffer
1027    /// / JumpToLocation). `Default` for `<CR>`.
1028    pub picker_open_target: lattice_picker::OpenTarget,
1029    /// YR.3: where a `FillCaller` accept should put its text, recorded
1030    /// when the picker was **opened**.
1031    ///
1032    /// Not resolved at accept: by then the picker has been dismissed and
1033    /// the modal state that identified the caller is gone, so resolving
1034    /// would read whatever context happens to be current. That is right
1035    /// often enough to pass a single-level test and wrong in exactly the
1036    /// picker-inside-a-prompt case this exists for.
1037    ///
1038    /// `None` means the picker was opened to act, not to answer, and a
1039    /// `FillCaller` arriving against it is a wiring bug the host reports
1040    /// rather than swallows.
1041    /// PC.1: the root the OPEN picker resolves against, overriding the active
1042    /// buffer's project until it closes.
1043    ///
1044    /// Beside its picker-scoped peers rather than threaded through
1045    /// `build_picker_context`, because a `live` source re-queries through
1046    /// `on_query_changed` — which sees the context and NOT the open's args — so
1047    /// the root has to outlive the single `init` call that carried it.
1048    ///
1049    /// `None` is the ordinary case: every picker before PC.1 resolved from the
1050    /// active buffer and still does.
1051    pub picker_root: Option<std::path::PathBuf>,
1052    pub picker_fill_target: Option<lattice_picker::FillTarget>,
1053    /// YR.6: the byte range on the `:` line a `FillCaller` should
1054    /// REPLACE rather than insert before.
1055    ///
1056    /// An argument picker is opened while the user is part-way through
1057    /// typing that argument (`:magit-checkout ma`), so the picked value
1058    /// has to take the place of `ma`. A plain insert — which is right
1059    /// for YR.5's `<C-r><C-r>`, where nothing was being replaced —
1060    /// would produce `mamain`, silently, and only for users who typed a
1061    /// prefix before opening the picker.
1062    ///
1063    /// Captured at open for the same reason the fill target is: by
1064    /// accept time the cursor has moved and the slot that identified
1065    /// the range is gone. `None` = insert at the cursor.
1066    pub picker_fill_replace: Option<(usize, usize)>,
1067    /// YR.5b: the picker the yank picker was opened *over*.
1068    ///
1069    /// `do_picker_accept` takes `self.picker` before applying the
1070    /// outcome, and `open_picker` replaces it — so without stashing, a
1071    /// yank picker opened from `:files` would destroy the `:files`
1072    /// picker on the way in and have nothing to fill on the way out.
1073    /// Restored both on accept (then filled) and on dismiss, because
1074    /// pressing Esc in the yank picker must return you to the list you
1075    /// were filtering rather than closing both.
1076    pub stashed_picker: Option<lattice_picker::Picker>,
1077
1078    /// T.12a: the theme to restore if the colorscheme picker is
1079    /// dismissed (`<Esc>`). Captured on the FIRST live preview as a
1080    /// `(palette, overrides)` snapshot of the theme active when the
1081    /// picker opened. `<Esc>` calls `ThemeRegistry::set_theme` with
1082    /// these to undo the preview; `<CR>` clears it (keeps the
1083    /// previewed theme). `None` when no colorscheme preview is in flight.
1084    pub pending_theme_preview_restore: Option<(
1085        lattice_theme::Palette,
1086        Vec<(lattice_theme::ElementName, lattice_theme::StyleSpec)>,
1087    )>,
1088
1089    /// Handle to the per-document actor (or, in M.1+, a
1090    /// composing multibuffer handle) and its snapshot cache.
1091    /// M.0: typed as the [`ActiveDocument`] newtype around
1092    /// `Arc<dyn Document>` so the slot can hold either a
1093    /// `RopeDocumentHandle` or a `MultibufferDocumentHandle`
1094    /// without kind-branching at the use site. `Default::
1095    /// default()` populates this with a placeholder rope
1096    /// handle whose actor is already gone — production code
1097    /// overwrites the slot before any traffic flows.
1098    pub document: lattice_runtime::ActiveDocument,
1099    pub snapshot_cache: SnapshotCache,
1100
1101    // DR.2 (decoration-retention): the `pane_highlights` /
1102    // `pane_highlight_keys` inactive-pane span cache + its
1103    // `refresh_pane_highlights` producer were retired here. Inactive
1104    // panes now render from their own retained per-pane `DisplayMatrix`
1105    // (the cells worker builds one for every visible pane), the same
1106    // canonical producer the active pane uses — one producer, zero
1107    // decoration recompute on focus change. See
1108    // `docs/dev/architecture/decoration-retention.md`.
1109    /// Active picker overlay. `None` outside picker mode.
1110    pub picker: Option<Picker>,
1111    /// Manual folds. v1 supports non-nested folds defined by line range.
1112    pub folds: Vec<lattice_core::Fold>,
1113    /// D.3.f.0 (2026-05-29): fold-provider registry. Holds the
1114    /// five built-in `Primary` providers (Manual / Indent /
1115    /// Markdown / Syntax / Lsp) and the list of registered
1116    /// `Overlay` providers, which are mode-owned (DX.3-C7):
1117    /// `diff-mode`'s `HunkFoldSource`, multibuffer's excerpt +
1118    /// file-boundary sources, all registered via the
1119    /// `FoldOverlayService` on mode activation).
1120    /// See `docs/dev/architecture/fold-architecture.md`.
1121    /// M.7: shared behind `Arc<Mutex>` so `FoldOverlayServiceImpl`
1122    /// can call `add_overlay`/`remove_overlay` from mode-activation
1123    /// context (outside `&mut Editor`) without blocking the UI thread.
1124    pub fold_registry: std::sync::Arc<std::sync::Mutex<crate::fold_provider::FoldRegistry>>,
1125
1126    /// BC.3b: boot-lifetime tick-callback registration tokens handed off from
1127    /// the `BootContext` via `into_registrations()`. A subsystem `install(boot)`
1128    /// that wires an off-keystroke `boot.inbound::<T>` drain (the first is the
1129    /// Claude Code IDE peer's write bus) produces an RAII token here; holding it
1130    /// for the editor's lifetime keeps the drain registered (dropping it would
1131    /// unregister the drain mid-session). Empty when no subsystem installs an
1132    /// inbound/tick drain at boot. Never read — held purely to keep the drains
1133    /// alive; the leading `_` documents that.
1134    pub _boot_tick_registrations: Vec<TickCallbackRegistration>,
1135    /// D.4.a (2026-05-29): scroll-binding pane groups. Each
1136    /// entry binds a set of `(pane, buffer)` pairs through a
1137    /// pluggable `RowMapper`; propagation runs at
1138    /// `publish_render_state` tail. Membership keyed on the
1139    /// pair so buffer changes within a pane suspend the
1140    /// binding automatically. Subsystems (diff D.4.d, future
1141    /// `:set scrollbind`, zen mode, `:windo`) add/drop their
1142    /// groups around lifecycle. See
1143    /// `docs/dev/architecture/pane-groups.md`.
1144    pub pane_groups: Vec<crate::pane_group::PaneGroup>,
1145    /// D.0b (2026-06-08): id of the singleton identity-mapper
1146    /// pane group that backs `:set scrollbind`. `None` when no
1147    /// panes currently have `scrollbind=true` (the group is
1148    /// dropped when the last member opts out). Rebuilt by
1149    /// `rebuild_scrollbind_group` on every `scrollbind`
1150    /// option-change cascade.
1151    pub scrollbind_group_id: Option<lattice_core::ui::pane::PaneGroupId>,
1152    /// D.8.e (2026-05-31): session key of the **singleton**
1153    /// `:diffthis` group, if any. `:diffthis` toggles per-buffer
1154    /// membership in this one group; other diff sessions
1155    /// (`:diffsplit`, AI-driven openDiff flows, future magit)
1156    /// run as independent `DiffSession`s and **don't** affect
1157    /// this field.
1158    ///
1159    /// State transitions (per
1160    /// `docs/dev/architecture/n-way-diff-membership.md` §6.2):
1161    /// - `None` → user runs `:diffthis` in any pane: create
1162    ///   N=1 dormant session keyed under the active buffer;
1163    ///   set this to `Some(active_buf)`.
1164    /// - `Some(g)` + active buffer not in g: extend the group
1165    ///   via `add_participant`; arity grows.
1166    /// - `Some(g)` + active buffer in g: shrink the group via
1167    ///   `remove_participant_buffer`; if arity drops to 0 the
1168    ///   subsystem auto-drops the session and this clears
1169    ///   back to `None`.
1170    pub diffthis_group: Option<lattice_core::BufferId>,
1171    /// D.8.e (2026-05-31): pane members corresponding to each
1172    /// participant of the diffthis group, in `:diffthis`-call
1173    /// order. The first entry is the pane the FIRST
1174    /// `:diffthis` invocation came from; subsequent entries
1175    /// are appended as the user invokes `:diffthis` in new
1176    /// panes. Used to construct / reshape the pane group when
1177    /// arity transitions across 2 (need scroll-bind +
1178    /// fillers).
1179    ///
1180    /// Cleared in lockstep with `diffthis_group` — both reset
1181    /// to empty / `None` when the group drops to arity 0.
1182    pub diffthis_members: Vec<crate::pane_group::PaneGroupMember>,
1183    /// Picker source registry -- `:picker` source kinds. Held behind
1184    /// `ArcSwap` (`PickerRegistryHandle`) so the plugin loader can register a
1185    /// loaded picker plugin's source at runtime by copy-on-write RCU while the
1186    /// picker-open path reads it wait-free (PL8.B). Shared as a service so
1187    /// `lattice-plugin-loader` reaches it without a host dep.
1188    pub picker_registry: lattice_picker::PickerRegistryHandle,
1189    /// Per-source MRU index that biases the picker's initial
1190    /// candidate ordering toward recently-accepted picks.
1191    pub picker_mru: PickerMruIndex,
1192    /// Optional on-disk persistence path for [`Self::picker_mru`].
1193    /// `None` for ephemeral / test installs.
1194    pub picker_mru_path: Option<PathBuf>,
1195    /// In-flight async picker init, if the active picker
1196    /// source's `init` returned a Future.
1197    pub pending_picker_init: Option<PendingPickerInit>,
1198    /// In-flight async picker accept, if the accepted source's
1199    /// `accept_async` returned a Future (a WASM plugin source).
1200    /// Its resolved outcome commits via
1201    /// `drain_pending_picker_accept`.
1202    pub pending_picker_accept: Option<PendingPickerAccept>,
1203    /// TR.2: in-flight async transient build, if the named menu's
1204    /// builder is guest-backed. Seated by
1205    /// `drain_pending_transient_build` on the async-landed wake — never
1206    /// by a keystroke, which is what makes a plugin menu open on the
1207    /// chord that asked for it.
1208    pub pending_transient_build: Option<crate::state::PendingTransientBuild>,
1209    /// Live-picker query state -- present only when the
1210    /// active picker source has `spec().live == true`.
1211    pub live_picker_query: Option<LivePickerQueryState>,
1212    /// CD.6a: the query the next picker to SEAT starts with, set by
1213    /// `Effect::OpenPicker { query }`. Consumed at seat for any source —
1214    /// unlike a live source's own seed, which only a live source has — and
1215    /// cleared if the open is refused.
1216    pub pending_picker_query: Option<String>,
1217    /// PI.1 (preview isolation): per-pane preview projection. A pane
1218    /// keeps its committed `PaneState.buffer_id`; the entry here (keyed
1219    /// by `PaneId`) records the buffer it currently *displays* plus the
1220    /// preview cursor / scroll. Baked into the published pane-tree
1221    /// leaves at `build_render_state` time so the renderers show the
1222    /// displayed buffer while `:ls` / modeline / dispatch read the
1223    /// committed one. Ephemeral: never persisted / snapshotted; cleared
1224    /// on accept / dismiss / selection-cleared. See
1225    /// `docs/dev/architecture/preview-isolation.md` §5 and
1226    /// [`crate::preview::PreviewOverride`].
1227    pub preview_overrides:
1228        std::collections::HashMap<lattice_core::ui::pane::PaneId, crate::preview::PreviewOverride>,
1229    /// PI.1: monotonic version bumped on every `preview_overrides`
1230    /// mutation. Folded into the panes-substate cache key (`panes_v`)
1231    /// so the published projection rebuilds when an override changes —
1232    /// the override lives outside `pane_tree.version()`.
1233    pub preview_overrides_version: u64,
1234    /// PBH.1: per-pane buffer history — the trail of buffers each pane
1235    /// has shown, walked with `<C-6>` / `<C-7>`.
1236    ///
1237    /// A **side table keyed by `PaneId`**, deliberately not a field on
1238    /// `PaneState`: that type is `Copy` and `PaneTree::split_active`
1239    /// copies it field-wise (`PaneState { id: PaneId::next(),
1240    /// ..new_state }`), so a history field there would be inherited by
1241    /// the split — the one behaviour this feature must not have.
1242    /// `PaneId::next()` is process-monotonic and never reuses ids, so
1243    /// a freshly split pane has no entry here and therefore starts with
1244    /// a fresh trail **by construction**, with nothing to remember to
1245    /// reset. Keeping it out of `PaneState` also keeps that type `Copy`,
1246    /// so split / close / layout stay allocation-free.
1247    ///
1248    /// Reaped by [`crate::dispatch::Editor::reconcile_pane_history`],
1249    /// which retains only ids still present in the tree rather than
1250    /// hooking each pane-removal site. See
1251    /// `docs/dev/architecture/pane-buffer-history.md` §4.
1252    pub pane_buffer_history: std::collections::HashMap<
1253        lattice_core::ui::pane::PaneId,
1254        crate::pane_history::PaneBufferHistory,
1255    >,
1256    /// PBH.3: set for the duration of a `<C-6>` / `<C-7>` walk so the
1257    /// buffer switch it performs is not recorded as a new visit.
1258    ///
1259    /// Without this the step back would push an entry, truncate the
1260    /// forward tail it was moving into, and make `<C-7>` permanently
1261    /// unreachable — the walk would eat its own future.
1262    pub(crate) walking_pane_history: bool,
1263    /// Shared typed-options registry (DESIGN.md §5.12).
1264    /// Every option's *current value* lives in here behind
1265    /// an `ArcSwap<T>`; `:set` parses against it; the
1266    /// customize buffer view (post-1.0) reads + writes
1267    /// through the same surface.
1268    /// User-set working directory (`:cd`). `None` falls back to
1269    /// `std::env::current_dir()` for path resolution.
1270    pub current_dir: Option<PathBuf>,
1271    pub config: Arc<ConfigRegistry>,
1272    /// Hot-path read cache for the option values.
1273    /// Repopulated by `rebuild_option_cache` after every
1274    /// `:set`. Accessor methods on App read this cached
1275    /// primitive directly (~1ns) instead of going through
1276    /// the registry's mutex + ArcSwap + downcast (~33ns).
1277    pub option_cache: OptionCache,
1278    /// Mode registry (M.1). Owns the catalogue of registered
1279    /// modes; activation / deactivation routes through here.
1280    pub mode_registry: lattice_mode::ModeRegistryHandle,
1281    /// SG.4a: the interned ids of the built-in signs (diagnostics + diff),
1282    /// registered into the sign registry at boot. The `builtin_element_ids`
1283    /// shape — a producer emitting a mark per visible line reads a field
1284    /// rather than hashing a name per line.
1285    pub builtin_sign_ids: lattice_mode::BuiltinSignIds,
1286    /// 2026-05-26: per-mode invocation runner table. Boot
1287    /// registers a runner function under each mode-id whose
1288    /// [`lattice_mode::Mode::invocation_runner`] returns
1289    /// `Some(id)`; [`Editor::run_invocation`] looks the runner
1290    /// up by walking the active modes on the active pane's
1291    /// buffer (minors first, then major) and calls the first
1292    /// match. Empty for modes that don't own dispatch
1293    /// (text-mode, completion-mode, semantic-tokens-mode, …).
1294    /// Replaces the hardcoded `match BufferKind` block in
1295    /// `run_invocation`; plugin-installed modes for plugin-
1296    /// installed buffer kinds extend the dispatcher through
1297    /// this map without touching host code.
1298    pub invocation_runners: HashMap<lattice_mode::ModeId, InvocationRunnerFn>,
1299    /// Typed service map subsystems hand off to modes so
1300    /// `Mode::on_activate` can pull subsystem handles via
1301    /// `ctx.service::<T>()`. Populated at boot; read-only
1302    /// after init.
1303    pub services: Arc<ServiceRegistry>,
1304    /// Per-`(buffer, mode)` Guard storage. Modes return an
1305    /// owned `Mode::Guard` from `on_activate`; the
1306    /// dispatcher stashes it here keyed by `(BufferId,
1307    /// ModeId)`. On deactivation the dispatcher drops the
1308    /// Guard, firing its `Drop` impl for synchronous
1309    /// cleanup. Wrapped in `Arc<Mutex<>>` because the
1310    /// spawned lifecycle task inserts from a worker thread.
1311    pub mode_guards: GuardStoreHandle,
1312    /// Per-buffer active modes (major + minors).
1313    ///
1314    /// Perf plan B.4: wrapped in [`Versioned`] so the modes
1315    /// sub-state cache can reuse its prior Arc across publishes
1316    /// when no mode toggle has fired. The `.insert` / `.remove`
1317    /// sites in dispatch autoref `&mut self.active_modes`, which
1318    /// bumps the version once per mutation.
1319    pub active_modes: Versioned<HashMap<BufferId, ActiveModes>>,
1320    /// TC.9b: activations refused only because the buffer did not offer a
1321    /// required capability YET, to be retried when it does.
1322    ///
1323    /// A buffer opens, its modes activate, and its first parse has not run —
1324    /// so a mode requiring `TREE_SITTER` is refused at exactly the moment
1325    /// every mode is activated, and without this nothing ever asks again. The
1326    /// user sees a mode that is simply never on.
1327    ///
1328    /// Only `MissingCapability` refusals land here. A conflict or a wrong kind
1329    /// is a decision, not a race, and retrying it would loop forever.
1330    ///
1331    /// A `Vec` because it is empty in every ordinary session — the retry pass
1332    /// costs a length check — and because the natural operations are "walk all"
1333    /// and "remove one", neither of which wants a map.
1334    pub deferred_mode_activations: Vec<(BufferId, lattice_mode::ModeId)>,
1335    /// Per-buffer mode-owned local state. Modes populate
1336    /// locals via the `BufferLocal` typed-map during
1337    /// `on_activate`; the App routes `&mut BufferLocals`
1338    /// into the registry's activation methods.
1339    ///
1340    /// Perf plan B.4: wrapped in [`Versioned`] for the same reason
1341    /// as `active_modes` — most publishes don't touch
1342    /// `buffer_locals`, so the deep typed-map clone in
1343    /// `build_render_state` can be avoided via Arc reuse.
1344    pub buffer_locals: Versioned<HashMap<BufferId, BufferLocals>>,
1345    /// Per-buffer mode-resolved options cache. Refreshed
1346    /// eagerly on mode toggle and option write.
1347    pub resolved_options: HashMap<BufferId, ResolvedOptions>,
1348    /// AR.0: on-disk fingerprint per file-backed Document buffer, stamped
1349    /// on load and after the buffer's own `:w`. The autoread watcher (AR.2)
1350    /// compares an incoming filesystem event against this to suppress
1351    /// self-writes and skip no-op touches. Non-file buffers (oil, help,
1352    /// synthetic) never get an entry — the map is keyed by the property
1353    /// "has an on-disk backing", not by `BufferKind`. See
1354    /// `docs/dev/architecture/autoread.md`.
1355    pub on_disk_fingerprints: HashMap<BufferId, crate::autoread::OnDiskFingerprint>,
1356    /// PI.4: monotonic version bumped whenever [`Self::resolved_options`]
1357    /// changes. Keys the published `ResolvedOptionsRenderState` cache so
1358    /// both renderer peers read per-buffer resolved options through ONE
1359    /// renderer-agnostic seam (`RenderState::resolved_option_for`) instead
1360    /// of each peer resolving options its own way.
1361    pub resolved_options_version: u64,
1362    /// Buffer-local explicit overrides (`:setlocal foo=bar`)
1363    /// per buffer. Inputs to resolution; the resolver chains
1364    /// these with mode contributions before writing
1365    /// [`Self::resolved_options`].
1366    pub buffer_local_overrides: HashMap<BufferId, OptionOverrideSet>,
1367    /// Receiver for `OptionChanged` events published by the
1368    /// option-cascade pipeline. `Option` only because the
1369    /// field needs to be `take`-able so the drain method can
1370    /// borrow `&mut self` for cascade work while iterating
1371    /// the receiver. Always `Some` between calls.
1372    pub option_change_rx: Option<tokio::sync::mpsc::UnboundedReceiver<Event>>,
1373    /// Free-form help topic registry (DESIGN.md §5.11).
1374    /// `:help` reads from this; built-ins are sourced from
1375    /// `docs/user/*.md` at build time. Plugins / future LSP
1376    /// integrations register additional topics through the
1377    /// same registry.
1378    ///
1379    /// CR.1: a copy-on-write RCU handle, not a fixed `Arc`. Every read
1380    /// site takes one `.load()` snapshot for the duration of its work,
1381    /// so a plugin loading mid-render affects the next `:help`, never
1382    /// half of this one. Also registered as a boot service under
1383    /// `HelpTopicRegistryHandle` so the plugin loader's drain can reach
1384    /// it without a host method.
1385    pub help_topics: HelpTopicRegistryHandle,
1386    /// T.4: builtin element ids interned once at boot from the
1387    /// `ThemeRegistryHandle` (looked up from [`Self::services`]).
1388    /// Snapshotted (Copy) into `RenderState` so a renderer read is
1389    /// `resolved.get(ids.<elem>)`. The registry handle itself lives
1390    /// only in `services` (it is `Arc<dyn ThemeRegistry>`, which has no
1391    /// `Default`, so it cannot be a field on this `derive(Default)`
1392    /// struct); `build_render_state` looks it up to snapshot
1393    /// `resolved()`.
1394    pub builtin_element_ids: crate::ui::theme::BuiltinElementIds,
1395    /// Buffer-level modal state machine (DESIGN.md §5.2).
1396    /// One of Normal / Insert / Visual / Op-pending /
1397    /// Command / Search / Replace.
1398    pub modal: ModalState,
1399    /// In-flight partial-chord stack from the trie. When the
1400    /// trie returns `LookupResult::Partial`, the dispatch
1401    /// layer appends the chord here; the next keystroke
1402    /// runs through the trie with this stack as prefix.
1403    /// Cleared on every non-`AbsorbPartialChord` action.
1404    pub partial_chord: Vec<KeyChord>,
1405    /// Grammar registry shared with the document actor by the
1406    /// `ArcSwap` handle. The actor calls `lattice_grammar::execute`
1407    /// with a wait-free snapshot from inside its own task. The App
1408    /// also reads it directly (`.load()`, or `.load_full()` where an
1409    /// owned snapshot must outlive a `&mut self` borrow) for the
1410    /// parser, completion pipeline, and introspection.
1411    ///
1412    /// PL8.B / B3b: held behind `ArcSwap` (was `Arc<CommandRegistry>`)
1413    /// so the plugin loader can RCU-register a runtime grammar
1414    /// contribution and `store` it; every reader picks it up on its
1415    /// next `.load()`. See [`lattice_grammar::CommandRegistryHandle`].
1416    pub registry: lattice_grammar::CommandRegistryHandle,
1417    /// In-process event bus (DESIGN.md §5.10). The App
1418    /// publishes editor lifecycle events
1419    /// (DocumentChanged, SelectionsChanged,
1420    /// ModalModeChanged, BeforeSave, DocumentSaved,
1421    /// BeforeQuit, OptionChanged) after observing the
1422    /// corresponding state transitions.
1423    pub event_bus: Arc<EventBus>,
1424    /// Built-in command-ids (`d`, `y`, `w`, `j`, …) -- the
1425    /// canonical `CommandId` values keymap registrations
1426    /// resolve against.
1427    pub builtins: Builtins,
1428    /// App-side typed action IDs (`CommandKind::Action`
1429    /// registrations). Each field is a `CommandId`
1430    /// resolving to an `ActionSpec` whose `apply` returns
1431    /// `Effect::AppAction(AppEffect::Foo)`.
1432    pub action_ids: ActionIds,
1433    /// Layered keymap registry (DESIGN.md §5.2.3).
1434    /// Populated at construction; the input dispatcher
1435    /// reads from it on every keystroke. Wait-free reads
1436    /// via internal `ArcSwap`; concurrent writes (mode
1437    /// push/pop, plugin registration, `:bind`) never stall
1438    /// the input path.
1439    pub keymap: KeymapHandle,
1440    /// `LayerId` of the active completion-popup minor-mode
1441    /// layer when the popup is open; `None` otherwise.
1442    /// Pushed / popped in lockstep with `insert_completion`.
1443    pub completion_popup_layer: Option<LayerId>,
1444    /// Pluggable completion pipeline (DESIGN.md §5.11.3). Owned by
1445    /// the host editor.
1446    pub completion_registry: lattice_completion::CompletionRegistry,
1447    /// Active command-line completion popup state (for `:` line).
1448    pub completion_state: Option<CompletionState>,
1449    /// Active **Insert-mode** completion popup (Phase 4.2.g).
1450    /// Distinct from `completion_state` (which drives the `:` line
1451    /// completion popup): this one floats over the buffer, shows
1452    /// candidates from sources (LSP / snippets / buffer-words /
1453    /// path / tree-sitter / plugin), and the host's keystroke
1454    /// dispatcher routes through a "completion-popup minor mode"
1455    /// keymap layer while it's `Some`. Behavioural spec lives in
1456    /// [`docs/dev/architecture/insert-completion.md`].
1457    pub insert_completion: Option<lattice_completion::InsertCompletionState>,
1458    /// Per-language snippet registry (Phase 4.2.g.4). Loaded
1459    /// at startup from bundled / user / project paths via
1460    /// `lattice-snippet::load`; the `gen:snippet` source
1461    /// consults it per-popup-trigger.
1462    /// CSM.5: held as `Arc<ArcSwap<...>>` so the mode-captured
1463    /// handle stays valid across `:reload-snippets`. Source reads
1464    /// load the current snapshot via `.load()` (wait-free); the
1465    /// reload path swaps the inner via `.store()` so the mode's
1466    /// next produce sees the fresh data.
1467    pub snippet_registry: Arc<arc_swap::ArcSwap<lattice_snippet::SnippetRegistry>>,
1468    /// SN.3b: shared cell holding the folded `snippet-mode`
1469    /// [`ActivationPolicy`](lattice_mode::ActivationPolicy).
1470    /// `register_snippet_modes` creates it (default `Global`) and the
1471    /// `snippet-mode` gate reads it on every `MajorEntered`; boot +
1472    /// the `snippet.activation` / `snippet.languages`
1473    /// `apply_option_cascade` arm fold config into it via
1474    /// `lattice_snippet::fold_activation_policy`.
1475    pub snippet_activation_policy: lattice_snippet::SnippetActivationPolicyHandle,
1476    /// SN.3c.0: app-lifetime registration tokens for modes'
1477    /// declarative *global* action handlers (`Mode::action_handlers()`,
1478    /// registered once at boot by
1479    /// [`crate::mode_action_handlers::register_mode_action_handlers`]).
1480    /// Held here so the handlers stay registered for the editor's
1481    /// whole lifetime; dropped at shutdown when `Editor` drops.
1482    pub global_action_handler_regs: Vec<lattice_mode::ActionHandlerRegistration>,
1483    /// Sidecar metadata for snippet candidates in the active
1484    /// insert-completion popup.
1485    /// CSM.5: retired. Snippet candidates now carry their stable
1486    /// `name` in the `Extension::payload` field; the accept path
1487    /// re-resolves the body via `Editor.snippet_registry.by_name`.
1488    /// Field kept as an empty Vec for one slice so callers that
1489    /// haven't migrated still compile; field deletion in a
1490    /// follow-up cleanup slice.
1491    pub insert_completion_snippet_meta: Vec<crate::state::SnippetCandidateMeta>,
1492    /// Per-session accept-count map for the insert-mode
1493    /// completion popup (Phase 4.2.g.5). Each accepted candidate
1494    /// bumps the counter for its `(text, kind)` pair; the ranker
1495    /// reads this map and adds a bounded bonus
1496    /// (`InsertRanker::FREQUENCY_BONUS_CAP`) so recently-accepted
1497    /// items bubble above tied peers next time.
1498    pub completion_accept_freq: HashMap<(String, lattice_completion::CandidateKind), u32>,
1499    /// TOML structural sections collected by the config loader at
1500    /// startup but not yet routed to their owners. Keyed by full
1501    /// dotted path (e.g. `"completion.per-language.markdown"`,
1502    /// `"plugin.rust-analyzer"`); value is the sub-table verbatim.
1503    /// Phase 4.2.g.5 (3b/3) drains the `completion.per-language.*`
1504    /// entries into `per_language_completion`; the plugin host
1505    /// (Phase 7) will drain `plugin.*`.
1506    pub pending_config_structural_sections: std::collections::BTreeMap<String, toml::Table>,
1507    /// Per-language insert-completion overrides (Phase 4.2.g.5
1508    /// (3b/3); spec at `docs/dev/architecture/insert-completion.md` §9).
1509    pub per_language_completion: HashMap<String, lattice_completion::PerLanguageOverrides>,
1510    /// `true` while the active insert-completion popup is in
1511    /// path-completion mode (Phase 4.2.g.6 (2/2)).
1512    pub completion_in_path_context: bool,
1513    /// Live snippet expansion (SN.2: relocated to a shared
1514    /// `SnippetSession` service so the `SnippetActiveMode`-owned
1515    /// `<Tab>` / `<S-Tab>` handlers can reach it). Active while a
1516    /// snippet is expanding; the session ends on `$0` consumption /
1517    /// `<Esc>` / cursor leaving the tabstop ranges. The same `Arc` is
1518    /// registered in `ServiceRegistry` under `SnippetSessionHandle`.
1519    pub snippet_session: lattice_snippet::SnippetSessionHandle,
1520    /// Session-backed minor modes reconciled on the active buffer
1521    /// each `sync_keymap_overlays` cycle (one entry per
1522    /// service-driven minor). Each pairs a predicate — reading a
1523    /// shared, mode-owned session service — with the minor's
1524    /// `ModeId`; the mode is active iff its predicate is true. Modes
1525    /// contribute these at boot (`active-snippet-mode` keys off the
1526    /// shared `SnippetSession`), so the generic overlay-sync carries
1527    /// no subsystem-specific `is_active()` literal
1528    /// (`feedback_mode_owns_its_surface`).
1529    pub session_backed_minors: Vec<SessionBackedMinor>,
1530    /// Per-language directories from which snippet packs are
1531    /// loaded on startup / `:reload-snippets` (Phase 4.2.g.4).
1532    pub snippet_dirs: Vec<PathBuf>,
1533    /// LIFO stack of snapshots taken every time the popup's content
1534    /// gets swapped in place by a help -> help link follow (e.g.
1535    /// `:describe-buffer` -> click `[text-mode](mode:text-mode)` ->
1536    /// `:describe-mode text-mode`). One popup buffer is reused
1537    /// across the navigation so jump-list / marks / search /
1538    /// register state stay coherent; this stack records what was
1539    /// in the buffer before each swap so `<C-o>` from inside the
1540    /// popup can restore the prior frame without leaving Help.
1541    pub popup_back_stack: Vec<crate::popup::PopupSnapshot>,
1542    /// Active buffer's cursor (DESIGN.md §5.1.1). Updated
1543    /// in lockstep with the active pane's stash so cross-
1544    /// pane jumps restore the right position.
1545    pub cursor: ProtoPosition,
1546    /// Sticky display-column target for `gj`/`gk` (vim's `w_curswant`).
1547    /// Stores the byte offset within the current wrap segment so
1548    /// consecutive display-line moves try to land at the same column.
1549    /// `None` between any non-display-line motion.
1550    /// First visible line in the viewport (0-based).
1551    pub scroll: u32,
1552    /// First visible display column in the viewport (0-based) —
1553    /// horizontal scroll for the active pane. Mirrors the active
1554    /// `PaneState::leftcol`; maintained by
1555    /// `ensure_cursor_horizontally_visible`. Always 0 when `wrap`
1556    /// is on (the body reflows, nothing is off-screen-right).
1557    pub leftcol: u32,
1558    /// Quit flag. The main loop reads this and tears down
1559    /// after the next paint. Set by `:q` / `:qa` / `Ctrl-C`
1560    /// / SIGINT.
1561    pub should_quit: bool,
1562    /// Last height we were drawn at; used by motion
1563    /// clamping and viewport scrolling. Updated by the
1564    /// renderer before each frame.
1565    ///
1566    /// IM.1: this is a **budget in line-heights**, not a count of rows. The
1567    /// two are the same number whenever rows are uniform — which is always,
1568    /// for the TUI — but they diverge once a row is taller than one line
1569    /// (a scaled heading, an IM.3 media block), because how many rows fit
1570    /// then depends on which rows. Spend it through
1571    /// [`Editor::row_weights`]; do not count rows against it.
1572    pub viewport_height: u32,
1573
1574    /// IM.1: per-source-line vertical cost overrides, in line-heights.
1575    ///
1576    /// Empty for every TUI buffer and for any GPUI buffer with no scaled or
1577    /// media rows, and the scroll walks short-circuit on that — see
1578    /// [`lattice_cells::RowWeights::is_uniform`]. Published by the renderer
1579    /// peer, which is the only layer that knows how tall it draws things.
1580    pub row_weights: std::sync::Arc<lattice_cells::RowWeights>,
1581    /// Last terminal width we were drawn at. Used by pane
1582    /// geometry (DESIGN.md §5.9 navigation needs to know
1583    /// which pane is horizontally adjacent). `None` until
1584    /// the renderer first records it.
1585    pub terminal_width: Option<u16>,
1586    /// IM.7a: the drawing peer's cell geometry in pixels, published through
1587    /// [`crate::action::Action::SetCellMetrics`].
1588    ///
1589    /// `None` until a peer that draws images reports it, and forever on one
1590    /// that does not — which is how a media block stays at its provisional
1591    /// reservation in the TUI without the host reading any image header.
1592    pub cell_metrics: Option<CellMetrics>,
1593    /// Which buffer the input pipeline currently routes to.
1594    /// When a help overlay is open this is `Help`; otherwise
1595    /// `Document`. Denormalized from
1596    /// `pane_tree.active().buffer` -- updated in lockstep
1597    /// with the active pane.
1598    pub active_buffer: BufferKind,
1599    /// Stable id for the *active* document buffer. Mirrors
1600    /// the active pane's `buffer_id` whenever that pane
1601    /// holds a Document leaf.
1602    pub document_buffer_id: BufferId,
1603    /// Unified buffer registry (DESIGN.md §5.9). Holds
1604    /// every open buffer regardless of kind -- documents,
1605    /// file trees, future outline / diagnostics views.
1606    pub buffers: BufferRegistry,
1607    /// ML.0b-2: shared modeline element service (descriptor registry +
1608    /// content store, ArcSwap-backed). The SAME `Arc` is registered into
1609    /// `services` at boot so modes reach it via
1610    /// `ctx.service::<ModelineServiceHandle>()`; the host reads
1611    /// `modeline.snapshot()` each `build_render_state` into
1612    /// `RenderState.modeline_elements`. `Arc<ModelineService>` is
1613    /// `Default`, so `#[derive(Default)]` on `Editor` still holds (the
1614    /// boot literal overrides it with the registered instance).
1615    pub modeline: lattice_mode::ModelineServiceHandle,
1616    /// ML.3: actor-thread drain channel for [`lattice_mode::ModelineElementUpdate`]
1617    /// events pushed by modes/plugins over the event bus. Boot subscribes
1618    /// a sender; `drain_modeline_element_updates` (in `run_tick_pending`)
1619    /// applies each into `modeline`'s content store (single-writer). A
1620    /// separate boot subscription fires `async_landed` so a pushed update
1621    /// repaints off-keystroke (§12 wake). `Option` is `Default` (None), so
1622    /// `#[derive(Default)]` on `Editor` still holds.
1623    pub modeline_update_rx:
1624        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_mode::ModelineElementUpdate>>,
1625    /// Cached `textDocument/selectionRange` chain for the
1626    /// smart-expansion operator.
1627    pub lsp_selection_chain: Option<LspSelectionChain>,
1628    /// Current step inside `lsp_selection_chain.ranges`.
1629    /// 0 = innermost; `chain.ranges.len() - 1` = outermost.
1630    pub lsp_selection_chain_index: usize,
1631    /// Cached `textDocument/documentHighlight` for the
1632    /// active buffer + symbol position.
1633    ///
1634    /// Phase 5.8.AF.5 / Slice 3b.0: the cache lives behind
1635    /// `Arc<ArcSwapOption<...>>` so the spawned task on the LSP
1636    /// runtime can store results directly when the response
1637    /// arrives — no channel, no UI-thread drain. Renderers read
1638    /// wait-free via `editor.render_state.load().lsp.document_highlights.load()`.
1639    pub lsp_document_highlights: std::sync::Arc<arc_swap::ArcSwapOption<DocumentHighlightCache>>,
1640    /// Cursor position at which the most recent
1641    /// `documentHighlight` request was issued.
1642    pub last_document_highlight_issue_cursor: Option<ProtoPosition>,
1643    /// Per-buffer cache of the last
1644    /// `textDocument/foldingRange` response.
1645    ///
1646    /// Phase 5.8.AF.5 / Slice 3b.1: `PerBufferCache<T>` so the
1647    /// spawned LSP request task can write results directly when
1648    /// the response arrives -- no channel, no UI-thread drain.
1649    /// Renderers read wait-free via
1650    /// `rs.lsp.folds.get_for(buffer_id)`.
1651    pub lsp_folds_cache: crate::per_buffer_cache::PerBufferCache<LspFoldsCache>,
1652    /// Phase 5.8.AF.5 / Slice 3b.1: the old drain
1653    /// (`drain_pending_folding_range`) called `recompute_folds()`
1654    /// inline after writing the cache so `self.folds` reflected
1655    /// the latest LSP response. The new shape has the task
1656    /// writing the cache off-thread; this tuple lets
1657    /// `maybe_request_folding_range` detect when the cache
1658    /// version has changed and trigger `recompute_folds()` on
1659    /// the renderer thread (where `&mut self.folds` is safe).
1660    /// `Some((buffer_id, document_version))` records the cache
1661    /// state last reflected into `self.folds`.
1662    pub last_recomputed_lsp_fold_version: Option<(BufferId, u64)>,
1663    /// OA.4d: `(buffer, text_version)` the current [`Editor::folds`] were
1664    /// computed from, so the tick can notice when they no longer match.
1665    ///
1666    /// Folds are otherwise seeded in two places, and neither covers a view
1667    /// that fills in ASYNCHRONOUSLY: `activate_buffer` seeds them when the
1668    /// buffer is activated — which for a provider view is while it is still
1669    /// EMPTY, before the scan lands — and `maybe_reparse_syntax` recomputes
1670    /// on the edit path, which a view nobody typed into never takes. So the
1671    /// agenda had no folds at all until something forced a redraw, and
1672    /// `<Tab>` was a no-op because there was nothing under the cursor to
1673    /// cycle.
1674    pub last_folded_text_version: Option<(BufferId, u64)>,
1675    /// Per-buffer `inlayHint` cache.
1676    ///
1677    /// Phase 5.8.AF.5 / Slice 3b.1: see `lsp_folds_cache` note.
1678    /// Renderers read wait-free via
1679    /// `rs.lsp.inlay_hints.get_for(buffer_id)`.
1680    pub lsp_inlay_hints_cache: crate::per_buffer_cache::PerBufferCache<LspInlayHintCache>,
1681    /// PL8.E: WASM gutter-decoration wiring (per-buffer cache + producer
1682    /// registry handle + off-keystroke paint generation + single-flight
1683    /// bookkeeping), bundled so this struct grows by one field. The per-tick
1684    /// `maybe_refresh_wasm_decorations` drives the producers off the render
1685    /// path; renderers read `rs.wasm_gutter_decorations`. Inert in
1686    /// `Editor::default()` (no registry wired).
1687    pub wasm_decorations: crate::wasm_decorations::WasmDecorationState,
1688    /// IM.7: inline-media producer wiring — the cache the virtual-row builder
1689    /// reads, plus the registry the loader registers into.
1690    pub wasm_media: crate::wasm_media::WasmMediaState,
1691    /// TC.3a: WASM sticky-context wiring (per-buffer scope cache + producer
1692    /// registry handle + off-keystroke paint generation + single-flight
1693    /// bookkeeping). The per-tick `maybe_refresh_wasm_context` drives the
1694    /// producers off the render path; per-pane resolution reads the cache.
1695    /// Inert in `Editor::default()` (no registry wired).
1696    pub wasm_context: crate::wasm_context::WasmContextState,
1697    /// Per-buffer `documentLink` cache.
1698    /// Per-buffer `textDocument/documentLink` cache. Phase
1699    /// 5.8.AF.5 / Slice 3b.4: `PerBufferCache<T>` so the spawned
1700    /// LSP request task writes results directly. Renderers read
1701    /// via `rs.lsp.document_links.get_for(buffer_id)`.
1702    pub lsp_document_links_cache: crate::per_buffer_cache::PerBufferCache<LspDocumentLinksCache>,
1703    /// Per-buffer code-lens cache.
1704    /// Per-buffer `textDocument/codeLens` cache. Phase 5.8.AF.5
1705    /// / Slice 3b.3: `PerBufferCache<T>` so the spawned LSP
1706    /// request task writes results directly. Renderers read
1707    /// via `rs.lsp.code_lens.get_for(buffer_id)`.
1708    pub lsp_code_lens_cache: crate::per_buffer_cache::PerBufferCache<LspCodeLensCache>,
1709    /// Per-buffer `documentColor` cache.
1710    /// Per-buffer `textDocument/documentColor` cache. Phase
1711    /// 5.8.AF.5 / Slice 3b.4: `PerBufferCache<T>`.
1712    pub lsp_document_color_cache: crate::per_buffer_cache::PerBufferCache<LspDocumentColorCache>,
1713    /// Per-buffer semantic-tokens cache.
1714    /// Per-buffer cache of the last `textDocument/semanticTokens/*`
1715    /// response. Phase 5.8.AF.5 / Slice 3b.2: `PerBufferCache<T>`
1716    /// so the spawned LSP request task writes results (Items /
1717    /// Delta-applied / Empty) directly when the response arrives.
1718    /// Renderers read wait-free via
1719    /// `rs.lsp.semantic_tokens.get_for(buffer_id)`.
1720    pub lsp_semantic_tokens_cache: crate::per_buffer_cache::PerBufferCache<LspSemanticTokensCache>,
1721    /// Per-buffer pull-diagnostics cache (keyed
1722    /// `result_id`s for `Unchanged` short-circuit).
1723    /// Per-buffer `textDocument/diagnostic` (pull) cache.
1724    /// Phase 5.8.AF.5 / Slice 3b.5: `PerBufferCache<T>`.
1725    pub lsp_pull_diagnostics_cache:
1726        crate::per_buffer_cache::PerBufferCache<LspPullDiagnosticsCache>,
1727    // ---- LSP subsystem handles + log/progress channels ----
1728    pub lsp: LspSupervisorHandle,
1729    /// ML.3c: handle to the `lattice-lsp`-owned progress/status store
1730    /// (decision A — the accumulator relocated out of the host). The
1731    /// modeline forwarder writes it; the host reads it here only for
1732    /// `:lsp-progress-cancel` (in-flight cancellable tokens). `Arc<…>` is
1733    /// `Default`, so `#[derive(Default)]` on `Editor` still holds.
1734    pub lsp_progress_store: lattice_lsp::modeline::LspProgressStoreHandle,
1735    pub lsp_diagnostics: DiagnosticsLayer,
1736    /// L4a.2 (lsp-architecture.md §15): inline cursor-line
1737    /// diagnostic-summary idle gate. `inline_diag_line` is the line
1738    /// the gate is currently timing (the cursor line at arm time);
1739    /// `inline_diag_deadline` is the [`tokio::time::Instant`] at which
1740    /// its summary becomes visible (the actor's pinned sleep targets
1741    /// it); `inline_diag_visible` flips true when that deadline passes
1742    /// and back to false on re-arm (new cursor line) / Insert mode /
1743    /// `ui.diagnostics.inline = off`. The published summary in
1744    /// `DiagnosticsRenderState::inline_summary` is recomputed each
1745    /// `build_render_state` while visible, so diagnostics landing on
1746    /// the line after the gate fires refresh it for free. See
1747    /// `update_inline_diag_gate` / `fire_inline_diag_gate`.
1748    pub inline_diag_line: Option<u32>,
1749    pub inline_diag_deadline: Option<tokio::time::Instant>,
1750    pub inline_diag_visible: bool,
1751    /// WK.4: the last `PartialChordPending` published, so the publisher
1752    /// can skip republishing an unchanged tuple. This is the publisher's
1753    /// own dedup cache — not subsystem state — and it is what keeps an
1754    /// ORDINARY keystroke's cost at a tuple compare that short-circuits
1755    /// on two empty slices. See `publish_partial_chord_pending`.
1756    pub last_partial_chord_event: Option<lattice_keymap::PartialChordPending>,
1757    pub lsp_logger: LspLogger,
1758    /// 4.4.l.2 / 5.8.AA.o / 5.8.AF.5: file-watcher service handle.
1759    /// `None` until the first actor with `workspace/didChangeWatchedFiles`
1760    /// capability is observed; at that point the actual watcher +
1761    /// notify event loop is spawned on the LSP runtime (see
1762    /// `crate::lsp_watcher::spawn_lsp_file_watcher_task`). Editor
1763    /// only sends `SyncSubscriptions` commands through this
1764    /// handle — no notify API calls, no event drains, ever run
1765    /// on the renderer's per-tick loop. Per paramount goal #4.
1766    pub lsp_watcher: Option<crate::lsp_watcher::LspFileWatcherHandle>,
1767    /// Editor-side memo of `server_id → CachedSubscription`. Used
1768    /// to detect whether the actor roster or its compiled
1769    /// subscriptions changed since the last `sync` call; only
1770    /// non-trivial diffs are pushed to the task. Mirrors the
1771    /// per-server map the task itself holds, but lives here so the
1772    /// "did anything change?" check stays a cheap fingerprint
1773    /// compare on the renderer's `refresh_lsp_file_watcher` path.
1774    pub lsp_watcher_subscriptions:
1775        std::collections::HashMap<String, crate::lsp_watcher::CachedSubscription>,
1776    /// Editor-side memo of currently-watched roots. Mirrors the
1777    /// task's set so we can skip sending `SyncSubscriptions` when
1778    /// nothing changed.
1779    pub lsp_watcher_watched_roots: std::collections::HashSet<std::path::PathBuf>,
1780    /// AR.3: handle to the autoread watcher task (spawned lazily on the
1781    /// first file-backed buffer with `autoread` on; dropped → task exits
1782    /// when the last such buffer closes). `Editor` only sends `Sync`
1783    /// commands through it — no notify calls on the renderer thread. See
1784    /// `docs/dev/architecture/autoread.md`.
1785    pub autoread_watcher: Option<crate::autoread::AutoreadWatcherHandle>,
1786    /// AR.3: the change stream from the watcher task, drained host-side by
1787    /// AR.4's reload policy. `None` until the watcher is spawned.
1788    pub autoread_changes:
1789        Option<tokio::sync::mpsc::UnboundedReceiver<crate::autoread::AutoreadChange>>,
1790    /// AR.3: order-independent hash of the last-synced desired watch set.
1791    /// The cheap "did the watch set change?" gate on `refresh_autoread_watcher`
1792    /// so buffer-switches that don't change the set skip the cmd-send.
1793    pub autoread_watch_fingerprint: u64,
1794    /// AR.4: the latest un-applied external change per buffer. The tick drain
1795    /// records watcher changes here (keyed by `BufferId`) and applies the
1796    /// *active* buffer's entry immediately; a background buffer's entry is
1797    /// applied when it next becomes active (vim's checktime-on-`BufEnter`).
1798    pub autoread_pending: std::collections::HashMap<BufferId, crate::autoread::AutoreadChange>,
1799    /// AR.5: buffers with an open autoread **conflict diff** (on-disk change +
1800    /// unsaved edits). While a buffer is in this set autoread stays hands-off
1801    /// for it — no re-opened resolver, no reload — so a resolve-then-save
1802    /// can't loop. Cleared when the buffer is reloaded (`:e!` → new id) or
1803    /// closed.
1804    pub autoread_conflict_open: std::collections::HashSet<BufferId>,
1805    /// Phase 5.8.AF.5 / Slice 3a: renderer's wait-free read
1806    /// contract. Published by `Editor::publish_render_state` at
1807    /// the end of every `dispatch()` tick. Renderers load via
1808    /// `editor.render_state.load_full()` once per frame and read
1809    /// every per-frame field through the returned snapshot.
1810    pub render_state: std::sync::Arc<arc_swap::ArcSwap<crate::render_state::RenderState>>,
1811    /// Phase 5.8.AF.5 / Slice X2: wake signal for the overlay
1812    /// worker. `publish_render_state` fires
1813    /// `overlay_wake.0.notify_one()` at its tail so the worker
1814    /// re-evaluates the syntax inputs published into
1815    /// `RenderState.syntax` and re-buckets overlay quads on a cache
1816    /// miss. `Notify` coalesces — a burst of publishes wakes the
1817    /// worker once, which is what we want (it always reads the
1818    /// latest published inputs anyway).
1819    ///
1820    /// display-line B4.2: renamed from `highlight_wake`; the dead
1821    /// span/row prepaint cache the worker also fed was deleted.
1822    pub overlay_wake: OverlayWake,
1823    /// Perf plan B.2 slice B.2.a: parallel cell carrying the
1824    /// worker's per-row pre-bucketed static-overlay quads
1825    /// (doc_highlight / all_matches / substitute) for the active
1826    /// pane's visible window. The Arc identity lives on `Editor` so
1827    /// `build_render_state` clones it into every snapshot; the
1828    /// overlay worker writes directly and renderer peers read the
1829    /// latest write via the published Arc without a republish
1830    /// round-trip.
1831    pub syntax_static_overlay_quads_cell:
1832        std::sync::Arc<arc_swap::ArcSwap<crate::render_state::StaticOverlayQuads>>,
1833    /// S2.1 (2026-05-26): cell-grid renderer output cell. Same
1834    /// stability pattern as `syntax_static_overlay_quads_cell`: the Arc
1835    /// identity lives on `Editor` so `build_render_state` clones
1836    /// it into every snapshot; the cell-builder worker (S2.2+)
1837    /// holds a sibling clone and writes directly via
1838    /// `cell.store(new_matrix)`. Empty `CellMatrix` until the
1839    /// worker lands.
1840    pub cells_matrix_cell: std::sync::Arc<arc_swap::ArcSwap<lattice_cells::CellMatrix>>,
1841    /// D.4.d.0 (2026-05-29): per-document cells-matrix
1842    /// registry. Each visible buffer gets its own
1843    /// `Arc<ArcSwap<CellMatrix>>` so the cells worker can
1844    /// rebuild per buffer, and the renderer can pull the
1845    /// matrix matching each pane's buffer at paint time
1846    /// (load-bearing for side-by-side diff — D.4 — where
1847    /// two panes show different buffers simultaneously).
1848    ///
1849    /// The active-document entry is stored under
1850    /// `document_buffer_id` and **shares its Arc identity
1851    /// with [`Self::cells_matrix_cell`]** so the existing
1852    /// hot path (cells_worker writing through the field,
1853    /// renderer reading through `RenderState.cells.matrix`)
1854    /// stays bit-identical until the worker iteration
1855    /// upgrade lands in D.4.d.1.
1856    ///
1857    /// Inserts are lazy via
1858    /// [`Self::cells_matrix_for`] — a buffer's entry shows
1859    /// up the first time anything asks for its matrix.
1860    /// Pruning of stale entries is deferred until the
1861    /// worker actually consumes the registry; for now,
1862    /// entries accumulate without harm because
1863    /// [`arc_swap::ArcSwap`] over an empty `CellMatrix` is
1864    /// a cheap idle resource.
1865    pub cells_matrices: std::sync::Arc<
1866        std::sync::Mutex<
1867            std::collections::HashMap<
1868                lattice_core::BufferId,
1869                std::sync::Arc<arc_swap::ArcSwap<lattice_cells::CellMatrix>>,
1870            >,
1871        >,
1872    >,
1873    /// B2.1 (2026-06-04): per-line display-cache output cell — the
1874    /// substrate that retires `cells_matrix_cell`. Same stability
1875    /// pattern: the Arc identity lives on `Editor` so the publisher
1876    /// clones it into every snapshot; the worker (B2.2) holds a
1877    /// sibling clone and writes via `cell.store(new_matrix)`. Empty
1878    /// `DisplayMatrix` until the worker build path lands.
1879    /// See `docs/dev/architecture/display-line.md`.
1880    pub display_matrix_cell:
1881        std::sync::Arc<arc_swap::ArcSwap<crate::display_matrix::DisplayMatrix>>,
1882    /// B2.1 (2026-06-04): per-document display-matrix registry.
1883    /// Mirror of [`Self::cells_matrices`] for the per-line cache.
1884    /// Each visible buffer gets its own `Arc<ArcSwap<DisplayMatrix>>`
1885    /// so the worker can rebuild per buffer and the renderer can pull
1886    /// the matrix matching each pane's buffer at paint time. The
1887    /// active-document entry is boot-seeded to share its Arc identity
1888    /// with [`Self::display_matrix_cell`]; other entries are inserted
1889    /// lazily via [`Self::display_matrix_for`].
1890    pub display_matrices: std::sync::Arc<
1891        std::sync::Mutex<
1892            std::collections::HashMap<
1893                lattice_core::BufferId,
1894                std::sync::Arc<arc_swap::ArcSwap<crate::display_matrix::DisplayMatrix>>,
1895            >,
1896        >,
1897    >,
1898    /// IG.2 (2026-08-16): per-document indentation-guide registry.
1899    /// Exact peer of [`Self::display_matrices`] — the guide layer is
1900    /// built in the same worker pass, from the same snapshot, and
1901    /// carries the same `MatrixVersion`, so it is keyed and reached
1902    /// the same way. Inserted lazily via [`Self::indent_guides_for`].
1903    pub indent_guides: std::sync::Arc<
1904        std::sync::Mutex<
1905            std::collections::HashMap<
1906                lattice_core::BufferId,
1907                std::sync::Arc<arc_swap::ArcSwap<crate::indent_guides::IndentGuides>>,
1908            >,
1909        >,
1910    >,
1911    /// TC.3b: per-PANE sticky-context registry. The one per-pane layer keyed
1912    /// by `PaneId` rather than `BufferId` — see
1913    /// [`crate::sticky_context`] for why the usual buffer keying is wrong
1914    /// here. Inserted lazily via [`Self::sticky_context_for`].
1915    pub sticky_contexts: std::sync::Arc<
1916        std::sync::Mutex<
1917            std::collections::HashMap<
1918                lattice_core::ui::pane::PaneId,
1919                std::sync::Arc<arc_swap::ArcSwap<crate::sticky_context::StickyContext>>,
1920            >,
1921        >,
1922    >,
1923    /// D.2.d (2026-05-29): diff subsystem instance. Holds the
1924    /// per-buffer `DiffSession` registry, the routing inverse
1925    /// index, and the per-session lazy debouncer. Reads through
1926    /// the host's `BufferTextProvider` impl (wired post-D.2.c
1927    /// when the first consumer slice lands) for live-rope
1928    /// baselines / current sources. `:describe-diff` reads
1929    /// `diff_subsystem.build_describe_diff_content()` directly.
1930    /// See `docs/dev/architecture/diff-system.md` §3.4.
1931    pub diff_subsystem: std::sync::Arc<crate::diff::subsystem::DiffSubsystem>,
1932    /// D.0a.1 (2026-05-29): virtual-rows worker output cell.
1933    /// Same stability pattern as `cells_matrix_cell`: the Arc
1934    /// identity lives on `Editor` so `build_render_state`
1935    /// clones it into every snapshot; the
1936    /// `virtual_rows_worker` holds a sibling clone and writes
1937    /// directly via `cell.store(new_matrix)`. Empty
1938    /// `VirtualRowMatrix` until the first provider registers
1939    /// and the worker rebuilds.
1940    pub virtual_rows_matrix_cell:
1941        std::sync::Arc<arc_swap::ArcSwap<lattice_cells::VirtualRowMatrix>>,
1942    /// D.4.d.2.0 (2026-05-29): per-document virtual-rows
1943    /// matrix registry. Mirror of [`Self::cells_matrices`]
1944    /// for the displacing-virtual-row primitive. Each
1945    /// visible buffer that takes the virtual-rows path gets
1946    /// its own `Arc<ArcSwap<VirtualRowMatrix>>` so the
1947    /// worker (after D.4.d.2.1.b) can rebuild per buffer,
1948    /// and the renderer can pull the right matrix per pane
1949    /// at paint time (load-bearing for side-by-side diff
1950    /// fillers — D.4 — where two panes show different
1951    /// hunks' filler rows simultaneously).
1952    ///
1953    /// The active-document entry is stored under
1954    /// `document_buffer_id` and **shares its Arc identity
1955    /// with [`Self::virtual_rows_matrix_cell`]** so the
1956    /// existing hot path (virtual_rows_worker writing
1957    /// through the field, renderer reading through
1958    /// `RenderState.virtual_rows.matrix`) stays bit-identical
1959    /// until the worker iteration upgrade lands in
1960    /// D.4.d.2.1.b.
1961    ///
1962    /// Inserts are lazy via
1963    /// [`Self::virtual_rows_matrix_for`] — a buffer's entry
1964    /// shows up the first time anything asks for its matrix.
1965    /// Pruning of stale entries is deferred until the worker
1966    /// actually consumes the registry.
1967    pub virtual_rows_matrices: std::sync::Arc<
1968        std::sync::Mutex<
1969            std::collections::HashMap<
1970                lattice_core::BufferId,
1971                std::sync::Arc<arc_swap::ArcSwap<lattice_cells::VirtualRowMatrix>>,
1972            >,
1973        >,
1974    >,
1975    /// D.0a.1 (2026-05-29): wake signal for the virtual-rows
1976    /// worker. `publish_render_state` fires `notify_one()`
1977    /// after every dispatch tick (permit-style coalescing
1978    /// mirrors `cells_wake`). Provider state changes fire the
1979    /// same signal directly to wake the worker between
1980    /// dispatch ticks.
1981    pub virtual_rows_wake: VirtualRowsWake,
1982    /// D.0a.1 (2026-05-29): the provider registry the
1983    /// virtual-rows worker iterates on every wake. Consumers
1984    /// (D.3 inline diff deletion-block provider, M.2
1985    /// multibuffer excerpt-header provider) register their
1986    /// providers here at slice-mount time and unregister at
1987    /// teardown.
1988    pub virtual_row_providers:
1989        std::sync::Arc<crate::virtual_rows_worker::VirtualRowProviderRegistry>,
1990    /// D.3.a.1 (2026-05-29): the bus-subscription guard from
1991    /// `DiffSubsystem::bind`. Held for the editor's lifetime;
1992    /// its `Drop` unsubscribes the bus + aborts the drainer
1993    /// task on editor teardown. Stored behind `Option` so the
1994    /// `Editor::default()` path (used by tests that don't
1995    /// boot through `editor_boot`) can leave it unset without
1996    /// the bind machinery firing.
1997    pub diff_subscription_guard: Option<crate::diff::subsystem::DiffSubscriptionGuard>,
1998    /// VCS.2 (2026-07-25): the bus-subscription guard from
1999    /// `VcsSubsystem::bind`. Held for the editor's lifetime;
2000    /// its `Drop` unsubscribes the bus + aborts the drainer
2001    /// task on editor teardown. Mirrors `diff_subscription_guard`.
2002    pub vcs_subscription_guard: Option<crate::vcs::VcsSubscriptionGuard>,
2003    /// D.3.a.1 (2026-05-29): per-session wake-forwarder
2004    /// `JoinHandle`s. `:diff` spawns a tokio task that awaits
2005    /// `DiffSession::publish_notify().notified()` and fires
2006    /// `VirtualRowsWake` on each publish; `:diffoff` aborts
2007    /// the task by `BufferId` and unregisters the provider.
2008    /// `tokio::sync::Mutex` is overkill here — mutation is
2009    /// `:diff`/`:diffoff` frequency, never per-frame.
2010    pub diff_forwarders: std::sync::Arc<
2011        std::sync::Mutex<
2012            std::collections::HashMap<lattice_core::BufferId, tokio::task::JoinHandle<()>>,
2013        >,
2014    >,
2015    /// I4 (Claude Code IDE peer, `openDiff`): host-drained inbound receiver for
2016    /// programmatic side-by-side diff requests. An off-thread producer (the IDE
2017    /// peer) `send`s a [`lattice_diff::ProgrammaticDiffRequest`] on the matching
2018    /// [`lattice_diff::ProgrammaticDiffBus`] (registered as a boot service); the
2019    /// `send` wakes the editor, and [`Self::drain_inbound_programmatic_diffs`]
2020    /// drains this receiver per tick, opening each diff on the actor thread. The
2021    /// open is irreducibly `&mut Editor` + lattice-diff types, so — like LSP
2022    /// `workspace/applyEdit` (`pending_apply_edit_rx`) — it is host-drained, not
2023    /// a mode-owned `Effect` handler.
2024    pub pending_programmatic_diff_rx:
2025        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_diff::ProgrammaticDiffRequest>>,
2026    /// I4: per-session "save the current (right) side to this path on Accept"
2027    /// map, keyed by the session's primary `BufferId` (the proposed/right
2028    /// buffer). Set when [`Self::open_programmatic_diff`] registers a session;
2029    /// honored in `tear_down_single_diff_session` (a `DiffOutcome::Accept` writes
2030    /// the buffer here before firing the bound oneshot — the openDiff
2031    /// `FILE_SAVED` contract: the review *is* the save). Removed on teardown.
2032    pub programmatic_diff_accept_paths:
2033        std::collections::HashMap<lattice_core::BufferId, std::path::PathBuf>,
2034    /// I4 (openDiff) D-fix.1: per programmatic-diff-session pane teardown info,
2035    /// keyed by the session's primary (proposed) `BufferId`. Recorded by
2036    /// `open_programmatic_diff`; consumed by `finish_programmatic_diff_panes`
2037    /// on `:diff-accept` / `:diff-reject` to close the transient diff panes and
2038    /// return focus to the originating (`:claude`) pane. Populated/cleared in
2039    /// lockstep with `programmatic_diff_accept_paths`.
2040    pub programmatic_diff_panes:
2041        std::collections::HashMap<lattice_core::BufferId, ProgrammaticDiffPanes>,
2042    /// D-fix.5: per diff-participant buffer, the last `HunkIndex`
2043    /// revision its folds were recomputed against. `refresh_diff_folds`
2044    /// (run each tick on the diff-publish wake) consults this to skip
2045    /// buffers whose hunks haven't moved — so a diff session's unchanged +
2046    /// hunk folds refresh off-keystroke when the async recompute
2047    /// publishes, without re-folding on every unrelated wake. Keyed by
2048    /// the participant `BufferId` (each side tracked independently, since
2049    /// each folds its own slot). Stale entries are harmless; the buffer's
2050    /// `diff-mode` deactivation drops its fold sources, and the next
2051    /// recompute simply finds none.
2052    pub diff_fold_seen_revisions: std::collections::HashMap<lattice_core::BufferId, u64>,
2053    /// Document version each buffer's overlay folds were last computed
2054    /// at — see `Editor::refresh_overlay_folds`. Peer of
2055    /// `diff_fold_seen_revisions`, keyed on the document rather than a
2056    /// diff session so it covers overlays whose buffer is edited out of
2057    /// band (magit-status's inline diff toggle).
2058    pub overlay_fold_seen_versions: std::collections::HashMap<lattice_core::BufferId, u64>,
2059    /// S2.1 (2026-05-26): wake signal for the cell-builder worker.
2060    /// `publish_render_state` fires `notify_one()` after every
2061    /// dispatch tick. The worker `notified().await`s; permit-style
2062    /// coalescing handles bursts.
2063    pub cells_wake: CellsWake,
2064    /// S2.4.b (2026-05-26): single-edit tracker for the
2065    /// cell-builder's incremental rebuild path. `Some(delta)`
2066    /// iff exactly one `apply_edit_blocking` (or LSP-applied
2067    /// edit) call has happened since the last
2068    /// `build_render_state` AND the previous publish cycle had no
2069    /// pending delta. Any second edit, batch, undo, redo, or
2070    /// other multi-edit path clears it back to `None` —
2071    /// conservatively forcing the worker to full-rebuild rather
2072    /// than risk applying a stale single-edit shift.
2073    /// `build_render_state` `take()`s and hands it to the cells
2074    /// substate.
2075    pub last_edit_for_cells: Option<lattice_cells::EditDelta>,
2076    /// Phase 5.8.AF.6 / Slice X1b: paint-request signal. The
2077    /// highlights worker fires `paint_request.notify_one()` after
2078    /// every `WorkerDecision::Recomputed` so renderer peers can
2079    /// schedule a paint even when no user input was in flight.
2080    /// `Notify` coalesces; bursts of recomputes wake the bridge
2081    /// once and a single paint covers the latest spans. The TUI
2082    /// peer's 100ms event-poll picks up the new cell naturally
2083    /// (no bridge needed); the GPUI peer spawns a foreground-
2084    /// executor future that awaits this Notify and calls
2085    /// `cx.notify()` to schedule a render.
2086    pub paint_request: std::sync::Arc<tokio::sync::Notify>,
2087    /// Slice B.1 (2026-06-03): "async work landed" wake. Fired by
2088    /// async completions that produce render-relevant state with no
2089    /// keystroke in flight — today the syntax reparse worker (via the
2090    /// `on_publish` Notify handed to each `SyntaxHandle`). The editor
2091    /// actor's loop `select!`s on this and runs `run_tick_pending` +
2092    /// `publish_render_state`, so an idle reparse repaints without
2093    /// waiting for the next key (closes the X1b idle-arrival gap for
2094    /// syntax; LSP-response tasks can fire the same Notify as a
2095    /// follow-up). Distinct from `paint_request`, which is the
2096    /// downstream UI-redraw signal fired after a worker publishes.
2097    pub async_landed: std::sync::Arc<tokio::sync::Notify>,
2098    pub lsp_log_event_rx: Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::LspLogPushed>>,
2099    /// Merged user+project `lsp.*` config tree. BC.8b: shared
2100    /// (`Arc<ArcSwap<…>>`) so the mode-owned `workspace/configuration` inbound
2101    /// handler (`lattice_lsp::configuration::make_handler`) reads the *current*
2102    /// tree; the host re-`store`s it on config reload.
2103    pub lsp_config_tree: std::sync::Arc<arc_swap::ArcSwap<toml::Table>>,
2104    /// Perf plan B.4.b: wrapped in [`Versioned`] so the buffers
2105    /// sub-state cache can elide the per-publish HashMap clone.
2106    /// Mutators (`buffer_uris.insert/remove`) autoref `&mut`,
2107    /// fire `DerefMut`, and bump.
2108    pub buffer_uris: Versioned<HashMap<BufferId, lattice_lsp::Uri>>,
2109    // ---- LSP server-initiated channels ----
2110    pub pending_apply_edit_rx:
2111        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::InboundApplyEdit>>,
2112    // BC.8b/BC.8c: `pending_configuration_rx` + `pending_show_document_rx`
2113    // removed — the `workspace/configuration` and `window/showDocument` buses
2114    // are now the generic `InboundBus`, drained per-tick through their mode-
2115    // owned handlers (`boot.inbound`), not host `Editor` receiver fields.
2116    pub pending_show_message_request_rx:
2117        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::InboundShowMessageRequest>>,
2118    pub lsp_pending_show_message_requests: HashMap<u32, lattice_lsp::InboundShowMessageRequest>,
2119    pub lsp_show_message_request_queue: std::collections::VecDeque<u32>,
2120    pub lsp_next_show_message_request_id: u32,
2121    /// CG.1/CG.2: the **foreground** cancellation slot — work the user
2122    /// explicitly triggered that may hold up the next interaction
2123    /// (project search, an LSP command, a WASM plugin call).
2124    /// Background work (indexing, file watchers, the LSP boot
2125    /// handshake) owns its own long-lived tokens and is deliberately
2126    /// out of scope — see `docs/dev/architecture/cancellation.md` §3.
2127    ///
2128    /// CG.2 moved this from an owned `Option<CancellationToken>` to a
2129    /// shared handle, and registered the same `Arc` as a service. The
2130    /// `Editor` is only one of its users: providers arm through
2131    /// `services.get::<ForegroundCancelHandle>()`, because the places
2132    /// that spawn cancellable work — action-handler closures, event
2133    /// subscriptions — hold `&self` services and never `&mut Editor`.
2134    ///
2135    /// `Arc<T: Default>` is `Default`, so `#[derive(Default)]` on
2136    /// `Editor` still holds.
2137    pub foreground_cancel: lattice_mode::ForegroundCancelHandle,
2138    // ---- LSP per-feature request channels (rx + token pairs) ----
2139    pub pending_hover_rx: Option<tokio::sync::mpsc::UnboundedReceiver<HoverOutcome>>,
2140    pub pending_hover_token: Option<CancellationToken>,
2141    /// 2026-05-27: cursor + scroll captured at K-press time, consumed
2142    /// by `open_floating_popup` when the LSP response arrives so the
2143    /// hover popup anchors to the invocation site rather than wherever
2144    /// the cursor has drifted to. One-shot — cleared after the popup
2145    /// opens (or when the request is cancelled).
2146    pub pending_hover_anchor: Option<(lattice_protocol::position::Position, u32)>,
2147    pub pending_definition_rx:
2148        Option<tokio::sync::mpsc::UnboundedReceiver<Vec<lattice_lsp::lsp_types::Location>>>,
2149    pub pending_definition_token: Option<CancellationToken>,
2150    pub pending_nav_kind: Option<LspNavKind>,
2151    /// LR.2 / EP.6: which terminus the in-flight references request is
2152    /// for.
2153    ///
2154    /// Recorded when the request is issued so the drain routes without
2155    /// guessing. Defaults to `Picker` because that is what `gr` has
2156    /// always done and what every existing caller means.
2157    pub pending_references_terminus: ReferencesTerminus,
2158    /// LR.3: when the in-flight references request is a *refresh*, the
2159    /// view to rebuild in place. `None` means "open a new view".
2160    ///
2161    /// Without this a refresh would open a SECOND `*references*` buffer
2162    /// beside the one the user pressed `gr` in — the same mistake
2163    /// `*problems*` refresh had to avoid, since `create_multibuffer_view`
2164    /// mints a fresh `BufferId` every call.
2165    pub refreshing_references_view: Option<lattice_core::BufferId>,
2166    pub pending_references_rx: Option<tokio::sync::mpsc::UnboundedReceiver<ReferencesOutcome>>,
2167    pub pending_references_token: Option<CancellationToken>,
2168    pub pending_symbols_rx: Option<tokio::sync::mpsc::UnboundedReceiver<SymbolsOutcome>>,
2169    pub pending_symbols_token: Option<CancellationToken>,
2170    pub pending_format_rx: Option<tokio::sync::mpsc::UnboundedReceiver<FormatOutcome>>,
2171    /// IN.8b: results from an external formatter run. Separate channel
2172    /// from the LSP one because the payloads differ -- see
2173    /// `ExternalFormatOutcome`.
2174    pub pending_external_format_rx:
2175        Option<tokio::sync::mpsc::UnboundedReceiver<crate::dispatch::ExternalFormatOutcome>>,
2176    pub pending_format_token: Option<CancellationToken>,
2177    pub pending_signature_help_rx:
2178        Option<tokio::sync::mpsc::UnboundedReceiver<SignatureHelpOutcome>>,
2179    pub pending_signature_help_token: Option<CancellationToken>,
2180    pub pending_completion_rx: Option<tokio::sync::mpsc::UnboundedReceiver<CompletionOutcome>>,
2181    pub pending_completion_token: Option<CancellationToken>,
2182    pub pending_completion_items: Option<Vec<CompletionItemRow>>,
2183    pub pending_moniker_rx: Option<tokio::sync::mpsc::UnboundedReceiver<String>>,
2184    pub pending_rename_rx: Option<tokio::sync::mpsc::UnboundedReceiver<RenameOutcome>>,
2185    pub pending_rename_token: Option<CancellationToken>,
2186    pub pending_code_action_rx: Option<tokio::sync::mpsc::UnboundedReceiver<CodeActionOutcome>>,
2187    pub pending_code_action_token: Option<CancellationToken>,
2188    pub pending_code_action_items: Option<Vec<CodeActionRow>>,
2189    pub pending_code_action_handle: Option<lattice_lsp::ServerHandle>,
2190    pub pending_selection_range_rx:
2191        Option<tokio::sync::mpsc::UnboundedReceiver<SelectionRangeOutcome>>,
2192    pub pending_selection_range_token: Option<CancellationToken>,
2193    pub pending_document_highlight_token: Option<CancellationToken>,
2194    // Phase 5.8.AF.5 / Slice 3b.0: `pending_document_highlight_rx`
2195    // retired -- the spawned task now writes directly into
2196    // `lsp_document_highlights` (`ArcSwapOption`) when the
2197    // response arrives. No channel, no drain.
2198    pub pending_folding_range_token: Option<CancellationToken>,
2199    // Phase 5.8.AF.5 / Slice 3b.1: `pending_folding_range_rx`
2200    // retired -- the spawned task writes directly into
2201    // `lsp_folds_cache` via `PerBufferCacheExt::insert_for`.
2202    pub pending_document_links_token: Option<CancellationToken>,
2203    // Phase 5.8.AF.5 / Slice 3b.4: `pending_document_links_rx`
2204    // retired -- spawned task writes directly into
2205    // `lsp_document_links_cache` via `PerBufferCacheExt::insert_for`.
2206    pub pending_code_lens_token: Option<CancellationToken>,
2207    // Phase 5.8.AF.5 / Slice 3b.3: `pending_code_lens_rx` retired
2208    // -- spawned task writes directly into `lsp_code_lens_cache`
2209    // via `PerBufferCacheExt::insert_for`.
2210    pub pending_code_lens_refresh_rx:
2211        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::LspCodeLensRefresh>>,
2212    pub pending_code_lens_items: Option<Vec<lattice_lsp::lsp_types::CodeLens>>,
2213    pub pending_code_lens_server: Option<Arc<str>>,
2214    pub pending_document_color_token: Option<CancellationToken>,
2215    // Phase 5.8.AF.5 / Slice 3b.4: `pending_document_color_rx`
2216    // retired -- spawned task writes directly into
2217    // `lsp_document_color_cache` via `PerBufferCacheExt::insert_for`.
2218    pub pending_color_presentations: Option<Vec<lattice_lsp::lsp_types::ColorPresentation>>,
2219    pub pending_color_range: Option<lattice_lsp::lsp_types::Range>,
2220    pub pending_inlay_hint_token: Option<CancellationToken>,
2221    // Phase 5.8.AF.5 / Slice 3b.1: `pending_inlay_hint_rx`
2222    // retired -- the spawned task writes directly into
2223    // `lsp_inlay_hints_cache` via `PerBufferCacheExt::insert_for`.
2224    pub pending_semantic_tokens_token: Option<CancellationToken>,
2225    // Phase 5.8.AF.5 / Slice 3b.2: `pending_semantic_tokens_rx`
2226    // retired -- the spawned task writes directly into
2227    // `lsp_semantic_tokens_cache` via `PerBufferCacheExt::insert_for`
2228    // (or `remove_for` on result_id mismatch in the Delta path).
2229    pub pending_pull_diagnostics_token: Option<CancellationToken>,
2230    // Phase 5.8.AF.5 / Slice 3b.5: `pending_pull_diagnostics_rx`
2231    // retired -- spawned task writes directly into
2232    // `lsp_pull_diagnostics_cache` + `lsp_diagnostics` layer.
2233    pub pending_diagnostic_refresh_rx:
2234        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::LspDiagnosticRefresh>>,
2235    pub pending_inlay_hint_refresh_rx:
2236        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::LspInlayHintRefresh>>,
2237    /// 2026-06-03: buffers whose server sent
2238    /// `workspace/inlayHint/refresh` since their hints were last
2239    /// requested. `drain_inlay_hint_refresh` marks here instead of
2240    /// wiping `lsp_inlay_hints_cache`, so the previously-resolved
2241    /// hints stay rendered until the refetch lands (no
2242    /// disappear-then-reappear flicker — `feedback_decorations_update_in_place`).
2243    /// `maybe_request_inlay_hint` consults this to force a refetch
2244    /// even when the document version is unchanged, and clears the
2245    /// entry once it issues the request.
2246    pub inlay_refresh_pending: std::collections::HashSet<lattice_core::BufferId>,
2247    /// 2026-06-03: same shape as [`Self::inlay_refresh_pending`] for
2248    /// `workspace/semanticTokens/refresh`. The semantic-token colour
2249    /// overlay renders directly from `lsp_semantic_tokens_cache` every
2250    /// frame, so wiping the cache on refresh blanked all LSP colouring
2251    /// until the refetch landed (whole-viewport flicker per keystroke).
2252    /// `drain_semantic_tokens_refresh` marks here instead; the prior
2253    /// tokens keep rendering and `maybe_request_semantic_tokens` forces
2254    /// a refetch (delta from the retained `result_id`) that swaps them
2255    /// in place. (Pull diagnostics render from the persistent
2256    /// `DiagnosticsLayer` and code lenses are picker-only, so neither
2257    /// needs this — audited 2026-06-03.)
2258    pub semantic_tokens_refresh_pending: std::collections::HashSet<lattice_core::BufferId>,
2259    pub pending_semantic_tokens_refresh_rx:
2260        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::LspSemanticTokensRefresh>>,
2261    pub pending_lsp_detach_rx:
2262        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_lsp::events::LspBufferDetached>>,
2263    pub pending_mode_lifecycle_rx:
2264        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_mode::ModeEvent>>,
2265    /// MA.2: receives `Event::MajorEntered` so the per-tick
2266    /// minor-activation resolver (`drain_minor_activation`) can
2267    /// auto-activate minors whose `ActivationPolicy` admits the
2268    /// just-entered major on this buffer's kind.
2269    pub pending_major_entered_rx:
2270        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_protocol::Event>>,
2271    /// CI.4: receives `Event::ModeEnablementRequested` (a plugin's `enable-mode`)
2272    /// so the per-tick `drain_mode_enablement` flips the mode registry and
2273    /// re-activates open buffers.
2274    pub pending_mode_enablement_rx:
2275        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_protocol::Event>>,
2276    /// Receives `Event::BufferOptionOverrideRequested` (a plugin's
2277    /// `set-option-in-buffer`) so the per-tick drain writes the buffer-local
2278    /// layer. `pending_mode_enablement_rx`'s shape and its reason: the layer
2279    /// lives here, on the Editor, and the guest holds only a `ConfigRegistry`
2280    /// handle — which is the global layer and the wrong scope.
2281    pub pending_buffer_option_override_rx:
2282        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_protocol::Event>>,
2283    /// OA.15a: receives `ProviderViewRefreshRequested` (a plugin's
2284    /// `refresh-view`) so the per-tick `drain_provider_view_refresh` re-opens
2285    /// the view. The guest cannot reach the activator, so the call is a request
2286    /// and this is where it is applied — `pending_mode_enablement_rx`'s shape,
2287    /// with a typed event so the wake comes for free.
2288    pub pending_provider_view_refresh_rx: Option<
2289        tokio::sync::mpsc::UnboundedReceiver<
2290            lattice_mode::provider_view::ProviderViewRefreshRequested,
2291        >,
2292    >,
2293    pub pending_insert_completion_async_rx:
2294        Option<tokio::sync::mpsc::UnboundedReceiver<lattice_completion::AsyncCompletionOutcome>>,
2295    pub pending_insert_completion_async_token: Option<CancellationToken>,
2296    pub pending_completion_resolve_rx:
2297        Option<tokio::sync::mpsc::UnboundedReceiver<CompletionResolveOutcome>>,
2298    pub pending_completion_resolve_token: Option<CancellationToken>,
2299    /// M.2.b.2 (2026-06-01): `RendererSignal`s accumulated by
2300    /// `impl ModeActivator for Editor` calls made through
2301    /// extension-crate code paths (`create_multibuffer_view` and
2302    /// future provider triggers). The trait surface returns `()`
2303    /// — keeping `RendererSignal` out of `lattice-mode` — so
2304    /// signals are stashed here until the App's dispatch loop
2305    /// drains them via
2306    /// [`Editor::drain_pending_renderer_signals`].
2307    pub pending_renderer_signals: Vec<RendererSignal>,
2308    /// OR.16: see [`Editor::drain_pending_renderer_effects`].
2309    pub pending_renderer_effects: Vec<lattice_grammar::Effect>,
2310}
2311
2312impl Editor {
2313    /// M.2.b.2 (2026-06-01): drain renderer signals accumulated
2314    /// by `impl ModeActivator for Editor` calls — extension-crate
2315    /// code (`lattice_multibuffer::create_multibuffer_view`,
2316    /// future provider triggers) drives activation through the
2317    /// trait surface that returns `()`, so the host loop must
2318    /// pull queued signals into the active `DispatchOutcome`
2319    /// after the call frame returns.
2320    #[must_use]
2321    pub fn drain_pending_renderer_signals(&mut self) -> Vec<RendererSignal> {
2322        std::mem::take(&mut self.pending_renderer_signals)
2323    }
2324
2325    /// OR.16: renderer-owned effects produced on a path with no renderer to
2326    /// hand them to, for the peers to apply on their next frame.
2327    ///
2328    /// The off-renderer paths (the async picker accept, the fill target, the
2329    /// picker's delete verb) apply what they can themselves; anything left is
2330    /// queued here instead of being dropped, which is how four features went
2331    /// missing before. Both peers drain this beside the tick's signals.
2332    #[must_use]
2333    pub fn drain_pending_renderer_effects(&mut self) -> Vec<lattice_grammar::Effect> {
2334        std::mem::take(&mut self.pending_renderer_effects)
2335    }
2336
2337    /// M.2.b.2 (2026-06-01): push a renderer-signal batch onto
2338    /// the trait-activator's pending queue. Called by
2339    /// [`crate::activator`]'s impl after each cascade returns.
2340    pub(crate) fn enqueue_renderer_signals(&mut self, mut signals: Vec<RendererSignal>) {
2341        self.pending_renderer_signals.append(&mut signals);
2342    }
2343
2344    /// 2026-05-26: register an invocation-runner function under
2345    /// the mode-id its owning [`lattice_mode::Mode`] declares via
2346    /// [`lattice_mode::Mode::invocation_runner`]. Called from
2347    /// `Editor::boot` for each built-in runner
2348    /// (`run_help_invocation` / `run_oil_invocation` /
2349    /// `run_file_tree_invocation` / `run_terminal_invocation`);
2350    /// plugins (post Phase 7) reuse this entry point for the
2351    /// modes they install. Overwrites silently on duplicate
2352    /// registration — boot order is the single writer.
2353    pub fn register_invocation_runner(
2354        &mut self,
2355        id: lattice_mode::ModeId,
2356        runner: InvocationRunnerFn,
2357    ) {
2358        self.invocation_runners.insert(id, runner);
2359    }
2360
2361    /// 2026-05-26: resolve the invocation runner for `buffer_id`
2362    /// by walking the active modes (minors most-recently-
2363    /// activated first, then major) and returning the first
2364    /// runner whose mode declared
2365    /// [`lattice_mode::Mode::invocation_runner`] and has a
2366    /// registered function on `self.invocation_runners`.
2367    /// Mirrors [`crate::pane_render::resolve_pane_render_mode`]
2368    /// — same walk, different table. Returns `None` when no
2369    /// active mode owns dispatch (Document panes today).
2370    pub fn resolve_invocation_runner(
2371        &self,
2372        buffer_id: lattice_core::BufferId,
2373    ) -> Option<InvocationRunnerFn> {
2374        let modes = self.active_modes.get(&buffer_id)?;
2375        for &minor_id in modes.minors().iter().rev() {
2376            let mode = self.mode_registry.load().get(minor_id)?;
2377            if let Some(runner_id) = mode.invocation_runner()
2378                && let Some(runner) = self.invocation_runners.get(&runner_id)
2379            {
2380                return Some(*runner);
2381            }
2382        }
2383        let major_id = modes.major()?;
2384        let mode = self.mode_registry.load().get(major_id)?;
2385        let runner_id = mode.invocation_runner()?;
2386        self.invocation_runners.get(&runner_id).copied()
2387    }
2388
2389    /// RV.1: is `id` the generic `action:view-refresh` the shared `gr`
2390    /// binds to?
2391    ///
2392    /// Resolved by name rather than cached in a field so there is no
2393    /// boot-ordering coupling between this and command registration.
2394    /// The cost is one `ServiceRegistry` lookup plus one name hash on
2395    /// the action-dispatch path — which runs at chord rate (human
2396    /// keypresses), not per frame or per glyph, so it is nowhere near
2397    /// paramount-goal-#1 territory. Memoize if a dispatch bench ever
2398    /// says otherwise.
2399    pub fn is_view_refresh_command(&self, id: lattice_protocol::ids::CommandId) -> bool {
2400        self.services
2401            .get::<lattice_grammar::CommandRegistryHandle>()
2402            .and_then(|reg| reg.load().id_by_name(lattice_mode::VIEW_REFRESH_ACTION))
2403            == Some(id)
2404    }
2405
2406    /// RV.1 (2026-08-10): resolve the refresh action for `buffer_id` by
2407    /// walking the active modes (minors most-recently-activated first,
2408    /// then major) and returning the `CommandId` of the first
2409    /// [`lattice_mode::Mode::refresh_action`] declared.
2410    ///
2411    /// Same walk as [`Self::resolve_invocation_runner`], different
2412    /// table — most-specific-wins, so a provider minor on a multibuffer
2413    /// beats the generic `MultibufferMode`. Backs the shared `gr` chord
2414    /// (`refreshable-view-mode`): the mode declares which of its own
2415    /// actions refreshes, the host walks and dispatches it.
2416    ///
2417    /// `None` when no active mode declares one — the caller echoes
2418    /// rather than swallowing the key, so a view without a refresh says
2419    /// so. See `docs/dev/architecture/mode-architecture.md` §5.5.
2420    pub fn resolve_refresh_action(
2421        &self,
2422        buffer_id: lattice_core::BufferId,
2423    ) -> Option<lattice_protocol::ids::CommandId> {
2424        let modes = self.active_modes.get(&buffer_id)?;
2425        let registry = self.mode_registry.load();
2426        let mut declared: Option<&'static str> = None;
2427        // Walk minors (most-recently-activated first), then the major.
2428        // The walk does NOT stop at the first hit: continuing costs a
2429        // handful of `Option` reads over the buffer's active modes and
2430        // buys a `debug!` naming every shadowed declaration. Two modes
2431        // both claiming the refresh is a wiring bug in one of them, and
2432        // silently picking one is how it would stay invisible.
2433        //
2434        // Unlike `resolve_invocation_runner`, a minor missing from the
2435        // registry skips rather than aborting the walk — one absent
2436        // minor must not mask a major's declaration.
2437        let candidates = modes
2438            .minors()
2439            .iter()
2440            .rev()
2441            .copied()
2442            .chain(modes.major())
2443            .filter_map(|id| registry.get(id).map(|m| (id, m)));
2444        for (mode_id, mode) in candidates {
2445            let Some(action) = mode.refresh_action() else {
2446                continue;
2447            };
2448            match declared {
2449                None => declared = Some(action),
2450                Some(winner) => tracing::debug!(
2451                    %mode_id,
2452                    shadowed = action,
2453                    winner,
2454                    "several active modes declare a refresh action; the most specific wins"
2455                ),
2456            }
2457        }
2458        let action = declared?;
2459        let cmd_reg = self
2460            .services
2461            .get::<lattice_grammar::CommandRegistryHandle>()?;
2462        let id = cmd_reg.load().id_by_name(action);
2463        if id.is_none() {
2464            // A mode declared an action name the command registry does
2465            // not know. Loud at debug rather than a silent dead key —
2466            // this is a wiring bug in the declaring crate.
2467            tracing::debug!(
2468                action,
2469                "refresh_action names an unregistered command; `gr` will report nothing to refresh"
2470            );
2471        }
2472        id
2473    }
2474
2475    /// OA.4b: is `id` the generic `action:view-fold-toggle` the shared
2476    /// `<Tab>` binds to? Peer of [`Self::is_view_refresh_command`], same
2477    /// resolve-by-name reasoning.
2478    pub fn is_view_fold_toggle_command(&self, id: lattice_protocol::ids::CommandId) -> bool {
2479        self.services
2480            .get::<lattice_grammar::CommandRegistryHandle>()
2481            .and_then(|reg| reg.load().id_by_name(lattice_mode::VIEW_FOLD_TOGGLE_ACTION))
2482            == Some(id)
2483    }
2484
2485    /// OA.4b: resolve the fold-toggle action for `buffer_id` by walking the
2486    /// active modes (minors most-recently-activated first, then major) and
2487    /// returning the `CommandId` of the first
2488    /// [`lattice_mode::Mode::fold_toggle_action`] declared.
2489    ///
2490    /// Identical walk to [`Self::resolve_refresh_action`], different table,
2491    /// and deliberately so: `<Tab>` and `gr` are the same shape of problem —
2492    /// one chord that means the same *verb* in every grouped view while the
2493    /// *body* stays each view's own. Most-specific wins, so a provider minor
2494    /// on a multibuffer beats the generic `MultibufferMode`.
2495    ///
2496    /// `None` when no active mode declares one — the caller leaves `<Tab>`
2497    /// alone, so an ordinary document keeps jump-list-forward.
2498    pub fn resolve_fold_toggle_action(
2499        &self,
2500        buffer_id: lattice_core::BufferId,
2501    ) -> Option<lattice_protocol::ids::CommandId> {
2502        let modes = self.active_modes.get(&buffer_id)?;
2503        let registry = self.mode_registry.load();
2504        let mut declared: Option<&'static str> = None;
2505        let candidates = modes
2506            .minors()
2507            .iter()
2508            .rev()
2509            .copied()
2510            .chain(modes.major())
2511            .filter_map(|id| registry.get(id).map(|m| (id, m)));
2512        for (mode_id, mode) in candidates {
2513            let Some(action) = mode.fold_toggle_action() else {
2514                continue;
2515            };
2516            match declared {
2517                None => declared = Some(action),
2518                Some(winner) => tracing::debug!(
2519                    %mode_id,
2520                    shadowed = action,
2521                    winner,
2522                    "several active modes declare a fold toggle; the most specific wins"
2523                ),
2524            }
2525        }
2526        let action = declared?;
2527        let cmd_reg = self
2528            .services
2529            .get::<lattice_grammar::CommandRegistryHandle>()?;
2530        let id = cmd_reg.load().id_by_name(action);
2531        if id.is_none() {
2532            tracing::debug!(
2533                action,
2534                "fold_toggle_action names an unregistered command; `<Tab>` will do nothing"
2535            );
2536        }
2537        id
2538    }
2539
2540    /// D.4.d.0 (2026-05-29): lazy port into the per-document
2541    /// [`Self::cells_matrices`] registry. Returns the matrix
2542    /// cell for `buffer_id`, inserting an empty
2543    /// `Arc<ArcSwap<CellMatrix>>` on first ask.
2544    ///
2545    /// Idempotent: every call for the same `buffer_id`
2546    /// returns the same `Arc` identity so renderer reads and
2547    /// worker writes stay coherent.
2548    ///
2549    /// The active document's entry is seeded at boot to
2550    /// share its `Arc` with [`Self::cells_matrix_cell`], so
2551    /// callers that resolve the active doc through either
2552    /// surface land on the same cell.
2553    pub fn cells_matrix_for(
2554        &self,
2555        buffer_id: lattice_core::BufferId,
2556    ) -> std::sync::Arc<arc_swap::ArcSwap<lattice_cells::CellMatrix>> {
2557        let mut map = self
2558            .cells_matrices
2559            .lock()
2560            .expect("cells_matrices mutex poisoned");
2561        map.entry(buffer_id).or_default().clone()
2562    }
2563
2564    /// B2.1 (2026-06-04): lazy port into the per-document
2565    /// [`Self::display_matrices`] registry. Mirror of
2566    /// [`Self::cells_matrix_for`] for the per-line display cache.
2567    /// Returns the matrix cell for `buffer_id`, inserting an empty
2568    /// `Arc<ArcSwap<DisplayMatrix>>` on first ask.
2569    ///
2570    /// Idempotent: every call for the same `buffer_id` returns the
2571    /// same `Arc` identity so renderer reads and worker writes stay
2572    /// coherent. The active document's entry is boot-seeded to share
2573    /// its `Arc` with [`Self::display_matrix_cell`].
2574    pub fn display_matrix_for(
2575        &self,
2576        buffer_id: lattice_core::BufferId,
2577    ) -> std::sync::Arc<arc_swap::ArcSwap<crate::display_matrix::DisplayMatrix>> {
2578        let mut map = self
2579            .display_matrices
2580            .lock()
2581            .expect("display_matrices mutex poisoned");
2582        map.entry(buffer_id).or_default().clone()
2583    }
2584
2585    /// IG.2 (2026-08-16): lazy port into [`Self::indent_guides`].
2586    /// Peer of [`Self::display_matrix_for`], with the same idempotence
2587    /// contract: every call for the same `buffer_id` returns the same
2588    /// `Arc` identity, so renderer reads and worker writes stay
2589    /// coherent.
2590    pub fn indent_guides_for(
2591        &self,
2592        buffer_id: lattice_core::BufferId,
2593    ) -> std::sync::Arc<arc_swap::ArcSwap<crate::indent_guides::IndentGuides>> {
2594        let mut map = self
2595            .indent_guides
2596            .lock()
2597            .expect("indent_guides mutex poisoned");
2598        map.entry(buffer_id).or_default().clone()
2599    }
2600
2601    /// TC.3b: the sticky-context cell for `pane_id`, inserting an empty layer
2602    /// on first ask. Keyed by PANE — two panes on one buffer resolve different
2603    /// context and must not share a cell.
2604    ///
2605    /// Same idempotence contract as the sibling accessors: every call for the
2606    /// same `pane_id` returns the same `Arc` identity, so renderer reads and
2607    /// worker writes stay coherent.
2608    pub fn sticky_context_for(
2609        &self,
2610        pane_id: lattice_core::ui::pane::PaneId,
2611    ) -> std::sync::Arc<arc_swap::ArcSwap<crate::sticky_context::StickyContext>> {
2612        let mut map = self
2613            .sticky_contexts
2614            .lock()
2615            .expect("sticky_contexts mutex poisoned");
2616        map.entry(pane_id).or_default().clone()
2617    }
2618
2619    /// D.4.d.2.0 (2026-05-29): lazy port into the per-document
2620    /// [`Self::virtual_rows_matrices`] registry. Mirror of
2621    /// [`Self::cells_matrix_for`] for the virtual-row pipeline.
2622    /// Returns the matrix cell for `buffer_id`, inserting an
2623    /// empty `Arc<ArcSwap<VirtualRowMatrix>>` on first ask.
2624    ///
2625    /// Idempotent: every call for the same `buffer_id`
2626    /// returns the same `Arc` identity so renderer reads and
2627    /// worker writes stay coherent.
2628    ///
2629    /// The active document's entry is seeded at boot to
2630    /// share its `Arc` with
2631    /// [`Self::virtual_rows_matrix_cell`], so callers that
2632    /// resolve the active doc through either surface land
2633    /// on the same cell.
2634    pub fn virtual_rows_matrix_for(
2635        &self,
2636        buffer_id: lattice_core::BufferId,
2637    ) -> std::sync::Arc<arc_swap::ArcSwap<lattice_cells::VirtualRowMatrix>> {
2638        let mut map = self
2639            .virtual_rows_matrices
2640            .lock()
2641            .expect("virtual_rows_matrices mutex poisoned");
2642        map.entry(buffer_id).or_default().clone()
2643    }
2644}