From ef61ebe04989cf205e927f35f26bb4e9fe0bc211 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 16 Jun 2025 10:13:25 +0530 Subject: [PATCH] editor: Support both cursor and mouse based columnar selection (#32779) Closes #32584 In https://github.com/zed-industries/zed/pull/31888, we changed the default `opt + shift` behavior to start columnar selection from the mouse position (Sublime-like behavior) instead of from the existing selection head (VSCode-like behavior). It turns out there is a use case for creating columnar selection from an existing selection head as well, such as creating a consecutive multi-cursor from existing selection head with just a click instead of dragging. This PR brings back columnar selection from the selection head via `opt + shift`, while retaining columnar selection from the mouse position, which is now mapped to the new `cmd + shift` binding. Note: If you like to swap the binding, you can use [existing multi cursor modifier setting](https://zed.dev/docs/configuring-zed?highlight=multi_cursor_modifier#multi-cursor-modifier). Release Notes: - Added `cmd + shift` to start columnar selection from the mouse position. - Restored `opt + shift` to create columnar selection (or consecutive multi-cursor on click) from the selection head. --- crates/editor/src/editor.rs | 152 +++++++++++++++++++++++------------ crates/editor/src/element.rs | 28 ++++--- 2 files changed, 116 insertions(+), 64 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5d6abab4f2..407cea2f9a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -447,6 +447,7 @@ pub enum SelectPhase { BeginColumnar { position: DisplayPoint, reset: bool, + mode: ColumnarMode, goal_column: u32, }, Extend { @@ -461,6 +462,12 @@ pub enum SelectPhase { End, } +#[derive(Clone, Debug, PartialEq)] +pub enum ColumnarMode { + FromMouse, + FromSelection, +} + #[derive(Clone, Debug)] pub enum SelectMode { Character, @@ -922,6 +929,16 @@ enum SelectionDragState { }, } +enum ColumnarSelectionState { + FromMouse { + selection_tail: Anchor, + display_point: Option, + }, + FromSelection { + selection_tail: 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, PartialEq, Eq)] @@ -949,8 +966,7 @@ pub struct Editor { /// When inline assist editors are linked, they all render cursors because /// typing enters text into each of them, even the ones that aren't focused. pub(crate) show_cursor_when_unfocused: bool, - columnar_selection_tail: Option, - columnar_display_point: Option, + columnar_selection_state: Option, add_selections_state: Option, select_next_state: Option, select_prev_state: Option, @@ -1893,8 +1909,7 @@ impl Editor { display_map: display_map.clone(), selections, scroll_manager: ScrollManager::new(cx), - columnar_selection_tail: None, - columnar_display_point: None, + columnar_selection_state: None, add_selections_state: None, select_next_state: None, select_prev_state: None, @@ -3216,7 +3231,8 @@ impl Editor { position, goal_column, reset, - } => self.begin_columnar_selection(position, goal_column, reset, window, cx), + mode, + } => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx), SelectPhase::Extend { position, click_count, @@ -3373,6 +3389,7 @@ impl Editor { position: DisplayPoint, goal_column: u32, reset: bool, + mode: ColumnarMode, window: &mut Window, cx: &mut Context, ) { @@ -3395,26 +3412,30 @@ impl Editor { SelectMode::Character, ); }); - if position.column() != goal_column { - self.columnar_display_point = Some(DisplayPoint::new(position.row(), goal_column)); - } else { - self.columnar_display_point = None; - } - } + }; let tail = self.selections.newest::(cx).tail(); - self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); + let selection_anchor = display_map.buffer_snapshot.anchor_before(tail); + self.columnar_selection_state = match mode { + ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse { + selection_tail: selection_anchor, + display_point: if reset { + if position.column() != goal_column { + Some(DisplayPoint::new(position.row(), goal_column)) + } else { + None + } + } else { + None + }, + }), + ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection { + selection_tail: selection_anchor, + }), + }; if !reset { - self.columnar_display_point = None; - self.select_columns( - tail.to_display_point(&display_map), - position, - goal_column, - &display_map, - window, - cx, - ); + self.select_columns(position, goal_column, &display_map, window, cx); } } @@ -3428,11 +3449,8 @@ impl Editor { ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - if let Some(tail) = self.columnar_selection_tail.as_ref() { - let tail = self - .columnar_display_point - .unwrap_or_else(|| tail.to_display_point(&display_map)); - self.select_columns(tail, position, goal_column, &display_map, window, cx); + if self.columnar_selection_state.is_some() { + self.select_columns(position, goal_column, &display_map, window, cx); } else if let Some(mut pending) = self.selections.pending_anchor() { let buffer = self.buffer.read(cx).snapshot(cx); let head; @@ -3519,7 +3537,7 @@ impl Editor { } fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { - self.columnar_selection_tail.take(); + self.columnar_selection_state.take(); if self.selections.pending_anchor().is_some() { let selections = self.selections.all::(cx); self.change_selections(None, window, cx, |s| { @@ -3531,13 +3549,26 @@ impl Editor { fn select_columns( &mut self, - tail: DisplayPoint, head: DisplayPoint, goal_column: u32, display_map: &DisplaySnapshot, window: &mut Window, cx: &mut Context, ) { + let Some(columnar_state) = self.columnar_selection_state.as_ref() else { + return; + }; + + let tail = match columnar_state { + ColumnarSelectionState::FromMouse { + selection_tail, + display_point, + } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)), + ColumnarSelectionState::FromSelection { selection_tail } => { + selection_tail.to_display_point(&display_map) + } + }; + let start_row = cmp::min(tail.row(), head.row()); let end_row = cmp::max(tail.row(), head.row()); let start_column = cmp::min(tail.column(), goal_column); @@ -3547,7 +3578,10 @@ impl Editor { let selection_ranges = (start_row.0..=end_row.0) .map(DisplayRow) .filter_map(|row| { - if !display_map.is_block_line(row) { + if (matches!(columnar_state, ColumnarSelectionState::FromMouse { .. }) + || start_column <= display_map.line_len(row)) + && !display_map.is_block_line(row) + { let start = display_map .clip_point(DisplayPoint::new(row, start_column), Bias::Left) .to_point(display_map); @@ -3565,15 +3599,19 @@ impl Editor { }) .collect::>(); - let mut non_empty_ranges = selection_ranges - .iter() - .filter(|selection_range| selection_range.start != selection_range.end) - .peekable(); - - let ranges = if non_empty_ranges.peek().is_some() { - non_empty_ranges.cloned().collect() - } else { - selection_ranges + let ranges = match columnar_state { + ColumnarSelectionState::FromMouse { .. } => { + let mut non_empty_ranges = selection_ranges + .iter() + .filter(|selection_range| selection_range.start != selection_range.end) + .peekable(); + if non_empty_ranges.peek().is_some() { + non_empty_ranges.cloned().collect() + } else { + selection_ranges + } + } + _ => selection_ranges, }; self.change_selections(None, window, cx, |s| { @@ -3596,11 +3634,11 @@ impl Editor { }; pending_nonempty_selection - || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) + || (self.columnar_selection_state.is_some() && self.selections.disjoint.len() > 1) } pub fn has_pending_selection(&self) -> bool { - self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() + self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some() } pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { @@ -7188,13 +7226,9 @@ impl Editor { ) } - fn multi_cursor_modifier( - cursor_event: bool, - modifiers: &Modifiers, - cx: &mut Context, - ) -> bool { + fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context) -> bool { let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; - if cursor_event { + if invert { match multi_cursor_setting { MultiCursorModifier::Alt => modifiers.alt, MultiCursorModifier::CmdOrCtrl => modifiers.secondary(), @@ -7207,8 +7241,21 @@ impl Editor { } } - fn columnar_selection_modifiers(multi_cursor_modifier: bool, modifiers: &Modifiers) -> bool { - modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 2 + fn columnar_selection_mode( + modifiers: &Modifiers, + cx: &mut Context, + ) -> Option { + if modifiers.shift && modifiers.number_of_modifiers() == 2 { + if Self::multi_cursor_modifier(false, modifiers, cx) { + Some(ColumnarMode::FromMouse) + } else if Self::multi_cursor_modifier(true, modifiers, cx) { + Some(ColumnarMode::FromSelection) + } else { + None + } + } else { + None + } } fn update_selection_mode( @@ -7218,10 +7265,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx); - if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers) - || self.selections.pending.is_none() - { + let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else { + return; + }; + if self.selections.pending.is_none() { return; } @@ -7233,6 +7280,7 @@ impl Editor { SelectPhase::BeginColumnar { position, reset: false, + mode, goal_column: point_for_position.exact_unclipped.column(), }, window, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9486a81e32..b7a89a5c76 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,13 +1,13 @@ use crate::{ ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, - CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, - ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, - DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, - Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, - FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, - InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, - MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, - PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, + CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, + ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, + DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, + EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, + FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, + HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, + LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, + PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, @@ -700,12 +700,15 @@ impl EditorElement { } 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) { + if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { editor.select( SelectPhase::BeginColumnar { position, - reset: true, + reset: match mode { + ColumnarMode::FromMouse => true, + ColumnarMode::FromSelection => false, + }, + mode: mode, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -725,7 +728,7 @@ impl EditorElement { editor.select( SelectPhase::Begin { position, - add: multi_cursor_modifier, + add: Editor::multi_cursor_modifier(true, &modifiers, cx), click_count, }, window, @@ -822,6 +825,7 @@ impl EditorElement { SelectPhase::BeginColumnar { position, reset: true, + mode: ColumnarMode::FromMouse, goal_column: point_for_position.exact_unclipped.column(), }, window,