From 77f76f14b0e3ef9c05f5834e6d272daa032e7e98 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 15 Jun 2026 19:00:25 -0400 Subject: [PATCH] pmacs-gpu: optimistically clear a diagnostic when an edit touches it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The squiggle was holding a stale wave over corrected text until rust-analyzer re-analyzed and republished (0.5-2s for a fixed error) — the daemon's hold-while-stale carries the diagnostic through the stale window and the GPU translates it through local edits, so a fixed line kept its red squiggle until republish. (The 2px bar did this too; the bold wave just made it glaring.) translate_decorations now drops a diagnostic decoration the moment a local edit touches its range, instead of translating it: editing or fixing an error clears that squiggle immediately. Scoped to the touched diagnostic — an error elsewhere still translates and holds, preserving the no-blink benefit — and to diagnostic kinds only, so selection / current-line decorations are untouched. If the error survives the edit, the next republish re-adds it. Co-Authored-By: Claude Opus 4.8 (1M context) --- pmacs-gpu/src/main.rs | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 9edaf66..cddab83 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -4381,6 +4381,19 @@ fn translate_style_spans(spans: &mut Vec, edit: TextProjectionEdit) { fn translate_decorations(decorations: &mut Vec, edit: TextProjectionEdit) { let mut translated = Vec::with_capacity(decorations.len()); for mut decoration in decorations.drain(..) { + // Optimistic clear (diagnostics only): an edit that touches a + // diagnostic's range invalidates it locally, so drop the + // squiggle now instead of holding a stale wave over the text + // you just changed until the LSP re-analyzes and republishes. + // Scoped to the *touched* diagnostic — an error elsewhere + // still translates and holds, so the no-blink benefit of the + // producer's hold-while-stale survives. Non-diagnostic + // decorations (selection / current-line) always translate. + if decoration_kind_to_underline_color(decoration.kind).is_some() + && edit_touches_range(decoration.range, edit) + { + continue; + } if let Some(range) = translate_byte_range(decoration.range, edit) { decoration.range = range; translated.push(decoration); @@ -4389,6 +4402,14 @@ fn translate_decorations(decorations: &mut Vec, edit: TextProjection *decorations = translated; } +/// True when `edit` touches `range` in the pre-edit coordinate space. +/// An insert (`old_end == start`) touches when its point lies within +/// `[start, end]` (inclusive — typing at either edge of an error +/// token counts); a delete/replace touches when its span overlaps. +fn edit_touches_range(range: ByteRange, edit: TextProjectionEdit) -> bool { + edit.start <= range.end && range.start <= edit.old_end +} + fn translate_inline_adornments(adornments: &mut [InlineAdornment], edit: TextProjectionEdit) { for adornment in adornments { adornment.at = translate_byte_position(adornment.at, edit); @@ -5496,6 +5517,45 @@ mod tests { ); } + #[test] + fn editing_a_diagnostic_clears_it_optimistically_but_holds_untouched_ones() { + let diag = |start, end| Decoration { + range: ByteRange { start, end }, + kind: DecorationKind::DiagnosticError, + }; + // Insert the missing char right at the 1-byte widened anchor of + // an end-of-line "expected COMMA": the squiggle clears now. + let mut decos = vec![diag(11, 12), diag(40, 50)]; + translate_decorations( + &mut decos, + TextProjectionEdit { + start: 11, + old_end: 11, + inserted_len: 1, + }, + ); + // The touched diagnostic is gone; the far one is kept (shifted + // right by the insert) — hold-while-stale still applies to it. + assert_eq!(decos.len(), 1, "only the touched diagnostic clears"); + assert_eq!(decos[0].range, ByteRange { start: 41, end: 51 }); + + // A non-diagnostic decoration over the edited region is never + // dropped — selection / current-line translate as before. + let mut sel = vec![Decoration { + range: ByteRange { start: 8, end: 14 }, + kind: DecorationKind::Selection, + }]; + translate_decorations( + &mut sel, + TextProjectionEdit { + start: 10, + old_end: 10, + inserted_len: 2, + }, + ); + assert_eq!(sel.len(), 1, "selection survives an edit in its range"); + } + #[test] fn source_line_range_handles_empty_and_leading_newline() { assert_eq!(source_line_range("", 0), (0, 0));