diff --git a/assets/settings/default.json b/assets/settings/default.json index 0fff7110a8..a486b2a50d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -217,6 +217,8 @@ "show_signature_help_after_edits": false, // Whether to show code action button at start of buffer line. "inline_code_actions": true, + // Whether to allow drag and drop text selection in buffer. + "drag_and_drop_selection": true, // What to do when go to definition yields no results. // // 1. Do nothing: `none` diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3c434a7455..db37a64e16 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -906,6 +906,18 @@ struct InlineBlamePopover { popover_state: InlineBlamePopoverState, } +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 }, + /// State when the mouse is dragging the selection in the editor. + Dragging { + selection: Selection, + drop_cursor: Selection, + }, +} + /// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have /// a breakpoint on them. #[derive(Clone, Copy, Debug)] @@ -1091,6 +1103,8 @@ pub struct Editor { hide_mouse_mode: HideMouseMode, pub change_list: ChangeList, inline_value_cache: InlineValueCache, + selection_drag_state: SelectionDragState, + drag_and_drop_selection_enabled: bool, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -1985,6 +1999,8 @@ impl Editor { .unwrap_or_default(), change_list: ChangeList::new(), mode, + selection_drag_state: SelectionDragState::None, + drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection, }; if let Some(breakpoints) = editor.breakpoint_store.as_ref() { editor @@ -3530,6 +3546,7 @@ impl Editor { pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { self.selection_mark_mode = false; + self.selection_drag_state = SelectionDragState::None; if self.clear_expanded_diff_hunks(cx) { cx.notify(); @@ -10584,6 +10601,56 @@ impl Editor { }); } + pub fn drop_selection( + &mut self, + point_for_position: Option, + is_cut: bool, + window: &mut Window, + cx: &mut Context, + ) -> 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::(); + 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; + } + } + _ => {} + } + } + self.selection_drag_state = SelectionDragState::None; + false + } + pub fn duplicate( &mut self, upwards: bool, @@ -18987,6 +19054,7 @@ impl Editor { self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default(); + self.drag_and_drop_selection_enabled = editor_settings.drag_and_drop_selection; } if old_cursor_shape != self.cursor_shape { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 0d14064ef8..803587a923 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -49,6 +49,7 @@ pub struct EditorSettings { #[serde(default)] pub diagnostics_max_severity: Option, pub inline_code_actions: bool, + pub drag_and_drop_selection: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -495,6 +496,11 @@ pub struct EditorSettingsContent { /// /// Default: true pub inline_code_actions: Option, + + /// Whether to allow drag and drop text selection in buffer. + /// + /// Default: true + pub drag_and_drop_selection: Option, } // Toolbar related settings diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d596646517..2436112909 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,8 +8,8 @@ use crate::{ InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, - SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, - ToggleFold, + SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, + StickyHeaderExcerpt, ToPoint, ToggleFold, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, display_map::{ Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk, @@ -78,10 +78,11 @@ use std::{ time::Duration, }; use sum_tree::Bias; -use text::BufferId; +use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*}; use unicode_segmentation::UnicodeSegmentation; +use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; @@ -619,6 +620,7 @@ impl EditorElement { let text_hitbox = &position_map.text_hitbox; let gutter_hitbox = &position_map.gutter_hitbox; + let point_for_position = position_map.point_for_position(event.position); let mut click_count = event.click_count; let mut modifiers = event.modifiers; @@ -632,6 +634,19 @@ impl EditorElement { return; } + if editor.drag_and_drop_selection_enabled && click_count == 1 { + let newest_anchor = editor.selections.newest_anchor(); + let snapshot = editor.snapshot(window, cx); + let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot)); + if point_for_position.intersects_selection(&selection) { + editor.selection_drag_state = SelectionDragState::ReadyToDrag { + selection: newest_anchor.clone(), + }; + cx.stop_propagation(); + return; + } + } + let is_singleton = editor.buffer().read(cx).is_singleton(); if click_count == 2 && !is_singleton { @@ -675,11 +690,8 @@ impl EditorElement { } } - let point_for_position = position_map.point_for_position(event.position); let position = point_for_position.previous_valid; - let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx); - if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) { editor.select( SelectPhase::BeginColumnar { @@ -818,6 +830,12 @@ impl EditorElement { let text_hitbox = &position_map.text_hitbox; let end_selection = editor.has_pending_selection(); 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; + } if end_selection { editor.select(SelectPhase::End, window, cx); @@ -881,12 +899,15 @@ impl EditorElement { window: &mut Window, cx: &mut Context, ) { - if !editor.has_pending_selection() { + if !editor.has_pending_selection() + && matches!(editor.selection_drag_state, SelectionDragState::None) + { return; } let text_bounds = position_map.text_hitbox.bounds; let point_for_position = position_map.point_for_position(event.position); + let mut scroll_delta = gpui::Point::::default(); let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); let top = text_bounds.origin.y + vertical_margin; @@ -918,15 +939,46 @@ impl EditorElement { scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); } - editor.select( - SelectPhase::Update { - position: point_for_position.previous_valid, - goal_column: point_for_position.exact_unclipped.column(), - scroll_delta, - }, - window, - cx, - ); + if !editor.has_pending_selection() { + let drop_anchor = position_map + .snapshot + .display_point_to_anchor(point_for_position.previous_valid, Bias::Left); + match editor.selection_drag_state { + SelectionDragState::Dragging { + ref mut drop_cursor, + .. + } => { + drop_cursor.start = drop_anchor; + drop_cursor.end = drop_anchor; + } + SelectionDragState::ReadyToDrag { ref selection } => { + let drop_cursor = Selection { + id: post_inc(&mut editor.selections.next_selection_id), + start: drop_anchor, + end: drop_anchor, + reversed: false, + goal: SelectionGoal::None, + }; + editor.selection_drag_state = SelectionDragState::Dragging { + selection: selection.clone(), + drop_cursor, + }; + } + _ => {} + } + editor.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } else { + editor.select( + SelectPhase::Update { + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), + scroll_delta, + }, + window, + cx, + ); + } } fn mouse_moved( @@ -1155,6 +1207,34 @@ impl EditorElement { let player = editor.current_user_player_color(cx); selections.push((player, layouts)); + + if let SelectionDragState::Dragging { + ref selection, + ref drop_cursor, + } = editor.selection_drag_state + { + if drop_cursor + .start + .cmp(&selection.start, &snapshot.buffer_snapshot) + .eq(&Ordering::Less) + || drop_cursor + .end + .cmp(&selection.end, &snapshot.buffer_snapshot) + .eq(&Ordering::Greater) + { + let drag_cursor_layout = SelectionLayout::new( + drop_cursor.clone(), + false, + CursorShape::Bar, + &snapshot.display_snapshot, + false, + false, + None, + ); + let absent_color = cx.theme().players().absent(); + selections.push((absent_color, vec![drag_cursor_layout])); + } + } } if let Some(collaboration_hub) = &editor.collaboration_hub { @@ -9235,6 +9315,35 @@ impl PointForPosition { None } } + + pub fn intersects_selection(&self, selection: &Selection) -> bool { + let Some(valid_point) = self.as_valid() else { + return false; + }; + let range = selection.range(); + + let candidate_row = valid_point.row(); + let candidate_col = valid_point.column(); + + let start_row = range.start.row(); + let start_col = range.start.column(); + let end_row = range.end.row(); + let end_col = range.end.column(); + + if candidate_row < start_row || candidate_row > end_row { + false + } else if start_row == end_row { + candidate_col >= start_col && candidate_col < end_col + } else { + if candidate_row == start_row { + candidate_col >= start_col + } else if candidate_row == end_row { + candidate_col < end_col + } else { + true + } + } + } } impl PositionMap { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 88bd2fb744..72be41566a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -915,6 +915,9 @@ 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); + }); } Vim::take_forced_motion(cx); if mode != Mode::Insert && mode != Mode::Replace { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b31be7cf85..e383e31b2d 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1216,6 +1216,16 @@ or `boolean` values +### Drag And Drop Selection + +- Description: Whether to allow drag and drop text selection in buffer. +- Setting: `drag_and_drop_selection` +- Default: `true` + +**Options** + +`boolean` values + ## Editor Toolbar - Description: Whether or not to show various elements in the editor toolbar.