helix: Fix line selection after undoing delete (#55365)

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>
This commit is contained in:
Mikhail PertsevandTom Houlé authored and GitHub committed 2026-06-05 16:50:02 +00:00
1 parent 8f950dbb7b
commit dd0c77bc5b
3 files changed
+28 -4

No files matched your search

+4
View File
@@ -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]
+13 -1
View File
@@ -146,6 +146,8 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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>) {
})
})
});
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);
});
+11 -3
View File
@@ -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<Self>) {
pub fn visual_delete(
&mut self,
line_mode: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<TransactionId> {
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<Self>) {
@@ -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