Skip to content

feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)#21932

Merged
jacob314 merged 2 commits intogoogle-gemini:mainfrom
aanari:feat/vim-tier1
Mar 11, 2026
Merged

feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)#21932
jacob314 merged 2 commits intogoogle-gemini:mainfrom
aanari:feat/vim-tier1

Conversation

@aanari
Copy link
Copy Markdown
Contributor

@aanari aanari commented Mar 10, 2026

A handful of normal mode commands that were missing:

  • X — delete char before cursor
  • ~ — toggle case
  • r{char} — replace char under cursor
  • f/F/t/T — find/till char on current line
  • ;/, — repeat last find forward/backward
  • df/dt/dF/dT — delete to found char (operator + find)
  • cf/ct/cF/cT — change to found char (delete + enter insert)

Added tests for all new paths.

A step towards resolving #21970

@aanari aanari requested a review from a team as a code owner March 10, 2026 21:29
@google-cla
Copy link
Copy Markdown

google-cla bot commented Mar 10, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the Vim emulation by integrating a suite of essential normal mode commands. These additions provide users with more robust text manipulation capabilities, bringing the editor closer to a full Vim experience. The changes involve extending the text buffer's action types and logic, updating the Vim hook to process these new commands, and ensuring comprehensive test coverage for reliability.

Highlights

  • New Normal Mode Commands: Implemented several missing Vim normal mode commands, including 'X' (delete character before cursor), '~' (toggle case), and 'r' (replace character).
  • Character Finding and Deletion: Added support for 'f', 'F', 't', 'T' commands to find/till characters on the current line, along with their operator-pending variants 'df', 'dt', 'dF', 'dT' for deletion, and 'cf', 'ct', 'cF', 'cT' for change operations.
  • Repeat Last Find: Introduced ';' and ',' commands to repeat the last character find operation forward and backward, respectively.
  • Comprehensive Testing: Included extensive unit tests for all new Vim commands, covering various scenarios, count prefixes, dot repeat functionality, and unicode character handling.
Changelog
  • packages/cli/src/ui/components/shared/text-buffer.ts
    • Added new TextBufferAction types for vim_delete_char_before, vim_toggle_case, vim_replace_char, vim_find_char_forward, vim_find_char_backward, vim_delete_to_char_forward, and vim_delete_to_char_backward.
    • Updated textBufferReducerLogic to handle the newly introduced Vim actions.
    • Introduced new useCallback functions within useTextBuffer to dispatch the new Vim actions.
    • Extended the TextBuffer interface with JSDoc and type definitions for the new Vim functions.
  • packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts
    • Added comprehensive unit tests for vim_delete_char_before (X), covering single/multiple deletions, clamping, and cursor behavior.
    • Included tests for vim_toggle_case (~), verifying case toggling for single and multiple characters, and boundary conditions.
    • Implemented tests for vim_replace_char (r), ensuring correct character replacement and count handling.
    • Added tests for vim_find_char_forward (f/t) and vim_find_char_backward (F/T), covering Nth occurrences and 'till' functionality.
    • Included tests for Unicode character support in find operations.
    • Added tests for vim_delete_to_char_forward (df/dt) and vim_delete_to_char_backward (dF/dT), verifying inclusive/exclusive deletion.
    • Ensured all new actions correctly push undo states.
  • packages/cli/src/ui/components/shared/vim-buffer-actions.ts
    • Added new action types to the VimAction extract.
    • Implemented the core logic for vim_delete_char_before, handling character deletion before the cursor.
    • Implemented the core logic for vim_toggle_case, managing character case changes.
    • Implemented the core logic for vim_replace_char, enabling character replacement.
    • Implemented vim_find_char_forward and vim_find_char_backward for efficient character searching.
    • Implemented vim_delete_to_char_forward and vim_delete_to_char_backward for operator-pending delete-to-character operations.
    • Ensured all new implementations correctly interact with the undo stack.
  • packages/cli/src/ui/hooks/vim.test.tsx
    • Updated vitest imports to include afterEach for better test cleanup.
    • Added mocks for the newly introduced Vim buffer functions.
    • Included tests for X, ~, r{char}, f{char}, F{char}, t{char}, T{char} commands.
    • Added tests for repeating last find operations with ; and ,.
    • Verified that Escape correctly cancels pending f and r operations.
    • Implemented tests for operator-pending commands like df{char}, dt{char}, dF{char}, dT{char}, and cf{char}.
    • Added tests for dot repeat (.) functionality for X and ~.
    • Included tests for count prefixes with X, ~, f, and r commands.
  • packages/cli/src/ui/hooks/vim.ts
    • Defined new CMD_TYPES for DELETE_CHAR_BEFORE ('X') and TOGGLE_CASE ('~').
    • Introduced PendingFindOp type and updated VimState to manage pending find operations and last find details.
    • Added SET_PENDING_FIND_OP and SET_LAST_FIND actions to the Vim reducer.
    • Updated Escape key handling to clear pending find operations.
    • Implemented logic to process pending find/replace operations by consuming the next character input.
    • Added handlers for X, ~, r, f, F, t, T to set the appropriate pending state and lastCommand.
    • Implemented handlers for ; and , to repeat the lastFind operation, including direction reversal for ,.
    • Updated useCallback dependencies to reflect the new state variables.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-cli gemini-cli bot added the status/need-issue Pull requests that need to have an associated issue. label Mar 10, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds several useful vim motions, along with comprehensive tests. The implementation is solid, but there are a couple of areas in packages/cli/src/ui/hooks/vim.ts with significant code duplication that could be refactored for better maintainability. I've provided suggestions to simplify the logic for handling pending find operations and for repeating find commands.

