// semantic_render.rs --- Instance-side semantic projection (T M11.2). //! The semantic projection seam. //! //! [`crate::instance_render::RenderState`] rasterizes the editor to a //! cell grid and ships [`InstanceMessage::CellDelta`]. `SemanticRenderState` //! is its sibling for `semantic_render` sessions: it reads the same //! [`EditorState`] but exits the pipeline *earlier* — it emits the //! structured byte-range styling the cell painter would otherwise have //! consumed, mapped through the active [`crate::highlight::Theme`], //! without the grid-packing step. Styling has one authority per //! language (policy A): tree-sitter spans from [`crate::syntax`] for //! grammar-backed languages, LSP semantic tokens //! ([`crate::lsp::LspManager::semantic_style_context`]) for languages //! with no bundled grammar (C/C++, …). The frontend lays the styling //! out locally over rope text it already holds via its `crdt_replica` //! `BufferMirror`. //! //! Contract boundary (see `docs/semantic-frontend-protocol.md`): the //! instance never learns a pixel. The only spatial fact it consumes is //! the buffer byte range the frontend declared on screen via //! [`crate::protocol::FrontendEvent::Viewport`]; styling is scoped to //! that range so a 100k-line file's styling is never shipped wholesale. //! //! Produced families: `StyleSpans` (M11.2; dual authority per above) //! and `Decorations` (M11.3), both span-granularity diffed (M11.4); //! `InlineAdornments` (Step 3, from the LSP inlay-hint store, //! M11.2-level suppression); `FileStyleSummary` (resolving Open Q#2 — //! per-line dominant style for a minimap, generation-keyed). //! `BlockAdornments` / `FoldState` / `ResourceOffer` remain wire- //! declared but unproduced. use std::collections::HashMap; use crate::buffer::BufferId; use crate::cell::Style; use crate::editor::EditorState; use crate::protocol::{ AdornmentContent, AdornmentPlacement, ByteRange, Decoration, DecorationKind, DecorationSegment, FrontendId, InlineAdornment, InstanceMessage, MenuPromptRow, StatuslineSegment, StyleSegment, StyleSpan, }; use crate::statusline::{ StatuslineEvaluation, StatuslineEvaluationOutcome, StatuslineEvaluationTarget, evaluate_statusline, }; /// The viewport a `semantic_render` frontend last declared. #[derive(Clone, Debug, Eq, PartialEq)] struct DeclaredViewport { buffer_id: BufferId, visible: ByteRange, /// The CRDT generation the frontend computed `visible` against. /// Recorded for the M11.4 "ignore a viewport that races a /// not-yet-applied edit" refinement; M11.2 always honors the most /// recent declaration verbatim. frontend_generation: u64, } /// The diff baseline for one family on one buffer: the /// declared-viewport region the set was computed for, the full /// scoped item set last shipped, and the CRDT generation that set /// was computed against. The next frame diffs against `items`; /// `visible` changing (or no entry) forces a `full` resync, and /// `generation` changing also forces a `full` — see T M11.7. /// /// **T M11.7 — `generation`-tracked full-resync.** Without this, /// edits broke the consumer's incremental-update contract: /// `changed_intervals` only ships dirty-range items on `full=false` /// frames, but after a text-shift the frontend's cached spans /// (indexed by *pre-edit* byte positions) need to be replaced /// wholesale — the post-edit positions have shifted under them. /// Forcing `full=true` on every generation transition makes the /// next emission a `replace_*` on the frontend side, which is the /// correct behavior. Cost: one extra full-viewport ship per edit; /// negligible on a local Unix socket and bounded by the viewport /// size. struct LastFrame { visible: ByteRange, items: Vec, generation: u64, } /// Cached `SearchPrompt` payload for cached-compare suppression /// (Q#SR5 / Q#RX6): `(query, active, total, regex, invalid)`. A `None` /// query means the last emission cleared the band. type SearchPromptFacts = (Option, Option, u32, bool, bool); /// Cached `MenuPrompt` payload for cached-compare suppression (Q#CM1): /// `(rows, active)`. Empty `rows` means the last emission closed the /// menu. type MenuPromptFacts = (Vec, Option); /// Cached `MinibufferPrompt` payload for cached-compare suppression /// (Q#MB1): `(prompt, input, cursor, candidates-window, selected, total)`. /// A `None` prompt means the minibuffer is closed. type MinibufferFacts = (Option, String, u32, Vec, Option, u32); /// Cached `CompletionPopup` payload for cached-compare suppression /// (Arc 1a Q#C5): `(anchor, prefix_len, rows-window, selected, total)`. /// A `None` anchor means the popup is closed. type CompletionPopupFacts = ( Option, u32, Vec, Option, u32, ); /// How many completion candidates the minibuffer ships per frame — a /// scrolled window around the selection, not the full (≤1024) list. const MB_VISIBLE: usize = 10; /// A window of up to [`MB_VISIBLE`] candidates around `selected`, plus /// the selection's index *within* that window. Keeps the selected row /// visible as the user cycles a long list. fn minibuffer_window(candidates: &[String], selected: Option) -> (Vec, Option) { if candidates.is_empty() { return (Vec::new(), None); } let sel = selected.unwrap_or(0).min(candidates.len() - 1); let start = sel .saturating_sub(MB_VISIBLE / 2) .min(candidates.len().saturating_sub(MB_VISIBLE)); let end = (start + MB_VISIBLE).min(candidates.len()); let window = candidates[start..end].to_vec(); let selected_in_window = selected.map(|s| (s - start) as u32); (window, selected_in_window) } /// Owns one `semantic_render` session's projection state: the last /// viewport the frontend declared, and the diff baseline per buffer /// for the `StyleSpans` and `Decorations` families. pub struct SemanticRenderState { /// The session this projection serves. Selection is per-window /// (per-frontend) state, so the decoration projection needs the /// fid to resolve *this* session's active window via /// `active_window_for`. Styling and diagnostics are per-buffer and /// do not consult it. frontend_id: FrontendId, /// `None` until the frontend's first [`Self::set_viewport`]. While /// `None`, [`Self::render_frame`] emits nothing: the frontend /// bootstraps its rope from `BufferSnapshot`, declares what is on /// screen, and only then receives styling for exactly that range. viewport: Option, /// Styling diff baseline, keyed by buffer (T M11.4). An unchanged /// frame ships nothing; a changed frame ships only the dirty /// byte-range segments. last_sent: HashMap>, /// Decorations diff baseline, tracked independently of `last_sent` /// so a styling change does not force a decorations re-send and /// vice versa. last_decorations: HashMap>, /// `InlineAdornments` baseline (T M11 producer arc, Step 3). The /// wire variant carries no `generation`/`full`/`segments`, so /// unlike the two families above this is only M11.2-level /// suppression: a whole-set re-send on any change, nothing when /// byte-identical. `LastFrame::items` reuse keeps the shape /// uniform even though no segment diffing applies. last_adornments: HashMap>, /// `FileStyleSummary` baseline (post-M11 minimap producer, /// resolving design-note Open Q#2). The whole-file dominant-style /// summary is expensive to compute on a 100k-line file, so the /// producer short-circuits on the last sent CRDT generation: a /// buffer at the same generation re-uses what the frontend /// already has and emits nothing. First emission happens on the /// first frame for a buffer; further emissions only after edits. /// `(crdt_generation, diag_epoch, syntax_epoch, face_epoch)` the /// last summary was computed against, plus that computed payload. /// Diagnostics arrive without a generation bump, so the diag /// epoch catches republishes (minimap marks, T M4.6 GPU parity); /// the theme epochs (Q#TH6) catch mid-session recolors — /// `face_epoch` belongs in the key because `ui.diag.*` feeds the /// marks. The payload copy backs the Q#TH6 payload-equality /// suppression: a face edit that leaves the summary unchanged /// (e.g. `ui.modeline`) recomputes once per mutation and emits /// nothing. The key advances on COMPUTATION, not emission — a /// suppressed send still inserts, or the whole-file recompute /// repeats every tick. last_summary: HashMap, /// `(name, modified, diag_errors, diag_warnings, message)` last /// emitted as `StatusFacts` (Q#S1; `message` since v15) — /// cached-compare suppression. A peer emission baseline ONLY: /// the diagnostic-count freeze deliberately holds no session /// state (rounds 3–4) — it is sourced from the diag store's /// retained vector, so it needs nothing here to survive the /// `on_buffer_snapshot_sent` reset and it holds for sessions /// with no history (a late joiner attaching mid-edit). last_status: HashMap)>, /// Last-emitted line-number gutter mode (UX gutter arc, protocol v14) — /// cached-compare suppression. Seeded to `Some(Off)` (the frontend's /// default) so an off gutter never emits. Per-frontend (one value), /// since this state carries one frontend's `frontend_id`. last_line_numbers: Option, /// Last emitted `SearchPrompt` payload per buffer, for /// cached-compare suppression (see [`SearchPromptFacts`]). last_search_prompt: HashMap, /// Last emitted `MenuPrompt` payload per buffer (Q#CM1), for /// cached-compare suppression (see [`MenuPromptFacts`]). last_menu_prompt: HashMap, /// Last emitted `MinibufferPrompt` payload (Q#MB1) — a single value, /// not per-buffer, because the minibuffer is one global core /// instance. last_minibuffer: Option, /// Last emitted `CompletionPopup` payload per buffer (Arc 1a /// Q#C5), for cached-compare suppression (see /// [`CompletionPopupFacts`]). last_completion_popup: HashMap, /// `StyleSpans` recompute gate (perf). `scoped_style_spans` runs /// the tree-sitter highlights query over the *whole declared /// viewport* (which the GPU frontend sets to the entire buffer) /// and clones the theme — too expensive to repeat on every tick. /// The styling depends only on the parse bundle, the CRDT /// generation, the viewport, and the theme's syntax epoch /// (Q#TH6) — never the cursor — so a gate built from those lets /// cursor-only ticks skip the query entirely while a mid-session /// `pmacs.theme.set` still re-ships recolored spans without an /// edit. Only the grammar (tree-sitter) path is gated; the /// LSP-token path has no comparably cheap handle and recomputes /// as before. last_style_gate: HashMap, /// The theme `face_epoch` the `ThemeFacts` producer last /// INSPECTED (Q#TH7) — `Option`, not a bare zero, because an /// unthemed daemon sits at `face_epoch == 0` and a `0 == 0` /// short-circuit would starve the first authoritative send. /// Advances on computation, not emission: an identical rebuild /// records the epoch it inspected even though nothing ships. last_face_epoch: Option, /// The face table the frontend believes (Q#TH7), seeded `None` so /// every attachment receives exactly one authoritative table — /// the empty table included — with its first emission after /// viewport declaration. A frontend retaining face state across /// attachments is therefore corrected even by an unthemed daemon. last_theme_faces: Option>, /// For v18 peers, the enabled provider-face set epoch inspected by /// `theme_facts_msg`. Kept separate from `last_face_epoch` so /// priority-only provider changes do not rebuild the face table. /// v16/v17 peers never read the registry and leave this `None`. last_statusline_face_set_epoch: Option, /// Whether the peer negotiated protocol >= 16 (PR #120 round 1 /// finding 3). Faces reach a semantic frontend through TWO /// channels: `ThemeFacts` (daemon write-loop gated) and the /// `ui.diag.*` colors folded into `FileStyleSummary` — an OLDER /// channel the version gate does not filter. A v15 peer must not /// receive face-derived minimap marks while its squiggles, signs, /// and counters stay unthemed, so this producer resolves faces /// only when the peer can apply the whole face table. peer_knows_theme_facts: bool, /// The font-pref `epoch` this producer last INSPECTED (Q#F5) — /// `Option`, not a bare zero, or an all-default daemon's `0 == 0` /// short-circuit would starve the first authoritative send. /// Advances on computation, not emission. last_font_epoch: Option, /// The preference the frontend believes (Q#F5), seeded `None` so /// every attachment receives exactly one authoritative /// `FontFacts` — the all-default `(None, None)` included. /// Bufferless: `on_buffer_snapshot_sent` never touches it. last_font_facts: Option<(Option, Option)>, /// Whether the peer negotiated protocol >= 17 (Q#F4). Unlike the /// theme case there is no pre-v17 side channel that could leak /// font state, so this gate has no summary-style companion /// filter. peer_knows_font_facts: bool, /// Whether the peer negotiated protocol >= 18 (Q#SL7). This gates /// callback evaluation in the producer, independently of the daemon's /// write-loop gate. peer_knows_statusline_segments: bool, /// Complete replacement baseline per buffer. `None` means the peer has /// never received an authoritative payload, so the first empty result /// must still be emitted. last_statusline: HashMap, Vec)>, /// Cached byte↔line table for the diagnostics projection, keyed /// by buffer revision. Building it costs an O(buffer) rope copy /// plus a full scan; before this cache, that ran on *every tick* /// while diagnostics were on screen (the table is only consulted /// when the store is non-stale and non-empty) — a steady-state /// CPU burn for a value that changes only when the buffer does. diag_line_cache: HashMap, } /// One [`SemanticRenderState::diag_line_cache`] entry: the line-start /// offsets and source length of a buffer at `revision`. struct DiagLineCache { revision: u64, line_starts: Vec, source_len: u64, } /// One [`SemanticRenderState::last_summary`] entry: the inputs the /// summary was computed against and the computed per-line payload. struct SummaryCache { /// `(crdt_generation, diag_epoch, syntax_epoch, face_epoch)`. key: (u64, u64, u64, u64), /// The computed summary — compared before emitting (Q#TH6 /// payload-equality suppression). lines: Vec