// 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, StyleSegment, StyleSpan, }; /// 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, } /// 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. last_summary: HashMap, } impl SemanticRenderState { /// Fresh session state for frontend `frontend_id`: no viewport /// declared, nothing sent. #[must_use] pub fn new(frontend_id: FrontendId) -> Self { Self { frontend_id, viewport: None, last_sent: HashMap::new(), last_decorations: HashMap::new(), last_adornments: HashMap::new(), last_summary: HashMap::new(), } } /// Record the frontend's declared on-screen byte range. Called by /// the dispatcher when it receives /// [`crate::protocol::FrontendEvent::Viewport`]. Replaces any /// prior declaration wholesale — the latest viewport wins. pub fn set_viewport(&mut self, buffer_id: BufferId, visible: ByteRange, generation: u64) { self.viewport = Some(DeclaredViewport { buffer_id, visible, frontend_generation: generation, }); } /// Project one frame. /// /// Returns up to three messages — [`InstanceMessage::StyleSpans`] /// (T M11.2), [`InstanceMessage::Decorations`] (T M11.3), and /// [`InstanceMessage::InlineAdornments`] (Step 3, from the LSP /// inlay-hint store) — each scoped to the declared viewport and /// each suppressed independently when byte-identical to its last /// send. Returns an empty vec before the frontend declares a /// viewport. /// /// `BlockAdornments` / `FoldState` are still deliberately *not* /// produced: pmacs has no instance-side blame / lens / fold / diff /// source yet. Their wire variants exist (T M11.1); their /// producers wire in when those features land — the same /// "declared, not yet wired" discipline. Emitting an empty message /// every frame would be waste, not honesty, so `InlineAdornments` is /// suppressed both when unchanged and when there is simply nothing /// to say (no hints, no prior non-empty send). #[allow(clippy::too_many_lines)] pub fn render_frame(&mut self, state: &EditorState) -> Vec { let Some(vp) = self.viewport.clone() else { // Emit nothing before the frontend declares a viewport. return Vec::new(); }; let generation = buffer_generation(state, vp.buffer_id); let mut out = Vec::new(); // --- StyleSpans (T M11.2 producer, T M11.4 diff) --- let spans = scoped_style_spans(state, &vp); let prev = self.last_sent.get(&vp.buffer_id); // Resync when there is no baseline, the declared viewport // region moved (scoping window changed), OR the CRDT // generation advanced (T M11.7: text edits shift byte // positions, so prior spans are stale and incremental // updates can't restore the full viewport — see // `LastFrame`'s doc comment). let full = prev.is_none_or(|p| p.visible != vp.visible || p.generation != generation); if full { // The first frame for this buffer/viewport. One segment // covering the declared viewport carries the whole scoped // set (possibly empty → frontend clears the viewport). self.last_sent.insert( vp.buffer_id, LastFrame { visible: vp.visible, items: spans.clone(), generation, }, ); out.push(InstanceMessage::StyleSpans { buffer_id: vp.buffer_id, generation, full: true, segments: vec![StyleSegment { range: vp.visible, spans, }], }); } else { let prev = prev.expect("checked is_none_or above"); let intervals = changed_intervals(&prev.items, &spans, |s| s.range); if !intervals.is_empty() { let segments = intervals .into_iter() .map(|range| StyleSegment { range, spans: clip_style_spans(range, &spans), }) .collect(); self.last_sent.insert( vp.buffer_id, LastFrame { visible: vp.visible, items: spans, generation, }, ); out.push(InstanceMessage::StyleSpans { buffer_id: vp.buffer_id, generation, full: false, segments, }); } // No dirty interval → styling unchanged → emit nothing. } // --- Decorations (T M11.3 producer, T M11.4 diff) --- let decorations = self.scoped_decorations(state, &vp); let prev = self.last_decorations.get(&vp.buffer_id); let full = prev.is_none_or(|p| p.visible != vp.visible || p.generation != generation); if full { self.last_decorations.insert( vp.buffer_id, LastFrame { visible: vp.visible, items: decorations.clone(), generation, }, ); out.push(InstanceMessage::Decorations { buffer_id: vp.buffer_id, generation, full: true, segments: vec![DecorationSegment { range: vp.visible, decorations, }], }); } else { let prev = prev.expect("checked is_none_or above"); let intervals = changed_intervals(&prev.items, &decorations, |d| d.range); if !intervals.is_empty() { let segments = intervals .into_iter() .map(|range| DecorationSegment { range, decorations: clip_decorations(range, &decorations), }) .collect(); self.last_decorations.insert( vp.buffer_id, LastFrame { visible: vp.visible, items: decorations, generation, }, ); out.push(InstanceMessage::Decorations { buffer_id: vp.buffer_id, generation, full: false, segments, }); } } // --- InlineAdornments (Step 3 producer) --- out.extend(self.inline_adornments_msg(state, &vp)); // --- FileStyleSummary (minimap producer; Open Q#2) --- out.extend(self.file_style_summary_msg(state, vp.buffer_id, generation)); out } /// The `InlineAdornments` message for this frame, or `None` when /// nothing should be sent. The wire variant has no /// `generation`/`full`/`segments`, so this is M11.2-level /// suppression only: the whole scoped set re-sends on any change, /// nothing when byte-identical, and never an empty frame when /// there is simply nothing to say. Updates the baseline on send. fn inline_adornments_msg( &mut self, state: &EditorState, vp: &DeclaredViewport, ) -> Option { let adornments = scoped_inline_adornments(state, vp); let should_emit = match self.last_adornments.get(&vp.buffer_id) { // First sight of this buffer: speak only if there is // something to show — no empty-frame spam. None => !adornments.is_empty(), // Re-send on a real change. `empty → empty` conveys // nothing and is suppressed; `non-empty → empty` *is* a // change worth sending so the frontend clears its overlay. Some(p) => { (p.visible != vp.visible || p.items != adornments) && !(adornments.is_empty() && p.items.is_empty()) } }; if !should_emit { return None; } // Adornments use the same `LastFrame` struct as // StyleSpans / Decorations, so we populate `generation` for // consistency. Adornments' diff predicate doesn't consult // it — the whole-set comparison at line 310 catches post- // edit position shifts directly — but tracking it keeps // the struct shape uniform. let generation = buffer_generation(state, vp.buffer_id); self.last_adornments.insert( vp.buffer_id, LastFrame { visible: vp.visible, items: adornments.clone(), generation, }, ); Some(InstanceMessage::InlineAdornments { buffer_id: vp.buffer_id, items: adornments, }) } /// The `FileStyleSummary` message for this frame, or `None`. The /// summary is keyed on CRDT `generation`: a buffer with an /// unchanged generation re-uses the cached summary and emits /// nothing. The first frame for a buffer always emits (the /// frontend needs the baseline). Updates the baseline on send. fn file_style_summary_msg( &mut self, state: &EditorState, buffer_id: BufferId, generation: u64, ) -> Option { if self.last_summary.get(&buffer_id).copied() == Some(generation) { return None; } let lines = scoped_file_summary(state, buffer_id); self.last_summary.insert(buffer_id, generation); Some(InstanceMessage::FileStyleSummary { buffer_id, generation, lines, }) } /// Project the [`Decoration`] set intersecting the declared /// viewport: the session's selection (instance-authoritative, /// byte-native) and LSP diagnostics (line/col → byte, severity → /// kind). Search-hit and current-line decorations are /// deliberately absent: pmacs has no instance-side search-hit /// store, and current-line is a pure cursor derivation the /// frontend already owns (it has `CursorByte`) — emitting it would /// couple a visual-motion concern to the instance, against the /// contract boundary. fn scoped_decorations(&self, state: &EditorState, vp: &DeclaredViewport) -> Vec { let core = state.core.borrow(); let mut out = Vec::new(); // Selection — per-window (per-frontend) state, already byte // offsets. Only this session's active window for the declared // buffer contributes. if let Some(win) = core.active_window_for(self.frontend_id) && win.buffer_id == vp.buffer_id && let Some((lo, hi)) = win.region() && let Some(range) = clip_to_viewport(lo, hi, vp) { out.push(Decoration { range, kind: DecorationKind::Selection, }); } // Diagnostics — keyed in the shared store by the file URI the // Lua LSP glue opened the document under. The URI is derived // from `vp.buffer_id` (see [`buffer_file_uri`]'s docs for why // not from `core.active_buffer_path()`). // // T M11.8 — skip emission while the store is stale (the // document has been edited since the last `publishDiagnostics` // absorption). Without this, the producer ships diagnostics // whose byte positions point at pre-edit text — the visible // wrong-position color artifact session-5 validation surfaced. // The LSP layer's `did_change_full` marks the URI stale; the // next `publishDiagnostics` absorbs and clears the flag. if let Some(uri) = buffer_file_uri(&core, vp.buffer_id) { let (diags, is_stale) = { let store = state.lsp_manager.borrow().diag_store(); let guard = store.lock().expect("diag store mutex poisoned"); (guard.for_uri(&uri).to_vec(), guard.is_stale(&uri)) }; if !is_stale && !diags.is_empty() { let registry = core.registry.clone(); let reg = registry.borrow(); if let Ok(buf) = reg.get(vp.buffer_id) { let source = buffer_source_bytes(buf); let line_starts = line_start_offsets(&source); for d in &diags { let lo = line_col_to_byte( &line_starts, source.len() as u64, d.start_line, d.start_col, ); let hi = line_col_to_byte( &line_starts, source.len() as u64, d.end_line, d.end_col, ); if let Some(range) = clip_to_viewport(lo, hi, vp) { out.push(Decoration { range, kind: severity_to_kind(d.severity), }); } } } } } out } } /// Project the LSP inlay-hint set intersecting the declared viewport /// into [`InlineAdornment`]s. Mirrors the diagnostics half of /// `scoped_decorations`: same path → URI → store (`for_uri`) lookup. /// Step 0 established inlay-hint columns are already pmacs byte /// offsets by the time they reach the store (the absorb path's /// `inbound_converted` rewrites the `Position`-shaped /// `InlayHint.position`), so `line_col_to_byte` — which treats the /// column as a byte offset — is exact here, no per-server encoding /// needed (unlike semantic-token styling). Takes no `self`: the /// session id is irrelevant (inlay hints are per-buffer, not /// per-window like selection), mirroring `scoped_style_spans`. /// /// Every hint is `AtOffset` (inlay hints are inline by definition) /// carrying `Text` with the (padding-applied) label and the default /// style — the instance has no inlay-specific theme face yet, and a /// fabricated one would be dishonest. Stale store entries are /// suppressed the same way stale diagnostics / semantic tokens are: /// a zero-width hint anchored to pre-edit text is still a byte range /// bug, even though it has no source-byte width of its own. fn scoped_inline_adornments(state: &EditorState, vp: &DeclaredViewport) -> Vec { let core = state.core.borrow(); let Some(uri) = buffer_file_uri(&core, vp.buffer_id) else { return Vec::new(); }; let hints = { let store = state.lsp_manager.borrow().inlay_hint_store(); let guard = store.lock().expect("inlay-hint store mutex poisoned"); if guard.is_stale(&uri) { return Vec::new(); } match guard.for_uri(&uri) { Some(resp) => resp.hints.clone(), None => return Vec::new(), } }; if hints.is_empty() { return Vec::new(); } let registry = core.registry.clone(); let reg = registry.borrow(); let Ok(buf) = reg.get(vp.buffer_id) else { return Vec::new(); }; let source = buffer_source_bytes(buf); let source_len = source.len() as u64; let line_starts = line_start_offsets(&source); let vis_start = vp.visible.start.min(source_len); let vis_end = vp.visible.end.min(source_len); let mut out = Vec::new(); for h in &hints { let at = line_col_to_byte(&line_starts, source_len, h.line, h.col); // An inlay hint occupies no bytes; include it when its anchor // lies within the declared viewport (half-open). if at < vis_start || at >= vis_end { continue; } let mut text = String::new(); if h.padding_left { text.push(' '); } text.push_str(&h.label); if h.padding_right { text.push(' '); } out.push(InlineAdornment { at, placement: AdornmentPlacement::AtOffset, content: AdornmentContent::Text { text, style: Style::default(), }, }); } out } /// Intersect `[lo, hi)` with the declared viewport (itself clamped to /// the source length is the caller's concern for styling; for /// decorations we clamp against the viewport only). `None` when the /// intersection is empty or degenerate. fn clip_to_viewport(lo: u64, hi: u64, vp: &DeclaredViewport) -> Option { let start = lo.max(vp.visible.start); let end = hi.min(vp.visible.end); if end <= start { return None; } Some(ByteRange { start, end }) } /// T M11.4 — the dirty byte intervals between two ordered item sets. /// /// Items are byte-anchored (`range_of` extracts the range). The /// symmetric difference (items in exactly one set, by `==`) bounds /// every byte whose covering set changed; its ranges are coalesced /// into maximal disjoint intervals — the segments the frontend will /// clear and repaint. Empty result ⇒ unchanged ⇒ the caller emits /// nothing. /// /// O(n·m) membership scans: a screenful is a few hundred items, far /// cheaper than re-shipping the whole viewport every frame, and only /// runs when the fast `prev == curr` slice check (caller side, via /// the order-stable producers) would have failed anyway. fn changed_intervals( prev: &[T], curr: &[T], range_of: impl Fn(&T) -> ByteRange, ) -> Vec { let mut changed: Vec = Vec::new(); for p in prev { if !curr.contains(p) { changed.push(range_of(p)); } } for c in curr { if !prev.contains(c) { changed.push(range_of(c)); } } coalesce_ranges(&mut changed) } /// Sort and merge overlapping or touching ranges into maximal /// disjoint intervals. Zero-width ranges are dropped (nothing to /// repaint). Consumes `ranges` (sorts in place). fn coalesce_ranges(ranges: &mut Vec) -> Vec { ranges.retain(|r| r.end > r.start); ranges.sort_by_key(|r| (r.start, r.end)); let mut out: Vec = Vec::new(); for r in ranges.iter().copied() { match out.last_mut() { // Touching (`>=`) merges too: adjacent dirty ranges become // one segment rather than two abutting clears. Some(last) if r.start <= last.end => last.end = last.end.max(r.end), _ => out.push(r), } } out } /// Every span intersecting `iv`, clipped to it, order preserved. fn clip_style_spans(iv: ByteRange, spans: &[StyleSpan]) -> Vec { spans .iter() .filter_map(|s| { let start = s.range.start.max(iv.start); let end = s.range.end.min(iv.end); (end > start).then_some(StyleSpan { range: ByteRange { start, end }, style: s.style, }) }) .collect() } /// Every decoration intersecting `iv`, clipped to it, order preserved. fn clip_decorations(iv: ByteRange, decos: &[Decoration]) -> Vec { decos .iter() .filter_map(|d| { let start = d.range.start.max(iv.start); let end = d.range.end.min(iv.end); (end > start).then_some(Decoration { range: ByteRange { start, end }, kind: d.kind, }) }) .collect() } /// Map an LSP diagnostic severity onto the wire decoration kind. fn severity_to_kind(sev: crate::diag::DiagnosticSeverity) -> DecorationKind { use crate::diag::DiagnosticSeverity as S; match sev { S::Error => DecorationKind::DiagnosticError, S::Warning => DecorationKind::DiagnosticWarning, S::Information => DecorationKind::DiagnosticInfo, S::Hint => DecorationKind::DiagnosticHint, } } /// Resolve `buffer_id` → `file://…` URI, using the same encoding the /// Lua side uses in `file_uri_for` so the result is byte-identical to /// the diag-store / inlay-store / semantic-token-store keys. /// /// **Bug-fix anchor (post-session-5 finding):** the producer used to /// derive this URI from `core.active_buffer_path()` — the *editor's* /// active buffer, not the buffer the frame is projecting. In multi- /// frontend setups (TUI + `pmacs-gpu`) those diverge: each frontend /// has its own active window, and when the daemon renders frontend B's /// frame it temporarily flips `active_frontend` to B but B's active /// buffer may be a scratch buffer with no file path. The diag / /// inlay / semantic-token lookups would then run against the wrong /// URI (or `None`) and return empty, so frontend B got no /// `Decorations` / `InlineAdornments` / LSP-driven `StyleSpans`. /// Routing the URI through `vp.buffer_id` fixes the multi-frontend /// case without disturbing the single-frontend one /// (`active_buffer_id == vp.buffer_id` there, so the resolved path is /// the same). fn buffer_file_uri(core: &crate::editor_core::EditorCore, buffer_id: BufferId) -> Option { let reg = core.registry.borrow(); let buf = reg.get(buffer_id).ok()?; let path = buf.file_path()?; Some(crate::lsp::path_to_file_uri(path)) } /// Snapshot a buffer's bytes (refcount-cheap rope slice, mirroring /// `diag.rs`'s render-time snapshot). fn buffer_source_bytes(buf: &crate::buffer::Buffer) -> Vec { let len = buf.len(); let mut bytes = vec![0u8; len as usize]; if !bytes.is_empty() { buf.snapshot_rope().slice(0, len, &mut bytes); } bytes } /// Byte offset of the start of each line (index 0 = byte 0; one entry /// per line, where a line is a maximal run ended by `\n`). fn line_start_offsets(source: &[u8]) -> Vec { let mut starts = vec![0u64]; for (i, b) in source.iter().enumerate() { if *b == b'\n' { starts.push(i as u64 + 1); } } starts } /// Translate an LSP `(line, col)` to a byte offset. pmacs v0.1 treats /// the LSP column as a byte offset within the line (see /// `crate::diag::Diagnostic`'s field docs); we clamp to the line's /// end and the source length so a stale diagnostic from before an /// edit can never index out of range. fn line_col_to_byte(line_starts: &[u64], source_len: u64, line: u32, col: u32) -> u64 { let li = line as usize; let Some(&line_start) = line_starts.get(li) else { return source_len; }; let line_end = line_starts .get(li + 1) .map_or(source_len, |&next| next.saturating_sub(1)); (line_start + u64::from(col)).min(line_end).min(source_len) } /// Compute the styled byte runs intersecting the declared viewport, /// mapped through the active theme. Spans are clipped to the viewport /// and to the parsed source length; runs that resolve to the default /// style are dropped (wire economy, and consistent with the grid /// path, which skips default-style merges). fn scoped_style_spans(state: &EditorState, vp: &DeclaredViewport) -> Vec { // Policy A — per-language styling authority. A grammar-backed // language (the registry hands out a view only for those) is // styled *solely* by tree-sitter; a language with no bundled // grammar (C/C++, …) is styled *solely* by LSP semantic tokens. // Never both: this is why the no-view branch hands off to the LSP // producer while a grammar-backed buffer whose parse isn't ready // yet returns empty rather than briefly borrowing LSP styling // (which would flicker two authorities on one buffer). let Some(handle) = state.syntax_registry.view(vp.buffer_id) else { return lsp_scoped_style_spans(state, vp); }; let Some(bundle) = handle.current() else { return Vec::new(); }; let Some(query) = state .syntax_registry .highlights_query(&bundle.language_name) else { return Vec::new(); }; let theme = state .syntax_registry .theme() .lock() .expect("theme mutex poisoned") .clone(); let source_len = bundle.source.len() as u64; let vis_start = vp.visible.start.min(source_len); let vis_end = vp.visible.end.min(source_len); if vis_end <= vis_start { return Vec::new(); } let capture_names = query.capture_names(); let highlights = crate::syntax::compute_highlight_spans(&query, &bundle); let mut out = Vec::new(); for hs in highlights { let s = u64::from(hs.start_byte).max(vis_start); let e = u64::from(hs.end_byte).min(vis_end); if e <= s { continue; // No overlap with the viewport. } let Some(name) = capture_names.get(hs.capture_index as usize) else { continue; }; let style = theme.lookup(name); if style == Style::default() { continue; // Nothing to render — skip the wire byte. } out.push(StyleSpan { range: ByteRange { start: s, end: e }, style, }); } out } /// Policy-A fallback for languages with no bundled tree-sitter /// grammar (C/C++, …): project the LSP semantic-token store into the /// same `StyleSpan` shape the tree-sitter path emits, so the existing /// M11.4 diff pipeline (`render_frame`) consumes it unchanged and the /// frontend never learns which producer fed it. The instance stays /// the single styling authority. /// /// `SemanticToken` `start`/`length` are LSP encoding units (UTF-16 /// for clangd's default) and — unlike inlay hints — are *not* /// byte-rewritten upstream, so this converts them per line via the /// owning server's negotiated encoding /// ([`crate::lsp::LspManager::semantic_style_context`]). Tokens are /// single-line by the LSP grammar, so per-line conversion is exact. fn lsp_scoped_style_spans(state: &EditorState, vp: &DeclaredViewport) -> Vec { let core = state.core.borrow(); let Some(uri) = buffer_file_uri(&core, vp.buffer_id) else { return Vec::new(); }; // Styling context (encoding + legend) and the token set resolve // the *same* server for `uri` (both via `for_uri`'s lowest-id // rule), so they describe one coherent source. let mgr = state.lsp_manager.borrow(); let Some(ctx) = mgr.semantic_style_context(&uri) else { return Vec::new(); }; let tokens = { let store = mgr.semantic_token_store(); let guard = store.lock().expect("semantic-token store mutex poisoned"); if guard.is_stale(&uri) { return Vec::new(); } match guard.for_uri(&uri) { Some((_, resp)) => resp.tokens.clone(), None => return Vec::new(), } }; drop(mgr); let registry = core.registry.clone(); let reg = registry.borrow(); let Ok(buf) = reg.get(vp.buffer_id) else { return Vec::new(); }; let source = buffer_source_bytes(buf); let source_len = source.len() as u64; let vis_start = vp.visible.start.min(source_len); let vis_end = vp.visible.end.min(source_len); if vis_end <= vis_start { return Vec::new(); } let line_starts = line_start_offsets(&source); let theme = state .syntax_registry .theme() .lock() .expect("theme mutex poisoned") .clone(); let mut out = Vec::new(); for t in &tokens { let li = t.line as usize; let Some(&ls) = line_starts.get(li) else { continue; // Token line past EOF (stale response) — skip. }; let le = line_starts .get(li + 1) .map_or(source_len, |&n| n.saturating_sub(1)); let Ok(line_text) = std::str::from_utf8(&source[ls as usize..le as usize]) else { continue; // Non-UTF-8 line — cannot do encoded conversion. }; let start_b = ls + crate::lsp::char_to_byte(line_text, t.start, ctx.encoding) as u64; let end_char = t.start.saturating_add(t.length); let end_b = ls + crate::lsp::char_to_byte(line_text, end_char, ctx.encoding) as u64; let s = start_b.max(vis_start); let e = end_b.min(vis_end); if e <= s { continue; // Empty, or no overlap with the viewport. } let Some(name) = ctx .legend .as_ref() .and_then(|lg| lg.type_name(t.token_type)) else { continue; // No legend / unknown type ⇒ cannot name a style. }; let style = theme.lookup(name); if style == Style::default() { continue; // Nothing to render — skip the wire byte (parity // with the tree-sitter path's default-style drop). } out.push(StyleSpan { range: ByteRange { start: s, end: e }, style, }); } out } /// Compute the per-line dominant style summary for the whole buffer: /// one [`Style`] per source line, in line order. The "dominant" style /// for a line is the one covering the most bytes among the styled /// runs (`scoped_style_spans` for the full buffer); a line with no /// styled runs takes [`Style::default`]. Reuses [`scoped_style_spans`] /// so the policy-A authority choice (tree-sitter for grammar-backed /// languages, LSP semantic tokens otherwise) is inherited automatically. /// /// `O(spans × lines)` in the worst case; the caller short-circuits on /// unchanged CRDT generation so this only runs on first sight of a /// buffer or after an edit, not per frame. fn scoped_file_summary(state: &EditorState, buffer_id: BufferId) -> Vec