editor: Fix select when click on existing selection (#32365)

Smit Barmase created

Follow-up for https://github.com/zed-industries/zed/pull/30671

Now, when clicking on an existing selection, the cursor will change on
`mouse_up` when `drag_and_drop_selection` is `true`. When
`drag_and_drop_selection` is `false`, it will change on `mouse_down`
(previous default).

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs  | 77 ++++++++++++++++---------------------
crates/editor/src/element.rs | 44 +++++++++++++++++++-
crates/vim/src/vim.rs        |  4 
3 files changed, 76 insertions(+), 49 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -910,7 +910,10 @@ enum SelectionDragState {
     /// State when no drag related activity is detected.
     None,
     /// State when the mouse is down on a selection that is about to be dragged.
-    ReadyToDrag { selection: Selection<Anchor> },
+    ReadyToDrag {
+        selection: Selection<Anchor>,
+        click_position: gpui::Point<Pixels>,
+    },
     /// State when the mouse is dragging the selection in the editor.
     Dragging {
         selection: Selection<Anchor>,
@@ -10601,54 +10604,42 @@ impl Editor {
         });
     }
 
-    pub fn drop_selection(
+    pub fn move_selection_on_drop(
         &mut self,
-        point_for_position: Option<PointForPosition>,
+        selection: &Selection<Anchor>,
+        target: DisplayPoint,
         is_cut: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> bool {
-        if let Some(point_for_position) = point_for_position {
-            match self.selection_drag_state {
-                SelectionDragState::Dragging { ref selection, .. } => {
-                    let snapshot = self.snapshot(window, cx);
-                    let selection_display =
-                        selection.map(|anchor| anchor.to_display_point(&snapshot));
-                    if !point_for_position.intersects_selection(&selection_display) {
-                        let point = point_for_position.previous_valid;
-                        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-                        let buffer = &display_map.buffer_snapshot;
-                        let mut edits = Vec::new();
-                        let insert_point = display_map
-                            .clip_point(point, Bias::Left)
-                            .to_point(&display_map);
-                        let text = buffer
-                            .text_for_range(selection.start..selection.end)
-                            .collect::<String>();
-                        if is_cut {
-                            edits.push(((selection.start..selection.end), String::new()));
-                        }
-                        let insert_anchor = buffer.anchor_before(insert_point);
-                        edits.push(((insert_anchor..insert_anchor), text));
-                        let last_edit_start = insert_anchor.bias_left(buffer);
-                        let last_edit_end = insert_anchor.bias_right(buffer);
-                        self.transact(window, cx, |this, window, cx| {
-                            this.buffer.update(cx, |buffer, cx| {
-                                buffer.edit(edits, None, cx);
-                            });
-                            this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
-                                s.select_anchor_ranges([last_edit_start..last_edit_end]);
-                            });
-                        });
-                        self.selection_drag_state = SelectionDragState::None;
-                        return true;
-                    }
-                }
-                _ => {}
-            }
+    ) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
+        let mut edits = Vec::new();
+        let insert_point = display_map
+            .clip_point(target, Bias::Left)
+            .to_point(&display_map);
+        let text = buffer
+            .text_for_range(selection.start..selection.end)
+            .collect::<String>();
+        if is_cut {
+            edits.push(((selection.start..selection.end), String::new()));
         }
+        let insert_anchor = buffer.anchor_before(insert_point);
+        edits.push(((insert_anchor..insert_anchor), text));
+        let last_edit_start = insert_anchor.bias_left(buffer);
+        let last_edit_end = insert_anchor.bias_right(buffer);
+        self.transact(window, cx, |this, window, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+            this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+                s.select_anchor_ranges([last_edit_start..last_edit_end]);
+            });
+        });
+    }
+
+    pub fn clear_selection_drag_state(&mut self) {
         self.selection_drag_state = SelectionDragState::None;
-        false
     }
 
     pub fn duplicate(

crates/editor/src/element.rs 🔗

@@ -641,6 +641,7 @@ impl EditorElement {
             if point_for_position.intersects_selection(&selection) {
                 editor.selection_drag_state = SelectionDragState::ReadyToDrag {
                     selection: newest_anchor.clone(),
+                    click_position: event.position,
                 };
                 cx.stop_propagation();
                 return;
@@ -832,9 +833,44 @@ impl EditorElement {
         let pending_nonempty_selections = editor.has_pending_nonempty_selection();
         let point_for_position = position_map.point_for_position(event.position);
 
-        let is_cut = !event.modifiers.control;
-        if editor.drop_selection(Some(point_for_position), is_cut, window, cx) {
-            return;
+        match editor.selection_drag_state {
+            SelectionDragState::ReadyToDrag {
+                selection: _,
+                ref click_position,
+            } => {
+                if event.position == *click_position {
+                    editor.select(
+                        SelectPhase::Begin {
+                            position: point_for_position.previous_valid,
+                            add: false,
+                            click_count: 1, // ready to drag state only occurs on click count 1
+                        },
+                        window,
+                        cx,
+                    );
+                    editor.selection_drag_state = SelectionDragState::None;
+                    cx.stop_propagation();
+                    return;
+                }
+            }
+            SelectionDragState::Dragging { ref selection, .. } => {
+                let snapshot = editor.snapshot(window, cx);
+                let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
+                if !point_for_position.intersects_selection(&selection_display) {
+                    let is_cut = !event.modifiers.control;
+                    editor.move_selection_on_drop(
+                        &selection.clone(),
+                        point_for_position.previous_valid,
+                        is_cut,
+                        window,
+                        cx,
+                    );
+                    editor.selection_drag_state = SelectionDragState::None;
+                    cx.stop_propagation();
+                    return;
+                }
+            }
+            _ => {}
         }
 
         if end_selection {
@@ -951,7 +987,7 @@ impl EditorElement {
                     drop_cursor.start = drop_anchor;
                     drop_cursor.end = drop_anchor;
                 }
-                SelectionDragState::ReadyToDrag { ref selection } => {
+                SelectionDragState::ReadyToDrag { ref selection, .. } => {
                     let drop_cursor = Selection {
                         id: post_inc(&mut editor.selections.next_selection_id),
                         start: drop_anchor,

crates/vim/src/vim.rs 🔗

@@ -915,8 +915,8 @@ impl Vim {
         if mode == Mode::Normal || mode != last_mode {
             self.current_tx.take();
             self.current_anchor.take();
-            self.update_editor(window, cx, |_, editor, window, cx| {
-                editor.drop_selection(None, false, window, cx);
+            self.update_editor(window, cx, |_, editor, _, _| {
+                editor.clear_selection_drag_state();
             });
         }
         Vim::take_forced_motion(cx);