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:
parent
b15aef4310
commit
4fe05530b0
6 changed files with 214 additions and 16 deletions
|
@ -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`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue