mirror of
https://github.com/openai/codex.git
synced 2026-11-03 16:23:51 +00:00
Add Vim character find and till motions (#40785)
## What changed - Add line-local `f`, `F`, `t`, and `T` motions to Vim normal mode. - Support the motions with `c`, `d`, and `y`, including dot-repeat for changes. - Expose separate configurable normal-mode and operator-motion bindings, while preserving existing custom keymaps that use the new defaults. - Keep character targeting aligned to extended grapheme boundaries and skip embedded composer elements. ## Testing - Cover forward and backward motion and operator ranges, missing and adjacent targets, cancellation, dot-repeat, custom bindings, shifted terminal key events, grapheme clusters, and embedded elements. GitOrigin-RevId: 2610cf9a13538d9ae9b890c01417ca25316c1d60
This commit is contained in:
1 parent
25a6e316c8
commit
98ee29c7cd
16 files changed
+703
-13
No files matched your search
@@ -186,6 +186,10 @@ pub(crate) struct VimNormalKeymap {
|
||||
pub(crate) move_word_end: Vec<KeyBinding>,
|
||||
pub(crate) move_line_start: Vec<KeyBinding>,
|
||||
pub(crate) move_line_end: Vec<KeyBinding>,
|
||||
pub(crate) find_forward: Vec<KeyBinding>,
|
||||
pub(crate) find_backward: Vec<KeyBinding>,
|
||||
pub(crate) till_forward: Vec<KeyBinding>,
|
||||
pub(crate) till_backward: Vec<KeyBinding>,
|
||||
pub(crate) delete_char: Vec<KeyBinding>,
|
||||
pub(crate) replace_char: Vec<KeyBinding>,
|
||||
pub(crate) repeat_last_change: Vec<KeyBinding>,
|
||||
@@ -219,6 +223,10 @@ pub(crate) struct VimOperatorKeymap {
|
||||
pub(crate) motion_word_end: Vec<KeyBinding>,
|
||||
pub(crate) motion_line_start: Vec<KeyBinding>,
|
||||
pub(crate) motion_line_end: Vec<KeyBinding>,
|
||||
pub(crate) motion_find_forward: Vec<KeyBinding>,
|
||||
pub(crate) motion_find_backward: Vec<KeyBinding>,
|
||||
pub(crate) motion_till_forward: Vec<KeyBinding>,
|
||||
pub(crate) motion_till_backward: Vec<KeyBinding>,
|
||||
pub(crate) select_inner_text_object: Vec<KeyBinding>,
|
||||
pub(crate) select_around_text_object: Vec<KeyBinding>,
|
||||
pub(crate) cancel: Vec<KeyBinding>,
|
||||
@@ -716,6 +724,10 @@ impl RuntimeKeymap {
|
||||
move_word_end: resolve_local!(keymap, defaults, vim_normal, move_word_end),
|
||||
move_line_start: resolve_local!(keymap, defaults, vim_normal, move_line_start),
|
||||
move_line_end: resolve_local!(keymap, defaults, vim_normal, move_line_end),
|
||||
find_forward: resolve_local!(keymap, defaults, vim_normal, find_forward),
|
||||
find_backward: resolve_local!(keymap, defaults, vim_normal, find_backward),
|
||||
till_forward: resolve_local!(keymap, defaults, vim_normal, till_forward),
|
||||
till_backward: resolve_local!(keymap, defaults, vim_normal, till_backward),
|
||||
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),
|
||||
@@ -801,6 +813,22 @@ impl RuntimeKeymap {
|
||||
keymap.vim_normal.move_line_end.as_ref(),
|
||||
vim_normal.move_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.find_forward.as_ref(),
|
||||
vim_normal.find_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.find_backward.as_ref(),
|
||||
vim_normal.find_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.till_forward.as_ref(),
|
||||
vim_normal.till_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.till_backward.as_ref(),
|
||||
vim_normal.till_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.delete_char.as_ref(),
|
||||
vim_normal.delete_char.as_slice(),
|
||||
@@ -879,6 +907,34 @@ impl RuntimeKeymap {
|
||||
})
|
||||
});
|
||||
}
|
||||
for (configured, bindings) in [
|
||||
(
|
||||
keymap.vim_normal.find_forward.as_ref(),
|
||||
&mut vim_normal.find_forward,
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.find_backward.as_ref(),
|
||||
&mut vim_normal.find_backward,
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.till_forward.as_ref(),
|
||||
&mut vim_normal.till_forward,
|
||||
),
|
||||
(
|
||||
keymap.vim_normal.till_backward.as_ref(),
|
||||
&mut vim_normal.till_backward,
|
||||
),
|
||||
] {
|
||||
if configured.is_none() {
|
||||
bindings.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),
|
||||
@@ -902,6 +958,30 @@ impl RuntimeKeymap {
|
||||
motion_word_end: resolve_local!(keymap, defaults, vim_operator, motion_word_end),
|
||||
motion_line_start: resolve_local!(keymap, defaults, vim_operator, motion_line_start),
|
||||
motion_line_end: resolve_local!(keymap, defaults, vim_operator, motion_line_end),
|
||||
motion_find_forward: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_operator,
|
||||
motion_find_forward
|
||||
),
|
||||
motion_find_backward: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_operator,
|
||||
motion_find_backward
|
||||
),
|
||||
motion_till_forward: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_operator,
|
||||
motion_till_forward
|
||||
),
|
||||
motion_till_backward: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
vim_operator,
|
||||
motion_till_backward
|
||||
),
|
||||
select_inner_text_object: resolve_local!(
|
||||
keymap,
|
||||
defaults,
|
||||
@@ -962,6 +1042,22 @@ impl RuntimeKeymap {
|
||||
keymap.vim_operator.motion_line_end.as_ref(),
|
||||
vim_operator.motion_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_find_forward.as_ref(),
|
||||
vim_operator.motion_find_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_find_backward.as_ref(),
|
||||
vim_operator.motion_find_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_till_forward.as_ref(),
|
||||
vim_operator.motion_till_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_till_backward.as_ref(),
|
||||
vim_operator.motion_till_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.cancel.as_ref(),
|
||||
vim_operator.cancel.as_slice(),
|
||||
@@ -978,6 +1074,34 @@ impl RuntimeKeymap {
|
||||
.select_around_text_object
|
||||
.retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding));
|
||||
}
|
||||
for (configured, bindings) in [
|
||||
(
|
||||
keymap.vim_operator.motion_find_forward.as_ref(),
|
||||
&mut vim_operator.motion_find_forward,
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_find_backward.as_ref(),
|
||||
&mut vim_operator.motion_find_backward,
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_till_forward.as_ref(),
|
||||
&mut vim_operator.motion_till_forward,
|
||||
),
|
||||
(
|
||||
keymap.vim_operator.motion_till_backward.as_ref(),
|
||||
&mut vim_operator.motion_till_backward,
|
||||
),
|
||||
] {
|
||||
if configured.is_none() {
|
||||
bindings.retain(|binding| {
|
||||
!configured_vim_operator_bindings_to_preserve.contains(binding)
|
||||
&& !chords.bindings.iter().any(|chord| {
|
||||
chord.action.context == KeymapContext::VimOperator
|
||||
&& chord.chord.prefix.parts() == binding.parts()
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let vim_text_object = VimTextObjectKeymap {
|
||||
word: resolve_local!(keymap, defaults, vim_text_object, word),
|
||||
@@ -1340,6 +1464,10 @@ impl RuntimeKeymap {
|
||||
plain(KeyCode::Char('$')),
|
||||
shift(KeyCode::Char('$'))
|
||||
],
|
||||
find_forward: default_bindings![plain(KeyCode::Char('f'))],
|
||||
find_backward: default_bindings![shift(KeyCode::Char('f'))],
|
||||
till_forward: default_bindings![plain(KeyCode::Char('t'))],
|
||||
till_backward: default_bindings![shift(KeyCode::Char('t'))],
|
||||
delete_char: default_bindings![plain(KeyCode::Char('x'))],
|
||||
replace_char: default_bindings![plain(KeyCode::Char('r'))],
|
||||
repeat_last_change: default_bindings![plain(KeyCode::Char('.'))],
|
||||
@@ -1374,6 +1502,10 @@ impl RuntimeKeymap {
|
||||
plain(KeyCode::Char('$')),
|
||||
shift(KeyCode::Char('$'))
|
||||
],
|
||||
motion_find_forward: default_bindings![plain(KeyCode::Char('f'))],
|
||||
motion_find_backward: default_bindings![shift(KeyCode::Char('f'))],
|
||||
motion_till_forward: default_bindings![plain(KeyCode::Char('t'))],
|
||||
motion_till_backward: default_bindings![shift(KeyCode::Char('t'))],
|
||||
select_inner_text_object: default_bindings![plain(KeyCode::Char('i'))],
|
||||
select_around_text_object: default_bindings![plain(KeyCode::Char('a'))],
|
||||
cancel: default_bindings![plain(KeyCode::Esc)],
|
||||
@@ -1875,6 +2007,10 @@ impl RuntimeKeymap {
|
||||
self.vim_normal.move_line_start.as_slice(),
|
||||
),
|
||||
("move_line_end", self.vim_normal.move_line_end.as_slice()),
|
||||
("find_forward", self.vim_normal.find_forward.as_slice()),
|
||||
("find_backward", self.vim_normal.find_backward.as_slice()),
|
||||
("till_forward", self.vim_normal.till_forward.as_slice()),
|
||||
("till_backward", self.vim_normal.till_backward.as_slice()),
|
||||
("delete_char", self.vim_normal.delete_char.as_slice()),
|
||||
("replace_char", self.vim_normal.replace_char.as_slice()),
|
||||
(
|
||||
@@ -1943,6 +2079,22 @@ impl RuntimeKeymap {
|
||||
"motion_line_end",
|
||||
self.vim_operator.motion_line_end.as_slice(),
|
||||
),
|
||||
(
|
||||
"motion_find_forward",
|
||||
self.vim_operator.motion_find_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
"motion_find_backward",
|
||||
self.vim_operator.motion_find_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
"motion_till_forward",
|
||||
self.vim_operator.motion_till_forward.as_slice(),
|
||||
),
|
||||
(
|
||||
"motion_till_backward",
|
||||
self.vim_operator.motion_till_backward.as_slice(),
|
||||
),
|
||||
(
|
||||
"select_inner_text_object",
|
||||
self.vim_operator.select_inner_text_object.as_slice(),
|
||||
@@ -2988,6 +3140,38 @@ mod tests {
|
||||
assert_eq!(runtime.vim_normal.repeat_last_change, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_legacy_vim_bindings_prune_new_navigation_defaults() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_left = Some(one("f"));
|
||||
keymap.vim_normal.move_right = Some(one("t"));
|
||||
keymap.vim_normal.move_up = Some(one("shift-f"));
|
||||
keymap.vim_operator.motion_right = Some(one("shift-t"));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
|
||||
|
||||
assert_eq!(runtime.vim_normal.find_forward, Vec::new());
|
||||
assert_eq!(runtime.vim_normal.find_backward, Vec::new());
|
||||
assert_eq!(runtime.vim_normal.till_forward, Vec::new());
|
||||
assert_eq!(runtime.vim_operator.motion_till_backward, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_vim_navigation_binding_still_conflicts_with_legacy_binding() {
|
||||
for key in ["f", "t"] {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.vim_normal.move_left = Some(one(key));
|
||||
let action = if key == "f" {
|
||||
keymap.vim_normal.find_forward = Some(one(key));
|
||||
"find_forward"
|
||||
} else {
|
||||
keymap.vim_normal.till_forward = Some(one(key));
|
||||
"till_forward"
|
||||
};
|
||||
expect_conflict(&keymap, "move_left", action);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_vim_normal_chord_prefix_prunes_new_repeat_default() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
|
||||
Reference in new issue
Block a user