feat: add Vim-style I and A and move redo to U
This commit is contained in:
+63
-10
@@ -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());
|
||||
|
||||
+3
-2
@@ -6,8 +6,9 @@ const HELP: &str = "vamp — modal editor with isolated Ruby plugins
|
||||
|
||||
Usage: vamp [--config PATH | --no-config] [FILE]
|
||||
|
||||
Normal: i/a insert, o/O new line, h/j/k/l move, 0/$ line ends,
|
||||
x delete, u undo, Ctrl-r redo, : command
|
||||
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
|
||||
u undo, U redo, : command
|
||||
Insert: Esc returns to normal
|
||||
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user