feat: add line-number gutter
This commit is contained in:
+161
-29
@@ -210,6 +210,17 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
|
||||
Constraint::Length(u16::from(area.height >= 2 || prompting)),
|
||||
])
|
||||
.areas(area);
|
||||
let digits = editor.lines().len().ilog10() as usize + 1;
|
||||
let gutter_width = (digits + 1) as u16;
|
||||
let [gutter, viewport] = Layout::horizontal([
|
||||
Constraint::Length(if viewport.width > gutter_width {
|
||||
gutter_width
|
||||
} else {
|
||||
0
|
||||
}),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(viewport);
|
||||
let height = usize::from(viewport.height);
|
||||
let width = usize::from(viewport.width);
|
||||
let cursor = editor.cursor();
|
||||
@@ -225,6 +236,21 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
|
||||
} else if column >= left.saturating_add(width) {
|
||||
*left = column + 1 - width;
|
||||
}
|
||||
if gutter.width > 0 {
|
||||
let numbers: Vec<Line> = (*top..top.saturating_add(height).min(editor.lines().len()))
|
||||
.map(|row| {
|
||||
let style = if row == cursor.row {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
Line::styled(format!("{:>digits$} ", row + 1), style)
|
||||
})
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(numbers), gutter);
|
||||
}
|
||||
let lines: Vec<Line> = editor
|
||||
.lines()
|
||||
.iter()
|
||||
@@ -457,6 +483,111 @@ mod tests {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gutter_tracks_scrolled_rows_without_scrolling_with_text() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"0123456789界z\n".repeat(11),
|
||||
)])
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
editor.handle(Input::Down);
|
||||
}
|
||||
editor.handle(Input::End);
|
||||
let area = ratatui::layout::Rect::new(3, 2, 8, 5);
|
||||
let mut terminal = Terminal::with_options(
|
||||
ratatui::backend::TestBackend::new(20, 10),
|
||||
ratatui::TerminalOptions {
|
||||
viewport: ratatui::Viewport::Fixed(area),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
assert_eq!((top, left), (8, 8));
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (y, expected) in [(2, " 9 "), (3, "10 "), (4, "11 ")] {
|
||||
let number: String = (3..6).map(|x| buffer[(x, y)].symbol()).collect();
|
||||
assert_eq!(number, expected);
|
||||
assert_eq!(buffer[(6, y)].symbol(), "8");
|
||||
assert_eq!(buffer[(3, y)].bg, Color::Reset);
|
||||
}
|
||||
assert!(buffer[(3, 4)].modifier.contains(Modifier::BOLD));
|
||||
assert!(!buffer[(3, 3)].modifier.contains(Modifier::BOLD));
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(10, 4)
|
||||
);
|
||||
editor.handle(Input::Down);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let last_number: String = (3..6).map(|x| buffer[(x, 4)].symbol()).collect();
|
||||
assert_eq!(last_number, "12 ");
|
||||
assert_eq!(buffer[(6, 4)].symbol(), " ");
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(6, 4)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
for count in [9usize, 10, 100] {
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
vec!["x"; count].join("\n"),
|
||||
)])
|
||||
.unwrap();
|
||||
let digits = count.ilog10() as u16 + 1;
|
||||
let gutter_width = digits + 1;
|
||||
for width in [12, gutter_width, gutter_width + 1, 1] {
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, 4)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let text_x = if width > gutter_width {
|
||||
gutter_width
|
||||
} else {
|
||||
0
|
||||
};
|
||||
assert_eq!(buffer[(text_x, 0)].symbol(), "x");
|
||||
assert_eq!(cursor, ratatui::layout::Position::new(text_x, 0));
|
||||
if text_x > 0 {
|
||||
let number: String = (0..text_x).map(|x| buffer[(x, 0)].symbol()).collect();
|
||||
assert_eq!(
|
||||
number,
|
||||
format!("{:>digits$} ", 1, digits = usize::from(digits))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(String::new())])
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(8, 5)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(0, 0)].symbol(), "1");
|
||||
assert_eq!(buffer[(0, 1)].symbol(), " ");
|
||||
assert_eq!(buffer[(0, 2)].symbol(), " ");
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(2, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_renders_query_no_matches_and_restores_underlying_prompt() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
@@ -737,14 +868,14 @@ mod tests {
|
||||
for selection in editor.selections() {
|
||||
if selection.start.row >= top && selection.start.row < top + 2 {
|
||||
let y = (selection.start.row - top) as u16;
|
||||
assert!(matches!(buffer[(0, y)].bg, Color::Blue | Color::Magenta));
|
||||
assert!(matches!(buffer[(2, y)].bg, Color::Blue | Color::Magenta));
|
||||
}
|
||||
}
|
||||
let status: String = (0..48).map(|x| buffer[(x, 2)].symbol()).collect();
|
||||
assert!(status.contains("5 selections"), "{status}");
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(0, (editor.cursor().row - top) as u16)
|
||||
ratatui::layout::Position::new(2, (editor.cursor().row - top) as u16)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -766,7 +897,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for caret in editor.carets() {
|
||||
let x = display_column(&editor.lines()[caret.row], caret.column) as u16;
|
||||
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
|
||||
assert_eq!(buffer[(x, caret.row as u16)].symbol(), " ");
|
||||
if caret != editor.cursor() {
|
||||
assert_eq!(buffer[(x, caret.row as u16)].bg, Color::Magenta);
|
||||
@@ -775,7 +906,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(
|
||||
display_column(&editor.lines()[editor.cursor().row], editor.cursor().column) as u16,
|
||||
2 + display_column(&editor.lines()[editor.cursor().row], editor.cursor().column)
|
||||
as u16,
|
||||
editor.cursor().row as u16
|
||||
)
|
||||
);
|
||||
@@ -796,54 +928,54 @@ mod tests {
|
||||
editor.handle(Input::Enter);
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Left);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(10, 6)).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(12, 6)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for x in [1, 2, 3, 4] {
|
||||
assert_eq!(buffer[(x, 0)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(2 + x, 0)].bg, Color::Blue);
|
||||
}
|
||||
assert_eq!(buffer[(4, 0)].symbol(), "界");
|
||||
assert_eq!(buffer[(6, 0)].bg, Color::Yellow);
|
||||
assert_eq!(buffer[(6, 0)].symbol(), "界");
|
||||
assert_eq!(buffer[(8, 0)].bg, Color::Yellow);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Down);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(8, 0)].symbol(), " ");
|
||||
assert_eq!(buffer[(8, 0)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(0, 1)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(0, 2)].bg, Color::Blue);
|
||||
assert_ne!(buffer[(1, 2)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(10, 0)].symbol(), " ");
|
||||
assert_eq!(buffer[(10, 0)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(2, 1)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(2, 2)].bg, Color::Blue);
|
||||
assert_ne!(buffer[(3, 2)].bg, Color::Blue);
|
||||
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::End);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(5, 6)).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(7, 6)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(left, 4);
|
||||
assert_eq!(buffer[(0, 0)].symbol(), "界");
|
||||
assert_eq!(buffer[(2, 0)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(4, 0)].symbol(), " ");
|
||||
assert_eq!(buffer[(2, 0)].symbol(), "界");
|
||||
assert_eq!(buffer[(4, 0)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(6, 0)].symbol(), " ");
|
||||
for x in [0, 2, 3, 4] {
|
||||
assert_eq!(buffer[(x, 0)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(2 + x, 0)].bg, Color::Blue);
|
||||
}
|
||||
assert_ne!(buffer[(0, 1)].bg, Color::Blue);
|
||||
assert_ne!(buffer[(2, 1)].bg, Color::Blue);
|
||||
editor.handle(Input::Escape);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(0, 0)].bg, Color::Yellow);
|
||||
assert_eq!(buffer[(2, 0)].bg, Color::Yellow);
|
||||
assert_ne!(buffer[(3, 0)].bg, Color::Blue);
|
||||
assert_ne!(buffer[(4, 0)].bg, Color::Blue);
|
||||
assert_eq!(buffer[(4, 0)].bg, Color::Yellow);
|
||||
assert_ne!(buffer[(5, 0)].bg, Color::Blue);
|
||||
assert_ne!(buffer[(6, 0)].bg, Color::Blue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -860,20 +992,20 @@ mod tests {
|
||||
}
|
||||
editor.handle(Input::Enter);
|
||||
editor.handle(Input::End);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(5, 5)).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(7, 5)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(left, 4);
|
||||
assert_eq!(buffer[(0, 0)].symbol(), "界");
|
||||
assert_eq!(buffer[(2, 0)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(2, 0)].symbol(), "界");
|
||||
assert_eq!(buffer[(4, 0)].symbol(), "e\u{301}");
|
||||
for x in [0, 2] {
|
||||
assert_eq!(buffer[(x, 0)].bg, Color::Yellow);
|
||||
assert_eq!(buffer[(2 + x, 0)].bg, Color::Yellow);
|
||||
}
|
||||
for x in 3..5 {
|
||||
assert_ne!(buffer[(x, 0)].bg, Color::Yellow);
|
||||
assert_ne!(buffer[(2 + x, 0)].bg, Color::Yellow);
|
||||
}
|
||||
editor.handle(Input::Char('/'));
|
||||
for ch in "界界".chars() {
|
||||
@@ -883,8 +1015,8 @@ mod tests {
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(0, 4)].symbol(), "界");
|
||||
assert_eq!(buffer[(2, 4)].symbol(), "界");
|
||||
assert_eq!(buffer[(1, 4)].symbol(), "界");
|
||||
assert_eq!(buffer[(3, 4)].symbol(), "界");
|
||||
editor.handle(Input::Backspace);
|
||||
editor.handle(Input::Backspace);
|
||||
editor.handle(Input::Char('\t'));
|
||||
|
||||
Reference in New Issue
Block a user