From dd0c77bc5bc54f24afc2fa93cee2b4b9feb6a1e1 Mon Sep 17 00:00:00 2001 From: Mikhail Pertsev Date: Fri, 5 Jun 2026 18:50:02 +0200 Subject: [PATCH] helix: Fix line selection after undoing delete (#55365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #55170 Fixes Helix mode undo selection history after `d` deletes the character under an empty cursor. Previously, `d` temporarily expanded the cursor to a one-character selection before deleting, and undo restored that temporary selection. When the deleted character was a newline, pressing `x` after undo selected additional line which was wrong. This preserves the original Helix selection as the undo-restored selection while keeping the existing delete behavior unchanged. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed Helix mode selecting one extra line after undoing a newline delete Co-authored-by: Tom Houlé <13155277+tomhoule@users.noreply.github.com> --- crates/vim/src/helix.rs | 4 ++++ crates/vim/src/normal.rs | 14 +++++++++++++- crates/vim/src/visual.rs | 14 +++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index e173f28820..040d60cfae 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -2829,6 +2829,10 @@ mod test { ˇ»line five"}, Mode::HelixNormal, ); + + cx.set_state("oneˇ\ntwo\nthree", Mode::HelixNormal); + cx.simulate_keystrokes("d u x"); + cx.assert_state("«one\nˇ»two\nthree", Mode::HelixNormal); } #[gpui::test] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index e2ce1fb128..027324bfab 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -146,6 +146,8 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| { vim.record_current_action(cx); + let original_selections = + vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors_arc()); vim.update_editor(cx, |_, editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(&mut |map, selection| { @@ -155,7 +157,17 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { }) }) }); - vim.visual_delete(false, window, cx); + let transaction_id = vim.visual_delete(false, window, cx); + if let (Some(original_selections), Some(transaction_id)) = + (original_selections, transaction_id) + { + let updated = vim.update_editor(cx, |_, editor, _| { + editor.modify_transaction_selection_history(transaction_id, |selections| { + selections.0 = original_selections; + }) + }); + debug_assert_ne!(updated, Some(false)); + } vim.switch_mode(Mode::HelixNormal, true, window, cx); }); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 077e098bb3..e715e98a65 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -10,6 +10,7 @@ use gpui::{Context, Window, actions}; use language::{Point, Selection, SelectionGoal}; use multi_buffer::MultiBufferRow; use search::BufferSearchBar; +use text::TransactionId; use util::ResultExt; use workspace::searchable::Direction; @@ -625,9 +626,14 @@ impl Vim { }); } - pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { + pub fn visual_delete( + &mut self, + line_mode: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option { self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { + let transaction_id = self.update_editor(cx, |vim, editor, cx| { let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = line_mode || editor.selections.line_mode(); editor.selections.set_line_mode(false); @@ -707,7 +713,9 @@ impl Vim { }); }) }); + let transaction_id = transaction_id.flatten(); self.switch_mode(Mode::Normal, true, window, cx); + transaction_id } pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { @@ -928,7 +936,7 @@ impl Vim { Some(Operator::Change) => self.substitute(None, false, window, cx), Some(Operator::Delete) => { self.stop_recording(cx); - self.visual_delete(false, window, cx) + self.visual_delete(false, window, cx); } Some(Operator::Yank) => self.visual_yank(false, window, cx), _ => {} // Ignoring other operators