mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Add dot-repeat to Vim mode (#40521)
## What changed - Bind `.` in Vim normal mode to repeat the last complete edit. - Record semantic delete, change, replace, paste, and insert-mode actions so repeats preserve motions, text objects, inserted text, and editor actions across keymap changes. - Expose `vim_normal.repeat_last_change` in the configurable keymap and keymap picker, including conflict handling for existing custom bindings. ## Testing - Add coverage for repeating deletes, replacements, complete changes, pasted and literal text, insert-mode editor actions, custom bindings, unavailable motions, and cleared buffers. GitOrigin-RevId: 3213df964811e7902377ff1d93641e59f4037914
This commit is contained in:
committed by
copyberry
parent
cbfd999db7
commit
c1db22a3cd
@@ -239,6 +239,8 @@ pub struct TuiVimNormalKeymap {
|
||||
pub delete_char: Option<KeybindingsSpec>,
|
||||
/// Replace the character under the cursor (`r`).
|
||||
pub replace_char: Option<KeybindingsSpec>,
|
||||
/// Repeat the last complete edit (`.`).
|
||||
pub repeat_last_change: Option<KeybindingsSpec>,
|
||||
/// Delete character under cursor and enter insert mode (`s`).
|
||||
pub substitute_char: Option<KeybindingsSpec>,
|
||||
/// Delete from cursor to end of line (`D`).
|
||||
|
||||
@@ -3801,6 +3801,7 @@
|
||||
"open_line_above": null,
|
||||
"open_line_below": null,
|
||||
"paste_after": null,
|
||||
"repeat_last_change": null,
|
||||
"replace_char": null,
|
||||
"start_change_operator": null,
|
||||
"start_delete_operator": null,
|
||||
@@ -4608,6 +4609,7 @@
|
||||
"open_line_above": null,
|
||||
"open_line_below": null,
|
||||
"paste_after": null,
|
||||
"repeat_last_change": null,
|
||||
"replace_char": null,
|
||||
"start_change_operator": null,
|
||||
"start_delete_operator": null,
|
||||
@@ -5016,6 +5018,14 @@
|
||||
],
|
||||
"description": "Paste after cursor (`p`)."
|
||||
},
|
||||
"repeat_last_change": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Repeat the last complete edit (`.`)."
|
||||
},
|
||||
"replace_char": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -51,7 +51,12 @@ use self::vim::VimMode;
|
||||
use self::vim::VimMotion;
|
||||
use self::vim::VimOperator;
|
||||
use self::vim::VimPending;
|
||||
use self::vim::VimTextObject;
|
||||
use self::vim::VimTextObjectScope;
|
||||
use self::vim_commands::VimAction;
|
||||
use self::vim_commands::VimCommandState;
|
||||
use self::vim_commands::VimEditTarget;
|
||||
use self::vim_commands::VimInsertPosition;
|
||||
|
||||
const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
|
||||
|
||||
@@ -131,6 +136,7 @@ pub(crate) struct TextArea {
|
||||
vim_enabled: bool,
|
||||
vim_mode: VimMode,
|
||||
vim_pending: VimPending,
|
||||
vim_commands: VimCommandState,
|
||||
editor_keymap: Arc<EditorKeymap>,
|
||||
vim_normal_keymap: VimNormalKeymap,
|
||||
vim_operator_keymap: VimOperatorKeymap,
|
||||
@@ -172,6 +178,7 @@ impl TextArea {
|
||||
vim_enabled: false,
|
||||
vim_mode: VimMode::Insert,
|
||||
vim_pending: VimPending::None,
|
||||
vim_commands: VimCommandState::default(),
|
||||
editor_keymap: defaults.editor,
|
||||
vim_normal_keymap: defaults.vim_normal,
|
||||
vim_operator_keymap: defaults.vim_operator,
|
||||
@@ -240,6 +247,8 @@ impl TextArea {
|
||||
self.cursor_pos = self.clamp_pos_to_nearest_boundary(self.cursor_pos);
|
||||
self.wrap_cache.replace(None);
|
||||
self.preferred_col = None;
|
||||
self.vim_pending = VimPending::None;
|
||||
self.vim_commands = VimCommandState::default();
|
||||
}
|
||||
|
||||
/// Enable or disable modal Vim editing for the textarea.
|
||||
@@ -251,6 +260,7 @@ impl TextArea {
|
||||
pub(crate) fn set_vim_enabled(&mut self, enabled: bool) {
|
||||
self.vim_enabled = enabled;
|
||||
self.vim_pending = VimPending::None;
|
||||
self.vim_commands = VimCommandState::default();
|
||||
self.vim_mode = if enabled {
|
||||
VimMode::Normal
|
||||
} else {
|
||||
@@ -386,6 +396,7 @@ impl TextArea {
|
||||
}
|
||||
|
||||
pub fn insert_str(&mut self, text: &str) {
|
||||
self.record_vim_inserted_text(text);
|
||||
self.insert_str_at(self.cursor_pos, text);
|
||||
}
|
||||
|
||||
@@ -551,7 +562,7 @@ impl TextArea {
|
||||
}
|
||||
|
||||
if keymap.delete_backward_word.is_pressed(event) {
|
||||
self.delete_backward_word();
|
||||
self.apply_vim_insert_action(VimAction::DeleteBackwardWord);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -569,27 +580,27 @@ impl TextArea {
|
||||
}
|
||||
|
||||
if keymap.delete_backward.is_pressed(event) {
|
||||
self.delete_backward(/*n*/ 1);
|
||||
self.apply_vim_insert_action(VimAction::DeleteBackward);
|
||||
return;
|
||||
}
|
||||
if keymap.delete_forward_word.is_pressed(event) {
|
||||
self.delete_forward_word();
|
||||
self.apply_vim_insert_action(VimAction::DeleteForwardWord);
|
||||
return;
|
||||
}
|
||||
if keymap.delete_forward.is_pressed(event) {
|
||||
self.delete_forward(/*n*/ 1);
|
||||
self.apply_vim_insert_action(VimAction::DeleteForward);
|
||||
return;
|
||||
}
|
||||
if keymap.kill_line_start.is_pressed(event) {
|
||||
self.kill_to_beginning_of_line();
|
||||
self.apply_vim_insert_action(VimAction::KillLineStart);
|
||||
return;
|
||||
}
|
||||
if keymap.kill_whole_line.is_pressed(event) {
|
||||
self.kill_current_line();
|
||||
self.apply_vim_insert_action(VimAction::KillLine);
|
||||
return;
|
||||
}
|
||||
if keymap.kill_line_end.is_pressed(event) {
|
||||
self.kill_to_end_of_line();
|
||||
self.apply_vim_insert_action(VimAction::KillLineEnd);
|
||||
return;
|
||||
}
|
||||
if keymap.yank.is_pressed(event) {
|
||||
@@ -597,27 +608,27 @@ impl TextArea {
|
||||
return;
|
||||
}
|
||||
if keymap.move_word_left.is_pressed(event) {
|
||||
self.set_cursor(self.beginning_of_previous_word());
|
||||
self.apply_vim_insert_action(VimAction::MoveWordLeft);
|
||||
return;
|
||||
}
|
||||
if keymap.move_word_right.is_pressed(event) {
|
||||
self.set_cursor(self.end_of_next_word());
|
||||
self.apply_vim_insert_action(VimAction::MoveWordRight);
|
||||
return;
|
||||
}
|
||||
if keymap.move_left.is_pressed(event) {
|
||||
self.move_cursor_left();
|
||||
self.apply_vim_insert_action(VimAction::MoveLeft);
|
||||
return;
|
||||
}
|
||||
if keymap.move_right.is_pressed(event) {
|
||||
self.move_cursor_right();
|
||||
self.apply_vim_insert_action(VimAction::MoveRight);
|
||||
return;
|
||||
}
|
||||
if keymap.move_up.is_pressed(event) {
|
||||
self.move_cursor_up();
|
||||
self.apply_vim_insert_action(VimAction::MoveUp);
|
||||
return;
|
||||
}
|
||||
if keymap.move_down.is_pressed(event) {
|
||||
self.move_cursor_down();
|
||||
self.apply_vim_insert_action(VimAction::MoveDown);
|
||||
return;
|
||||
}
|
||||
if keymap.move_line_start.is_pressed(event) {
|
||||
@@ -629,7 +640,7 @@ impl TextArea {
|
||||
..
|
||||
}
|
||||
);
|
||||
self.move_cursor_to_beginning_of_line(move_up_at_bol);
|
||||
self.apply_vim_insert_action(VimAction::MoveLineStart { move_up_at_bol });
|
||||
return;
|
||||
}
|
||||
if keymap.move_line_end.is_pressed(event) {
|
||||
@@ -641,7 +652,7 @@ impl TextArea {
|
||||
..
|
||||
}
|
||||
);
|
||||
self.move_cursor_to_end_of_line(move_down_at_eol);
|
||||
self.apply_vim_insert_action(VimAction::MoveLineEnd { move_down_at_eol });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -663,25 +674,33 @@ impl TextArea {
|
||||
}
|
||||
|
||||
fn handle_vim_input(&mut self, event: KeyEvent) {
|
||||
let prior_mode = self.vim_mode;
|
||||
match self.vim_mode {
|
||||
VimMode::Insert => self.handle_vim_insert(event),
|
||||
VimMode::Normal => self.handle_vim_normal(event),
|
||||
}
|
||||
if prior_mode == VimMode::Insert && self.vim_mode == VimMode::Normal {
|
||||
self.finish_pending_vim_change();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_vim_insert(&mut self, event: KeyEvent) {
|
||||
if matches!(event.code, KeyCode::Esc) {
|
||||
let bol = self.beginning_of_current_line();
|
||||
if self.cursor_pos > bol {
|
||||
self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos).max(bol);
|
||||
}
|
||||
self.enter_vim_normal_mode();
|
||||
self.leave_vim_insert_mode();
|
||||
return;
|
||||
}
|
||||
let keymap = self.editor_keymap.clone();
|
||||
self.input_with_keymap(event, &keymap);
|
||||
}
|
||||
|
||||
fn leave_vim_insert_mode(&mut self) {
|
||||
let bol = self.beginning_of_current_line();
|
||||
if self.cursor_pos > bol {
|
||||
self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos).max(bol);
|
||||
}
|
||||
self.enter_vim_normal_mode();
|
||||
}
|
||||
|
||||
fn handle_vim_normal(&mut self, event: KeyEvent) {
|
||||
let pending = std::mem::replace(&mut self.vim_pending, VimPending::None);
|
||||
match pending {
|
||||
@@ -701,44 +720,27 @@ impl TextArea {
|
||||
}
|
||||
|
||||
if self.vim_normal_keymap.enter_insert.is_pressed(event) {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Insert(VimInsertPosition::Cursor));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.append_after_cursor.is_pressed(event) {
|
||||
let next = self.next_atomic_boundary(self.cursor_pos);
|
||||
self.set_cursor(next);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Insert(VimInsertPosition::AfterCursor));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.append_line_end.is_pressed(event) {
|
||||
self.set_cursor(self.end_of_current_line());
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Insert(VimInsertPosition::LineEnd));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.insert_line_start.is_pressed(event) {
|
||||
self.set_cursor(self.first_non_blank_of_current_line());
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Insert(VimInsertPosition::LineStart));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.open_line_below.is_pressed(event) {
|
||||
let eol = self.end_of_current_line();
|
||||
let old_len = self.text.len();
|
||||
let insert_at = if eol < old_len { eol + 1 } else { eol };
|
||||
self.insert_str_at(insert_at, "\n");
|
||||
let cursor = if eol < old_len {
|
||||
insert_at
|
||||
} else {
|
||||
insert_at + 1
|
||||
};
|
||||
self.set_cursor(cursor);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Insert(VimInsertPosition::OpenBelow));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.open_line_above.is_pressed(event) {
|
||||
let bol = self.beginning_of_current_line();
|
||||
self.insert_str_at(bol, "\n");
|
||||
self.set_cursor(bol);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Insert(VimInsertPosition::OpenAbove));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.move_left.is_pressed(event) {
|
||||
@@ -778,23 +780,19 @@ impl TextArea {
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.delete_char.is_pressed(event) {
|
||||
self.delete_forward_kill(/*n*/ 1);
|
||||
self.start_vim_edit(VimAction::Delete(VimEditTarget::Character));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.substitute_char.is_pressed(event) {
|
||||
if self.cursor_pos < self.end_of_current_line() {
|
||||
self.delete_forward_kill(/*n*/ 1);
|
||||
}
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Change(VimEditTarget::Character));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.delete_to_line_end.is_pressed(event) {
|
||||
self.vim_kill_to_end_of_line();
|
||||
self.start_vim_edit(VimAction::Delete(VimEditTarget::LineEnd));
|
||||
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;
|
||||
self.start_vim_edit(VimAction::Change(VimEditTarget::LineEnd));
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.yank_line.is_pressed(event) {
|
||||
@@ -802,7 +800,7 @@ impl TextArea {
|
||||
return;
|
||||
}
|
||||
if self.vim_normal_keymap.paste_after.is_pressed(event) {
|
||||
self.paste_after_cursor();
|
||||
self.start_vim_edit(VimAction::PasteAfter);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
@@ -834,7 +832,7 @@ impl TextArea {
|
||||
|
||||
fn handle_vim_operator(&mut self, op: VimOperator, event: KeyEvent) -> bool {
|
||||
if op == VimOperator::Delete && self.vim_operator_keymap.delete_line.is_pressed(event) {
|
||||
self.kill_current_line();
|
||||
self.start_vim_edit(VimAction::Delete(VimEditTarget::Line));
|
||||
return true;
|
||||
}
|
||||
if op == VimOperator::Yank && self.vim_operator_keymap.yank_line.is_pressed(event) {
|
||||
@@ -853,7 +851,15 @@ impl TextArea {
|
||||
}
|
||||
|
||||
if let Some(motion) = self.vim_motion_for_event(event) {
|
||||
self.apply_vim_operator(op, motion);
|
||||
match op {
|
||||
VimOperator::Delete => {
|
||||
self.start_vim_edit(VimAction::Delete(VimEditTarget::Motion(motion)));
|
||||
}
|
||||
VimOperator::Change => {
|
||||
self.start_vim_edit(VimAction::Change(VimEditTarget::Motion(motion)));
|
||||
}
|
||||
VimOperator::Yank => self.apply_vim_operator(op, motion),
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if op == VimOperator::Change
|
||||
@@ -862,9 +868,7 @@ impl TextArea {
|
||||
.start_change_operator
|
||||
.is_pressed(event)
|
||||
{
|
||||
let range = self.beginning_of_current_line()..self.end_of_current_line();
|
||||
self.kill_line_range(range);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
self.start_vim_edit(VimAction::Change(VimEditTarget::Line));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
@@ -882,8 +886,24 @@ impl TextArea {
|
||||
let Some(object) = self.vim_text_object_for_event(event) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(range) = self.text_object_range(object, scope) {
|
||||
self.apply_vim_operator_to_range(op, range);
|
||||
match op {
|
||||
VimOperator::Delete => {
|
||||
self.start_vim_edit(VimAction::Delete(VimEditTarget::TextObject {
|
||||
scope,
|
||||
object,
|
||||
}));
|
||||
}
|
||||
VimOperator::Change => {
|
||||
self.start_vim_edit(VimAction::Change(VimEditTarget::TextObject {
|
||||
scope,
|
||||
object,
|
||||
}));
|
||||
}
|
||||
VimOperator::Yank => {
|
||||
if let Some(range) = self.text_object_range(object, scope) {
|
||||
self.apply_vim_operator_to_range(op, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,13 +1,293 @@
|
||||
//! Pending Vim editing commands.
|
||||
//! Semantic Vim editing transactions and complete-change replay.
|
||||
|
||||
use super::TextArea;
|
||||
use super::VimMode;
|
||||
use super::VimMotion;
|
||||
use super::VimOperator;
|
||||
use super::VimPending;
|
||||
use super::VimTextObject;
|
||||
use super::VimTextObjectScope;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum VimEdit {
|
||||
Editor(VimEditorEdit),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct VimEditorEdit(VimAction);
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum VimInsertPosition {
|
||||
Cursor,
|
||||
AfterCursor,
|
||||
LineStart,
|
||||
LineEnd,
|
||||
OpenAbove,
|
||||
OpenBelow,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum VimEditTarget {
|
||||
Character,
|
||||
Line,
|
||||
LineEnd,
|
||||
Motion(VimMotion),
|
||||
TextObject {
|
||||
scope: VimTextObjectScope,
|
||||
object: VimTextObject,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum VimAction {
|
||||
Insert(VimInsertPosition),
|
||||
Delete(VimEditTarget),
|
||||
Change(VimEditTarget),
|
||||
Replace(char),
|
||||
PasteAfter,
|
||||
DeleteBackward,
|
||||
DeleteForward,
|
||||
DeleteBackwardWord,
|
||||
DeleteForwardWord,
|
||||
KillLineStart,
|
||||
KillLine,
|
||||
KillLineEnd,
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
MoveUp,
|
||||
MoveDown,
|
||||
MoveWordLeft,
|
||||
MoveWordRight,
|
||||
MoveLineStart { move_up_at_bol: bool },
|
||||
MoveLineEnd { move_down_at_eol: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct VimCommandState {
|
||||
pending_change: Vec<VimEdit>,
|
||||
last_change: Vec<VimEdit>,
|
||||
changed: bool,
|
||||
replaying: bool,
|
||||
}
|
||||
|
||||
impl TextArea {
|
||||
pub(crate) fn vim_repeat_actions(&self) -> Option<Vec<VimEdit>> {
|
||||
(!self.vim_commands.last_change.is_empty()).then(|| self.vim_commands.last_change.clone())
|
||||
}
|
||||
|
||||
pub(super) fn record_vim_inserted_text(&mut self, text: &str) {
|
||||
if !self.vim_enabled
|
||||
|| self.vim_mode != VimMode::Insert
|
||||
|| self.vim_commands.replaying
|
||||
|| self.vim_commands.pending_change.is_empty()
|
||||
|| text.is_empty()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(VimEdit::Text(pending)) = self.vim_commands.pending_change.last_mut() {
|
||||
pending.push_str(text);
|
||||
} else {
|
||||
self.vim_commands
|
||||
.pending_change
|
||||
.push(VimEdit::Text(text.to_owned()));
|
||||
}
|
||||
self.vim_commands.changed = true;
|
||||
}
|
||||
|
||||
pub(super) fn apply_vim_insert_action(&mut self, action: VimAction) {
|
||||
let recording = self.vim_enabled
|
||||
&& self.vim_mode == VimMode::Insert
|
||||
&& !self.vim_commands.replaying
|
||||
&& !self.vim_commands.pending_change.is_empty();
|
||||
let prior_len = self.text.len();
|
||||
self.apply_vim_editor_action(action);
|
||||
let changed = self.text.len() != prior_len;
|
||||
let deletion = matches!(
|
||||
action,
|
||||
VimAction::DeleteBackward
|
||||
| VimAction::DeleteForward
|
||||
| VimAction::DeleteBackwardWord
|
||||
| VimAction::DeleteForwardWord
|
||||
| VimAction::KillLineStart
|
||||
| VimAction::KillLine
|
||||
| VimAction::KillLineEnd
|
||||
);
|
||||
if recording && (changed || !deletion) {
|
||||
self.vim_commands
|
||||
.pending_change
|
||||
.push(VimEdit::Editor(VimEditorEdit(action)));
|
||||
}
|
||||
self.vim_commands.changed |= recording && changed;
|
||||
}
|
||||
|
||||
pub(super) fn start_vim_edit(&mut self, action: VimAction) -> bool {
|
||||
let prior_len = self.text.len();
|
||||
self.vim_commands.pending_change = vec![VimEdit::Editor(VimEditorEdit(action))];
|
||||
self.vim_commands.changed = false;
|
||||
if !self.apply_vim_editor_action(action) {
|
||||
self.vim_commands.pending_change.clear();
|
||||
return false;
|
||||
}
|
||||
self.vim_commands.changed =
|
||||
self.text.len() != prior_len || matches!(action, VimAction::Replace(_));
|
||||
if self.vim_mode == VimMode::Normal {
|
||||
self.finish_pending_vim_change();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn finish_pending_vim_change(&mut self) {
|
||||
if self.vim_commands.changed {
|
||||
self.vim_commands.last_change = std::mem::take(&mut self.vim_commands.pending_change);
|
||||
} else {
|
||||
self.vim_commands.pending_change.clear();
|
||||
}
|
||||
self.vim_commands.changed = false;
|
||||
}
|
||||
|
||||
pub(crate) fn begin_vim_repeat(&mut self) -> Option<Vec<VimEdit>> {
|
||||
let edits = self.vim_repeat_actions()?;
|
||||
self.vim_commands.replaying = true;
|
||||
Some(edits)
|
||||
}
|
||||
|
||||
pub(crate) fn finish_vim_repeat(&mut self) {
|
||||
if self.vim_mode == VimMode::Insert {
|
||||
self.leave_vim_insert_mode();
|
||||
}
|
||||
self.vim_pending = VimPending::None;
|
||||
self.vim_commands.replaying = false;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_vim_edit(&mut self, edit: &VimEdit) -> bool {
|
||||
match edit {
|
||||
VimEdit::Editor(VimEditorEdit(action)) => self.apply_vim_editor_action(*action),
|
||||
VimEdit::Text(text) => {
|
||||
if self.vim_mode != VimMode::Insert {
|
||||
return false;
|
||||
}
|
||||
self.insert_str(text);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_vim_editor_action(&mut self, action: VimAction) -> bool {
|
||||
let prior_len = self.text.len();
|
||||
match action {
|
||||
VimAction::Insert(position) => {
|
||||
match position {
|
||||
VimInsertPosition::Cursor => {}
|
||||
VimInsertPosition::AfterCursor => {
|
||||
self.set_cursor(self.next_atomic_boundary(self.cursor_pos));
|
||||
}
|
||||
VimInsertPosition::LineStart => {
|
||||
self.set_cursor(self.first_non_blank_of_current_line());
|
||||
}
|
||||
VimInsertPosition::LineEnd => self.set_cursor(self.end_of_current_line()),
|
||||
VimInsertPosition::OpenAbove => {
|
||||
let bol = self.beginning_of_current_line();
|
||||
self.insert_str_at(bol, "\n");
|
||||
self.set_cursor(bol);
|
||||
}
|
||||
VimInsertPosition::OpenBelow => {
|
||||
let eol = self.end_of_current_line();
|
||||
let insert_at = if eol < prior_len { eol + 1 } else { eol };
|
||||
self.insert_str_at(insert_at, "\n");
|
||||
self.set_cursor(if eol < prior_len {
|
||||
insert_at
|
||||
} else {
|
||||
insert_at + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
VimAction::Delete(target) | VimAction::Change(target) => {
|
||||
let operator = if matches!(action, VimAction::Delete(_)) {
|
||||
VimOperator::Delete
|
||||
} else {
|
||||
VimOperator::Change
|
||||
};
|
||||
match target {
|
||||
VimEditTarget::Character => {
|
||||
if self.cursor_pos < self.end_of_current_line() {
|
||||
self.delete_forward_kill(/*n*/ 1);
|
||||
}
|
||||
if operator == VimOperator::Change {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
VimEditTarget::Line => {
|
||||
if operator == VimOperator::Delete {
|
||||
self.kill_current_line();
|
||||
} else {
|
||||
let range =
|
||||
self.beginning_of_current_line()..self.end_of_current_line();
|
||||
self.kill_line_range(range);
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
VimEditTarget::LineEnd => {
|
||||
self.vim_kill_to_end_of_line();
|
||||
if operator == VimOperator::Change {
|
||||
self.vim_mode = VimMode::Insert;
|
||||
}
|
||||
}
|
||||
VimEditTarget::Motion(motion) => self.apply_vim_operator(operator, motion),
|
||||
VimEditTarget::TextObject { scope, object } => {
|
||||
let Some(range) = self.text_object_range(object, scope) else {
|
||||
return false;
|
||||
};
|
||||
self.apply_vim_operator_to_range(operator, range);
|
||||
}
|
||||
}
|
||||
if operator == VimOperator::Change {
|
||||
return self.vim_mode == VimMode::Insert;
|
||||
}
|
||||
return self.text.len() != prior_len;
|
||||
}
|
||||
VimAction::Replace(ch) => {
|
||||
if self.cursor_pos >= self.end_of_current_line() {
|
||||
return false;
|
||||
}
|
||||
let start = self.cursor_pos;
|
||||
let end = self.next_atomic_boundary(start);
|
||||
self.replace_range(start..end, &ch.to_string());
|
||||
self.set_cursor(start + usize::from(ch == '\n'));
|
||||
}
|
||||
VimAction::PasteAfter => {
|
||||
self.paste_after_cursor();
|
||||
return self.text.len() != prior_len;
|
||||
}
|
||||
VimAction::DeleteBackward => self.delete_backward(/*n*/ 1),
|
||||
VimAction::DeleteForward => self.delete_forward(/*n*/ 1),
|
||||
VimAction::DeleteBackwardWord => self.delete_backward_word(),
|
||||
VimAction::DeleteForwardWord => self.delete_forward_word(),
|
||||
VimAction::KillLineStart => self.kill_to_beginning_of_line(),
|
||||
VimAction::KillLine => self.kill_current_line(),
|
||||
VimAction::KillLineEnd => self.kill_to_end_of_line(),
|
||||
VimAction::MoveLeft => self.move_cursor_left(),
|
||||
VimAction::MoveRight => self.move_cursor_right(),
|
||||
VimAction::MoveUp => self.move_cursor_up(),
|
||||
VimAction::MoveDown => self.move_cursor_down(),
|
||||
VimAction::MoveWordLeft => self.set_cursor(self.beginning_of_previous_word()),
|
||||
VimAction::MoveWordRight => self.set_cursor(self.end_of_next_word()),
|
||||
VimAction::MoveLineStart { move_up_at_bol } => {
|
||||
self.move_cursor_to_beginning_of_line(move_up_at_bol);
|
||||
}
|
||||
VimAction::MoveLineEnd { move_down_at_eol } => {
|
||||
self.move_cursor_to_end_of_line(move_down_at_eol);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn handle_vim_extra_command(&mut self, event: KeyEvent) -> bool {
|
||||
if self.vim_normal_keymap.replace_char.is_pressed(event)
|
||||
&& self.cursor_pos < self.end_of_current_line()
|
||||
@@ -15,6 +295,17 @@ impl TextArea {
|
||||
self.vim_pending = VimPending::Replace;
|
||||
return true;
|
||||
}
|
||||
if self.vim_normal_keymap.repeat_last_change.is_pressed(event) {
|
||||
if let Some(edits) = self.begin_vim_repeat() {
|
||||
for edit in edits {
|
||||
if !self.apply_vim_edit(&edit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.finish_vim_repeat();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -22,10 +313,7 @@ impl TextArea {
|
||||
match pending {
|
||||
VimPending::Replace => {
|
||||
if let Some(ch) = vim_command_char(event) {
|
||||
let start = self.cursor_pos;
|
||||
let end = self.next_atomic_boundary(start);
|
||||
self.replace_range(start..end, &ch.to_string());
|
||||
self.set_cursor(start + usize::from(ch == '\n'));
|
||||
self.start_vim_edit(VimAction::Replace(ch));
|
||||
}
|
||||
}
|
||||
VimPending::None | VimPending::Operator(_) | VimPending::TextObject { .. } => {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::super::TextArea;
|
||||
use super::VimAction;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
@@ -58,6 +59,181 @@ fn replace_character_preserves_normal_mode_and_grapheme_boundaries() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_replays_delete_replace_and_complete_change() {
|
||||
let mut textarea = vim_textarea("alpha beta gamma", /*cursor*/ 0);
|
||||
keys(&mut textarea, "dw.");
|
||||
assert_eq!(textarea.text(), "gamma");
|
||||
|
||||
let mut textarea = vim_textarea("abc", /*cursor*/ 0);
|
||||
keys(&mut textarea, "rXl.");
|
||||
assert_eq!(textarea.text(), "XXc");
|
||||
|
||||
let mut textarea = vim_textarea("one two three", /*cursor*/ 0);
|
||||
keys(&mut textarea, "cwX");
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, "w.");
|
||||
assert_eq!(textarea.text(), "X X three");
|
||||
assert_eq!(textarea.vim_mode_label(), Some("Normal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_records_pasted_insertions_and_survives_keymap_changes() {
|
||||
let mut textarea = vim_textarea("", /*cursor*/ 0);
|
||||
keys(&mut textarea, "i");
|
||||
textarea.insert_str("foo");
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, ".");
|
||||
assert_eq!(textarea.text(), "fofooo");
|
||||
|
||||
let mut textarea = vim_textarea("one two three", /*cursor*/ 0);
|
||||
keys(&mut textarea, "dw");
|
||||
let mut keymap = crate::keymap::RuntimeKeymap::defaults();
|
||||
keymap.vim_normal.start_delete_operator = vec![crate::key_hint::plain(KeyCode::Char('z'))];
|
||||
textarea.set_keymap_bindings(&keymap);
|
||||
keys(&mut textarea, ".");
|
||||
assert_eq!(textarea.text(), "three");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_aborts_when_change_motion_cannot_start() {
|
||||
let mut textarea = vim_textarea("one\ntwo\nthree", /*cursor*/ 0);
|
||||
keys(&mut textarea, "cjfoo");
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, "j");
|
||||
let original = (textarea.text().to_owned(), textarea.cursor());
|
||||
|
||||
keys(&mut textarea, ".");
|
||||
|
||||
assert_eq!(
|
||||
(textarea.text(), textarea.cursor()),
|
||||
(original.0.as_str(), original.1)
|
||||
);
|
||||
assert_eq!(textarea.vim_mode_label(), Some("Normal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_distinguishes_insert_mode_deletions_from_noops() {
|
||||
let mut textarea = vim_textarea("abcd", /*cursor*/ 3);
|
||||
keys(&mut textarea, "i");
|
||||
textarea.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, "l.");
|
||||
assert_eq!(textarea.text(), "ad");
|
||||
|
||||
let mut textarea = vim_textarea("abc", /*cursor*/ 0);
|
||||
keys(&mut textarea, "xi");
|
||||
textarea.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, ".");
|
||||
assert_eq!(textarea.text(), "c");
|
||||
|
||||
let mut textarea = vim_textarea("one two", /*cursor*/ 0);
|
||||
keys(&mut textarea, "i");
|
||||
textarea.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
|
||||
keys(&mut textarea, "X");
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, "w.");
|
||||
assert_eq!(textarea.text(), "Xone Xtwo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_omits_ineffective_deletions_before_inserted_text() {
|
||||
for (action, cursor, repeat_cursor, expected) in [
|
||||
(VimAction::DeleteBackward, 0, 5, "Xone Xtwo\n"),
|
||||
(VimAction::DeleteBackwardWord, 0, 5, "Xone Xtwo\n"),
|
||||
(VimAction::KillLineStart, 0, 5, "Xone Xtwo\n"),
|
||||
(VimAction::DeleteForward, 8, 4, "one Xtwo\nX"),
|
||||
(VimAction::DeleteForwardWord, 8, 4, "one Xtwo\nX"),
|
||||
(VimAction::KillLineEnd, 8, 4, "one Xtwo\nX"),
|
||||
(VimAction::KillLine, 8, 4, "one Xtwo\nX"),
|
||||
] {
|
||||
let mut textarea = vim_textarea("one two\n", cursor);
|
||||
keys(&mut textarea, "i");
|
||||
textarea.apply_vim_insert_action(action);
|
||||
keys(&mut textarea, "X");
|
||||
escape(&mut textarea);
|
||||
textarea.set_cursor(repeat_cursor);
|
||||
|
||||
keys(&mut textarea, ".");
|
||||
|
||||
assert_eq!(textarea.text(), expected, "{action:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_replays_resolved_insert_actions_after_editor_keymap_changes() {
|
||||
let mut textarea = vim_textarea("abc", /*cursor*/ 0);
|
||||
keys(&mut textarea, "ix");
|
||||
textarea.input(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
keys(&mut textarea, "y");
|
||||
escape(&mut textarea);
|
||||
|
||||
let mut keymap = crate::keymap::RuntimeKeymap::defaults();
|
||||
std::sync::Arc::make_mut(&mut keymap.editor).move_left =
|
||||
vec![crate::key_hint::plain(KeyCode::Char('z'))];
|
||||
textarea.set_keymap_bindings(&keymap);
|
||||
keys(&mut textarea, ".");
|
||||
|
||||
assert_eq!(textarea.text(), "yxyxabc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_replays_literal_tab_insertions() {
|
||||
let mut textarea = vim_textarea("", /*cursor*/ 0);
|
||||
keys(&mut textarea, "i");
|
||||
textarea.insert_str("\t");
|
||||
escape(&mut textarea);
|
||||
|
||||
keys(&mut textarea, ".");
|
||||
|
||||
assert_eq!(textarea.text(), "\t\t");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_replays_literal_newlines_with_custom_editor_bindings() {
|
||||
let mut textarea = vim_textarea("", /*cursor*/ 0);
|
||||
std::sync::Arc::make_mut(&mut textarea.editor_keymap).insert_newline =
|
||||
vec![crate::key_hint::plain(KeyCode::Char('n'))];
|
||||
keys(&mut textarea, "i");
|
||||
textarea.insert_str("\n");
|
||||
escape(&mut textarea);
|
||||
|
||||
keys(&mut textarea, ".");
|
||||
|
||||
assert_eq!(textarea.text(), "\n\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_uses_configured_binding_and_supports_unbinding() {
|
||||
let mut textarea = vim_textarea("one two three", /*cursor*/ 0);
|
||||
let mut keymap = crate::keymap::RuntimeKeymap::defaults();
|
||||
keymap.vim_normal.repeat_last_change = vec![crate::key_hint::plain(KeyCode::Char('z'))];
|
||||
textarea.set_keymap_bindings(&keymap);
|
||||
|
||||
keys(&mut textarea, "dwz");
|
||||
assert_eq!(textarea.text(), "three");
|
||||
|
||||
keymap.vim_normal.repeat_last_change.clear();
|
||||
textarea.set_keymap_bindings(&keymap);
|
||||
keys(&mut textarea, "z");
|
||||
assert_eq!(textarea.text(), "three");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_the_buffer_discards_incomplete_dot_repeat_history() {
|
||||
let mut textarea = vim_textarea("", /*cursor*/ 0);
|
||||
keys(&mut textarea, "iold");
|
||||
textarea.set_text_clearing_elements("");
|
||||
textarea.enter_vim_normal_mode();
|
||||
|
||||
keys(&mut textarea, "iX");
|
||||
escape(&mut textarea);
|
||||
keys(&mut textarea, ".");
|
||||
|
||||
assert_eq!(textarea.text(), "XX");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_character_uses_configured_binding_and_supports_unbinding() {
|
||||
let mut textarea = vim_textarea("abc", /*cursor*/ 0);
|
||||
@@ -157,3 +333,24 @@ fn editor_commands_have_visual_snapshot_coverage() {
|
||||
^
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_repeat_has_visual_snapshot_coverage() {
|
||||
let mut textarea = vim_textarea("alpha beta\ngamma delta", /*cursor*/ 0);
|
||||
let mut states = Vec::new();
|
||||
for command in ["dw", "."] {
|
||||
keys(&mut textarea, command);
|
||||
states.push(format!(
|
||||
"{command}: {}\n{}^",
|
||||
textarea.text().replace('\n', "\\n"),
|
||||
" ".repeat(textarea.cursor())
|
||||
));
|
||||
}
|
||||
insta::assert_snapshot!(states.join("\n\n"), @r###"
|
||||
dw: beta\ngamma delta
|
||||
^
|
||||
|
||||
.: gamma delta
|
||||
^
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ pub(crate) struct VimNormalKeymap {
|
||||
pub(crate) move_line_end: Vec<KeyBinding>,
|
||||
pub(crate) delete_char: Vec<KeyBinding>,
|
||||
pub(crate) replace_char: Vec<KeyBinding>,
|
||||
pub(crate) repeat_last_change: Vec<KeyBinding>,
|
||||
pub(crate) substitute_char: Vec<KeyBinding>,
|
||||
pub(crate) delete_to_line_end: Vec<KeyBinding>,
|
||||
pub(crate) change_to_line_end: Vec<KeyBinding>,
|
||||
@@ -717,6 +718,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),
|
||||
replace_char: resolve_local!(keymap, defaults, vim_normal, replace_char),
|
||||
repeat_last_change: resolve_local!(keymap, defaults, vim_normal, repeat_last_change),
|
||||
substitute_char: resolve_local!(keymap, defaults, vim_normal, substitute_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),
|
||||
@@ -811,6 +813,10 @@ impl RuntimeKeymap {
|
||||
keymap.vim_normal.substitute_char.as_ref(),
|
||||
vim_normal.substitute_char.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.repeat_last_change.as_ref(),
|
||||
vim_normal.repeat_last_change.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.change_to_line_end.as_ref(),
|
||||
vim_normal.change_to_line_end.as_slice(),
|
||||
@@ -864,6 +870,15 @@ impl RuntimeKeymap {
|
||||
})
|
||||
});
|
||||
}
|
||||
if keymap.vim_normal.repeat_last_change.is_none() {
|
||||
vim_normal.repeat_last_change.retain(|binding| {
|
||||
!configured_vim_normal_bindings_to_preserve.contains(binding)
|
||||
&& !chords.bindings.iter().any(|chord| {
|
||||
chord.action.context == KeymapContext::VimNormal
|
||||
&& chord.chord.prefix.parts() == binding.parts()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let mut vim_operator = VimOperatorKeymap {
|
||||
delete_line: resolve_local!(keymap, defaults, vim_operator, delete_line),
|
||||
@@ -1327,6 +1342,7 @@ impl RuntimeKeymap {
|
||||
],
|
||||
delete_char: default_bindings![plain(KeyCode::Char('x'))],
|
||||
replace_char: default_bindings![plain(KeyCode::Char('r'))],
|
||||
repeat_last_change: default_bindings![plain(KeyCode::Char('.'))],
|
||||
substitute_char: default_bindings![plain(KeyCode::Char('s'))],
|
||||
delete_to_line_end: default_bindings![
|
||||
shift(KeyCode::Char('d')),
|
||||
@@ -1861,6 +1877,10 @@ impl RuntimeKeymap {
|
||||
("move_line_end", self.vim_normal.move_line_end.as_slice()),
|
||||
("delete_char", self.vim_normal.delete_char.as_slice()),
|
||||
("replace_char", self.vim_normal.replace_char.as_slice()),
|
||||
(
|
||||
"repeat_last_change",
|
||||
self.vim_normal.repeat_last_change.as_slice(),
|
||||
),
|
||||
(
|
||||
"substitute_char",
|
||||
self.vim_normal.substitute_char.as_slice(),
|
||||
@@ -2958,6 +2978,30 @@ mod tests {
|
||||
expect_conflict(&keymap, "move_left", "replace_char");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_normal_bindings_prune_new_repeat_default() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_left = Some(one("."));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert_eq!(runtime.vim_normal.repeat_last_change, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_vim_normal_chord_prefix_prunes_new_repeat_default() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_line_start = Some(one(". g"));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert!(runtime.vim_normal.repeat_last_change.is_empty());
|
||||
assert!(runtime.chords.bindings.iter().any(|binding| {
|
||||
binding.action.context == KeymapContext::VimNormal
|
||||
&& binding.chord.prefix == key_hint::plain(KeyCode::Char('.'))
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_operator_bindings_prune_new_text_object_defaults() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
|
||||
@@ -287,6 +287,7 @@ define_runtime_action_bindings! {
|
||||
move_line_end,
|
||||
delete_char,
|
||||
replace_char,
|
||||
repeat_last_change,
|
||||
substitute_char,
|
||||
delete_to_line_end,
|
||||
change_to_line_end,
|
||||
|
||||
@@ -1099,6 +1099,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_repeat_last_change_render_snapshot() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params_for_selected_action(
|
||||
&runtime,
|
||||
&TuiKeymap::default(),
|
||||
"vim_normal",
|
||||
"repeat_last_change",
|
||||
);
|
||||
let rendered = render_picker(params, /*width*/ 120);
|
||||
let repeat_row = rendered
|
||||
.lines()
|
||||
.find(|line| line.contains("Repeat Last Change"))
|
||||
.expect("repeat-last-change row should render");
|
||||
|
||||
assert_snapshot!("keymap_picker_repeat_last_change", repeat_row);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_all_tab_items_remain_searchable() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
|
||||
@@ -138,6 +138,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", "replace_char", "Replace the character under the cursor."),
|
||||
action("vim_normal", "Vim normal", "repeat_last_change", "Repeat the last complete edit."),
|
||||
action("vim_normal", "Vim normal", "substitute_char", "Delete the character under the cursor and enter insert mode."),
|
||||
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."),
|
||||
@@ -291,6 +292,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", "replace_char") => Some(&mut keymap.vim_normal.replace_char),
|
||||
("vim_normal", "repeat_last_change") => Some(&mut keymap.vim_normal.repeat_last_change),
|
||||
("vim_normal", "substitute_char") => Some(&mut keymap.vim_normal.substitute_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),
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
118 actions, 1 customized, 4 unbound.
|
||||
119 actions, 1 customized, 4 unbound.
|
||||
|
||||
[All] Common Customized (1) Unbound (4) App Composer Editor Vim Navigation Agents Approval Debug
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
119 actions, 0 customized, 5 unbound.
|
||||
120 actions, 0 customized, 5 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug
|
||||
|
||||
|
||||
+2
-2
@@ -2,14 +2,14 @@
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: snapshot
|
||||
---
|
||||
tab: All (118 selectable)
|
||||
tab: All (119 selectable)
|
||||
tab: Common (20 selectable)
|
||||
tab: Customized (0) (0 selectable)
|
||||
tab: Unbound (4) (4 selectable)
|
||||
tab: App (14 selectable)
|
||||
tab: Composer (5 selectable)
|
||||
tab: Editor (17 selectable)
|
||||
tab: Vim (49 selectable)
|
||||
tab: Vim (50 selectable)
|
||||
tab: Navigation (20 selectable)
|
||||
tab: Agents (5 selectable)
|
||||
tab: Approval (8 selectable)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
118 actions, 1 customized, 4 unbound.
|
||||
119 actions, 1 customized, 4 unbound.
|
||||
|
||||
[All] Common Customized (1) Unbound (4) App Composer Editor Vim Navigation Agents Approval Debug
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: "render_picker(params, 78)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
118 actions, 0 customized, 4 unbound.
|
||||
119 actions, 0 customized, 4 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (4) App Composer Editor Vim
|
||||
Navigation Agents Approval Debug
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: repeat_row
|
||||
---
|
||||
› Vim normal Repeat Last Change .
|
||||
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
118 actions, 0 customized, 4 unbound.
|
||||
119 actions, 0 customized, 4 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (4) App Composer Editor Vim Navigation Agents Approval Debug
|
||||
|
||||
|
||||
Reference in New Issue
Block a user