fix replace in normal and visual modes

Kay Simmons created

Change summary

crates/editor/src/movement.rs | 23 ++++++++++++++++
crates/vim/src/normal.rs      | 50 +++++++++++++++++++++++++++++++----
crates/vim/src/visual.rs      | 51 +++++++++++++++++++++++-------------
3 files changed, 100 insertions(+), 24 deletions(-)

Detailed changes

crates/editor/src/movement.rs 🔗

@@ -352,6 +352,29 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
     start..end
 }
 
+pub fn split_display_range_by_lines(
+    map: &DisplaySnapshot,
+    range: Range<DisplayPoint>,
+) -> Vec<Range<DisplayPoint>> {
+    let mut result = Vec::new();
+
+    let mut start = range.start;
+    // Loop over all the covered rows until the one containing the range end
+    for row in range.start.row()..range.end.row() {
+        let row_end_column = map.line_len(row);
+        let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
+        if start != end {
+            result.push(start..end);
+        }
+        start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
+    }
+
+    // Add the final range from the start of the last end to the original range end.
+    result.push(start..range.end);
+
+    result
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/vim/src/normal.rs 🔗

@@ -429,14 +429,42 @@ pub(crate) fn normal_replace(text: &str, cx: &mut MutableAppContext) {
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
-                editor.change_selections(None, cx, |s| {
-                    s.move_with(|map, selection| {
-                        *selection.end.column_mut() += 1;
-                        selection.end = map.clip_point(selection.end, Bias::Right);
-                    });
+                let (map, display_selections) = editor.selections.all_display(cx);
+                // Selections are biased right at the start. So we need to store
+                // anchors that are biased left so that we can restore the selections
+                // after the change
+                let stable_anchors = editor
+                    .selections
+                    .disjoint_anchors()
+                    .into_iter()
+                    .map(|selection| {
+                        let start = selection.start.bias_left(&map.buffer_snapshot);
+                        start..start
+                    })
+                    .collect::<Vec<_>>();
+
+                let edits = display_selections
+                    .into_iter()
+                    .map(|selection| {
+                        let mut range = selection.range();
+                        *range.end.column_mut() += 1;
+                        range.end = map.clip_point(range.end, Bias::Right);
+
+                        (
+                            range.start.to_offset(&map, Bias::Left)
+                                ..range.end.to_offset(&map, Bias::Left),
+                            text,
+                        )
+                    })
+                    .collect::<Vec<_>>();
+
+                editor.buffer().update(cx, |buffer, cx| {
+                    buffer.edit(edits, None, cx);
                 });
-                editor.insert(text, cx);
                 editor.set_clip_at_line_ends(true, cx);
+                editor.change_selections(None, cx, |s| {
+                    s.select_anchor_ranges(stable_anchors);
+                });
             });
         });
         vim.pop_operator(cx)
@@ -487,6 +515,16 @@ mod test {
         .await;
     }
 
+    // #[gpui::test]
+    // async fn test_enter(cx: &mut gpui::TestAppContext) {
+    //     let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
+    //     cx.assert_all(indoc! {"
+    //         ˇThe qˇuick broˇwn
+    //         ˇfox jumps"
+    //     })
+    //     .await;
+    // }
+
     #[gpui::test]
     async fn test_k(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);

crates/vim/src/visual.rs 🔗

@@ -2,7 +2,7 @@ use std::borrow::Cow;
 
 use collections::HashMap;
 use editor::{
-    display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
+    display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
 };
 use gpui::{actions, MutableAppContext, ViewContext};
 use language::{AutoindentMode, SelectionGoal};
@@ -318,32 +318,47 @@ pub(crate) fn visual_replace(text: &str, line: bool, cx: &mut MutableAppContext)
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
                 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
-                let mut new_selections = Vec::new();
-                editor.buffer().update(cx, |buffer, cx| {
-                    let mut edits = Vec::new();
-                    for selection in selections.iter() {
-                        let mut selection = selection.clone();
-                        if !line && !selection.reversed {
-                            // Head is at the end of the selection. Adjust the end position to
-                            // to include the character under the cursor.
-                            *selection.end.column_mut() = selection.end.column() + 1;
-                            selection.end = display_map.clip_point(selection.end, Bias::Right);
-                        }
 
-                        let range = selection
-                            .map(|p| p.to_offset(&display_map, Bias::Right))
-                            .range();
-                        new_selections.push(range.start..range.start);
+                // Selections are biased right at the start. So we need to store
+                // anchors that are biased left so that we can restore the selections
+                // after the change
+                let stable_anchors = editor
+                    .selections
+                    .disjoint_anchors()
+                    .into_iter()
+                    .map(|selection| {
+                        let start = selection.start.bias_left(&display_map.buffer_snapshot);
+                        start..start
+                    })
+                    .collect::<Vec<_>>();
+
+                let mut edits = Vec::new();
+                for selection in selections.iter() {
+                    let mut selection = selection.clone();
+                    if !line && !selection.reversed {
+                        // Head is at the end of the selection. Adjust the end position to
+                        // to include the character under the cursor.
+                        *selection.end.column_mut() = selection.end.column() + 1;
+                        selection.end = display_map.clip_point(selection.end, Bias::Right);
+                    }
+
+                    for row_range in
+                        movement::split_display_range_by_lines(&display_map, selection.range())
+                    {
+                        let range = row_range.start.to_offset(&display_map, Bias::Right)
+                            ..row_range.end.to_offset(&display_map, Bias::Right);
                         let text = text.repeat(range.len());
                         edits.push((range, text));
                     }
+                }
 
+                editor.buffer().update(cx, |buffer, cx| {
                     buffer.edit(edits, None, cx);
                 });
-                editor.change_selections(None, cx, |s| s.select_ranges(new_selections));
+                editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
             });
         });
-        vim.pop_operator(cx)
+        vim.switch_mode(Mode::Normal, false, cx);
     });
 }