Skip to content

vim: Restore cursor position when dismissing buffer search#47732

Merged
ConradIrwin merged 2 commits intozed-industries:mainfrom
lex00:fix/8048-vim-search-dismiss-cursor
Jan 30, 2026
Merged

vim: Restore cursor position when dismissing buffer search#47732
ConradIrwin merged 2 commits intozed-industries:mainfrom
lex00:fix/8048-vim-search-dismiss-cursor

Conversation

@lex00
Copy link
Contributor

@lex00 lex00 commented Jan 27, 2026

Fixes #8048

Summary

In vim mode, pressing Escape to dismiss the buffer search now correctly restores the cursor to its original position, rather than leaving it at the first match.

Problem

When using vim's / command to search:

  1. User positions cursor at line X
  2. User presses / to open search, types a query
  3. Matches are highlighted, cursor may visually jump to first match
  4. User presses Escape to dismiss without navigating
  5. Bug: Cursor ends up at first match instead of line X

This breaks vim parity where Escape should cancel the search and restore cursor position.

Solution

The fix leverages the focused() callback in vim.rs, which is called when the editor regains focus after the search bar is dismissed.

Key insight: When search starts via /, the cursor position is saved in SearchState.prior_selections. When search is submitted with Enter, search_submit() drains these selections. But when search is dismissed with Escape, they remain.

So in focused(), if:

  • prior_selections is non-empty, AND
  • The search bar's is_dismissed() returns true

...then we know the user dismissed the search (Escape) rather than submitted it (Enter), and we restore the cursor.

Why not handle buffer_search::Dismiss directly?

The initial approach tried to register a vim handler for the Dismiss action. This didn't work because when Escape is pressed, the search bar (which has focus) handles the Cancel action internally and calls its dismiss() method directly—it doesn't dispatch Dismiss through the action system. The vim handler registered on the editor was never invoked.

Test Plan

  • Added test_search_dismiss_restores_cursor — verifies cursor restoration when search is dismissed
  • Added test_search_dismiss_restores_cursor_no_matches — verifies behavior when query has no matches
  • All 455 vim tests pass
  • Manual testing confirms fix works with both / and cmd-f

Release Notes

Fixes zed-industries#8048

When vim search is dismissed with Escape, the cursor now returns to its
original position instead of jumping to the first match.

The fix leverages the focused() callback in vim.rs which is called when
the editor regains focus after the search bar is dismissed. It checks if
prior_selections were saved when search started and restores them if the
search bar is now dismissed (indicating Escape was pressed, not Enter).

Test plan:
- Added test_search_dismiss_restores_cursor
- Added test_search_dismiss_restores_cursor_no_matches
- All 455 vim tests pass
@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Jan 27, 2026
@zed-industries-bot
Copy link
Contributor

zed-industries-bot commented Jan 27, 2026

Warnings
⚠️

This PR is missing release notes.

Please add a "Release Notes" section that describes the change:

Release Notes:

- Added/Fixed/Improved ...

If your change is not user-facing, you can use "N/A" for the entry:

Release Notes:

- N/A

Generated by 🚫 dangerJS against d335841

@ConradIrwin
Copy link
Member

Thanks for this!

I'm not super happy with this living in the focused handler, but it does seem to work. There's an odd edge case:

  • put cursor on line 1, /b to match something on line 2, click on line 3. click on a different pane. click the "x" in the search bar. The cursor will go back to line 1 not line 3.

I don't think that's a deal breaker per-se; but it's indicative of the structure of the code not quite matching the problem.

Do you have other ideas for how we could structure this to be less intrusive?

@lex00
Copy link
Contributor Author

lex00 commented Jan 27, 2026

Thanks for the review!

I dug into the architecture to find a cleaner solution.

The Problem

My focused() approach doesn't distinguish between:

  • Search bar dismissed → editor regains focus (should restore cursor)
  • User clicked in editor while search open → editor regains focus (should NOT restore cursor)

Recommended Solution: Add Event::Dismissed to BufferSearchBar

This pattern already exists in ProjectSearchView which has ViewEvent::Dismiss. The implementation would be:

1. Add to buffer_search.rs:

pub enum Event {
    UpdateLocation,
    Dismissed,  // New
}