@aanari aanari changed the title vim: add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends) feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends) Mar 10, 2026
@aanari aanari force-pushed the feat/vim-tier1 branch 7 times, most recently from 72c3bfe to b1d09f1 Compare March 11, 2026 00:12
@jacob314
Copy link
Copy Markdown
Contributor

Generated by `/review-frontend` running with Gemini 3.1 Pro, reviewed by Jacob.

I have reviewed the frontend implementation for PR #21932 (`feat/vim-tier1`), focusing on the addition of the new Vim motions: `X`, `~`, `r`, `f`/`F`/`t`/`T`, and their operator combinations (`df`, `ct`, etc.).

Review Summary

  1. Architecture & State Management:
    The approach of isolating the new `pendingFindOp` state into the reducer logic inside `useVim` is excellent. It integrates seamlessly into the existing command parsing architecture without leaking dangling state, enabling operators like `c` and `d` to interact correctly with find motions (e.g., `cf`, `dt`).

  2. Unicode & Emoji Support (Bug found & fixed):
    While reviewing the character interception logic for `r`, `f`, `F`, `t`, `T`, I identified a bug related to Javascript's string `.length` semantics and surrogate pairs (emojis).

    • The Bug: In `packages/cli/src/ui/hooks/vim.ts` around line 674, the condition `if (targetChar && targetChar.length === 1)` was used to determine if the user pressed a valid single character to search for. If the user pressed `f` followed by an emoji like `😀`, the emoji has a `.length` of `2` in Javascript. This would silently discard the operation and reset the pending state.
    • The Fix: I changed the validation logic to `Array.from(targetChar).length === 1` to strictly validate if the string evaluates to a single Unicode code point.
    • Validation: I added an integration test (`f{emoji}: calls vimFindCharForward with emoji length > 1`) within `vim.test.tsx` to lock in this behavior.
  3. Test Completeness:
    The `TextBuffer` action unit tests (`vim-buffer-actions.test.ts`) are extremely comprehensive. They flawlessly capture bounds checking, undo history tracking, and correct inclusive/exclusive character deletion logic (`dt` vs `df`).

Current Status

I've fixed the bug locally and ran the `preflight` equivalence checks on the UI package (`npm run build`, `npm run lint`, and `npm run test` on the affected files). All 5,900+ workspace tests are passing. I also cleaned up an obsolete UI snapshot while running the tests.

The workspace contains the uncommitted fixes for you to review and push.

