feat: add syntax highlighting to text buffers

This commit is contained in:
Valdemar Lorenzen
2026-10-09 10:51:33 +02:00
parent 981011096e
commit 7c1e4c3ca0
7 changed files with 1359 additions and 54 deletions
+512 -51
View File
@@ -30,6 +30,7 @@ use crate::{
editor::{Editor, Input, Mode, Palette},
files::{DockSide, FilePicker, FilePresentation, FileView},
session::EditorSession,
syntax::{SyntaxCache, SyntaxKind},
};
pub const CAPABILITIES: &[&str] = &["text", "styled_text"];
@@ -115,13 +116,16 @@ impl Drop for TerminalSession {
#[derive(Default)]
struct BufferViewports {
offsets: BTreeMap<usize, (usize, usize)>,
syntax: BTreeMap<usize, SyntaxCache>,
}
impl BufferViewports {
fn draw(&mut self, frame: &mut Frame, editor: &Editor) {
self.offsets.retain(|id, _| editor.has_buffer(*id));
self.syntax.retain(|id, _| editor.has_buffer(*id));
let (top, left) = self.offsets.entry(editor.buffer_id()).or_default();
draw(frame, editor, top, left);
let syntax = self.syntax.entry(editor.buffer_id()).or_default();
draw(frame, editor, top, left, syntax);
}
}
@@ -216,7 +220,14 @@ fn visible_line(line: &str, left: usize, width: usize) -> String {
visible
}
fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
fn draw(
frame: &mut Frame,
editor: &Editor,
top: &mut usize,
left: &mut usize,
syntax: &mut SyntaxCache,
) {
syntax.update(editor.lines(), editor.path(), editor.text_revision());
let area = frame.area();
if area.width == 0 || area.height == 0 {
return;
@@ -291,6 +302,49 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
.map(|line| Line::from(visible_line(line, *left, width)))
.collect();
frame.render_widget(Paragraph::new(lines), viewport);
let buffer = frame.buffer_mut();
for (row, line) in editor.lines().iter().enumerate().skip(*top).take(height) {
let spans = syntax.line(row);
if spans.is_empty() {
continue;
}
let mut span_index = 0;
let mut column = 0;
let right = left.saturating_add(width);
for (byte, grapheme) in line.grapheme_indices(true) {
if column >= right {
break;
}
let cells = if grapheme == "\t" {
TAB_WIDTH - column % TAB_WIDTH
} else {
UnicodeWidthStr::width(grapheme)
};
let end = column + cells;
while span_index < spans.len() && spans[span_index].end_byte <= byte {
span_index += 1;
}
if let Some(span) = spans.get(span_index)
&& span.start_byte <= byte
&& end > *left
&& (grapheme == "\t" || (column >= *left && end <= right))
{
let color = match span.kind {
SyntaxKind::Comment => Color::DarkGray,
SyntaxKind::String => Color::Green,
SyntaxKind::Keyword => Color::Magenta,
SyntaxKind::Number => Color::Yellow,
SyntaxKind::Type | SyntaxKind::Markup => Color::Cyan,
SyntaxKind::Function => Color::LightBlue,
};
let y = viewport.y + (row - *top) as u16;
for cell in column.max(*left)..end.min(right) {
buffer[(viewport.x + (cell - *left) as u16, y)].set_fg(color);
}
}
column = end;
}
}
if let Some(found) = editor.search_match()
&& found.start.row >= *top
&& found.start.row < top.saturating_add(height)
@@ -380,6 +434,11 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
if let Some(count) = editor.count() {
write!(status, " count: {count}").unwrap();
}
if let Some(error) = syntax.error() {
write!(status, " syntax: {error}").unwrap();
} else if let Some(language) = syntax.language() {
write!(status, " {language}").unwrap();
}
frame.render_widget(
Paragraph::new(status).style(Style::default().fg(Color::Black).bg(Color::Cyan)),
status_area,
@@ -786,6 +845,387 @@ mod tests {
.join("\n")
}
#[test]
fn syntax_tokens_refresh_after_native_edits_and_undo_without_changing_text_on_draw() {
let directory = Directory::new();
let path = directory.file("number.rs", "42");
let mut editor = Editor::open(Some(path), vec![]).unwrap();
editor.set_clipboard_mode("separate").unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
let revision = editor.text_revision();
editor.handle(Input::Right);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(editor.text_revision(), revision);
assert_eq!(editor.lines(), &["42"]);
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
editor.handle(Input::Home);
editor.handle(Input::Char('v'));
editor.handle(Input::Right);
editor.handle(Input::Char('c'));
for ch in "\"ok\"".chars() {
editor.handle(Input::Char(ch));
}
editor.handle(Input::Escape);
assert_eq!(editor.lines(), &["\"ok\""]);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Green);
assert!(editor.text_revision() > revision);
editor.handle(Input::Char('u'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(editor.lines(), &["42"]);
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
editor
.apply(vec![crate::editor::Effect::Replace(
"fn main() { let n = 42; let s = \"ok\"; } // note".into(),
)])
.unwrap();
editor.handle(Input::Home);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let buffer = terminal.backend().buffer();
let source = &editor.lines()[0];
for (token, color) in [
("fn", Color::Magenta),
("main", Color::LightBlue),
("let", Color::Magenta),
("42", Color::Yellow),
("\"ok\"", Color::Green),
] {
let x = 2 + source.find(token).unwrap() as u16;
assert_eq!(buffer[(x, 0)].fg, color, "{token}");
assert_eq!(buffer[(x, 0)].bg, Color::Reset);
}
}
#[test]
fn syntax_continuations_above_viewport_refresh_when_the_opening_line_changes() {
let directory = Directory::new();
for (name, before, after, color) in [
(
"comment.rs",
"/*\nhidden\nvisible\n*/",
"//\nhidden\nvisible\n//",
Color::DarkGray,
),
(
"string.py",
"text = \"\"\"\nhidden\nvisible\n\"\"\"",
"text = \"\"\nhidden\nvisible\n\"\"",
Color::Green,
),
] {
let path = directory.file(name, before);
let mut editor = Editor::open(Some(path), vec![]).unwrap();
editor.handle(Input::Down);
editor.handle(Input::Down);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(30, 3)).unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(viewports.offsets[&editor.buffer_id()].0, 2);
assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "v");
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, color, "{name}");
editor
.apply(vec![crate::editor::Effect::Replace(after.into())])
.unwrap();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "v");
assert_eq!(
terminal.backend().buffer()[(2, 0)].fg,
Color::Reset,
"{name}"
);
editor.handle(Input::Char('u'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, color, "{name}");
}
}
#[test]
fn syntax_colors_whole_unicode_graphemes_and_clipped_tabs_without_painting_wide_blanks() {
let directory = Directory::new();
let source = "#a\t界e\u{301}👩‍🚀z";
let mut editor = Editor::open(Some(directory.file("unicode.py", source)), vec![]).unwrap();
let mut syntax = SyntaxCache::new();
for (offset, width, caret, colors) in [
(2, 5, 4, vec![Color::DarkGray; 5]),
(5, 2, 4, vec![Color::Reset, Color::DarkGray]),
(7, 2, 5, vec![Color::DarkGray; 2]),
(8, 2, 6, vec![Color::Reset, Color::DarkGray]),
(
0,
5,
2,
vec![
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
Color::DarkGray,
Color::Reset,
],
),
] {
editor.handle(Input::Home);
for _ in 0..caret {
editor.handle(Input::Right);
}
let area = Rect::new(3, 2, width + 2, 4);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(20, 8),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
let (mut top, mut left) = (0, offset);
let rendered = terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap()
.buffer
.clone();
assert_eq!(left, offset);
let text_area = Rect::new(area.x + 2, area.y, width, 1);
let mut expected = ratatui::buffer::Buffer::empty(text_area);
use ratatui::widgets::Widget;
Paragraph::new(visible_line(source, offset, usize::from(width)))
.render(text_area, &mut expected);
let buffer = terminal.backend().buffer();
for (cell, color) in colors.into_iter().enumerate() {
let position = (text_area.x + cell as u16, text_area.y);
assert_eq!(buffer[position].symbol(), expected[position].symbol());
assert_eq!(rendered[position].fg, color, "left={offset}, cell={cell}");
assert_eq!(rendered[position].bg, Color::Reset);
}
assert_eq!(buffer[(area.x, area.y)].fg, Color::Cyan);
assert_eq!(buffer[(0, 0)].fg, Color::Reset);
assert_eq!(editor.lines(), &[source]);
}
}
#[test]
fn search_visual_selection_and_secondary_carets_override_syntax_foreground() {
let directory = Directory::new();
let mut editor = Editor::open(
Some(directory.file("overlays.py", "# word\n# word\n# word")),
vec![],
)
.unwrap();
editor.handle(Input::Char('/'));
for ch in "word".chars() {
editor.handle(Input::Char(ch));
}
editor.handle(Input::Enter);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 6)).unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let found = editor.search_match().unwrap();
let y = found.start.row as u16;
for x in 4..8 {
assert_eq!(terminal.backend().buffer()[(x, y)].fg, Color::Black);
assert_eq!(terminal.backend().buffer()[(x, y)].bg, Color::Yellow);
}
editor.handle(Input::Char('v'));
editor.handle(Input::Char('e'));
editor.handle(Input::Ctrl('a'));
assert_eq!(editor.selection_count(), 3);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let buffer = terminal.backend().buffer();
for selection in editor.selections() {
let y = selection.start.row as u16;
let start =
display_column(&editor.lines()[selection.start.row], selection.start.column);
let end = display_column(&editor.lines()[selection.end.row], selection.end.column);
for column in start..end {
let x = 2 + column as u16;
if buffer[(x, y)].bg == Color::Magenta {
assert_eq!(buffer[(x, y)].fg, Color::Black);
} else {
assert_eq!(buffer[(x, y)].fg, Color::White);
assert_eq!(buffer[(x, y)].bg, Color::Blue);
}
}
assert_eq!(buffer[(2, y)].fg, Color::DarkGray);
}
for caret in editor
.carets()
.into_iter()
.filter(|caret| *caret != editor.cursor())
{
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
assert_eq!(buffer[(x, caret.row as u16)].fg, Color::Black);
assert_eq!(buffer[(x, caret.row as u16)].bg, Color::Magenta);
}
}
#[test]
fn syntax_redetects_saved_paths_without_text_changes_and_plain_text_keeps_default_color() {
let directory = Directory::new();
let mut editor =
Editor::open(Some(directory.file("source.rs", "let value = 42;")), vec![]).unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Magenta);
let revision = editor.text_revision();
for name in ["source.vampunknown", "source.txt", "again.rs"] {
editor.execute(&format!("w {}", directory.0.join(name).display()));
assert_eq!(editor.text_revision(), revision);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
if name.ends_with(".rs") {
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Magenta);
assert_eq!(
viewports.syntax[&editor.buffer_id()].language(),
Some("Rust")
);
} else {
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Reset);
assert!(viewports.syntax[&editor.buffer_id()].line(0).is_empty());
assert!(viewports.syntax[&editor.buffer_id()].language().is_none());
}
assert_eq!(editor.lines(), &["let value = 42;"]);
}
}
#[test]
fn syntax_caches_stay_independent_across_buffer_edits_and_retire_closed_ids() {
let directory = Directory::new();
let mut editor = Editor::open(Some(directory.file("a.rs", "42")), vec![]).unwrap();
let a_id = editor.buffer_id();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
editor.execute(&format!(
"edit {}",
directory.file("b.py", "# note").display()
));
let b_id = editor.buffer_id();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::DarkGray);
editor
.apply(vec![crate::editor::Effect::Replace("\"ok\"".into())])
.unwrap();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Green);
editor.execute(&format!("buffer {a_id}"));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
assert_eq!(viewports.syntax[&a_id].language(), Some("Rust"));
assert_eq!(viewports.syntax[&b_id].language(), Some("Python"));
editor.execute(&format!("buffer {b_id}"));
editor.execute(&format!("bdelete {a_id}"));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Green);
assert!(!viewports.syntax.contains_key(&a_id));
assert!(!viewports.offsets.contains_key(&a_id));
assert_eq!(viewports.syntax.len(), 1);
}
#[test]
fn highlighted_editor_text_stays_out_of_docked_and_floating_picker_surfaces() {
let directory = Directory::new();
let path = directory.file("panels.rs", &vec!["// note"; 30].join("\n"));
let area = Rect::new(3, 2, 100, 24);
for (name, side) in [
("left", Some(DockSide::Left)),
("right", Some(DockSide::Right)),
("top", Some(DockSide::Top)),
("bottom", Some(DockSide::Bottom)),
("floating", None),
] {
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
if side.is_some() {
editor.set_file_explorer_side(name).unwrap();
}
editor.handle(Input::Char(' '));
editor.handle(Input::Char(if side.is_some() { 'e' } else { 'o' }));
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(110, 30),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let workspace = Rect::new(area.x, area.y, area.width, area.height - 2);
let (text, panel) = if let Some(side) = side {
let (text, panel) = dock_layout(workspace, side, true);
(text, panel.unwrap())
} else {
(workspace, Rect::new(area.x + 8, area.y + 3, 84, 18))
};
let mut expected = Terminal::with_options(
ratatui::backend::TestBackend::new(110, 30),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(panel),
},
)
.unwrap();
expected
.draw(|frame| {
draw_file_list(
frame,
editor.file_picker().unwrap(),
panel,
editor.file_navigation_hint(),
)
})
.unwrap();
let buffer = terminal.backend().buffer();
let expected = expected.backend().buffer();
for y in panel.y..panel.bottom() {
for x in panel.x..panel.right() {
assert_eq!(buffer[(x, y)], expected[(x, y)], "{name}: {x},{y}");
}
}
assert_eq!(buffer[(text.x + 3, text.y)].symbol(), "/");
assert_eq!(buffer[(text.x + 3, text.y)].fg, Color::DarkGray);
assert_eq!(buffer[(text.x, text.y)].fg, Color::Cyan);
assert_eq!(buffer[(area.x, area.bottom() - 2)].bg, Color::Cyan);
assert_eq!(buffer[(0, 0)].fg, Color::Reset);
}
}
#[test]
fn translate_distinguishes_control_j_from_enter_and_normalizes_control_letters() {
for (code, modifiers, expected) in [
@@ -879,11 +1319,12 @@ mod tests {
editor.set_file_explorer_side("bottom").unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
for key in ['o', 'e'] {
editor.handle(Input::Char(' '));
editor.handle(Input::Char(key));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains(&hint), "{text}");
@@ -892,7 +1333,7 @@ mod tests {
}
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains(&hint), "{text}");
@@ -1362,8 +1803,9 @@ mod tests {
.collect();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let query_cursor = terminal.get_cursor_position().unwrap();
assert!(screen_text(&terminal).contains(&format!("> {query}")));
@@ -1384,7 +1826,7 @@ mod tests {
filtered
);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains(&format!("filter: {query}")), "{text}");
@@ -1405,7 +1847,7 @@ mod tests {
editor.handle(Input::Char('i'));
assert_eq!(editor.file_picker().unwrap().query, query);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!(terminal.get_cursor_position().unwrap(), query_cursor);
editor.handle(Input::Escape);
@@ -1417,7 +1859,7 @@ mod tests {
Path::new(query)
);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert_eq!(
@@ -1437,7 +1879,7 @@ mod tests {
Path::new(query)
);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert_eq!(cursor.y, query_cursor.y);
@@ -1455,7 +1897,7 @@ mod tests {
Path::new(query)
);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!(terminal.get_cursor_position().unwrap(), cursor);
editor.handle(Input::Ctrl('t'));
@@ -1481,6 +1923,7 @@ mod tests {
editor.set_project_root(directory.0.clone());
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
editor.handle(Input::Char('i'));
@@ -1495,7 +1938,7 @@ mod tests {
assert_eq!(editor.file_picker().unwrap().view, FileView::List);
assert_eq!(editor.file_picker().unwrap().query, "n");
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains("List"));
assert_eq!(terminal.get_cursor_position().unwrap().y, 1);
@@ -1504,7 +1947,7 @@ mod tests {
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "n");
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!(terminal.get_cursor_position().unwrap().y, 1);
editor.handle(Input::Escape);
@@ -1516,7 +1959,7 @@ mod tests {
assert!(!editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "n");
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains("filter: n"));
let cursor = terminal.get_cursor_position().unwrap();
@@ -1549,6 +1992,7 @@ mod tests {
.unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(200, 30)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
for (key, expected) in [('o', FileView::Tree), ('e', FileView::List)] {
editor.handle(Input::Char(' '));
editor.handle(Input::Char(key));
@@ -1567,7 +2011,7 @@ mod tests {
] {
assert_eq!(editor.file_picker().unwrap().view, view);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains(editor.file_navigation_hint()), "{text}");
@@ -1623,7 +2067,7 @@ mod tests {
}
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains(editor.selection_navigation_hint()));
}
@@ -1648,6 +2092,7 @@ mod tests {
)
.unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
assert!(editor.file_picker().unwrap().focused);
@@ -1659,7 +2104,7 @@ mod tests {
] {
editor.set_file_explorer_side(name).unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let workspace = Rect::new(area.x, area.y, area.width, area.height - 2);
let (editor_area, panel) = dock_layout(workspace, side, true);
@@ -1675,7 +2120,7 @@ mod tests {
assert!(panel.contains(terminal.get_cursor_position().unwrap()));
editor.handle(Input::Escape);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(editor_area.contains(terminal.get_cursor_position().unwrap()));
let buffer = terminal.backend().buffer();
@@ -1727,14 +2172,15 @@ mod tests {
)
.unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let before = (top, left);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let panel = Rect::new(area.x + 13, area.y + 4, 84, 18);
assert_eq!((top, left), before);
@@ -1855,6 +2301,7 @@ mod tests {
)
.unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
for (name, side) in [
("left", DockSide::Left),
("right", DockSide::Right),
@@ -1876,7 +2323,7 @@ mod tests {
terminal.resize(area).unwrap();
let area = terminal.get_frame().area();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
if width > 0 && height > 0 {
let workspace = Rect::new(
@@ -1894,7 +2341,7 @@ mod tests {
}
editor.handle(Input::Escape);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
if width > 0 && height > 0 {
let cursor = terminal.get_cursor_position().unwrap();
@@ -1940,15 +2387,16 @@ mod tests {
let mut editor = Editor::open(Some(path), vec![]).unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 6)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
editor.handle(Input::Char(' '));
editor.handle(Input::Char('b'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains("leader: Space b"));
editor.handle(Input::Char('x'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(!screen_text(&terminal).contains("leader:"));
assert_eq!(editor.lines(), &["keep"]);
@@ -1966,8 +2414,9 @@ mod tests {
}
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 10)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains(", b n"));
assert!(!screen_text(&terminal).contains("Space b n"));
@@ -1975,7 +2424,7 @@ mod tests {
.bind("normal", ",", Some("core.command".into()))
.unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains("unbound"));
assert!(!screen_text(&terminal).contains(", b n"));
@@ -2214,16 +2663,17 @@ mod tests {
.unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(60, 6)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
editor.handle(Input::Char('1'));
editor.handle(Input::Char('0'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains("count: 10"));
editor.handle(Input::Escape);
editor.handle(Input::Down);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(!screen_text(&terminal).contains("count:"));
assert_eq!(
@@ -2253,8 +2703,9 @@ mod tests {
)
.unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!((top, left), (8, 8));
let buffer = terminal.backend().buffer();
@@ -2272,7 +2723,7 @@ mod tests {
);
editor.handle(Input::Down);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
let last_number: String = (3..6).map(|x| buffer[(x, 4)].symbol()).collect();
@@ -2285,7 +2736,7 @@ mod tests {
editor.handle(Input::Up);
editor.handle(Input::Up);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
for (y, expected) in [(2, "10 "), (3, " 1 "), (4, " 2 ")] {
@@ -2295,7 +2746,7 @@ mod tests {
editor.handle(Input::Ctrl('e'));
assert_eq!(editor.selection_count(), 2);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
for (y, expected) in [(2, " 1 "), (3, "11 "), (4, " 1 ")] {
@@ -2308,6 +2759,7 @@ mod tests {
fn gutter_resizes_at_digit_boundaries_and_yields_to_narrow_text() {
let mut editor = Editor::open(None, vec![]).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
for count in [9usize, 10, 100] {
editor
.apply(vec![crate::editor::Effect::Replace(
@@ -2320,7 +2772,7 @@ mod tests {
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(width, 4)).unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
let buffer = terminal.backend().buffer();
@@ -2345,7 +2797,7 @@ mod tests {
.unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(8, 5)).unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(0, 0)].symbol(), "1");
@@ -2370,8 +2822,9 @@ mod tests {
}
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 16)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("> no-such-command"), "{text}");
@@ -2384,7 +2837,7 @@ mod tests {
assert_eq!(editor.mode(), Mode::Search);
assert_eq!(editor.command_line(), "saved search");
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert!(screen_text(&terminal).contains("/saved search"));
}
@@ -2404,8 +2857,9 @@ mod tests {
let first_name = palette.entries[0].name.clone();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 10)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains(&selected.name), "{text}");
@@ -2439,8 +2893,9 @@ mod tests {
}
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 10)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("sample | Ctrl+x | NORMAL"), "{text}");
@@ -2452,7 +2907,7 @@ mod tests {
editor.handle(Input::Char(ch));
}
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("sample | unbound | NORMAL"), "{text}");
@@ -2549,18 +3004,19 @@ mod tests {
editor.handle(Input::End);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(8, 4)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let viewport = (top, left);
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!((top, left), viewport);
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!((top, left), viewport);
let cursor = terminal.get_cursor_position().unwrap();
@@ -2582,6 +3038,7 @@ mod tests {
editor.handle(Input::Down);
editor.handle(Input::End);
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
for prompting in [false, true] {
if prompting {
editor.handle(Input::Char('/'));
@@ -2599,7 +3056,7 @@ mod tests {
)
.unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert!(area.contains(cursor), "{area:?}: {cursor:?}");
@@ -2645,8 +3102,9 @@ mod tests {
assert_eq!(editor.selections().len(), 5);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(48, 4)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
for selection in editor.selections() {
@@ -2676,8 +3134,9 @@ mod tests {
editor.handle(Input::Char('A'));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(48, 5)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
for caret in editor.carets() {
@@ -2714,8 +3173,9 @@ mod tests {
editor.handle(Input::Left);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(12, 6)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
for x in [1, 2, 3, 4] {
@@ -2726,7 +3186,7 @@ mod tests {
editor.handle(Input::Down);
editor.handle(Input::Down);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(10, 0)].symbol(), " ");
@@ -2740,7 +3200,7 @@ mod tests {
editor.handle(Input::End);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(7, 6)).unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(left, 4);
@@ -2753,7 +3213,7 @@ mod tests {
assert_ne!(buffer[(2, 1)].bg, Color::Blue);
editor.handle(Input::Escape);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(2, 0)].bg, Color::Yellow);
@@ -2778,8 +3238,9 @@ mod tests {
editor.handle(Input::End);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(7, 5)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(left, 4);
@@ -2796,7 +3257,7 @@ mod tests {
editor.handle(Input::Char(ch));
}
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(1, 4)].symbol(), "界");
@@ -2805,7 +3266,7 @@ mod tests {
editor.handle(Input::Backspace);
editor.handle(Input::Char('\t'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
.unwrap();
assert_eq!(
terminal.get_cursor_position().unwrap(),