feat: repeat last native edit with dot
This commit is contained in:
@@ -28,6 +28,7 @@ Without a file, vamp opens an unnamed buffer. A nonexistent file opens an empty
|
|||||||
| Normal, Visual | Decimal prefixes repeat cursor/word/search motions: `5j`, `3k`, `2w`, `4e`, `2n`; Esc cancels a pending count |
|
| Normal, Visual | Decimal prefixes repeat cursor/word/search motions: `5j`, `3k`, `2w`, `4e`, `2n`; Esc cancels a pending count |
|
||||||
| Normal | `J` (Shift+j) joins each targeted line with the next line |
|
| 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 | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode |
|
||||||
|
| Normal, Visual | `.` repeats the last successful native edit against the current targets; one undo group |
|
||||||
| Normal | `/` opens search; `n` finds the next match; `N` (Shift+n) finds the previous match |
|
| Normal | `/` opens search; `n` finds the next match; `N` (Shift+n) finds the previous match |
|
||||||
| Normal | `v` starts Visual selections; `p`/`P` paste internal clipboard after/before each caret; Esc keeps only the primary caret |
|
| 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 |
|
| Normal, Visual | Ctrl+k clones above; Ctrl+e clones below |
|
||||||
@@ -63,6 +64,20 @@ Bracketed paste broadcasts the same literal text to all Insert carets in a separ
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### Repeat last edit
|
||||||
|
|
||||||
|
`.` repeats the session's last successful native edit in Normal/Visual. Insertion placements (`i`/`I`/`a`/`A`/`o`/`O`), Insert typing and edit-context motions/newline/backspace/delete, Visual change/cut, Normal deletion, clipboard/literal paste, and line joining are recorded as resolved edit operations—not keystrokes, command invocations, or old byte offsets. Remapping keys afterward does not change the recorded operation. Insert `.` and popup query `.` remain literal. A count before dot is consumed and repeats once, consistent with other non-motion commands.
|
||||||
|
|
||||||
|
In Normal, Visual change/cut targeting replays its resolved motions and counts at each current caret: `vecitem` followed later by `.` changes a fresh whole word, even if its length differs. If targeting was rebuilt by cloning, occurrence gathering, search, or overlap merging, repeat uses relative whole-grapheme spans instead. Equal spans broadcast; unequal spans require matching target counts. In Visual, the current explicit ranges replace the stored targeting. Target-building commands themselves do not recreate old cursors.
|
||||||
|
|
||||||
|
Clipboard paste captures the fragments actually pasted, so a later yank does not change repeat's payload. One fragment broadcasts; equal fragment/target counts pair; mismatches refuse atomically. Other per-target edit data, such as partially effective Insert deletion, obeys the same conservative distribution policy. A refusal or cardinality change during replay leaves text, caret/primary state, clipboard, and history unchanged.
|
||||||
|
|
||||||
|
Each repeat returns to Normal and creates one undo entry restoring the full caret set and primary. Original typing/paste/typing boundaries remain separate; dot replays the selected committed group as one transaction. Typed text retains scalar insertion boundaries, preserving combining/ZWJ interactions with surrounding graphemes; bracketed paste remains one bulk literal operation. Trailing Insert movement is not repeated.
|
||||||
|
|
||||||
|
The last recipe is session-wide across buffers, but pending Insert groups and undo/redo remain buffer-local. No-op/identical edits, rejected input, navigation, yank, explorer filtering, saving, undo/redo, and plugin effects do not replace it. Successful buffer parking and ordinary explicit Ex execution commit pending native typing at existing boundaries; failed buffer requests do not split that group. Plugins never execute through dot. Repeat does not consume or recursively replace its recipe. Rebind with `config.core.repeat_last_edit`, including Normal/Visual leader routes.
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Submitting an empty search repeats the last query. Esc cancels without moving the cursor or replacing the previous query/match. A missing match reports the query, clears the highlight, and leaves the cursor unchanged; searching without a previous query also leaves the cursor unchanged. Search never edits text or adds undo history. Buffer edits and undo/redo clear the highlight but retain the query, so subsequent navigation searches the current text again.
|
Submitting an empty search repeats the last query. Esc cancels without moving the cursor or replacing the previous query/match. A missing match reports the query, clears the highlight, and leaves the cursor unchanged; searching without a previous query also leaves the cursor unchanged. Search never edits text or adds undo history. Buffer edits and undo/redo clear the highlight but retain the query, so subsequent navigation searches the current text again.
|
||||||
@@ -242,6 +257,7 @@ Key notation follows [Herdr's readable style](https://herdr.dev/docs/configurati
|
|||||||
| Normal | `join_lines`, `undo`, `redo`, `command` |
|
| Normal | `join_lines`, `undo`, `redo`, `command` |
|
||||||
| Normal | `search`, `paste_before`, `paste_after` |
|
| Normal | `search`, `paste_before`, `paste_after` |
|
||||||
| Normal, Visual | `select`, `search_next`, `search_previous` |
|
| Normal, Visual | `select`, `search_next`, `search_previous` |
|
||||||
|
| Normal, Visual | `repeat_last_edit` |
|
||||||
| Normal, Visual | `file_picker`, `file_explorer` |
|
| Normal, Visual | `file_picker`, `file_explorer` |
|
||||||
| Normal, Insert, Visual | `line_start`, `line_end` |
|
| Normal, Insert, Visual | `line_start`, `line_end` |
|
||||||
| Normal, Insert, Visual, Tree | `left`, `right` |
|
| Normal, Insert, Visual, Tree | `left`, `right` |
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Vamp.configure do |config|
|
|||||||
config.bind "g", "greet"
|
config.bind "g", "greet"
|
||||||
config.bind "leader g", "greet"
|
config.bind "leader g", "greet"
|
||||||
config.bind "ctrl+r", config.core.redo
|
config.bind "ctrl+r", config.core.redo
|
||||||
|
config.bind "leader r", config.core.repeat_last_edit
|
||||||
config.bind "ctrl+c", config.core.normal, mode: config.modes.insert
|
config.bind "ctrl+c", config.core.normal, mode: config.modes.insert
|
||||||
config.bind "ctrl+f", config.core.search
|
config.bind "ctrl+f", config.core.search
|
||||||
config.bind "ctrl+c", config.core.normal, mode: config.modes.search
|
config.bind "ctrl+c", config.core.normal, mode: config.modes.search
|
||||||
|
|||||||
+1085
-80
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
|||||||
w/b/e word next/previous/end, W/B/E whitespace-delimited WORDs
|
w/b/e word next/previous/end, W/B/E whitespace-delimited WORDs
|
||||||
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
||||||
u undo, U redo, : command, / search, n/N next/previous match
|
u undo, U redo, : command, / search, n/N next/previous match
|
||||||
|
Normal/Visual: . repeats last native edit at current targets (one undo group)
|
||||||
v select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
v select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
||||||
Esc keeps only the primary caret
|
Esc keeps only the primary caret
|
||||||
Visual: motions extend selections, y copy, d/x/Delete cut, c change
|
Visual: motions extend selections, y copy, d/x/Delete cut, c change
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ module Vamp
|
|||||||
%w[
|
%w[
|
||||||
insert insert_first_nonblank append append_line_end open_below open_above
|
insert insert_first_nonblank append append_line_end open_below open_above
|
||||||
left right down up join_lines line_start line_end delete undo redo command
|
left right down up join_lines line_start line_end delete undo redo command
|
||||||
normal newline backspace submit palette
|
normal newline backspace submit palette repeat_last_edit
|
||||||
search search_next search_previous
|
search search_next search_previous
|
||||||
word_next word_previous word_end big_word_next big_word_previous big_word_end
|
word_next word_previous word_end big_word_next big_word_previous big_word_end
|
||||||
select copy cut change paste_before paste_after
|
select copy cut change paste_before paste_after
|
||||||
|
|||||||
+371
@@ -1006,6 +1006,377 @@ end
|
|||||||
assert!(session.editor().should_quit());
|
assert!(session.editor().should_quit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_dot_replaces_fresh_words_and_each_repeat_has_one_undo() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let buffer = fixture.file("buffer.txt", "old elephant zebra");
|
||||||
|
let mut session = EditorSession::open(Some(buffer), vec!["text".into()], None).unwrap();
|
||||||
|
for key in ['v', 'e', 'c'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
for ch in "item".chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert_eq!(session.editor().lines(), &["item elephant zebra"]);
|
||||||
|
for key in ['w', '3', '.'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
assert_eq!(session.editor().lines(), &["item item zebra"]);
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item elephant zebra"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
for key in ['w', 'w', 'v', 'e', '.'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
assert_eq!(session.editor().lines(), &["item elephant item"]);
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item elephant zebra"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["old elephant zebra"]);
|
||||||
|
assert!(!session.editor().dirty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_repeat_remaps_use_resolved_motions_and_current_visual_ranges() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", "old enormous tail end");
|
||||||
|
let mut session = fixture.open(
|
||||||
|
r#"
|
||||||
|
Vamp.configure do |config|
|
||||||
|
config.bind ".", nil
|
||||||
|
config.bind ".", nil, mode: config.modes.visual
|
||||||
|
config.bind "r", config.core.repeat_last_edit
|
||||||
|
config.bind "r", config.core.repeat_last_edit, mode: config.modes.visual
|
||||||
|
config.bind "e", config.core.left, mode: config.modes.visual
|
||||||
|
config.bind "ctrl+f", config.core.word_end, mode: config.modes.visual
|
||||||
|
end
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('v'));
|
||||||
|
session.handle(Input::Ctrl('f'));
|
||||||
|
session.handle(Input::Char('c'));
|
||||||
|
for ch in "item".chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
session.handle(Input::Char('w'));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item enormous tail end"]);
|
||||||
|
session.handle(Input::Char('r'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item item tail end"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item enormous tail end"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
for key in ['w', 'v', '2'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
session.handle(Input::Ctrl('f'));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Visual);
|
||||||
|
assert_eq!(session.editor().lines(), &["item enormous tail end"]);
|
||||||
|
session.handle(Input::Char('r'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item item end"]);
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item enormous tail end"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["old enormous tail end"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeat_recipe_crosses_buffers_but_pending_insert_and_undo_stay_per_buffer() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", "alpha");
|
||||||
|
let second = fixture.file("second.txt", "beta");
|
||||||
|
let mut session = fixture.open("");
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
for ch in "one".chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
session.execute(&format!("edit {}", second.display()));
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
assert_eq!(session.editor().lines(), &["beta"]);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["onebeta"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["beta"]);
|
||||||
|
session.execute("buffer 1");
|
||||||
|
assert_eq!(session.editor().lines(), &["onealpha"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["alpha"]);
|
||||||
|
session.handle(Input::Char('U'));
|
||||||
|
assert_eq!(session.editor().lines(), &["onealpha"]);
|
||||||
|
session.execute("buffer 2");
|
||||||
|
assert_eq!(session.editor().lines(), &["beta"]);
|
||||||
|
session.handle(Input::Char('U'));
|
||||||
|
assert_eq!(session.editor().lines(), &["onebeta"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
for ch in "two".chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
session.execute("buffer 1");
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["twoonealpha"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["onealpha"]);
|
||||||
|
session.execute("buffer 2");
|
||||||
|
assert_eq!(session.editor().lines(), &["twoonebeta"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["onebeta"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["beta"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_effects_commit_pending_typing_without_replacing_or_reinvoking_native_recipe() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", "base");
|
||||||
|
fixture.file(
|
||||||
|
"effects.rb",
|
||||||
|
r##"
|
||||||
|
Vamp.plugin "effects", requires: ["text"]
|
||||||
|
calls = 0
|
||||||
|
Vamp.command("effect") do |editor, args|
|
||||||
|
calls += 1
|
||||||
|
File.open("calls.txt", "a") { |file| file.puts "#{calls} #{args}" }
|
||||||
|
case args
|
||||||
|
when "replace"
|
||||||
|
editor.replace "plugin"
|
||||||
|
when "insert"
|
||||||
|
editor.insert "?"
|
||||||
|
end
|
||||||
|
editor.message "calls=#{calls}"
|
||||||
|
end
|
||||||
|
"##,
|
||||||
|
);
|
||||||
|
let mut session = fixture.open(
|
||||||
|
r#"
|
||||||
|
Vamp.configure do |config|
|
||||||
|
config.plugin "effects.rb"
|
||||||
|
end
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::Char('!'));
|
||||||
|
session.execute("effect replace");
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
assert_eq!(session.editor().lines(), &["plugin"]);
|
||||||
|
assert_eq!(session.editor().message(), "calls=1");
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["!plugin"]);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(fixture.0.join("calls.txt")).unwrap(),
|
||||||
|
"1 replace\n"
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["plugin"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["!base"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["base"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.execute("effect insert");
|
||||||
|
assert_eq!(session.editor().lines(), &["?base"]);
|
||||||
|
assert_eq!(session.editor().message(), "calls=2");
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["!?base"]);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(fixture.0.join("calls.txt")).unwrap(),
|
||||||
|
"1 replace\n2 insert\n"
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["?base"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["base"]);
|
||||||
|
session.execute("effect message");
|
||||||
|
assert_eq!(session.editor().message(), "calls=3");
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["!base"]);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(fixture.0.join("calls.txt")).unwrap(),
|
||||||
|
"1 replace\n2 insert\n3 message\n"
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["base"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_save_commits_pending_change_and_repeat_does_not_save_again() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let buffer = fixture.file("buffer.txt", "old elephant");
|
||||||
|
let mut session = fixture.open("");
|
||||||
|
for key in ['v', 'e', 'c'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
for ch in "item".chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
session.execute("w");
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
assert_eq!(fs::read_to_string(&buffer).unwrap(), "item elephant");
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('w'));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item item"]);
|
||||||
|
assert!(session.editor().dirty());
|
||||||
|
assert_eq!(fs::read_to_string(&buffer).unwrap(), "item elephant");
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["item elephant"]);
|
||||||
|
assert!(!session.editor().dirty());
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["old elephant"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dots_in_insert_and_focused_queries_are_literal_and_excluded_from_recipe() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", "alpha");
|
||||||
|
let mut session = fixture.open(
|
||||||
|
r#"
|
||||||
|
Vamp.configure do |config|
|
||||||
|
config.file_explorer_view = "list"
|
||||||
|
config.file_picker_view = "list"
|
||||||
|
config.bind "ctrl+f", config.core.file_explorer
|
||||||
|
config.bind "ctrl+o", config.core.file_picker
|
||||||
|
end
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::Char('X'));
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Ctrl('f'));
|
||||||
|
for ch in ".query".chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
assert_eq!(session.editor().file_picker().unwrap().query, ".query");
|
||||||
|
assert_eq!(session.editor().lines(), &["Xalpha"]);
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert!(!session.editor().file_picker().unwrap().focused);
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
session.handle(Input::Ctrl('o'));
|
||||||
|
session.handle(Input::Ctrl('u'));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().file_picker().unwrap().query, ".");
|
||||||
|
assert_eq!(session.editor().lines(), &["Xalpha"]);
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert!(session.editor().file_picker().is_none());
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["XXalpha"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["Xalpha"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
session.handle(Input::Ctrl('p'));
|
||||||
|
assert!(
|
||||||
|
session
|
||||||
|
.editor()
|
||||||
|
.palette()
|
||||||
|
.unwrap()
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.name == "core.repeat_last_edit")
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().palette().unwrap().query, ".");
|
||||||
|
assert_eq!(session.editor().lines(), &[".Xalpha"]);
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
session.handle(Input::Char('!'));
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert_eq!(session.editor().lines(), &[".!Xalpha"]);
|
||||||
|
for prompt in [':', '/'] {
|
||||||
|
session.handle(Input::Char(prompt));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().command_line(), ".");
|
||||||
|
assert_eq!(session.editor().lines(), &[".!Xalpha"]);
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
}
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &[".!.!Xalpha"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &[".!Xalpha"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["Xalpha"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["alpha"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_clipboard_paste_uses_captured_text_after_a_different_yank() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", "one two three");
|
||||||
|
let mut session = fixture.open("");
|
||||||
|
for key in ['v', 'e', 'y'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('P'));
|
||||||
|
assert_eq!(session.editor().lines(), &["oneone two three"]);
|
||||||
|
for key in ['w', 'v', 'e', 'y'] {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["oneoneone two three"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["oneone two three"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('P'));
|
||||||
|
assert_eq!(session.editor().lines(), &["twooneone two three"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["oneone two three"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["one two three"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn literal_paste_repeats_without_dispatch_and_preserves_native_input_groups() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", "seed");
|
||||||
|
let mut session = fixture.open("");
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::Char('a'));
|
||||||
|
session.handle(Input::Paste(".:q!\r\n界".into()));
|
||||||
|
session.handle(Input::Char('b'));
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert_eq!(session.editor().lines(), &["a.:q!", "界bseed"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["a.:q!", "界seed"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["aseed"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["seed"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &["bseed"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["seed"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::Paste(".:q!\r\n界".into()));
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert_eq!(session.editor().lines(), &[".:q!", "界seed"]);
|
||||||
|
session.handle(Input::Home);
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().lines(), &[".:q!", ".:q!", "界界seed"]);
|
||||||
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Normal);
|
||||||
|
assert!(!session.editor().should_quit());
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &[".:q!", "界seed"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["seed"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn typed_visual_change_remaps_c_and_command_execution_commits_one_replacement() {
|
fn typed_visual_change_remaps_c_and_command_execution_commits_one_replacement() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user