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.
This commit is contained in:
parent
6150c26bd2
commit
ef61ebe049
2 changed files with 116 additions and 64 deletions
|
@ -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<DisplayPoint>,
|
||||
},
|
||||
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<Anchor>,
|
||||
columnar_display_point: Option<DisplayPoint>,
|
||||
columnar_selection_state: Option<ColumnarSelectionState>,
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
select_next_state: Option<SelectNextState>,
|
||||
select_prev_state: Option<SelectNextState>,
|
||||
|
@ -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<Self>,
|
||||
) {
|
||||
|
@ -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::<Point>(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>) {
|
||||
self.columnar_selection_tail.take();
|
||||
self.columnar_selection_state.take();
|
||||
if self.selections.pending_anchor().is_some() {
|
||||
let selections = self.selections.all::<usize>(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<Self>,
|
||||
) {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<Self>) {
|
||||
|
@ -7188,13 +7226,9 @@ impl Editor {
|
|||
)
|
||||
}
|
||||
|
||||
fn multi_cursor_modifier(
|
||||
cursor_event: bool,
|
||||
modifiers: &Modifiers,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> 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<Self>,
|
||||
) -> Option<ColumnarMode> {
|
||||
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<Self>,
|
||||
) {
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue