feat: add Vim-style I and A and move redo to U

This commit is contained in:
Valdemar Lorenzen
2026-10-05 15:42:19 +02:00
parent ae1fe5223a
commit f81405a5ed
3 changed files with 68 additions and 13 deletions
+63 -10
View File
@@ -259,7 +259,7 @@ impl Editor {
}
pub fn bind(&mut self, key: char, command: String) -> Result<(), String> {
if key.is_control() || "iaoOhjklx0$u:".contains(key) {
if key.is_control() || "iIaAoOhjklx0$uU:".contains(key) {
return Err(format!("Key {key:?} is reserved for editing"));
}
if self.owner(&command).is_none() {
@@ -273,11 +273,22 @@ impl Editor {
match self.mode {
Mode::Normal => match input {
Input::Char('i') => self.begin_insert(),
Input::Char('I') => {
self.begin_insert();
self.cursor.column = self.lines[self.cursor.row]
.graphemes(true)
.take_while(|grapheme| matches!(*grapheme, " " | "\t"))
.count();
}
Input::Char('a') => {
self.begin_insert();
self.cursor.column =
(self.cursor.column + 1).min(grapheme_count(&self.lines[self.cursor.row]));
}
Input::Char('A') => {
self.begin_insert();
self.move_end();
}
Input::Char('o') | Input::Char('O') => {
self.begin_insert();
if input == Input::Char('o') {
@@ -301,7 +312,7 @@ impl Editor {
}
}
Input::Char('u') => self.undo(),
Input::Ctrl('r') => self.redo(),
Input::Char('U') => self.redo(),
Input::Char(':') => {
self.mode = Mode::Command;
self.command_line.clear();
@@ -811,6 +822,48 @@ mod tests {
}
}
#[test]
fn uppercase_u_redoes_and_control_r_no_longer_does() {
let mut editor = editor();
editor.handle(Input::Char('i'));
type_text(&mut editor, "changed");
editor.handle(Input::Escape);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
editor.handle(Input::Ctrl('r'));
assert_eq!(editor.context().text, "");
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "changed");
}
#[test]
fn uppercase_insert_keys_target_line_positions_and_preserve_undo_cursor() {
for (original, key, expected) in [
(" \t界e\u{301} tail", 'I', " \tX界e\u{301} tail"),
(" \t ", 'I', " \t X"),
("", 'I', "X"),
(" \t界e\u{301} tail", 'A', " \t界e\u{301} tailX"),
("", 'A', "X"),
] {
let mut editor = editor();
editor
.apply(vec![Effect::Replace(original.into())])
.unwrap();
editor.handle(if key == 'I' { Input::End } else { Input::Right });
let before = editor.cursor();
editor.handle(Input::Char(key));
assert_eq!(editor.mode(), Mode::Insert);
type_text(&mut editor, "X");
editor.handle(Input::Escape);
assert_eq!(editor.context().text, expected);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, original);
assert_eq!(editor.cursor(), before);
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, expected);
}
}
#[test]
fn paste_is_ignored_in_normal_and_command_modes_without_dispatching() {
let mut editor = editor();
@@ -852,7 +905,7 @@ mod tests {
assert_eq!(editor.context().text, expected);
}
for expected in ["a", "a界e\u{301}\t\n:q!\n", "a界e\u{301}\t\n:q!\nz"] {
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, expected);
}
}
@@ -880,7 +933,7 @@ mod tests {
editor.handle(Input::Escape);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "beforeafter");
}
}
@@ -896,7 +949,7 @@ mod tests {
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
assert!(!editor.dirty());
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "hello\n世界");
editor.handle(Input::Char('O'));
type_text(&mut editor, "above");
@@ -1027,7 +1080,7 @@ mod tests {
fn bindings_dispatch_only_normal_mode_and_essential_keys_stay_protected() {
let mut editor = editor();
editor.register(plugin("greeting", &["greet"])).unwrap();
for key in "iaoOhjklx0$u:\u{1b}\r".chars() {
for key in "iIaAoOhjklx0$uU:\u{1b}\r".chars() {
assert!(editor.bind(key, "greet".into()).is_err());
}
editor.bind('g', "greet".into()).unwrap();
@@ -1110,7 +1163,7 @@ mod tests {
assert_eq!(editor.message(), "ok");
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "one\ntwo");
editor
.apply(vec![Effect::Replace("replacement".into())])
@@ -1165,11 +1218,11 @@ mod tests {
editor
.apply(vec![Effect::Message("notice".into())])
.unwrap();
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "a");
editor.handle(Input::Char('u'));
editor.apply(vec![Effect::Insert("b".into())]).unwrap();
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "b");
}
@@ -1201,7 +1254,7 @@ mod tests {
assert!(!editor.dirty());
editor.handle(Input::Char('u'));
assert!(editor.dirty());
editor.handle(Input::Ctrl('r'));
editor.handle(Input::Char('U'));
assert!(!editor.dirty());
editor.execute("q");
assert!(editor.should_quit());