feat: implement selection-first multi-target editing
This commit is contained in:
@@ -17,16 +17,18 @@ Without a file, vamp opens an unnamed buffer. A nonexistent file opens an empty
|
||||
|
||||
| Mode | Keys |
|
||||
| --- | --- |
|
||||
| Normal | `i` insert before cursor; `a` insert after cursor; `o`/`O` open line below/above |
|
||||
| Normal | `I` (Shift+i) insert before the first nonblank, skipping spaces/tabs; `A` (Shift+a) insert at line end |
|
||||
| Normal | `h`/`j`/`k`/`l` or arrows; `0`/`$` or Home/End |
|
||||
| Normal | `w`/`b`/`e` next word start, previous/current word start, word end; `W`/`B`/`E` for whitespace-delimited WORDs |
|
||||
| Normal | `J` (Shift+j) join the current line with the next line |
|
||||
| Normal | `i` insert before each caret; `a` insert after each caret; `o`/`O` open lines below/above |
|
||||
| Normal, Visual | `I` (Shift+i) / `A` (Shift+a) insert at first nonblank / line end; Visual splits each touched row into a caret first |
|
||||
| Normal | `h`/`j`/`k`/`l` or arrows move every caret; `0`/`$` or Home/End |
|
||||
| Normal | `w`/`b`/`e` move every caret to next word start, previous/current word start, word end; `W`/`B`/`E` for whitespace-delimited WORDs |
|
||||
| Normal | `J` (Shift+j) joins each targeted line with the next line |
|
||||
| Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode |
|
||||
| Normal | `/` opens search; `n` finds the next match; `N` (Shift+n) finds the previous match |
|
||||
| Normal | `v` starts Visual selection; `p`/`P` paste internal clipboard after/before the cursor |
|
||||
| Visual | Cursor/word motions and `n`/`N` extend selection; `y` copies; `d`/`x`/Delete cuts; `c` changes selection; `v`/Esc cancels |
|
||||
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End; Esc returns to normal |
|
||||
| Normal | `v` starts Visual selections; `p`/`P` paste internal clipboard after/before each caret; Esc keeps only the primary caret |
|
||||
| Normal, Visual | Ctrl+k clones above; Ctrl+e clones below (Ctrl+j aliases Enter in terminals) |
|
||||
| Visual | Cursor/word motions and `n`/`N` extend every selection; `y` copies; `d`/`x`/Delete cuts; `c` changes selections; `v`/Esc cancels highlighting |
|
||||
| Visual | Ctrl+l splits touched rows into Normal carets at first nonblank; Ctrl+n adds the next occurrence; Ctrl+s skips the primary occurrence; Ctrl+a selects all occurrences |
|
||||
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End at every caret; Esc returns to Normal, retaining carets |
|
||||
| Command | Enter executes; Backspace edits; Esc cancels |
|
||||
| Search | Type a literal query; Enter finds the next match; Backspace edits; Esc cancels |
|
||||
|
||||
@@ -36,17 +38,19 @@ Word motions separate runs of Unicode letters/numbers/underscore from runs of pu
|
||||
|
||||
`w`/`W` move to the next start. `b`/`B` move to the current start, or the previous start when already at one. `e`/`E` move to the current end when before it, otherwise the next end. Motions cross lines but never wrap. Truly empty lines are stops for start motions; whitespace-only lines are skipped. End motions skip empty lines while looking for a word. With no further target, motions stop at the buffer edge, including trailing whitespace or an empty final line.
|
||||
|
||||
These keys type literal text in Insert mode. The corresponding actions can also be bound in Insert mode to move the insertion cursor without splitting its undo group. Motions never edit text, add history, or clear an active search highlight.
|
||||
These keys type literal text in Insert mode. The corresponding actions can also be bound in Insert mode to move all insertion carets without splitting their undo group. Motions never edit text, add history, or clear an active search highlight.
|
||||
|
||||
Visual selection is characterwise and inclusive of both endpoints, normalized when moving backward. Whole combining sequences and ZWJ emoji are selected together. `$`/End can select the newline after a non-final row; empty-line newlines are selectable too. Selected newlines are shown as one colored blank cell after the line. Blue selection highlighting takes precedence over yellow search highlighting.
|
||||
Editing uses an ordered selection set with one primary target. Cursor and word motions apply to every target; Visual motions keep each target's anchor and move its active endpoint. Whole combining sequences and ZWJ emoji remain intact. Visual selection is characterwise and inclusive of the endpoints; its read-only ranges are end-exclusive. `$`/End can select the newline after a non-final row; empty-line newlines are selectable too. Selected newlines appear as a blue blank cell after the line. Blue highlighting overrides yellow search highlighting; secondary carets are magenta, including on empty lines and at line ends. Only the primary owns the hardware cursor and viewport. The status line counts all targets, including offscreen targets.
|
||||
|
||||
Copy and cut return to Normal mode at the selection start; cancellation keeps the focus cursor. Both clamp the cursor to a valid Normal-mode position. `p`/`P` paste after/before the current grapheme, including multiline text. The cursor lands on the last inserted grapheme, or the new row's start when the clipboard ends in a newline. Cut and each paste are separate undo actions. Copy, selection movement, and cancellation do not add history or clear redo. Undo/redo restore text and cursor, not the selection or clipboard.
|
||||
Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred terminal-cell columns (tabs use four-cell stops); Visual cloning preserves both endpoints. Duplicate carets and truly overlapping selections merge; adjacent ranges remain distinct. Visual Ctrl+l creates one Normal caret at the first nonblank of each touched row. Visual `I`/`A` similarly create one Insert caret per touched row at first nonblank/end. Occurrence actions use the primary selection as a literal, potentially multiline, grapheme-aligned query: Ctrl+n adds the next non-overlapping occurrence and wraps; Ctrl+s moves only the primary to that occurrence; Ctrl+a selects all occurrences.
|
||||
|
||||
Visual-mode `c` removes the selection into the internal clipboard and enters Insert mode at its start. Deletion and subsequent replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing an empty final-row selection enters Insert mode without overwriting the clipboard.
|
||||
Copy and cut return to Normal at each selection start; Visual cancellation keeps each active endpoint and removes highlighting. Normal Esc collapses to the primary. `p`/`P` paste after/before each current grapheme, including multiline text. Each caret lands on its last inserted grapheme, or the new row's start when its fragment ends in a newline. Every multi-target edit is planned against the original buffer and committed as one undo action, not one per target. Copy, movement, and cancellation do not add history or clear redo. Undo/redo restore text, the full caret set, and primary identity in Normal mode, not Visual highlighting or the clipboard.
|
||||
|
||||
Bracketed paste retains its existing separate undo group, distinct from typing before/after it. A rejected paste leaves the pending change intact. Normal-mode operator grammar such as `cw` is not supported; `c` types literal text in Insert mode.
|
||||
Visual `c` removes all selected text into the internal clipboard and enters Insert at each selection start. Deletion and replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing only empty final-row selections enters Insert without overwriting the clipboard.
|
||||
|
||||
The clipboard is internal to the editor session and independent of the system clipboard. Empty clipboard paste does not edit text; copying/cutting an empty final-row selection does not overwrite an existing clipboard. Open a new search or enter Ex commands after leaving Visual mode. Successful text effects or explicit command execution also leave Visual mode; rejected effects preserve the selection.
|
||||
Bracketed paste broadcasts the same literal text to all Insert carets in a separate undo group, distinct from typing before/after it. A rejected paste leaves the pending change intact. Normal-mode operator grammar such as `cw` is not supported; `c` types literal text in Insert mode.
|
||||
|
||||
The clipboard is internal to the editor session and independent of the system clipboard. Copy/cut store fragments in document order: one fragment broadcasts to every caret; matching fragment/caret counts pair in document order; any other count refuses paste without changing state. Empty clipboard paste does not edit text; copying/cutting only empty final-row selections does not overwrite the clipboard. Open a new search or enter Ex commands after leaving Visual mode. Plugin commands execute once, not once per target: successful text effects collapse to the primary and apply once; rejected or message-only effects preserve the selection set. There is no selection-aware plugin API yet.
|
||||
|
||||
Search is case-sensitive and single-line, with no regular expressions or Unicode normalization. Matches start and end on whole grapheme boundaries, including combining marks and emoji. Next/previous navigation skips the current position and wraps through the buffer; a sole match wraps back to itself. The active match is highlighted yellow.
|
||||
|
||||
@@ -62,14 +66,14 @@ Paste is backend input: it is ignored in Normal, Command, Search, and Visual mod
|
||||
|
||||
One crate, four modules, no workspace or frontend-specific types in the editing backend:
|
||||
|
||||
- `editor`: owns buffer, cursor, modes, Visual selection and the internal clipboard, search state/navigation and grapheme-coordinate range presentation, key notation and binding resolution, modal paste policy, undo/redo, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
||||
- `editor`: owns buffer, the primary cursor and selection set, modes and internal clipboard fragments, search state/navigation and grapheme-coordinate range presentation, key notation and binding resolution, modal paste policy, undo/redo, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
||||
- `session`: `EditorSession` owns the editor and its Ruby hosts together. It loads configuration, registers eligible plugins before installing bindings, executes selected commands, and retires both host and command ownership on invocation or effect-validation failure.
|
||||
- `tui`: translates terminal events to session input, renders read-only editor state, and advertises `text` and `styled_text` capabilities. It has no plugin lifecycle callback, host storage, or paste acceptance policy.
|
||||
- `ruby`: evaluates configuration and runs isolated plugin hosts. Protocol, deadlines, and process cleanup stay inside this module.
|
||||
- The executable selects paths and frontend capabilities, opens an editor session, and launches the TUI.
|
||||
|
||||
Frontends drive `EditorSession::handle` or `execute` and inspect `editor()`; there is no mutable editor getter that could independently alter plugin registration. Pasted text uses `Input::Paste(String)` through the same input seam as keys. Plugin effects are applied internally so a rejected result also retires its owner; there is no generic frontend effects entry point.
|
||||
Frontends inspect `selection()` for an ordered, end-exclusive range of grapheme coordinates. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. Selection normalization, clipboard contents, editing, and history remain inside the editor module.
|
||||
Frontends inspect `selections()` for document-ordered, end-exclusive Visual ranges of grapheme coordinates (empty outside Visual), `carets()` for all active cursors in document order, `cursor()` for the primary, and `selection_count()` for the total target count. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. Selection normalization, clipboard contents, edit planning, and history remain inside the editor module.
|
||||
|
||||
The terminal adapter owns raw mode, alternate-screen state, bracketed paste, and its panic hook for one session lifetime. Entry failure, normal return, I/O errors, and unwinding panics restore the caller's hook; active-terminal cleanup runs before caller panic diagnostics. Panics retain their original payload and propagate rather than being swallowed. Terminal state and panic hooks are process-global: run one TUI at a time and do not replace its hook while that session is active.
|
||||
|
||||
@@ -111,15 +115,16 @@ Terminals share `ctrl+h` with `backspace`, `ctrl+i` with `tab`, and `ctrl+j`/`ct
|
||||
|
||||
| Available modes | Methods on `config.core` |
|
||||
| --- | --- |
|
||||
| Normal | `insert`, `insert_first_nonblank`, `append`, `append_line_end`, `open_below`, `open_above` |
|
||||
| Normal | `insert`, `append`, `open_below`, `open_above` |
|
||||
| Normal, Visual | `insert_first_nonblank`, `append_line_end`, `clone_above`, `clone_below` |
|
||||
| Normal | `join_lines`, `undo`, `redo`, `command` |
|
||||
| Normal | `search`, `paste_before`, `paste_after` |
|
||||
| Normal, Visual | `select`, `search_next`, `search_previous` |
|
||||
| Normal, Insert, Visual | `left`, `down`, `up`, `right`, `line_start`, `line_end` |
|
||||
| Normal, Insert, Visual | `word_next`, `word_previous`, `word_end`, `big_word_next`, `big_word_previous`, `big_word_end` |
|
||||
| Normal, Insert | `delete` |
|
||||
| Visual | `copy`, `cut`, `change` |
|
||||
| Insert, Command, Search, Visual | `normal` |
|
||||
| Visual | `copy`, `cut`, `change`, `select_lines`, `select_next`, `skip_occurrence`, `select_all` |
|
||||
| Normal, Insert, Command, Search, Visual | `normal` |
|
||||
| Insert, Command, Search | `backspace` |
|
||||
| Insert | `newline` |
|
||||
| Command, Search | `submit` |
|
||||
@@ -153,7 +158,7 @@ end
|
||||
|
||||
Run `:greet vamp`, or press `g` with the example configuration. `examples/plugins/greeting.rb` also provides `:upper` to uppercase the buffer.
|
||||
|
||||
Callbacks receive an immutable context snapshot: `editor.text`, `editor.row`, `editor.column`, `editor.mode`, and `editor.path`. Row/column are **one-based grapheme positions**; mode is `NORMAL`; path is a string or `nil`. Arguments are the remaining command text, without surrounding whitespace.
|
||||
Callbacks receive an immutable primary-only context snapshot: `editor.text`, `editor.row`, `editor.column`, `editor.mode`, and `editor.path`. Row/column are **one-based grapheme positions**; mode is `NORMAL`; path is a string or `nil`. Arguments are the remaining command text, without surrounding whitespace. Commands run once regardless of target count; successful text effects collapse to the primary before editing once. The plugin API does not expose selections.
|
||||
|
||||
`editor.insert(text)`, `editor.replace(text)`, and `editor.message(text)` queue effects. The context remains the pre-command snapshot even after queuing an effect. All effects are validated before the backend changes anything; buffer changes from one successful invocation form one undo action. Exceptions, process exits, timeouts, malformed replies, and invalid effects disable that plugin and discard its effects. Other plugins and built-ins remain available. Plugin `puts`, explicit `STDOUT.write`, and stderr cannot overwrite the TUI; diagnostic output is discarded, while exceptions appear in the status message.
|
||||
|
||||
|
||||
+1314
-516
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -11,9 +11,13 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
||||
h/j/k/l move, 0/$ line ends, x delete, J join lines
|
||||
w/b/e word next/previous/end, W/B/E whitespace-delimited WORDs
|
||||
u undo, U redo, : command, / search, n/N next/previous match
|
||||
v select, p/P paste after/before cursor
|
||||
Visual: motions extend selection, y copy, d/x/Delete cut, c change, v/Esc cancel
|
||||
Insert: Esc returns to normal
|
||||
v select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
||||
Esc keeps only the primary caret
|
||||
Visual: motions extend selections, y copy, d/x/Delete cut, c change
|
||||
I/A insert at each selected line's first nonblank/end
|
||||
Ctrl+l split rows, Ctrl+n add occurrence, Ctrl+s skip, Ctrl+a all
|
||||
v/Esc returns to Normal retaining carets
|
||||
Insert: Esc returns to Normal retaining carets
|
||||
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
||||
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ module Vamp
|
||||
search search_next search_previous
|
||||
word_next word_previous word_end big_word_next big_word_previous big_word_end
|
||||
select copy cut change paste_before paste_after
|
||||
select_lines clone_above clone_below select_next skip_occurrence select_all
|
||||
].each do |name|
|
||||
action = Action.new("core.#{name}".freeze).freeze
|
||||
define_singleton_method(name) { action }
|
||||
|
||||
+51
-2
@@ -155,6 +155,55 @@ Vamp.command("healthy") { |editor, args| editor.replace "healthy #{args}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_selection_actions_edit_together_and_invoke_plugins_once() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.file("buffer.txt", " alpha\nb\n gamma");
|
||||
fixture.file(
|
||||
"once.rb",
|
||||
r#"
|
||||
Vamp.plugin "once", requires: ["text"]
|
||||
Vamp.command("once") { |editor, _| editor.insert "!" }
|
||||
"#,
|
||||
);
|
||||
let mut session = fixture.open(
|
||||
r#"
|
||||
Vamp.configure do |config|
|
||||
config.plugin "once.rb"
|
||||
config.bind "r", config.core.select_lines, mode: config.modes.visual
|
||||
config.bind "z", config.core.clone_below
|
||||
config.bind "f", "once"
|
||||
end
|
||||
"#,
|
||||
);
|
||||
session.handle(Input::Char('v'));
|
||||
session.handle(Input::Char('j'));
|
||||
session.handle(Input::Char('j'));
|
||||
session.handle(Input::Char('r'));
|
||||
assert_eq!(session.editor().selection_count(), 3);
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Char('?'));
|
||||
session.handle(Input::Escape);
|
||||
assert_eq!(session.editor().lines(), &[" alpha?", "b?", " gamma?"]);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().lines(), &[" alpha", "b", " gamma"]);
|
||||
assert_eq!(session.editor().selection_count(), 3);
|
||||
session.handle(Input::Char('f'));
|
||||
assert_eq!(session.editor().selection_count(), 1);
|
||||
assert_eq!(
|
||||
session
|
||||
.editor()
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| line.matches('!').count())
|
||||
.sum::<usize>(),
|
||||
1
|
||||
);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().lines(), &[" alpha", "b", " gamma"]);
|
||||
assert_eq!(session.editor().selection_count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_rejection_installs_no_commands_and_keeps_other_plugins_and_core() {
|
||||
let fixture = Fixture::new();
|
||||
@@ -468,7 +517,7 @@ end
|
||||
session.handle(Input::Ctrl('v'));
|
||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Visual);
|
||||
session.handle(Input::Ctrl('f'));
|
||||
assert_eq!(session.editor().selection().unwrap().end.column, 3);
|
||||
assert_eq!(session.editor().selections()[0].end.column, 3);
|
||||
session.handle(Input::Ctrl('c'));
|
||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||
assert!(!session.editor().dirty());
|
||||
@@ -491,7 +540,7 @@ end
|
||||
session.handle(Input::Ctrl('v'));
|
||||
session.handle(Input::Ctrl('g'));
|
||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||
assert_eq!(session.editor().selection(), None);
|
||||
assert!(session.editor().selections().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+87
-3
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user