feat: add opt-in trailing whitespace highlighting
This commit is contained in:
+125
@@ -1865,6 +1865,9 @@ fn draw_document(
|
||||
area,
|
||||
segment.start,
|
||||
);
|
||||
if editor.highlight_trailing_whitespace() {
|
||||
paint_trailing_whitespace(frame, line, area, segment.start);
|
||||
}
|
||||
paint_editor_line(
|
||||
frame,
|
||||
editor,
|
||||
@@ -2212,6 +2215,21 @@ fn paint_cells(frame: &mut Frame, area: Rect, left: usize, start: usize, end: us
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_trailing_whitespace(frame: &mut Frame, line: &str, area: Rect, left: usize) {
|
||||
let prefix = line.trim_end_matches(char::is_whitespace);
|
||||
if prefix.len() == line.len() {
|
||||
return;
|
||||
}
|
||||
paint_cells(
|
||||
frame,
|
||||
area,
|
||||
left,
|
||||
display_column(prefix, usize::MAX),
|
||||
display_column(line, usize::MAX),
|
||||
Style::default().bg(Color::Red),
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_grapheme_range(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
@@ -2919,6 +2937,7 @@ fn draw_diff(
|
||||
),
|
||||
wrapped,
|
||||
panels.diff_colors,
|
||||
editor.is_some_and(Editor::highlight_trailing_whitespace),
|
||||
);
|
||||
text_widths[0] = text_area.width;
|
||||
if let Some(editor) =
|
||||
@@ -2955,6 +2974,7 @@ fn draw_diff(
|
||||
),
|
||||
wrapped,
|
||||
panels.diff_colors,
|
||||
editor.is_some_and(Editor::highlight_trailing_whitespace),
|
||||
);
|
||||
text_widths[1] = text_area.width;
|
||||
if let Some(editor) =
|
||||
@@ -3109,6 +3129,7 @@ fn draw_diff_pane(
|
||||
viewport: (usize, &mut usize, Option<u16>),
|
||||
wrapped: Option<&[WrappedDiffRow]>,
|
||||
colors: DiffColors,
|
||||
highlight_trailing_whitespace: bool,
|
||||
) -> Rect {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return area;
|
||||
@@ -3281,6 +3302,9 @@ fn draw_diff_pane(
|
||||
}
|
||||
}
|
||||
}
|
||||
if highlight_trailing_whitespace {
|
||||
paint_trailing_whitespace(frame, &text, text_area, left);
|
||||
}
|
||||
}
|
||||
Rect::new(
|
||||
content.x + gutter as u16,
|
||||
@@ -4859,6 +4883,107 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_whitespace_is_opt_in_and_preserves_wrapping_and_selection() {
|
||||
let directory = Directory::new();
|
||||
let source = "a b\t \u{3000}\n \t\n\nabcdefg ";
|
||||
let path = directory.file("whitespace.txt", source);
|
||||
let config = directory.file(
|
||||
"config.rb",
|
||||
"Vamp.configure { |c| c.highlight_trailing_whitespace = true; c.git_gutter = 'none' }",
|
||||
);
|
||||
let mut session = EditorSession::open(Some(path), vec![], Some(&config)).unwrap();
|
||||
session.execute("word_wrap.toggle");
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(10, 10)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (y, range) in [(0, 5..9), (1, 2..6), (3, 9..10), (4, 2..3)] {
|
||||
for x in 2..10 {
|
||||
// The backend skips the covered cell of a wide grapheme.
|
||||
if (x, y) == (8, 0) {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(
|
||||
buffer[(x, y)].bg == Color::Red,
|
||||
range.contains(&x),
|
||||
"({x}, {y})"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(buffer[(2, 2)].bg, Color::Reset);
|
||||
session.handle(Input::End);
|
||||
session.handle(Input::Char('v'));
|
||||
session.handle(Input::Left);
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.backend().buffer()[(6, 0)].bg, Color::Blue);
|
||||
assert_eq!(session.editor().lines().join("\n"), source);
|
||||
assert!(!session.editor().dirty());
|
||||
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace("x \t".into())])
|
||||
.unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_ne!(terminal.backend().buffer()[(7, 0)].bg, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_whitespace_covers_both_diff_sides_and_unified_rows() {
|
||||
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||
for wrapped in [false, true] {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_diff_display_options(mode, crate::panes::SplitAxis::Vertical);
|
||||
install_readonly_diff(
|
||||
&mut editor,
|
||||
syntax_diff_file("whitespace.txt", "old \t", "new \t"),
|
||||
);
|
||||
editor.set_highlight_trailing_whitespace(true);
|
||||
if wrapped {
|
||||
editor.execute("word_wrap.toggle");
|
||||
}
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(80, 15)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let buffer = terminal.backend().buffer();
|
||||
for word in ["old", "new"] {
|
||||
let start = buffer
|
||||
.content
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(index, cell)| {
|
||||
(cell.symbol() == &word[..1]
|
||||
&& buffer
|
||||
.content
|
||||
.get(index + 1)
|
||||
.is_some_and(|cell| cell.symbol() == &word[1..2])
|
||||
&& buffer
|
||||
.content
|
||||
.get(index + 2)
|
||||
.is_some_and(|cell| cell.symbol() == &word[2..3]))
|
||||
.then_some(index)
|
||||
})
|
||||
.unwrap();
|
||||
for offset in 3..8 {
|
||||
assert_eq!(
|
||||
buffer.content[start + offset].bg,
|
||||
Color::Red,
|
||||
"{mode:?} {wrapped} {word}"
|
||||
);
|
||||
}
|
||||
assert_ne!(buffer.content[start + 8].bg, Color::Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_distance_follows_document_and_diff_visual_rows_and_columns() {
|
||||
let directory = Directory::new();
|
||||
|
||||
Reference in New Issue
Block a user