@@ -35,6 +35,9 @@ const CMD_TYPES = {
CHANGE_BIG_WORD_BACKWARD: 'cB',
CHANGE_BIG_WORD_END: 'cE',
DELETE_CHAR: 'x',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not your fault but did notice that the behavior of DEETE_CHAR doesn't seem quite aligned with vim. could be nice to fix that as a follow up now that the DELETE_CHAR_BEFORE implementation is solid.

Copy link
Copy Markdown
Contributor Author

@aanari aanari Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acked, will fix in follow-up.

const { op, operator, count: findCount } = state.pendingFindOp;
dispatch({ type: 'SET_PENDING_FIND_OP', pendingFindOp: undefined });
dispatch({ type: 'CLEAR_COUNT' });
if (targetChar && targetChar.length === 1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only issue. this shouldn't be targetChar.length. you should
call
toCodePoints(targetChar) and check whether that is 1. Otherwise you will get confused about emojis.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch - updated!

Copy link
Copy Markdown
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm
Approved with one edge case to fix.

String.length counts UTF-16 code units, not codepoints, so it returns 2
for emoji and other chars above U+FFFF. Use toCodePoints() to correctly
accept exactly one logical character as the target.
@gemini-cli
Copy link
Copy Markdown
Contributor

gemini-cli bot commented Mar 11, 2026

Hi there! Thank you for your contribution to Gemini CLI.

To improve our contribution process and better track changes, we now require all pull requests to be associated with an existing issue, as announced in our recent discussion and as detailed in our CONTRIBUTING.md.

This pull request is being closed because it is not currently linked to an issue. Once you have updated the description of this PR to link an issue (e.g., by adding Fixes #123 or Related to #123), it will be automatically reopened.

How to link an issue:
Add a keyword followed by the issue number (e.g., Fixes #123) in the description of your pull request. For more details on supported keywords and how linking works, please refer to the GitHub Documentation on linking pull requests to issues.

Thank you for your understanding and for being a part of our community!

@gemini-cli gemini-cli bot closed this Mar 11, 2026
@jacob314 jacob314 reopened this Mar 11, 2026
@jacob314 jacob314 enabled auto-merge March 11, 2026 03:08
@gemini-cli gemini-cli bot added area/core Issues related to User Interface, OS Support, Core Functionality 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item. and removed status/need-issue Pull requests that need to have an associated issue. labels Mar 11, 2026
@jacob314 jacob314 added this pull request to the merge queue Mar 11, 2026
Merged via the queue into google-gemini:main with commit 8b09ccc Mar 11, 2026
29 checks passed
aanari added a commit to aanari/gemini-cli that referenced this pull request Mar 11, 2026
In NORMAL mode the cursor can never rest past the last character of a
line. Several delete commands left the cursor one position past the new
line end after shrinking the line:

  x  (vim_delete_char)
  dw / dW  (vim_delete_word_forward / vim_delete_big_word_forward)
  de / dE  (vim_delete_word_end / vim_delete_big_word_end)
  D        (vim_delete_to_end_of_line)
  df / dt  (vim_delete_to_char_forward)

Added a clampNormalCursor() helper in vim-buffer-actions.ts and applied
it to every delete action that stays in NORMAL mode. Change actions
(cw, C, cf, ...) are intentionally excluded since they immediately
enter INSERT mode where the cursor is allowed past the last character.

Flagged by @jacob314 in review of google-gemini#21932.
aanari added a commit to aanari/gemini-cli that referenced this pull request Mar 11, 2026
In NORMAL mode the cursor can never rest past the last character of a
line. Several delete commands left the cursor one position past the new
line end after shrinking the line:

  x  (vim_delete_char)
  dw / dW  (vim_delete_word_forward / vim_delete_big_word_forward)
  de / dE  (vim_delete_word_end / vim_delete_big_word_end)
  D        (vim_delete_to_end_of_line)
  df / dt  (vim_delete_to_char_forward)

Added a clampNormalCursor() helper in vim-buffer-actions.ts and applied
it to every delete action that stays in NORMAL mode. Change actions
(cw, C, cf, ...) are intentionally excluded since they immediately
enter INSERT mode where the cursor is allowed past the last character.

Flagged by @jacob314 in review of google-gemini#21932.
theerud pushed a commit to theerud/gemini-cli that referenced this pull request Mar 11, 2026
In NORMAL mode the cursor can never rest past the last character of a
line. Several delete commands left the cursor one position past the new
line end after shrinking the line:

  x  (vim_delete_char)
  dw / dW  (vim_delete_word_forward / vim_delete_big_word_forward)
  de / dE  (vim_delete_word_end / vim_delete_big_word_end)
  D        (vim_delete_to_end_of_line)
  df / dt  (vim_delete_to_char_forward)

Added a clampNormalCursor() helper in vim-buffer-actions.ts and applied
it to every delete action that stays in NORMAL mode. Change actions
(cw, C, cf, ...) are intentionally excluded since they immediately
enter INSERT mode where the cursor is allowed past the last character.

Flagged by @jacob314 in review of google-gemini#21932.
e-kotov pushed a commit to e-kotov/gemini-cli that referenced this pull request Mar 11, 2026
JaisalJain pushed a commit to JaisalJain/gemini-cli that referenced this pull request Mar 11, 2026
liamhelmer pushed a commit to badal-io/gemini-cli that referenced this pull request Mar 12, 2026
SUNDRAM07 pushed a commit to SUNDRAM07/gemini-cli that referenced this pull request Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants