feat(tui): add vim change-to-line-end binding

This commit is contained in:
Felipe Coury
2026-05-25 15:48:12 -03:00
parent 11bf378594
commit 5f86c5259b
5 changed files with 92 additions and 1 deletions
+2
View File
@@ -223,6 +223,8 @@ pub struct TuiVimNormalKeymap {
pub delete_char: Option<KeybindingsSpec>,
/// Delete from cursor to end of line (`D`).
pub delete_to_line_end: Option<KeybindingsSpec>,
/// Change from cursor to end of line and enter insert mode (`C`).
pub change_to_line_end: Option<KeybindingsSpec>,
/// Yank the entire line (`Y`).
pub yank_line: Option<KeybindingsSpec>,
/// Paste after cursor (`p`).
+10
View File
@@ -2824,6 +2824,7 @@
"append_after_cursor": null,
"append_line_end": null,
"cancel_operator": null,
"change_to_line_end": null,
"delete_char": null,
"delete_to_line_end": null,
"enter_insert": null,
@@ -3504,6 +3505,7 @@
"append_after_cursor": null,
"append_line_end": null,
"cancel_operator": null,
"change_to_line_end": null,
"delete_char": null,
"delete_to_line_end": null,
"enter_insert": null,
@@ -3790,6 +3792,14 @@
],
"description": "Cancel a pending operator and return to normal mode."
},
"change_to_line_end": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Change from cursor to end of line and enter insert mode (`C`)."
},
"delete_char": {
"allOf": [
{
+67 -1
View File
@@ -736,7 +736,12 @@ impl TextArea {
return;
}
if self.vim_normal_keymap.delete_to_line_end.is_pressed(event) {
self.kill_to_end_of_line();
self.vim_kill_to_end_of_line();
return;
}
if self.vim_normal_keymap.change_to_line_end.is_pressed(event) {
self.vim_kill_to_end_of_line();
self.vim_mode = VimMode::Insert;
return;
}
if self.vim_normal_keymap.yank_line.is_pressed(event) {
@@ -1035,6 +1040,13 @@ impl TextArea {
}
}
fn vim_kill_to_end_of_line(&mut self) {
let eol = self.end_of_current_line();
if self.cursor_pos < eol {
self.kill_range(self.cursor_pos..eol);
}
}
pub fn kill_to_beginning_of_line(&mut self) {
let bol = self.beginning_of_current_line();
let range = if self.cursor_pos == bol {
@@ -2321,6 +2333,60 @@ mod tests {
assert_eq!(t.cursor(), 11);
}
#[test]
fn vim_shift_c_changes_to_line_end_and_enters_insert_mode() {
let mut t = ta_with("hello world\nnext line");
t.set_cursor(/*pos*/ 6);
t.set_vim_enabled(/*enabled*/ true);
t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SHIFT));
assert_eq!(t.text(), "hello \nnext line");
assert_eq!(t.vim_mode_label(), Some("Insert"));
assert_eq!(t.cursor(), 6);
assert_eq!(t.kill_buffer, "world");
}
#[test]
fn vim_uppercase_c_changes_to_line_end() {
let mut t = ta_with("hello world\nnext line");
t.set_cursor(/*pos*/ 6);
t.set_vim_enabled(/*enabled*/ true);
t.input(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE));
assert_eq!(t.text(), "hello \nnext line");
assert_eq!(t.vim_mode_label(), Some("Insert"));
assert_eq!(t.cursor(), 6);
}
#[test]
fn vim_d_at_line_end_does_not_remove_newline() {
let mut t = ta_with("hello\nworld");
t.set_cursor(/*pos*/ "hello".len());
t.set_vim_enabled(/*enabled*/ true);
t.input(KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE));
assert_eq!(t.text(), "hello\nworld");
assert_eq!(t.vim_mode_label(), Some("Normal"));
assert_eq!(t.kill_buffer, "");
}
#[test]
fn vim_c_at_line_end_enters_insert_without_removing_newline() {
let mut t = ta_with("hello\nworld");
t.set_cursor(/*pos*/ "hello".len());
t.set_vim_enabled(/*enabled*/ true);
t.input(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE));
assert_eq!(t.text(), "hello\nworld");
assert_eq!(t.vim_mode_label(), Some("Insert"));
assert_eq!(t.cursor(), "hello".len());
assert_eq!(t.kill_buffer, "");
}
#[test]
fn vim_shift_o_opens_line_above_with_shift_only_binding() {
let mut t = ta_with("hello\nworld");
+10
View File
@@ -157,6 +157,7 @@ pub(crate) struct VimNormalKeymap {
pub(crate) move_line_end: Vec<KeyBinding>,
pub(crate) delete_char: Vec<KeyBinding>,
pub(crate) delete_to_line_end: Vec<KeyBinding>,
pub(crate) change_to_line_end: Vec<KeyBinding>,
pub(crate) yank_line: Vec<KeyBinding>,
pub(crate) paste_after: Vec<KeyBinding>,
pub(crate) start_delete_operator: Vec<KeyBinding>,
@@ -487,6 +488,7 @@ impl RuntimeKeymap {
move_line_end: resolve_local!(keymap, defaults, vim_normal, move_line_end),
delete_char: resolve_local!(keymap, defaults, vim_normal, delete_char),
delete_to_line_end: resolve_local!(keymap, defaults, vim_normal, delete_to_line_end),
change_to_line_end: resolve_local!(keymap, defaults, vim_normal, change_to_line_end),
yank_line: resolve_local!(keymap, defaults, vim_normal, yank_line),
paste_after: resolve_local!(keymap, defaults, vim_normal, paste_after),
start_delete_operator: resolve_local!(
@@ -984,6 +986,10 @@ impl RuntimeKeymap {
shift(KeyCode::Char('d')),
plain(KeyCode::Char('D'))
],
change_to_line_end: default_bindings![
shift(KeyCode::Char('c')),
plain(KeyCode::Char('C'))
],
yank_line: default_bindings![shift(KeyCode::Char('y')), plain(KeyCode::Char('Y'))],
paste_after: default_bindings![plain(KeyCode::Char('p'))],
start_delete_operator: default_bindings![plain(KeyCode::Char('d'))],
@@ -1404,6 +1410,10 @@ impl RuntimeKeymap {
"delete_to_line_end",
self.vim_normal.delete_to_line_end.as_slice(),
),
(
"change_to_line_end",
self.vim_normal.change_to_line_end.as_slice(),
),
("yank_line", self.vim_normal.yank_line.as_slice()),
("paste_after", self.vim_normal.paste_after.as_slice()),
(
+3
View File
@@ -135,6 +135,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("vim_normal", "Vim normal", "move_line_end", "Move to the end of the line."),
action("vim_normal", "Vim normal", "delete_char", "Delete the character under the cursor."),
action("vim_normal", "Vim normal", "delete_to_line_end", "Delete from cursor to end of line."),
action("vim_normal", "Vim normal", "change_to_line_end", "Change from cursor to end of line and enter insert mode."),
action("vim_normal", "Vim normal", "yank_line", "Yank the entire line."),
action("vim_normal", "Vim normal", "paste_after", "Paste after the cursor."),
action("vim_normal", "Vim normal", "start_delete_operator", "Begin a delete operator and wait for a motion."),
@@ -275,6 +276,7 @@ pub(super) fn binding_slot<'a>(
("vim_normal", "move_line_end") => Some(&mut keymap.vim_normal.move_line_end),
("vim_normal", "delete_char") => Some(&mut keymap.vim_normal.delete_char),
("vim_normal", "delete_to_line_end") => Some(&mut keymap.vim_normal.delete_to_line_end),
("vim_normal", "change_to_line_end") => Some(&mut keymap.vim_normal.change_to_line_end),
("vim_normal", "yank_line") => Some(&mut keymap.vim_normal.yank_line),
("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after),
("vim_normal", "start_delete_operator") => Some(&mut keymap.vim_normal.start_delete_operator),
@@ -397,6 +399,7 @@ pub(super) fn bindings_for_action<'a>(
("vim_normal", "move_line_end") => Some(runtime_keymap.vim_normal.move_line_end.as_slice()),
("vim_normal", "delete_char") => Some(runtime_keymap.vim_normal.delete_char.as_slice()),
("vim_normal", "delete_to_line_end") => Some(runtime_keymap.vim_normal.delete_to_line_end.as_slice()),
("vim_normal", "change_to_line_end") => Some(runtime_keymap.vim_normal.change_to_line_end.as_slice()),
("vim_normal", "yank_line") => Some(runtime_keymap.vim_normal.yank_line.as_slice()),
("vim_normal", "paste_after") => Some(runtime_keymap.vim_normal.paste_after.as_slice()),
("vim_normal", "start_delete_operator") => Some(runtime_keymap.vim_normal.start_delete_operator.as_slice()),