Skip to content

Vim/Helix: Prevent undo grouping when any LSP completion#53980

Merged
SomeoneToIgnore merged 3 commits into
zed-industries:mainfrom
HuaGu-Dragon:fix_vim_undo
May 14, 2026
Merged

Vim/Helix: Prevent undo grouping when any LSP completion#53980
SomeoneToIgnore merged 3 commits into
zed-industries:mainfrom
HuaGu-Dragon:fix_vim_undo

Conversation

@HuaGu-Dragon

@HuaGu-Dragon HuaGu-Dragon commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content is consistent with the UI/UX checklist
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Summary

Prevent undo grouping when an LSP completion includes extra edits so that the completion and its extra edits are applied and reverted atomically.

Problem

When applying an LSP completion that also applies extra edits, the editor may merge that completion into the surrounding undo group or split the transaction while waiting for edits, causing undo to leave the buffer in an inconsistent state.

Solution

  • Always block undo merging for completions

Closes #ISSUE

Release Notes:

  • Prevent undo grouping when any LSP completion

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Apr 15, 2026
@zed-codeowner-coordinator zed-codeowner-coordinator Bot requested review from a team, SomeoneToIgnore and cole-miller and removed request for a team April 15, 2026 12:44
@SomeoneToIgnore SomeoneToIgnore self-assigned this Apr 15, 2026
@SomeoneToIgnore SomeoneToIgnore requested review from ConradIrwin and removed request for SomeoneToIgnore April 29, 2026 08:04
@SomeoneToIgnore

Copy link
Copy Markdown
Contributor

Unassigned myself as apparently do not understand Vim specific related to all this.

I have assumed we need a generic fix like #52699 or similar and got quite confused on the current impl.

@HuaGu-Dragon

Copy link
Copy Markdown
Contributor Author

Unassigned myself as apparently do not understand Vim specific related to all this.

I have assumed we need a generic fix like #52699 or similar and got quite confused on the current impl.

Yeah....
Perhaps I should change the logic for group transactions rather than using a hack here.

@HuaGu-Dragon

Copy link
Copy Markdown
Contributor Author

Maybe I need some help here. After digging deeper, the core issue is very specific to Vim/Helix mode:

Vim tracks an entire Insert mode session as a single undo group via self.current_tx. When confirming an LSP completion, the editor inserts the main text (moving the cursor) and then await additional text edits. During this await, vim.rs sees the cursor move (current_anchor != newest), assumes the user manually moved it (e.g., via arrow keys), and prematurely drops current_tx, breaking the undo group.

While it might be tempting to add metadata to the generic Transaction struct to tell Vim "this is a completion, don't break the group", doing so for a Vim-specific behavior would require a huge refactor and break the crate layering?

@HuaGu-Dragon HuaGu-Dragon force-pushed the fix_vim_undo branch 2 times, most recently from 5108974 to f7fdf28 Compare May 4, 2026 05:59
@HuaGu-Dragon

Copy link
Copy Markdown
Contributor Author

Maybe explicitly calls buffer.finalize_last_transaction() is better, since project/src/lsp_store.rs also do that before applying the additional text.

buffer.finalize_last_transaction();
buffer.start_transaction();

we can actually solve this elegantly without adding any Vim-specific boolean flags (completion_additional_edits_pending) to the Editor struct.

@SomeoneToIgnore SomeoneToIgnore left a comment

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.

This definitely looks like a proper direction!

2 small questions I have left:

  • pending LSP completion edits are the additional text edits, if I understand the issue right?

If so, then can we move the fix under if let Some checks in

https://github.com/zed-industries/zed/blob/d4f06ee3740b0da1bd55476a5ff19d7c2ac1e57f/crates/editor/src/editor.rs#L6981-L6991

Or do you think this fix should apply to all, "synchronous" also, edits?

  • The only fix-related comment I've found is

/// Prevent the last transaction from being grouped with any subsequent transactions,
/// even if they occur with the buffer's undo grouping duration.
pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> {

and that does not explain much what happens here.
Can we add a small comment on why this is needed and how the rest of the code interacts with it?

If it helps, a good example of such comment is a few lines above: https://github.com/zed-industries/zed/blob/d4f06ee3740b0da1bd55476a5ff19d7c2ac1e57f/crates/editor/src/editor.rs#L6953-L6954

@HuaGu-Dragon

Copy link
Copy Markdown
Contributor Author

This definitely looks like a proper direction!

2 small questions I have left:

  • pending LSP completion edits are the additional text edits, if I understand the issue right?

If so, then can we move the fix under if let Some checks in

https://github.com/zed-industries/zed/blob/d4f06ee3740b0da1bd55476a5ff19d7c2ac1e57f/crates/editor/src/editor.rs#L6981-L6991

Or do you think this fix should apply to all, "synchronous" also, edits?

  • The only fix-related comment I've found is

/// Prevent the last transaction from being grouped with any subsequent transactions,
/// even if they occur with the buffer's undo grouping duration.
pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> {

and that does not explain much what happens here. Can we add a small comment on why this is needed and how the rest of the code interacts with it?

If it helps, a good example of such comment is a few lines above: https://github.com/zed-industries/zed/blob/d4f06ee3740b0da1bd55476a5ff19d7c2ac1e57f/crates/editor/src/editor.rs#L6953-L6954

Yes — the pending LSP completion edits are the additional text edits.

Regarding your second question: I intentionally placed the fix (finalize_last_transaction) outside the if let block so that it applies to all completions, including synchronous ones. Here is my reasoning:

  • After we enter completion, the cursor moves, then vim.rs detects this in local_selections_changed And Vim immediately issues a group_until_transaction. Since apply_edits.await is asynchronous, If we wait to finalize until inside the if let Some block, the merge_transaction will be incorrect.

zed/crates/vim/src/vim.rs

Lines 1941 to 1949 in 8a8293b

let newest = editor.read(cx).selections.newest_anchor().clone();
let is_multicursor = editor.read(cx).selections.count() > 1;
if self.mode == Mode::Insert && self.current_tx.is_some() {
if let Some(current_anchor) = &self.current_anchor {
if current_anchor != &newest
&& let Some(tx_id) = self.current_tx.take()
{
self.update_editor(cx, |_, editor, cx| {
editor.group_until_transaction(tx_id, cx)

  • You raised a great point about whether this should apply to all synchronous edits as well. I tested the native behavior in Neovim and Helix, When you type let a = and accept the completion Some, pressing u should only revert Some, not the let a = typed beforehand. (This is also why the existing test case expects the state to revert to 2. instead of 2). I also add a test to cover this.

Here is the video about their behaviour. Sorry for that I do not have neovim locally, so I asked my friend to have a test in his machine, but the video have a bit blurry.

Neovim
neovim.mp4
Helix
helix.mp4

Does this solve your concerns?

@HuaGu-Dragon HuaGu-Dragon changed the title Vim/Helix: Fix undo with pending LSP completion edits Vim/Helix: Prevent undo grouping when any LSP completion May 14, 2026
@HuaGu-Dragon

Copy link
Copy Markdown
Contributor Author

Sorry for the delay—I've been under academic pressure
I updated the PR title and description and rebased the branch to resolve conflicts.

@SomeoneToIgnore SomeoneToIgnore left a comment

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.

Thank you!

@SomeoneToIgnore SomeoneToIgnore added this pull request to the merge queue May 14, 2026
Merged via the queue into zed-industries:main with commit 75f0940 May 14, 2026
33 checks passed
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.

5 participants