From 7cdd808db23caa9082b542abbd13b1cace73a501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Ram=C3=B3n=20Guevara?= <50140021+praguevara@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:29:58 +0200 Subject: [PATCH] helix: Fix replace in helix mode (#34789) Closes https://github.com/zed-industries/zed/issues/33076 Release Notes: - Fixed replace command on helix mode: now it actually replaces what was selected and keeps the replaced text selected to better match helix --- crates/vim/src/helix.rs | 104 +++++++++++++++++++++++++++++++++++++++- crates/vim/src/vim.rs | 1 + 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 798af3bff3..ca93c9c1de 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1,8 +1,8 @@ -use editor::{DisplayPoint, Editor, movement}; +use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement}; use gpui::{Action, actions}; use gpui::{Context, Window}; use language::{CharClassifier, CharKind}; -use text::SelectionGoal; +use text::{Bias, SelectionGoal}; use crate::{ Vim, @@ -341,6 +341,80 @@ impl Vim { }); }); } + + pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + self.update_editor(window, cx, |_, editor, window, cx| { + editor.transact(window, cx, |editor, window, cx| { + let (map, selections) = editor.selections.all_display(cx); + + // Store selection info for positioning after edit + let selection_info: Vec<_> = selections + .iter() + .map(|selection| { + let range = selection.range(); + let start_offset = range.start.to_offset(&map, Bias::Left); + let end_offset = range.end.to_offset(&map, Bias::Left); + let was_empty = range.is_empty(); + let was_reversed = selection.reversed; + ( + map.buffer_snapshot.anchor_at(start_offset, Bias::Left), + end_offset - start_offset, + was_empty, + was_reversed, + ) + }) + .collect(); + + let mut edits = Vec::new(); + for selection in &selections { + let mut range = selection.range(); + + // For empty selections, extend to replace one character + if range.is_empty() { + range.end = movement::saturating_right(&map, range.start); + } + + let byte_range = range.start.to_offset(&map, Bias::Left) + ..range.end.to_offset(&map, Bias::Left); + + if !byte_range.is_empty() { + let replacement_text = text.repeat(byte_range.len()); + edits.push((byte_range, replacement_text)); + } + } + + editor.edit(edits, cx); + + // Restore selections based on original info + let snapshot = editor.buffer().read(cx).snapshot(cx); + let ranges: Vec<_> = selection_info + .into_iter() + .map(|(start_anchor, original_len, was_empty, was_reversed)| { + let start_point = start_anchor.to_point(&snapshot); + if was_empty { + // For cursor-only, collapse to start + start_point..start_point + } else { + // For selections, span the replaced text + let replacement_len = text.len() * original_len; + let end_offset = start_anchor.to_offset(&snapshot) + replacement_len; + let end_point = snapshot.offset_to_point(end_offset); + if was_reversed { + end_point..start_point + } else { + start_point..end_point + } + } + }) + .collect(); + + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(ranges); + }); + }); + }); + self.switch_mode(Mode::HelixNormal, true, window, cx); + } } #[cfg(test)] @@ -603,4 +677,30 @@ mod test { Mode::Insert, ); } + + #[gpui::test] + async fn test_replace(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // No selection (single character) + cx.set_state("ˇaa", Mode::HelixNormal); + + cx.simulate_keystrokes("r x"); + + cx.assert_state("ˇxa", Mode::HelixNormal); + + // Cursor at the beginning + cx.set_state("«ˇaa»", Mode::HelixNormal); + + cx.simulate_keystrokes("r x"); + + cx.assert_state("«ˇxx»", Mode::HelixNormal); + + // Cursor at the end + cx.set_state("«aaˇ»", Mode::HelixNormal); + + cx.simulate_keystrokes("r x"); + + cx.assert_state("«xxˇ»", Mode::HelixNormal); + } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 95a08d7c66..c747c30462 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1639,6 +1639,7 @@ impl Vim { Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { self.visual_replace(text, window, cx) } + Mode::HelixNormal => self.helix_replace(&text, window, cx), _ => self.clear_operator(window, cx), }, Some(Operator::Digraph { first_char }) => {