From afd5e80466d2922bf0269dd91c05b74f034cd832 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 12 Jun 2026 10:24:31 -0400 Subject: [PATCH] producer: widen zero-width diagnostics to a visible byte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User validation found nothing rendered in pmacs-gpu for the missing-comma error — rust-analyzer's zero-width EOL anchor. The TUI's anchor-cell fix lives in DiagnosticView, but the semantic wire ships Decorations straight from line/col conversion: a zero-width range clips to None at the frontend and overlaps no glyph, so the GPU drew nothing. Widen at the producer (one byte; forward mid-line, backward at EOL where forward covers only the glyph-less newline) so every semantic frontend gets a paintable range. Co-Authored-By: Claude Fable 5 --- src/semantic_render.rs | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/semantic_render.rs b/src/semantic_render.rs index e86a257..ba77da7 100644 --- a/src/semantic_render.rs +++ b/src/semantic_render.rs @@ -638,6 +638,8 @@ impl SemanticRenderState { for d in &diags { let lo = line_col_to_byte(line_starts, source_len, d.start_line, d.start_col); let hi = line_col_to_byte(line_starts, source_len, d.end_line, d.end_col); + let (lo, hi) = + widen_zero_width_diag(lo, hi, d.start_line, line_starts, source_len); if let Some(range) = clip_to_viewport(lo, hi, vp) { out.push(Decoration { range, @@ -733,6 +735,38 @@ fn scoped_inline_adornments(state: &EditorState, vp: &DeclaredViewport) -> Vec (u64, u64) { + if hi > lo { + return (lo, hi); + } + // Content end excludes the trailing newline, same semantics as + // the summary's per-line ranges. + let content_end = line_starts + .get(start_line as usize + 1) + .map_or(source_len, |&next| next.saturating_sub(1)); + if lo >= content_end { + (lo.saturating_sub(1), lo) + } else { + (lo, (lo + 1).min(source_len)) + } +} + 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); @@ -2772,6 +2806,22 @@ mod tests { assert_eq!(lines[3], Style::default(), "trailing empty line → default"); } + #[test] + fn zero_width_diagnostics_widen_to_a_visible_byte() { + // "abc\nde" — line starts [0, 4], source_len 6; line 0 + // content is bytes [0, 3) (newline excluded). + let ls = vec![0u64, 4]; + // Mid-line anchor: widen forward. + assert_eq!(widen_zero_width_diag(1, 1, 0, &ls, 6), (1, 2)); + // End-of-line anchor (the rust-analyzer "expected COMMA" + // shape): widen backward — forward would cover only the \n. + assert_eq!(widen_zero_width_diag(3, 3, 0, &ls, 6), (2, 3)); + // End-of-file anchor on the last line. + assert_eq!(widen_zero_width_diag(6, 6, 1, &ls, 6), (5, 6)); + // Non-empty ranges pass through untouched. + assert_eq!(widen_zero_width_diag(1, 3, 0, &ls, 6), (1, 3)); + } + #[test] fn file_style_summary_marks_diagnostic_lines_and_refreshes_on_republish() { use crate::cell::Color;