editor: Add support for drag_and_drop_selection (#30671)

Closes #4958 

Release Notes:

- Added support for drag and drop text selection. It can be disabled by
setting `drag_and_drop_selection` to `false`.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
CharlesChen0823 2025-06-09 12:51:18 +08:00 committed by GitHub
parent b15aef4310
commit 4fe05530b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 214 additions and 16 deletions

View file

@ -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`

View file

@ -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 {

View file

@ -49,6 +49,7 @@ pub struct EditorSettings {
#[serde(default)]
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
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<bool>,
/// Whether to allow drag and drop text selection in buffer.
///
/// Default: true
pub drag_and_drop_selection: Option<bool>,
}
// Toolbar related settings

View file

@ -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 {

View file

@ -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 {

View file

@ -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.