@@ -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<Anchor> },
+ /// State when the mouse is dragging the selection in the editor.
+ Dragging {
+ selection: Selection<Anchor>,
+ drop_cursor: Selection<Anchor>,
+ },
+}
+
/// 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>) {
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<PointForPosition>,
+ 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;
+ }
+ }
+ _ => {}
+ }
+ }
+ 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 {
@@ -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<Editor>,
) {
- 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::<f32>::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<DisplayPoint>) -> 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 {