editor: Add duplicate selection command (#21154)

CharlesChen0823 created

Closes #4890 

Release Notes:

- Add duplicate selection command for editor

Change summary

crates/editor/src/actions.rs      |  1 +
crates/editor/src/editor.rs       | 22 ++++++++++++++++++++++
crates/editor/src/editor_tests.rs | 22 ++++++++++++++++++++++
crates/editor/src/element.rs      |  1 +
4 files changed, 46 insertions(+)

Detailed changes

crates/editor/src/actions.rs 🔗

@@ -251,6 +251,7 @@ gpui::actions!(
         DisplayCursorNames,
         DuplicateLineDown,
         DuplicateLineUp,
+        DuplicateSelection,
         ExpandAllHunkDiffs,
         ExpandMacroRecursively,
         FindAllReferences,

crates/editor/src/editor.rs 🔗

@@ -6119,6 +6119,28 @@ impl Editor {
         });
     }
 
+    pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        let selections = self.selections.all::<Point>(cx);
+
+        let mut edits = Vec::new();
+        for selection in selections.iter() {
+            let start = selection.start;
+            let end = selection.end;
+            let text = buffer.text_for_range(start..end).collect::<String>();
+            edits.push((selection.end..selection.end, text));
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+    }
+
     pub fn duplicate_line(&mut self, upwards: bool, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;

crates/editor/src/editor_tests.rs 🔗

@@ -3895,6 +3895,28 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
             ]
         );
     });
+
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+        build_editor(buffer, cx)
+    });
+    _ = view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
+                DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
+            ])
+        });
+        view.duplicate_selection(&DuplicateSelection, cx);
+        assert_eq!(view.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
+                DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
+            ]
+        );
+    });
 }
 
 #[gpui::test]

crates/editor/src/element.rs 🔗

@@ -218,6 +218,7 @@ impl EditorElement {
         register_action(view, cx, Editor::cut_to_end_of_line);
         register_action(view, cx, Editor::duplicate_line_up);
         register_action(view, cx, Editor::duplicate_line_down);
+        register_action(view, cx, Editor::duplicate_selection);
         register_action(view, cx, Editor::move_line_up);
         register_action(view, cx, Editor::move_line_down);
         register_action(view, cx, Editor::transpose);