diff --git a/src/diag.rs b/src/diag.rs index cc2ea7e..0db0b2e 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -35,7 +35,7 @@ use serde_json::Value; use unicode_width::UnicodeWidthChar; use crate::buffer::Buffer; -use crate::cell::{CellCoord, CellGrid, Color, Style, UnderlineStyle}; +use crate::cell::{CellCoord, CellGrid, Color, Glyph, Style, UnderlineStyle}; use crate::overlay::merge_styles; use crate::view::{View, Viewport}; @@ -572,17 +572,47 @@ impl View for DiagnosticView { } } - // Paint the column-0 markers last so a marker is visible even - // when an underline span also touches column 0 (bg and - // underline merge independently). - if max_cols > 0 { - for (row_offset, severity) in line_markers { - let cell = cells.at(CellCoord::new( - cell_origin.row + row_offset, - cell_origin.col, - )); - cell.style = merge_styles(cell.style, marker_style_for(severity)); - } + // Paint the per-line severity markers last so one is visible even + // when an underline span also touches the same cell. + paint_line_markers( + cells, + cell_origin, + viewport.gutter_w, + max_cols, + &line_markers, + ); + } +} + +/// Paint one severity marker per diagnostic line (UX gutter sub-arc 2). +/// +/// When the window reserves a gutter (`gutter_w > 0`), draw the severity +/// *sign glyph* in the gutter's leading column (`cell_origin.col - +/// gutter_w`, i.e. window column 0), colored by severity. Without a gutter, +/// fall back to the legacy column-0 *background* marker on the line's first +/// text cell — the "fake gutter" that predates a real gutter column. +fn paint_line_markers( + cells: &mut CellGrid<'_>, + cell_origin: CellCoord, + gutter_w: u32, + max_cols: u32, + line_markers: &std::collections::HashMap, +) { + for (&row_offset, &severity) in line_markers { + let row = cell_origin.row + row_offset; + if gutter_w > 0 { + let cell = cells.at(CellCoord::new( + row, + cell_origin.col.saturating_sub(gutter_w), + )); + cell.glyph = Glyph::Char(severity.gutter_glyph()); + cell.style = Style { + fg: severity.underline_color(), + ..Style::default() + }; + } else if max_cols > 0 { + let cell = cells.at(CellCoord::new(row, cell_origin.col)); + cell.style = merge_styles(cell.style, marker_style_for(severity)); } } } @@ -968,6 +998,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 10), + gutter_w: 0, }, &mut grid, ); @@ -1031,6 +1062,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(3, 10), + gutter_w: 0, }, &mut grid, ); @@ -1054,6 +1086,80 @@ mod tests { assert_eq!(grid.get(CellCoord::new(2, 0)).style.bg, Color::Default); } + #[test] + fn gutter_sign_replaces_the_column_marker_when_a_gutter_is_reserved() { + use crate::cell::{Cell, CellSize, Glyph, UnderlineStyle}; + + let store = make_shared_store(); + { + let mut guard = store.lock().expect("diag store"); + guard.set( + "file:///a", + vec![ + // Line 0: Hint + Error overlap → the sign shows Error. + diag(0, DiagnosticSeverity::Hint, "h"), + diag(0, DiagnosticSeverity::Error, "e"), + // Line 1: zero-width Warning (invisible to underline). + Diagnostic { + start_line: 1, + start_col: 2, + end_line: 1, + end_col: 2, + severity: DiagnosticSeverity::Warning, + message: "w".to_owned(), + source: None, + code: None, + }, + ], + ); + } + + let mut buf = Buffer::new(crate::buffer::BufferId::next(), "test.c"); + buf.apply_edit(crate::buffer::EditOp::Insert { + pos: 0, + bytes: b"hello\nworld\nclean\n", + }) + .expect("seed buffer"); + + // A 2-cell gutter: text is shifted to column 2, signs land at + // window column 0 (`cell_origin.col - gutter_w`). + let mut view = DiagnosticView::new("file:///a", store); + let mut backing = vec![Cell::default(); 30]; + let mut grid = CellGrid { + cells: &mut backing, + stride: 10, + size: CellSize::new(3, 10), + }; + view.render( + &buf, + Viewport { + buffer_start: 0, + buffer_end: buf.len(), + cell_origin: CellCoord::new(0, 2), + cell_size: CellSize::new(3, 8), + gutter_w: 2, + }, + &mut grid, + ); + + // Line 0: Error sign glyph 'E' in red at the gutter's leading col. + assert_eq!(grid.get(CellCoord::new(0, 0)).glyph, Glyph::Char('E')); + assert_eq!(grid.get(CellCoord::new(0, 0)).style.fg, Color::Indexed(1)); + // Line 1: Warning sign 'W' in yellow. + assert_eq!(grid.get(CellCoord::new(1, 0)).glyph, Glyph::Char('W')); + assert_eq!(grid.get(CellCoord::new(1, 0)).style.fg, Color::Indexed(3)); + // The legacy background marker on the first *text* cell is NOT + // painted when a gutter carries the sign instead. + assert_eq!(grid.get(CellCoord::new(0, 2)).style.bg, Color::Default); + // The squiggle still lands in the shifted text area (col 2 + 2). + assert_eq!( + grid.get(CellCoord::new(1, 4)).style.underline, + UnderlineStyle::Curly + ); + // Line 2: clean — no sign glyph. + assert_eq!(grid.get(CellCoord::new(2, 0)).glyph, Glyph::Char(' ')); + } + #[test] fn end_of_line_zero_width_error_squiggles_the_cell_past_eol() { // The missing-comma shape: rust-analyzer anchors "expected @@ -1099,6 +1205,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(2, 10), + gutter_w: 0, }, &mut grid, ); diff --git a/src/editor.rs b/src/editor.rs index 41108c9..cdf4ce7 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1635,16 +1635,20 @@ pub fn paint_frame( buffer_end: buf.len(), cell_origin: CellCoord::new(rect.origin.row, rect.origin.col + gutter_w), cell_size: crate::cell::CellSize::new(inner_rows, rect.size.cols - gutter_w), + gutter_w, }; - // Composition (T M2.9): base text_view paints first, then - // each overlay in attach order. See [`crate::view::View`]. + // Composition (T M2.9): base text_view paints first, then the + // gutter numbers — before the overlays, so a diagnostic overlay + // can draw its severity sign into the gutter's leading column + // without the gutter's own blank pass erasing it — then each + // overlay in attach order. See [`crate::view::View`]. window.text_view.render(buf, viewport, grid); - for overlay in &mut window.overlays { - overlay.render(buf, viewport, grid); - } if gutter_w > 0 { paint_line_number_gutter(grid, window, &rect, inner_rows, gutter_w); } + for overlay in &mut window.overlays { + overlay.render(buf, viewport, grid); + } paint_local_selection(grid, buf, window, &rect, inner_rows, gutter_w); // Mode line for this window. Painted last so the line // itself is always visible regardless of overlay activity. @@ -4839,6 +4843,7 @@ mod tests { buffer_end: buf.len(), cell_origin: rect.origin, cell_size: CellSize::new(rect.size.rows, rect.size.cols), + gutter_w: 0, }; let mut grid = CellGrid { cells: &mut backing, @@ -4972,6 +4977,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(24, 80), + gutter_w: 0, }; // Two no-op overlays: probe the dispatch cost only. diff --git a/src/highlight.rs b/src/highlight.rs index 3f06e89..bce8c71 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -861,6 +861,7 @@ mod tests { buffer_end: u64::MAX, cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 20), + gutter_w: 0, }; let registry = state.core.borrow().registry.clone(); let reg = registry.borrow(); @@ -957,6 +958,7 @@ mod tests { buffer_end: u64::MAX, cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 20), + gutter_w: 0, }; let registry = state.core.borrow().registry.clone(); let reg = registry.borrow(); @@ -1075,6 +1077,7 @@ mod tests { buffer_end: u64::MAX, cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 20), + gutter_w: 0, }; let registry = state.core.borrow().registry.clone(); let reg = registry.borrow(); diff --git a/src/overlay.rs b/src/overlay.rs index 8b96fdc..c079bb2 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -431,6 +431,7 @@ mod tests { buffer_end: u64::MAX, cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(rows, cols), + gutter_w: 0, } } diff --git a/src/search.rs b/src/search.rs index 2fbffa7..1e486ec 100644 --- a/src/search.rs +++ b/src/search.rs @@ -590,6 +590,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 10), + gutter_w: 0, }, &mut grid, ); @@ -617,6 +618,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 10), + gutter_w: 0, }, &mut grid2, ); @@ -657,6 +659,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(rows, cols), + gutter_w: 0, }, &mut grid, ); diff --git a/src/text_view.rs b/src/text_view.rs index b552ae4..7f22daa 100644 --- a/src/text_view.rs +++ b/src/text_view.rs @@ -564,6 +564,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 16), + gutter_w: 0, }, &mut grid, ); @@ -591,6 +592,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 16), + gutter_w: 0, }, &mut grid, ); @@ -621,6 +623,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(5, 5), + gutter_w: 0, }, &mut grid, ); @@ -652,6 +655,7 @@ mod tests { buffer_end: buf.len(), cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(1, 5), + gutter_w: 0, }, &mut grid, ); diff --git a/src/view.rs b/src/view.rs index 3f81367..139c599 100644 --- a/src/view.rs +++ b/src/view.rs @@ -136,6 +136,12 @@ pub struct Viewport { pub cell_origin: CellCoord, /// Number of cells the viewport occupies. pub cell_size: CellSize, + /// Width of the line-number gutter reserved to the *left* of + /// `cell_origin` (UX gutter arc). `0` when no gutter. Overlays that + /// want to draw in the gutter (e.g. the diagnostic sign) reach it at + /// `cell_origin.col - gutter_w`; overlays that only touch the text area + /// ignore it (the origin is already shifted past the gutter). + pub gutter_w: u32, } // --------------------------------------------------------------------------- diff --git a/tests/m4_acceptance.rs b/tests/m4_acceptance.rs index 9e1dfd9..bfa07d4 100644 --- a/tests/m4_acceptance.rs +++ b/tests/m4_acceptance.rs @@ -495,6 +495,7 @@ fn render_active_window_to_grid( buffer_end: buf.len(), cell_origin: rect.origin, cell_size: CellSize::new(rect.size.rows, rect.size.cols), + gutter_w: 0, }; let mut grid = CellGrid { cells: &mut backing,