feat: add configurable whitespace markers
This commit is contained in:
+104
-3
@@ -995,6 +995,15 @@ fn display_column(line: &str, grapheme_column: usize) -> usize {
|
||||
}
|
||||
|
||||
fn visible_line(line: &str, left: usize, width: usize) -> String {
|
||||
visible_text(line, left, width, None)
|
||||
}
|
||||
|
||||
fn visible_text(
|
||||
line: &str,
|
||||
left: usize,
|
||||
width: usize,
|
||||
whitespace: Option<&BTreeMap<char, char>>,
|
||||
) -> String {
|
||||
let mut visible = String::new();
|
||||
let mut column = 0;
|
||||
let right = left.saturating_add(width);
|
||||
@@ -1009,6 +1018,19 @@ fn visible_line(line: &str, left: usize, width: usize) -> String {
|
||||
break;
|
||||
}
|
||||
if end > left || (cells == 0 && column >= left) {
|
||||
let marker = whitespace.and_then(|characters| {
|
||||
let ch = grapheme.chars().next()?;
|
||||
(grapheme.len() == ch.len_utf8())
|
||||
.then(|| characters.get(&ch))
|
||||
.flatten()
|
||||
});
|
||||
if let Some(&marker) = marker {
|
||||
for cell in column.max(left)..end.min(right) {
|
||||
visible.push(if cell == column { marker } else { ' ' });
|
||||
}
|
||||
column = end;
|
||||
continue;
|
||||
}
|
||||
if grapheme == "\t" || column < left || end > right {
|
||||
for _ in column.max(left)..end.min(right) {
|
||||
visible.push(' ');
|
||||
@@ -1834,10 +1856,11 @@ fn draw_document(
|
||||
.visible
|
||||
.iter()
|
||||
.map(|&(row, _, segment)| {
|
||||
Line::from(visible_line(
|
||||
Line::from(visible_text(
|
||||
&editor.lines()[row],
|
||||
segment.start,
|
||||
segment.end.saturating_sub(segment.start).min(width),
|
||||
Some(editor.whitespace_characters()),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
@@ -2938,6 +2961,7 @@ fn draw_diff(
|
||||
wrapped,
|
||||
panels.diff_colors,
|
||||
editor.is_some_and(Editor::highlight_trailing_whitespace),
|
||||
editor.map(Editor::whitespace_characters),
|
||||
);
|
||||
text_widths[0] = text_area.width;
|
||||
if let Some(editor) =
|
||||
@@ -2975,6 +2999,7 @@ fn draw_diff(
|
||||
wrapped,
|
||||
panels.diff_colors,
|
||||
editor.is_some_and(Editor::highlight_trailing_whitespace),
|
||||
editor.map(Editor::whitespace_characters),
|
||||
);
|
||||
text_widths[1] = text_area.width;
|
||||
if let Some(editor) =
|
||||
@@ -3130,6 +3155,7 @@ fn draw_diff_pane(
|
||||
wrapped: Option<&[WrappedDiffRow]>,
|
||||
colors: DiffColors,
|
||||
highlight_trailing_whitespace: bool,
|
||||
whitespace_characters: Option<&BTreeMap<char, char>>,
|
||||
) -> Rect {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return area;
|
||||
@@ -3237,8 +3263,13 @@ fn draw_diff_pane(
|
||||
Cow::Borrowed(source_text.as_str())
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(visible_line(&text, left, text_width.min(width)))
|
||||
.style(Style::default().fg(Color::Reset).bg(background)),
|
||||
Paragraph::new(visible_text(
|
||||
&text,
|
||||
left,
|
||||
text_width.min(width),
|
||||
whitespace_characters,
|
||||
))
|
||||
.style(Style::default().fg(Color::Reset).bg(background)),
|
||||
Rect::new(content.x + gutter as u16, row_area.y, width as u16, 1),
|
||||
);
|
||||
if gutter > 0 && wrapped.is_none_or(|rows| rows[screen].position.continuation == 0) {
|
||||
@@ -4883,6 +4914,73 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_characters_preserve_source_geometry_and_overlay_styles() {
|
||||
let characters =
|
||||
BTreeMap::from([(' ', '·'), ('\t', '→'), ('\u{a0}', '⍽'), ('\u{3000}', '_')]);
|
||||
let source = "a \tb\u{a0}\u{3000} c";
|
||||
assert_eq!(visible_text(source, 0, 20, Some(&characters)), "a·→ b⍽_ ·c");
|
||||
assert_eq!(visible_text(source, 3, 6, Some(&characters)), " b⍽_ ·");
|
||||
assert_eq!(visible_text(source, 7, 2, Some(&characters)), " ·");
|
||||
assert_eq!(visible_text(source, 6, 1, Some(&characters)), "_");
|
||||
assert_eq!(
|
||||
visible_text(" \u{301}", 0, 4, Some(&characters)),
|
||||
" \u{301}"
|
||||
);
|
||||
assert_eq!(visible_text("", 0, 4, Some(&characters)), "");
|
||||
|
||||
let directory = Directory::new();
|
||||
let source = "a b\t \u{3000}\n \t\n\nabcdefg ";
|
||||
let path = directory.file("markers.txt", source);
|
||||
let config = directory.file(
|
||||
"config.rb",
|
||||
r#"Vamp.configure { |c| c.whitespace_characters = { " " => "·", "\t" => "→", "\u3000" => "_" }; c.highlight_trailing_whitespace = true; c.git_gutter = "none" }"#,
|
||||
);
|
||||
let mut session = EditorSession::open(Some(path.clone()), 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 (x, y, marker) in [
|
||||
(3, 0, "·"),
|
||||
(5, 0, "→"),
|
||||
(6, 0, "·"),
|
||||
(7, 0, "_"),
|
||||
(2, 1, "·"),
|
||||
(3, 1, "→"),
|
||||
(9, 3, "·"),
|
||||
(2, 4, "·"),
|
||||
] {
|
||||
assert_eq!(buffer[(x, y)].symbol(), marker, "({x}, {y})");
|
||||
}
|
||||
assert_eq!(buffer[(8, 0)].symbol(), " ");
|
||||
assert_eq!(buffer[(7, 0)].bg, Color::Red);
|
||||
assert_eq!(buffer[(2, 2)].symbol(), " ");
|
||||
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)].symbol(), "·");
|
||||
assert_eq!(terminal.backend().buffer()[(6, 0)].bg, Color::Blue);
|
||||
assert_eq!(session.editor().lines().join("\n"), source);
|
||||
assert!(!session.editor().dirty());
|
||||
assert_eq!(fs::read_to_string(path).unwrap(), source);
|
||||
|
||||
session
|
||||
.editor_mut()
|
||||
.set_whitespace_characters(BTreeMap::new());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.backend().buffer()[(6, 0)].symbol(), " ");
|
||||
assert_eq!(terminal.backend().buffer()[(7, 0)].symbol(), "\u{3000}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_whitespace_is_opt_in_and_preserves_wrapping_and_selection() {
|
||||
let directory = Directory::new();
|
||||
@@ -4945,6 +5043,7 @@ mod tests {
|
||||
syntax_diff_file("whitespace.txt", "old \t", "new \t"),
|
||||
);
|
||||
editor.set_highlight_trailing_whitespace(true);
|
||||
editor.set_whitespace_characters(BTreeMap::from([(' ', '·'), ('\t', '→')]));
|
||||
if wrapped {
|
||||
editor.execute("word_wrap.toggle");
|
||||
}
|
||||
@@ -4979,6 +5078,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
assert_ne!(buffer.content[start + 8].bg, Color::Red);
|
||||
assert_eq!(buffer.content[start + 3].symbol(), "·");
|
||||
assert_eq!(buffer.content[start + 4].symbol(), "→");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user