2. Emit in dismiss():

pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
    self.dismissed = true;
    // ... existing code ...
    cx.emit(Event::Dismissed);  // New
}

3. In vim, subscribe when search starts:

// In search() when deploying search bar:
let subscription = cx.subscribe(&search_bar, |vim, _, event, cx| {
    if let search::buffer_search::Event::Dismissed = event {
        let prior_selections: Vec<_> = vim.search.prior_selections.drain(..).collect();
        if !prior_selections.is_empty() {
            vim.update_editor(cx, |_, editor, cx| {
                editor.change_selections(Default::default(), window, cx, |s| {
                    s.select_ranges(prior_selections);
                });
            });
        }
    }
});
// Store subscription in SearchState

Why This Is Better

  1. Explicit event - Vim only acts on dismiss, not on every focus change
  2. Follows existing patterns - ProjectSearchView already does this
  3. No edge cases - If user clicks elsewhere before dismissing, that's a separate selection change; prior_selections are restored only on actual dismiss
  4. Cleaner separation - Search bar owns the dismiss event, vim reacts to it

Alternative Considered

I also considered clearing prior_selections in local_selections_changed when user moves cursor while search is open. But this is fragile because activate_match also triggers SelectionsChanged { local: true }, so we'd need to track whether the editor had focus at the time of the change.

Would you prefer I implement the Event::Dismissed approach? I can update both the search crate and vim crate.

@MrSubidubi MrSubidubi changed the title fix(vim): restore cursor position when dismissing buffer search vim: Restore cursor position when dismissing buffer search Jan 27, 2026
@ConradIrwin
Copy link
Member

thanks. let's try that and see if it works.

@lex00
Copy link
Contributor Author

lex00 commented Jan 27, 2026

Implemented the Event::Dismissed approach as discussed, with an important correction:

Clarification on the edge case: In my earlier comment, I incorrectly claimed that the Event::Dismissed approach would have "no edge cases." That was wrong—the edge case you identified (click on line 3 while search open, then dismiss → cursor incorrectly goes to line 1) would still occur with just the event subscription.

The fix now addresses this properly:

  1. Event::Dismissed - Added to BufferSearchBar and emitted when search is dismissed. Vim subscribes to this and restores prior_selections if non-empty.

  2. Edge case handling in focused() - When the editor gains focus while the search bar is still open (not dismissed), prior_selections is cleared. This handles the case where the user clicks in the editor before dismissing—they've explicitly navigated away, so we shouldn't restore to the original position.

This cleanly separates concerns:

  • focused(): "User navigated while search open → clear saved position"
  • Dismissed subscription: "Search dismissed → restore saved position (if any remain)"

Added a test that verifies the edge case: opening search, focusing editor while search is open, then dismissing—cursor stays at the match location rather than being restored.

All 456 vim tests pass.

@ConradIrwin
Copy link
Member

Thanks

@ConradIrwin ConradIrwin enabled auto-merge (squash) January 28, 2026 03:12
Restructure the search dismiss cursor restoration to use an explicit
Event::Dismissed pattern instead of checking state in the focused()
handler.

Changes:
- Add Event::Dismissed to BufferSearchBar, emitted when search is dismissed
- Subscribe to this event in vim's search() to restore prior_selections
- In focused(), clear prior_selections if editor gains focus while search
  is still open (handles the edge case where user clicks in editor before
  dismissing)
- Add test for the edge case: clicking in editor then dismissing should
  not restore cursor to original position

This separates concerns more cleanly:
- focused(): "User navigated while search open → clear saved position"
- Dismissed subscription: "Search dismissed → restore saved position"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
auto-merge was automatically disabled January 28, 2026 05:29

Head branch was pushed to by a user without write access

@lex00 lex00 force-pushed the fix/8048-vim-search-dismiss-cursor branch from 8918858 to d335841 Compare January 28, 2026 05:29
@ConradIrwin ConradIrwin merged commit 609e915 into zed-industries:main Jan 30, 2026
27 checks passed
@lex00 lex00 deleted the fix/8048-vim-search-dismiss-cursor branch March 1, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vim: dismissing buffer search can change cursor position

3 participants