feat(tui): add vim text object bindings (#24382)

## Why

Vim mode currently supports some normal-mode operators and motions, but
common text-object combinations like `ciw`, `daw`, `di(`, and
quote/bracket variants are still missing. That makes the composer feel
incomplete for users who expect operator + text object editing to work
inside prompts.

Closes #21383.

## What Changed

- Add Vim pending-state support for operator/text-object sequences.
- Add `c` as a normal-mode operator for text objects, so combinations
like `ciw` delete the object and enter insert mode.
- Support word, WORD, delimiter, and quote text objects:
  - `iw`, `aw`, `iW`, `aW`
  - `i(`, `a(`, `i)`, `a)`, `ib`, `ab`
  - `i[`, `a[`, `i]`, `a]`
  - `i{`, `a{`, `i}`, `a}`, `iB`, `aB`
  - `i"`, `a"`, `i'`, `a'`, `i\``, `a\``
- Add configurable keymap entries and keymap picker coverage for the new
Vim text-object context.
- Regenerate the config schema and update keymap picker snapshots.

## How to Test

Manual smoke test:

1. Start Codex with Vim composer mode enabled.
2. Type a draft such as:
   ```text
   alpha beta gamma
   call(foo[bar], {"x": "hello world"})
   say "one \"two\" three" now
   ```
3. Put the cursor on `beta`, press `ciw`, and confirm `beta` is removed
and the composer enters insert mode.
4. Escape back to normal mode, put the cursor on `gamma`, press `daw`,
and confirm `gamma` plus surrounding whitespace is removed.
5. Put the cursor inside `foo[bar]`, press `di[`, and confirm only `bar`
is removed.
6. Put the cursor inside `call(...)`, press `da(`, and confirm the whole
parenthesized section is removed.
7. Put the cursor inside the quoted text, press `ci"`, and confirm the
quote contents are removed and insert mode starts.
8. Verify cancellation does not edit text: press `d` then `Esc`, and
press `d` then `i` then `Esc`.

Targeted tests:

- `cargo test -p codex-tui --lib vim_`
- `cargo nextest run -p codex-tui keymap_setup::tests`

Additional local checks:

- `just write-config-schema`
- `just fmt`
- `just fix -p codex-tui`
- `git diff --check`
- `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`

Local full-suite note: `just test -p codex-tui` ran to completion. The
keymap snapshot failures were expected and accepted. Two unrelated
guardian feature-flag tests still fail locally:
-
`app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
-
`app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`

`just argument-comment-lint` is currently blocked locally by Bazel
analysis before the lint runs because `compiler-rt` has an empty
`include/sanitizer/*.h` glob in the local Bazel cache. The touched Rust
diff was manually inspected for opaque positional literals.
This commit is contained in:
Felipe Coury
2026-05-27 15:15:03 -03:00
committed by GitHub
Unverified
parent b1cbf622ad
commit 8d398d3c52
12 changed files with 1148 additions and 81 deletions
+36
View File
@@ -140,6 +140,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
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."),
action("vim_normal", "Vim normal", "start_yank_operator", "Begin a yank operator and wait for a motion."),
action("vim_normal", "Vim normal", "start_change_operator", "Begin a change operator and wait for a text object."),
action("vim_normal", "Vim normal", "cancel_operator", "Cancel a pending Vim operator."),
action("vim_operator", "Vim operator", "delete_line", "Repeat delete operator to delete the whole line."),
action("vim_operator", "Vim operator", "yank_line", "Repeat yank operator to yank the whole line."),
@@ -152,7 +153,18 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("vim_operator", "Vim operator", "motion_word_end", "Operator motion to end of word."),
action("vim_operator", "Vim operator", "motion_line_start", "Operator motion to line start."),
action("vim_operator", "Vim operator", "motion_line_end", "Operator motion to line end."),
action("vim_operator", "Vim operator", "select_inner_text_object", "Select an inner text object."),
action("vim_operator", "Vim operator", "select_around_text_object", "Select an around text object."),
action("vim_operator", "Vim operator", "cancel", "Cancel the pending operator."),
action("vim_text_object", "Vim text object", "word", "Target the current word."),
action("vim_text_object", "Vim text object", "big_word", "Target the current WORD."),
action("vim_text_object", "Vim text object", "parentheses", "Target enclosing parentheses."),
action("vim_text_object", "Vim text object", "brackets", "Target enclosing brackets."),
action("vim_text_object", "Vim text object", "braces", "Target enclosing braces."),
action("vim_text_object", "Vim text object", "double_quote", "Target enclosing double quotes."),
action("vim_text_object", "Vim text object", "single_quote", "Target enclosing single quotes."),
action("vim_text_object", "Vim text object", "backtick", "Target enclosing backticks."),
action("vim_text_object", "Vim text object", "cancel", "Cancel the pending text object."),
action("pager", "Pager", "scroll_up", "Scroll up by one row."),
action("pager", "Pager", "scroll_down", "Scroll down by one row."),
action("pager", "Pager", "page_up", "Scroll up by one page."),
@@ -269,6 +281,7 @@ pub(super) fn binding_slot<'a>(
("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after),
("vim_normal", "start_delete_operator") => Some(&mut keymap.vim_normal.start_delete_operator),
("vim_normal", "start_yank_operator") => Some(&mut keymap.vim_normal.start_yank_operator),
("vim_normal", "start_change_operator") => Some(&mut keymap.vim_normal.start_change_operator),
("vim_normal", "cancel_operator") => Some(&mut keymap.vim_normal.cancel_operator),
("vim_operator", "delete_line") => Some(&mut keymap.vim_operator.delete_line),
("vim_operator", "yank_line") => Some(&mut keymap.vim_operator.yank_line),
@@ -281,7 +294,18 @@ pub(super) fn binding_slot<'a>(
("vim_operator", "motion_word_end") => Some(&mut keymap.vim_operator.motion_word_end),
("vim_operator", "motion_line_start") => Some(&mut keymap.vim_operator.motion_line_start),
("vim_operator", "motion_line_end") => Some(&mut keymap.vim_operator.motion_line_end),
("vim_operator", "select_inner_text_object") => Some(&mut keymap.vim_operator.select_inner_text_object),
("vim_operator", "select_around_text_object") => Some(&mut keymap.vim_operator.select_around_text_object),
("vim_operator", "cancel") => Some(&mut keymap.vim_operator.cancel),
("vim_text_object", "word") => Some(&mut keymap.vim_text_object.word),
("vim_text_object", "big_word") => Some(&mut keymap.vim_text_object.big_word),
("vim_text_object", "parentheses") => Some(&mut keymap.vim_text_object.parentheses),
("vim_text_object", "brackets") => Some(&mut keymap.vim_text_object.brackets),
("vim_text_object", "braces") => Some(&mut keymap.vim_text_object.braces),
("vim_text_object", "double_quote") => Some(&mut keymap.vim_text_object.double_quote),
("vim_text_object", "single_quote") => Some(&mut keymap.vim_text_object.single_quote),
("vim_text_object", "backtick") => Some(&mut keymap.vim_text_object.backtick),
("vim_text_object", "cancel") => Some(&mut keymap.vim_text_object.cancel),
("pager", "scroll_up") => Some(&mut keymap.pager.scroll_up),
("pager", "scroll_down") => Some(&mut keymap.pager.scroll_down),
("pager", "page_up") => Some(&mut keymap.pager.page_up),
@@ -380,6 +404,7 @@ pub(super) fn bindings_for_action<'a>(
("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()),
("vim_normal", "start_yank_operator") => Some(runtime_keymap.vim_normal.start_yank_operator.as_slice()),
("vim_normal", "start_change_operator") => Some(runtime_keymap.vim_normal.start_change_operator.as_slice()),
("vim_normal", "cancel_operator") => Some(runtime_keymap.vim_normal.cancel_operator.as_slice()),
("vim_operator", "delete_line") => Some(runtime_keymap.vim_operator.delete_line.as_slice()),
("vim_operator", "yank_line") => Some(runtime_keymap.vim_operator.yank_line.as_slice()),
@@ -392,7 +417,18 @@ pub(super) fn bindings_for_action<'a>(
("vim_operator", "motion_word_end") => Some(runtime_keymap.vim_operator.motion_word_end.as_slice()),
("vim_operator", "motion_line_start") => Some(runtime_keymap.vim_operator.motion_line_start.as_slice()),
("vim_operator", "motion_line_end") => Some(runtime_keymap.vim_operator.motion_line_end.as_slice()),
("vim_operator", "select_inner_text_object") => Some(runtime_keymap.vim_operator.select_inner_text_object.as_slice()),
("vim_operator", "select_around_text_object") => Some(runtime_keymap.vim_operator.select_around_text_object.as_slice()),
("vim_operator", "cancel") => Some(runtime_keymap.vim_operator.cancel.as_slice()),
("vim_text_object", "word") => Some(runtime_keymap.vim_text_object.word.as_slice()),
("vim_text_object", "big_word") => Some(runtime_keymap.vim_text_object.big_word.as_slice()),
("vim_text_object", "parentheses") => Some(runtime_keymap.vim_text_object.parentheses.as_slice()),
("vim_text_object", "brackets") => Some(runtime_keymap.vim_text_object.brackets.as_slice()),
("vim_text_object", "braces") => Some(runtime_keymap.vim_text_object.braces.as_slice()),
("vim_text_object", "double_quote") => Some(runtime_keymap.vim_text_object.double_quote.as_slice()),
("vim_text_object", "single_quote") => Some(runtime_keymap.vim_text_object.single_quote.as_slice()),
("vim_text_object", "backtick") => Some(runtime_keymap.vim_text_object.backtick.as_slice()),
("vim_text_object", "cancel") => Some(runtime_keymap.vim_text_object.cancel.as_slice()),
("pager", "scroll_up") => Some(runtime_keymap.pager.scroll_up.as_slice()),
("pager", "scroll_down") => Some(runtime_keymap.pager.scroll_down.as_slice()),
("pager", "page_up") => Some(runtime_keymap.pager.page_up.as_slice()),
+1 -1
View File
@@ -104,7 +104,7 @@ const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[
id: "vim-shortcuts",
label: "Vim",
description: "Vim normal-mode and operator shortcuts.",
contexts: &["vim_normal", "vim_operator"],
contexts: &["vim_normal", "vim_operator", "vim_text_object"],
},
KeymapContextTab {
id: "navigation-shortcuts",