feat: add Visual change with grouped replacement undo
This commit is contained in:
+133
-11
@@ -146,6 +146,7 @@ enum Action {
|
||||
Select,
|
||||
Copy,
|
||||
Cut,
|
||||
Change,
|
||||
PasteBefore,
|
||||
PasteAfter,
|
||||
}
|
||||
@@ -170,6 +171,7 @@ impl Action {
|
||||
(Mode::Normal | Mode::Visual, "core.select") => Select,
|
||||
(Mode::Visual, "core.copy") => Copy,
|
||||
(Mode::Visual, "core.cut") => Cut,
|
||||
(Mode::Visual, "core.change") => Change,
|
||||
(Mode::Normal, "core.paste_before") => PasteBefore,
|
||||
(Mode::Normal, "core.paste_after") => PasteAfter,
|
||||
(Mode::Normal | Mode::Insert | Mode::Visual, "core.left") => Left,
|
||||
@@ -345,6 +347,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
for (key, action) in [
|
||||
(Input::Char('y'), Copy),
|
||||
(Input::Char('d'), Cut),
|
||||
(Input::Char('c'), Change),
|
||||
(Input::Char('x'), Cut),
|
||||
(Input::Delete, Cut),
|
||||
] {
|
||||
@@ -723,8 +726,7 @@ impl Editor {
|
||||
self.message.clear();
|
||||
}
|
||||
}
|
||||
Action::Copy => self.copy_selection(false),
|
||||
Action::Cut => self.copy_selection(true),
|
||||
Action::Copy | Action::Cut | Action::Change => self.edit_selection(action),
|
||||
Action::PasteBefore => self.paste_clipboard(false),
|
||||
Action::PasteAfter => self.paste_clipboard(true),
|
||||
Action::Normal => {
|
||||
@@ -957,13 +959,20 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_selection(&mut self, cut: bool) {
|
||||
fn edit_selection(&mut self, action: Action) {
|
||||
let Some(selection) = self.selection() else {
|
||||
return;
|
||||
};
|
||||
let cut = matches!(action, Action::Cut | Action::Change);
|
||||
let changing = matches!(action, Action::Change);
|
||||
if selection.start == selection.end {
|
||||
self.clear_selection();
|
||||
self.set_message("Nothing selected");
|
||||
if changing {
|
||||
self.begin_insert();
|
||||
self.message.clear();
|
||||
} else {
|
||||
self.set_message("Nothing selected");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let start = selection.start;
|
||||
@@ -1006,16 +1015,25 @@ impl Editor {
|
||||
tail.replace_range(..end_byte, &self.lines[start.row][..start_byte]);
|
||||
self.lines[start.row] = tail;
|
||||
}
|
||||
self.search_match = None;
|
||||
}
|
||||
if changing {
|
||||
self.mode = Mode::Insert;
|
||||
}
|
||||
self.clamp_cursor();
|
||||
if let Some(before) = before {
|
||||
self.record(before);
|
||||
}
|
||||
self.set_message(if cut {
|
||||
"Cut selection"
|
||||
if changing {
|
||||
self.insert_start = before;
|
||||
self.message.clear();
|
||||
} else {
|
||||
"Copied selection"
|
||||
});
|
||||
if let Some(before) = before {
|
||||
self.record(before);
|
||||
}
|
||||
self.set_message(if cut {
|
||||
"Cut selection"
|
||||
} else {
|
||||
"Copied selection"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_clipboard(&mut self, after: bool) {
|
||||
@@ -1527,6 +1545,110 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_change_replaces_reversed_multiline_graphemes_in_one_undo_group() {
|
||||
let mut editor = editor();
|
||||
let original = "pre界e\u{301}\n\n👩🚀tail\npost";
|
||||
editor
|
||||
.apply(vec![Effect::Replace(original.into())])
|
||||
.unwrap();
|
||||
submit_search(&mut editor, "👩🚀");
|
||||
assert_eq!(editor.search_match().unwrap().start.row, 2);
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Up);
|
||||
for _ in 0..3 {
|
||||
editor.handle(Input::Right);
|
||||
}
|
||||
editor.handle(Input::Char('c'));
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.selection(), None);
|
||||
assert_eq!(editor.search_match(), None);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 3 });
|
||||
assert_eq!(editor.context().text, "pretail\npost");
|
||||
editor.handle(Input::Paste("\0".into()));
|
||||
assert_eq!(editor.context().text, "pretail\npost");
|
||||
type_text(&mut editor, "α\nβ");
|
||||
editor.handle(Input::Escape);
|
||||
let replacement = "preα\nβtail\npost";
|
||||
assert_eq!(editor.context().text, replacement);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, original);
|
||||
editor.handle(Input::Char('U'));
|
||||
assert_eq!(editor.context().text, replacement);
|
||||
editor.handle(Input::Char('u'));
|
||||
editor.handle(Input::Char('P'));
|
||||
assert_eq!(
|
||||
editor.context().text,
|
||||
"pre界e\u{301}\n\n👩🚀界e\u{301}\n\n👩🚀tail\npost"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_change_at_eof_and_empty_ranges_preserves_insertion_and_clipboard_semantics() {
|
||||
let mut editor = editor();
|
||||
editor.apply(vec![Effect::Replace("abX".into())]).unwrap();
|
||||
editor.handle(Input::End);
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Char('c'));
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 2 });
|
||||
type_text(&mut editor, "YZ");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, "abYZ");
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "abX");
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Char('c'));
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, "ab");
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "abX");
|
||||
editor.handle(Input::Char('P'));
|
||||
assert_eq!(editor.context().text, "abXX");
|
||||
|
||||
editor.apply(vec![Effect::Replace("".into())]).unwrap();
|
||||
editor.handle(Input::Char('p'));
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "");
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Char('c'));
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.cursor(), Cursor::default());
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('U'));
|
||||
assert_eq!(editor.context().text, "X");
|
||||
editor.handle(Input::Char('u'));
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Char('c'));
|
||||
type_text(&mut editor, "new");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, "new");
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "");
|
||||
editor.handle(Input::Char('p'));
|
||||
assert_eq!(editor.context().text, "X");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_change_with_identical_replacement_preserves_redo() {
|
||||
let mut editor = editor();
|
||||
editor.apply(vec![Effect::Replace("abc".into())]).unwrap();
|
||||
editor.handle(Input::Char('i'));
|
||||
type_text(&mut editor, "X");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "abc");
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Char('e'));
|
||||
editor.handle(Input::Char('c'));
|
||||
type_text(&mut editor, "abc");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, "abc");
|
||||
editor.handle(Input::Char('U'));
|
||||
assert_eq!(editor.context().text, "Xabc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_ranges_copy_both_directions_and_cut_and_paste_multiline_graphemes() {
|
||||
let mut editor = editor();
|
||||
|
||||
Reference in New Issue
Block a user