pmacs-gpu: optimistically clear a diagnostic when an edit touches it

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) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-06-15 19:00:25 -04:00
parent 4d71bf7455
commit 77f76f14b0
1 changed files with 60 additions and 0 deletions

View File

@ -4381,6 +4381,19 @@ fn translate_style_spans(spans: &mut Vec<StyleSpan>, edit: TextProjectionEdit) {
fn translate_decorations(decorations: &mut Vec<Decoration>, 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<Decoration>, 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));