diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 2992310..ad17dc3 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -3071,7 +3071,15 @@ fn advance_minimap_col(col: usize, ch: char) -> usize { } fn minimap_style_color(style: CellStyle) -> [f32; 4] { - match style.fg { + // A set underline_color is the producer's diagnostic mark for the + // line (protocol v6, T M4.6 parity) — the minimap's gutter sign. + // It outranks the syntax-dominant fg so error/warning lines read + // at a glance. + let color = match style.underline_color { + CellColor::Default => style.fg, + marked => marked, + }; + match color { CellColor::Default => MINIMAP_DEFAULT_LINE, CellColor::Rgb(r, g, b) => rgb_to_minimap_color(r, g, b), CellColor::Indexed(idx) => { diff --git a/src/diag.rs b/src/diag.rs index 994569b..97b30c7 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -81,6 +81,21 @@ impl DiagnosticSeverity { } } + /// Canonical severity color (T M4.6 / protocol v6): the + /// `underline_color` of this severity's squiggle, the column-0 + /// marker background, and the minimap mark all share it. Indexed + /// 1/3/6/8 (red / yellow / cyan / gray) for 8/16-color terminal + /// portability. + #[must_use] + pub fn underline_color(self) -> Color { + match self { + Self::Error => Color::Indexed(1), + Self::Warning => Color::Indexed(3), + Self::Information => Color::Indexed(6), + Self::Hint => Color::Indexed(8), + } + } + fn from_lsp_value(v: Option<&Value>) -> Self { match v.and_then(Value::as_i64) { Some(1) => Self::Error, @@ -187,6 +202,12 @@ pub struct DiagnosticStore { /// here on the assumption that a fresh `publishDiagnostics` /// corresponds to the latest sent version. stale_uris: std::collections::HashSet, + /// Per-URI change counter, bumped on every [`Self::set`] / + /// [`Self::clear`]. Diagnostics arrive without a CRDT generation + /// bump, so generation-keyed caches (the `FileStyleSummary` + /// producer's) additionally key on this to know a republish + /// happened (T M4.6 GPU parity). + epochs: HashMap, } impl DiagnosticStore { @@ -207,6 +228,7 @@ impl DiagnosticStore { diags.sort_by(Diagnostic::compare_by_position); let uri = uri.into(); self.stale_uris.remove(&uri); + *self.epochs.entry(uri.clone()).or_insert(0) += 1; if diags.is_empty() { self.by_uri.remove(&uri); } else { @@ -219,6 +241,16 @@ impl DiagnosticStore { pub fn clear(&mut self, uri: &str) { self.by_uri.remove(uri); self.stale_uris.remove(uri); + *self.epochs.entry(uri.to_owned()).or_insert(0) += 1; + } + + /// Monotonic per-URI change counter: how many times `set` / + /// `clear` ran for this URI. `0` for a URI never written. + /// Consumers cache against this to detect republishes that no + /// CRDT generation bump announces. + #[must_use] + pub fn epoch_for(&self, uri: &str) -> u64 { + self.epochs.get(uri).copied().unwrap_or(0) } /// Mark `uri`'s stored diagnostics as stale (T M11.8). Called @@ -338,8 +370,7 @@ const TAB_WIDTH: u32 = 8; fn error_style() -> Style { Style { underline: UnderlineStyle::Curly, - // Indexed 1 (red) is portable across 8/16-color terminals. - underline_color: Color::Indexed(1), + underline_color: DiagnosticSeverity::Error.underline_color(), ..Style::default() } } @@ -348,8 +379,7 @@ fn error_style() -> Style { fn warning_style() -> Style { Style { underline: UnderlineStyle::Curly, - // Indexed 3: yellow. - underline_color: Color::Indexed(3), + underline_color: DiagnosticSeverity::Warning.underline_color(), ..Style::default() } } @@ -358,8 +388,7 @@ fn warning_style() -> Style { fn info_style() -> Style { Style { underline: UnderlineStyle::Single, - // Indexed 6: cyan. - underline_color: Color::Indexed(6), + underline_color: DiagnosticSeverity::Information.underline_color(), ..Style::default() } } @@ -368,9 +397,7 @@ fn info_style() -> Style { fn hint_style() -> Style { Style { underline: UnderlineStyle::Dotted, - // Indexed 8: bright black ("gray") — present on 16-color - // terminals, subtle by design for hints. - underline_color: Color::Indexed(8), + underline_color: DiagnosticSeverity::Hint.underline_color(), ..Style::default() } } @@ -391,14 +418,8 @@ fn style_for(severity: DiagnosticSeverity) -> Style { /// style-only), and zero-width diagnostics — invisible to the /// underline pass — still get a visible artifact. fn marker_style_for(severity: DiagnosticSeverity) -> Style { - let bg = match severity { - DiagnosticSeverity::Error => Color::Indexed(1), - DiagnosticSeverity::Warning => Color::Indexed(3), - DiagnosticSeverity::Information => Color::Indexed(6), - DiagnosticSeverity::Hint => Color::Indexed(8), - }; Style { - bg, + bg: severity.underline_color(), ..Style::default() } } @@ -881,6 +902,24 @@ mod tests { } } + #[test] + fn epoch_bumps_on_set_and_clear_per_uri() { + let mut store = DiagnosticStore::new(); + assert_eq!(store.epoch_for("file:///a"), 0); + store.set("file:///a", vec![diag(0, DiagnosticSeverity::Error, "x")]); + assert_eq!(store.epoch_for("file:///a"), 1); + // An empty set (server reports clean) still counts — the + // consumer must refresh to drop its marks. + store.set("file:///a", vec![]); + assert_eq!(store.epoch_for("file:///a"), 2); + store.clear("file:///a"); + assert_eq!(store.epoch_for("file:///a"), 3); + // mark_stale is not a content change; other URIs are isolated. + store.mark_stale("file:///a"); + assert_eq!(store.epoch_for("file:///a"), 3); + assert_eq!(store.epoch_for("file:///b"), 0); + } + #[test] fn view_advertises_diagnostic_kind() { // `pmacs.window._overlay_kinds()` introspection (task #23 wire-up, diff --git a/src/semantic_render.rs b/src/semantic_render.rs index aee239b..e86a257 100644 --- a/src/semantic_render.rs +++ b/src/semantic_render.rs @@ -113,7 +113,11 @@ pub struct SemanticRenderState { /// 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, + /// `(crdt_generation, diag_epoch)` the last emitted summary was + /// computed against. Diagnostics arrive without a generation + /// bump, so the epoch half catches republishes (minimap marks, + /// T M4.6 GPU parity). + last_summary: 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) @@ -464,11 +468,17 @@ impl SemanticRenderState { if grammar_style_parse_not_ready(state, buffer_id) { return None; } - if self.last_summary.get(&buffer_id).copied() == Some(generation) { + // Diagnostics fold into the summary (minimap marks) but + // publish without a generation bump — key the cache on the + // diag store's per-URI epoch as well, so a republish + // refreshes the marks and anything else stays suppressed. + let diag_epoch = diagnostics_epoch(state, buffer_id); + if self.last_summary.get(&buffer_id).copied() == Some((generation, diag_epoch)) { return None; } let lines = scoped_file_summary(state, buffer_id); - self.last_summary.insert(buffer_id, generation); + self.last_summary + .insert(buffer_id, (generation, diag_epoch)); Some(InstanceMessage::FileStyleSummary { buffer_id, generation, @@ -838,6 +848,19 @@ fn diagnostics_store_stale(state: &EditorState, buffer_id: BufferId) -> bool { guard.is_stale(&uri) } +/// The diag store's per-URI change epoch for `buffer_id`'s file, `0` +/// for buffers with no file URI or no diagnostics history. Keys the +/// `FileStyleSummary` cache (see [`SemanticRenderState::last_summary`]). +fn diagnostics_epoch(state: &EditorState, buffer_id: BufferId) -> u64 { + let core = state.core.borrow(); + let Some(uri) = buffer_file_uri(&core, buffer_id) else { + return 0; + }; + let store = state.lsp_manager.borrow().diag_store(); + let guard = store.lock().expect("diag store mutex poisoned"); + guard.epoch_for(&uri) +} + /// Style-family staleness for the LSP-token authority. True only for /// a buffer with **no** tree-sitter view (policy A routes those /// through `lsp_scoped_style_spans`) whose semantic-token store entry @@ -1182,6 +1205,9 @@ fn scoped_file_summary(state: &EditorState, buffer_id: BufferId) -> Vec