feat: implement selection-first multi-target editing

This commit is contained in:
Valdemar Lorenzen
2026-10-08 10:42:16 +02:00
parent 53fcd7a1f2
commit 8ef92a7b15
6 changed files with 1485 additions and 544 deletions
+87 -3
View File
@@ -242,7 +242,7 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
buffer[(area.x + (column - *left) as u16, y)].set_style(style);
}
}
if let Some(selection) = editor.selection() {
for selection in editor.selections() {
let first = selection.start.row.max(*top);
let last = selection.end.row.min(top.saturating_add(height - 1));
let style = Style::default().fg(Color::White).bg(Color::Blue);
@@ -267,6 +267,19 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
}
}
}
for caret in editor.carets().into_iter().filter(|caret| *caret != cursor) {
if caret.row < *top || caret.row >= top.saturating_add(height) {
continue;
}
let column = display_column(&editor.lines()[caret.row], caret.column);
if column >= *left && column < left.saturating_add(width) {
frame.buffer_mut()[(
area.x + (column - *left) as u16,
area.y + (caret.row - *top) as u16,
)]
.set_style(Style::default().fg(Color::Black).bg(Color::Magenta));
}
}
let path = editor
.path()
.map(|p| p.to_string_lossy().into_owned())
@@ -276,12 +289,17 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
.map(|c| if c.is_control() { '�' } else { c })
.collect();
let status = format!(
" {} {}{} {}:{}",
" {} {}{} {}:{}{}",
editor.mode(),
path,
if editor.dirty() { " [+]" } else { "" },
cursor.row + 1,
cursor.column + 1
cursor.column + 1,
if editor.selection_count() > 1 {
format!(" {} selections", editor.selection_count())
} else {
String::new()
}
);
frame.render_widget(
Paragraph::new(status).style(Style::default().fg(Color::Black).bg(Color::Cyan)),
@@ -326,6 +344,72 @@ mod tests {
assert_eq!(visible_line(text, 5, 2), " e\u{301}");
}
#[test]
fn selection_set_renders_all_ranges_and_counts_offscreen_targets() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace(
"界\n界\n界\n界\n界".into(),
)])
.unwrap();
editor.handle(Input::Home);
editor.handle(Input::Char('v'));
editor.handle(Input::Ctrl('a'));
assert_eq!(editor.selection_count(), 5);
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);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let buffer = terminal.backend().buffer();
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));
}
}
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)
);
}
#[test]
fn secondary_carets_render_on_empty_lines_and_at_line_end() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace("a\n\n界".into())])
.unwrap();
editor.handle(Input::Home);
editor.handle(Input::Ctrl('e'));
editor.handle(Input::Ctrl('e'));
assert_eq!(editor.selection_count(), 3);
editor.handle(Input::Char('A'));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(48, 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();
for caret in editor.carets() {
let x = 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);
}
}
assert_eq!(
terminal.get_cursor_position().unwrap(),
ratatui::layout::Position::new(
display_column(&editor.lines()[editor.cursor().row], editor.cursor().column) as u16,
editor.cursor().row as u16
)
);
}
#[test]
fn visual_selection_highlights_graphemes_newlines_and_clipped_ranges_over_search() {
let mut editor = Editor::open(None, vec![]).unwrap();