feat: add Vim-style I and A and move redo to U
This commit is contained in:
@@ -18,8 +18,9 @@ Without a file, vamp opens an unnamed buffer. A nonexistent file opens an empty
|
|||||||
| Mode | Keys |
|
| Mode | Keys |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Normal | `i` insert before cursor; `a` insert after cursor; `o`/`O` open line below/above |
|
| 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 | `h`/`j`/`k`/`l` or arrows; `0`/`$` or Home/End |
|
||||||
| Normal | `x`/Delete delete; `u` undo; Ctrl-r redo; `:` enter command mode |
|
| Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode |
|
||||||
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End; Esc returns to normal |
|
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End; Esc returns to normal |
|
||||||
| Command | Enter executes; Backspace edits; Esc cancels |
|
| Command | Enter executes; Backspace edits; Esc cancels |
|
||||||
|
|
||||||
|
|||||||
+63
-10
@@ -259,7 +259,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind(&mut self, key: char, command: String) -> Result<(), String> {
|
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"));
|
return Err(format!("Key {key:?} is reserved for editing"));
|
||||||
}
|
}
|
||||||
if self.owner(&command).is_none() {
|
if self.owner(&command).is_none() {
|
||||||
@@ -273,11 +273,22 @@ impl Editor {
|
|||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => match input {
|
Mode::Normal => match input {
|
||||||
Input::Char('i') => self.begin_insert(),
|
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') => {
|
Input::Char('a') => {
|
||||||
self.begin_insert();
|
self.begin_insert();
|
||||||
self.cursor.column =
|
self.cursor.column =
|
||||||
(self.cursor.column + 1).min(grapheme_count(&self.lines[self.cursor.row]));
|
(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') => {
|
Input::Char('o') | Input::Char('O') => {
|
||||||
self.begin_insert();
|
self.begin_insert();
|
||||||
if input == Input::Char('o') {
|
if input == Input::Char('o') {
|
||||||
@@ -301,7 +312,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Input::Char('u') => self.undo(),
|
Input::Char('u') => self.undo(),
|
||||||
Input::Ctrl('r') => self.redo(),
|
Input::Char('U') => self.redo(),
|
||||||
Input::Char(':') => {
|
Input::Char(':') => {
|
||||||
self.mode = Mode::Command;
|
self.mode = Mode::Command;
|
||||||
self.command_line.clear();
|
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]
|
#[test]
|
||||||
fn paste_is_ignored_in_normal_and_command_modes_without_dispatching() {
|
fn paste_is_ignored_in_normal_and_command_modes_without_dispatching() {
|
||||||
let mut editor = editor();
|
let mut editor = editor();
|
||||||
@@ -852,7 +905,7 @@ mod tests {
|
|||||||
assert_eq!(editor.context().text, expected);
|
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"] {
|
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);
|
assert_eq!(editor.context().text, expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -880,7 +933,7 @@ mod tests {
|
|||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
editor.handle(Input::Char('u'));
|
editor.handle(Input::Char('u'));
|
||||||
assert_eq!(editor.context().text, "");
|
assert_eq!(editor.context().text, "");
|
||||||
editor.handle(Input::Ctrl('r'));
|
editor.handle(Input::Char('U'));
|
||||||
assert_eq!(editor.context().text, "beforeafter");
|
assert_eq!(editor.context().text, "beforeafter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -896,7 +949,7 @@ mod tests {
|
|||||||
editor.handle(Input::Char('u'));
|
editor.handle(Input::Char('u'));
|
||||||
assert_eq!(editor.context().text, "");
|
assert_eq!(editor.context().text, "");
|
||||||
assert!(!editor.dirty());
|
assert!(!editor.dirty());
|
||||||
editor.handle(Input::Ctrl('r'));
|
editor.handle(Input::Char('U'));
|
||||||
assert_eq!(editor.context().text, "hello\n世界");
|
assert_eq!(editor.context().text, "hello\n世界");
|
||||||
editor.handle(Input::Char('O'));
|
editor.handle(Input::Char('O'));
|
||||||
type_text(&mut editor, "above");
|
type_text(&mut editor, "above");
|
||||||
@@ -1027,7 +1080,7 @@ mod tests {
|
|||||||
fn bindings_dispatch_only_normal_mode_and_essential_keys_stay_protected() {
|
fn bindings_dispatch_only_normal_mode_and_essential_keys_stay_protected() {
|
||||||
let mut editor = editor();
|
let mut editor = editor();
|
||||||
editor.register(plugin("greeting", &["greet"])).unwrap();
|
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());
|
assert!(editor.bind(key, "greet".into()).is_err());
|
||||||
}
|
}
|
||||||
editor.bind('g', "greet".into()).unwrap();
|
editor.bind('g', "greet".into()).unwrap();
|
||||||
@@ -1110,7 +1163,7 @@ mod tests {
|
|||||||
assert_eq!(editor.message(), "ok");
|
assert_eq!(editor.message(), "ok");
|
||||||
editor.handle(Input::Char('u'));
|
editor.handle(Input::Char('u'));
|
||||||
assert_eq!(editor.context().text, "");
|
assert_eq!(editor.context().text, "");
|
||||||
editor.handle(Input::Ctrl('r'));
|
editor.handle(Input::Char('U'));
|
||||||
assert_eq!(editor.context().text, "one\ntwo");
|
assert_eq!(editor.context().text, "one\ntwo");
|
||||||
editor
|
editor
|
||||||
.apply(vec![Effect::Replace("replacement".into())])
|
.apply(vec![Effect::Replace("replacement".into())])
|
||||||
@@ -1165,11 +1218,11 @@ mod tests {
|
|||||||
editor
|
editor
|
||||||
.apply(vec![Effect::Message("notice".into())])
|
.apply(vec![Effect::Message("notice".into())])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
editor.handle(Input::Ctrl('r'));
|
editor.handle(Input::Char('U'));
|
||||||
assert_eq!(editor.context().text, "a");
|
assert_eq!(editor.context().text, "a");
|
||||||
editor.handle(Input::Char('u'));
|
editor.handle(Input::Char('u'));
|
||||||
editor.apply(vec![Effect::Insert("b".into())]).unwrap();
|
editor.apply(vec![Effect::Insert("b".into())]).unwrap();
|
||||||
editor.handle(Input::Ctrl('r'));
|
editor.handle(Input::Char('U'));
|
||||||
assert_eq!(editor.context().text, "b");
|
assert_eq!(editor.context().text, "b");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1201,7 +1254,7 @@ mod tests {
|
|||||||
assert!(!editor.dirty());
|
assert!(!editor.dirty());
|
||||||
editor.handle(Input::Char('u'));
|
editor.handle(Input::Char('u'));
|
||||||
assert!(editor.dirty());
|
assert!(editor.dirty());
|
||||||
editor.handle(Input::Ctrl('r'));
|
editor.handle(Input::Char('U'));
|
||||||
assert!(!editor.dirty());
|
assert!(!editor.dirty());
|
||||||
editor.execute("q");
|
editor.execute("q");
|
||||||
assert!(editor.should_quit());
|
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]
|
Usage: vamp [--config PATH | --no-config] [FILE]
|
||||||
|
|
||||||
Normal: i/a insert, o/O new line, h/j/k/l move, 0/$ line ends,
|
Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
||||||
x delete, u undo, Ctrl-r redo, : command
|
h/j/k/l move, 0/$ line ends, x delete
|
||||||
|
u undo, U redo, : command
|
||||||
Insert: Esc returns to normal
|
Insert: Esc returns to normal
|
||||||
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user