From 1569b662ff8ede1d126b27d44966ddbf0e207cbf Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Wed, 9 Jul 2025 00:08:23 -0700 Subject: [PATCH] editor: Change `drag_and_drop_selection` cursor on delay elapsed + Add `drag_and_drop_selection` delay setting (#33928) When [`drag_and_drop_selection` is true](https://zed.dev/docs/configuring-zed#drag-and-drop-selection), users can make a selection in the buffer and then drag and drop it to a new location. However, the editor forces users to wait 300ms after mouse down before dragging. If users try to drag before this delay has elapsed, they will create a new text selection instead, which can create the impression that drag and drop does not work. I made two changes to improve the UX of this feature: * If users do not want a delay before drag and drop is enabled, they can set the `drag_and_drop_selection.delay_ms` setting to 0. * If the user has done a mouse down on a text selection, the cursor changes to a copy affordance as soon as the configured delay has elapsed, rather than waiting for them to start dragging. This way they don't need to guess at when the delay has elapsed. The default settings for this feature are now: ``` "drag_and_drop_selection": { "enabled": true, "delay_ms": 300 } ``` Closes #33915 Before: https://github.com/user-attachments/assets/7b2f986f-9c67-4b2b-a10e-757c3e9c934b After: https://github.com/user-attachments/assets/726d0dbf-e58b-41ad-93d2-1a758640b422 Release Notes: - Migrate `drag_and_drop_selection` setting to `drag_and_drop_selection.enabled`. - Add `drag_and_drop_selection.delay_ms` setting to configure the delay that must elapse before drag and drop is allowed. - Show a ready to drag cursor affordance as soon as the delay has elapsed --------- Co-authored-by: Smit Barmase --- assets/settings/default.json | 7 +++- crates/editor/src/editor.rs | 3 -- crates/editor/src/editor_settings.rs | 28 +++++++++++--- crates/editor/src/element.rs | 25 +++++++++++-- crates/migrator/src/migrations.rs | 6 +++ .../src/migrations/m_2025_07_08/settings.rs | 37 +++++++++++++++++++ crates/migrator/src/migrator.rs | 8 ++++ docs/src/configuring-zed.md | 13 ++++--- 8 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2025_07_08/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index dc1040e1d0..8c105b2c1e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -228,7 +228,12 @@ // 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, + "drag_and_drop_selection": { + // When true, enables drag and drop text selection in buffer. + "enabled": true, + // The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. + "delay": 300 + }, // 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 03e2124742..c5fe0db74c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1170,7 +1170,6 @@ pub struct Editor { pub change_list: ChangeList, inline_value_cache: InlineValueCache, selection_drag_state: SelectionDragState, - drag_and_drop_selection_enabled: bool, next_color_inlay_id: usize, colors: Option, folding_newlines: Task<()>, @@ -2202,7 +2201,6 @@ impl Editor { change_list: ChangeList::new(), mode, selection_drag_state: SelectionDragState::None, - drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection, folding_newlines: Task::ready(()), }; if let Some(breakpoints) = editor.breakpoint_store.as_ref() { @@ -19899,7 +19897,6 @@ 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 d7b8bac359..5d8379ddfb 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -52,7 +52,7 @@ pub struct EditorSettings { #[serde(default)] pub diagnostics_max_severity: Option, pub inline_code_actions: bool, - pub drag_and_drop_selection: bool, + pub drag_and_drop_selection: DragAndDropSelection, pub lsp_document_colors: DocumentColorsRenderMode, } @@ -275,6 +275,26 @@ pub struct ScrollbarAxes { pub vertical: bool, } +/// Whether to allow drag and drop text selection in buffer. +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct DragAndDropSelection { + /// When true, enables drag and drop text selection in buffer. + /// + /// Default: true + #[serde(default = "default_true")] + pub enabled: bool, + + /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. + /// + /// Default: 300 + #[serde(default = "default_drag_and_drop_selection_delay_ms")] + pub delay: u64, +} + +fn default_drag_and_drop_selection_delay_ms() -> u64 { + 300 +} + /// Which diagnostic indicators to show in the scrollbar. /// /// Default: all @@ -536,10 +556,8 @@ 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, + /// Drag and drop related settings + pub drag_and_drop_selection: Option, /// How to render LSP `textDocument/documentColor` colors in the editor. /// diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a446338335..8a5bfb3bab 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -87,7 +87,6 @@ use util::{RangeExt, ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; -const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300); /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] @@ -644,7 +643,11 @@ impl EditorElement { return; } - if editor.drag_and_drop_selection_enabled && click_count == 1 { + if EditorSettings::get_global(cx) + .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)); @@ -1022,7 +1025,10 @@ impl EditorElement { ref click_position, ref mouse_down_time, } => { - if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx).drag_and_drop_selection.delay, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { let drop_cursor = Selection { id: post_inc(&mut editor.selections.next_selection_id), start: drop_anchor, @@ -5710,6 +5716,19 @@ impl EditorElement { let editor = self.editor.read(cx); if editor.mouse_cursor_hidden { window.set_window_cursor_style(CursorStyle::None); + } else if let SelectionDragState::ReadyToDrag { + mouse_down_time, .. + } = &editor.selection_drag_state + { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx).drag_and_drop_selection.delay, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { + window.set_cursor_style( + CursorStyle::DragCopy, + &layout.position_map.text_hitbox, + ); + } } else if matches!( editor.selection_drag_state, SelectionDragState::Dragging { .. } diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index 4e3839358b..9db597e964 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -93,3 +93,9 @@ pub(crate) mod m_2025_06_27 { pub(crate) use settings::SETTINGS_PATTERNS; } + +pub(crate) mod m_2025_07_08 { + mod settings; + + pub(crate) use settings::SETTINGS_PATTERNS; +} diff --git a/crates/migrator/src/migrations/m_2025_07_08/settings.rs b/crates/migrator/src/migrations/m_2025_07_08/settings.rs new file mode 100644 index 0000000000..c9656491ce --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_07_08/settings.rs @@ -0,0 +1,37 @@ +use std::ops::Range; +use tree_sitter::{Query, QueryMatch}; + +use crate::MigrationPatterns; +use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; + +pub const SETTINGS_PATTERNS: MigrationPatterns = &[( + SETTINGS_ROOT_KEY_VALUE_PATTERN, + migrate_drag_and_drop_selection, +)]; + +fn migrate_drag_and_drop_selection( + contents: &str, + mat: &QueryMatch, + query: &Query, +) -> Option<(Range, String)> { + let name_ix = query.capture_index_for_name("name")?; + let name_range = mat.nodes_for_capture_index(name_ix).next()?.byte_range(); + let name = contents.get(name_range)?; + + if name != "drag_and_drop_selection" { + return None; + } + + let value_ix = query.capture_index_for_name("value")?; + let value_node = mat.nodes_for_capture_index(value_ix).next()?; + let value_range = value_node.byte_range(); + let value = contents.get(value_range.clone())?; + + match value { + "true" | "false" => { + let replacement = format!("{{\n \"enabled\": {}\n }}", value); + Some((value_range, replacement)) + } + _ => None, + } +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index be32b2734e..b425f7f1d5 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -160,6 +160,10 @@ pub fn migrate_settings(text: &str) -> Result> { migrations::m_2025_06_27::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_27, ), + ( + migrations::m_2025_07_08::SETTINGS_PATTERNS, + &SETTINGS_QUERY_2025_07_08, + ), ]; run_migrations(text, migrations) } @@ -270,6 +274,10 @@ define_query!( SETTINGS_QUERY_2025_06_27, migrations::m_2025_06_27::SETTINGS_PATTERNS ); +define_query!( + SETTINGS_QUERY_2025_07_08, + migrations::m_2025_07_08::SETTINGS_PATTERNS +); // custom query static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 8bba431554..eec9da60dd 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1218,13 +1218,16 @@ or ### Drag And Drop Selection -- Description: Whether to allow drag and drop text selection in buffer. +- Description: Whether to allow drag and drop text selection in buffer. `delay` is the milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - Setting: `drag_and_drop_selection` -- Default: `true` +- Default: -**Options** - -`boolean` values +```json +"drag_and_drop_selection": { + "enabled": true, + "delay": 300 +} +``` ## Editor Toolbar