diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index ae15492b804274..26b7518f87b999 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -491,8 +491,8 @@ "bindings": { "ctrl-[": "editor::Outdent", "ctrl-]": "editor::Indent", - "shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above - "shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below + "shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above + "shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below "ctrl-shift-k": "editor::DeleteLine", "alt-up": "editor::MoveLineUp", "alt-down": "editor::MoveLineDown", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 50528dca6f97e8..d8c9428d7fee5b 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -539,10 +539,10 @@ "bindings": { "cmd-[": "editor::Outdent", "cmd-]": "editor::Indent", - "cmd-ctrl-p": "editor::AddSelectionAbove", // Insert cursor above - "cmd-alt-up": "editor::AddSelectionAbove", - "cmd-ctrl-n": "editor::AddSelectionBelow", // Insert cursor below - "cmd-alt-down": "editor::AddSelectionBelow", + "cmd-ctrl-p": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], // Insert cursor above + "cmd-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], + "cmd-ctrl-n": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], // Insert cursor below + "cmd-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], "cmd-shift-k": "editor::DeleteLine", "alt-up": "editor::MoveLineUp", "alt-down": "editor::MoveLineDown", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 570766e92ce8ad..01ed737ccc3d5c 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -500,8 +500,8 @@ "bindings": { "ctrl-[": "editor::Outdent", "ctrl-]": "editor::Indent", - "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above - "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below + "ctrl-shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above + "ctrl-shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below "ctrl-shift-k": "editor::DeleteLine", "alt-up": "editor::MoveLineUp", "alt-down": "editor::MoveLineDown", diff --git a/assets/keymaps/linux/atom.json b/assets/keymaps/linux/atom.json index 86ee068b06ef38..98992b19fac720 100644 --- a/assets/keymaps/linux/atom.json +++ b/assets/keymaps/linux/atom.json @@ -24,8 +24,8 @@ "ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor "f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next "shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous - "alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below - "alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above + "alt-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // editor:add-selection-below + "alt-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // editor:add-selection-above "ctrl-j": "editor::JoinLines", // editor:join-lines "ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines "ctrl-up": "editor::MoveLineUp", // editor:move-line-up diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json index f526db45ff29e0..eefd59e5bd1aa4 100644 --- a/assets/keymaps/linux/sublime_text.json +++ b/assets/keymaps/linux/sublime_text.json @@ -28,8 +28,8 @@ { "context": "Editor", "bindings": { - "ctrl-alt-up": "editor::AddSelectionAbove", - "ctrl-alt-down": "editor::AddSelectionBelow", + "ctrl-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], + "ctrl-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], "ctrl-shift-up": "editor::MoveLineUp", "ctrl-shift-down": "editor::MoveLineDown", "ctrl-shift-m": "editor::SelectLargerSyntaxNode", diff --git a/assets/keymaps/macos/atom.json b/assets/keymaps/macos/atom.json index df48e51767e545..ca015b667faa05 100644 --- a/assets/keymaps/macos/atom.json +++ b/assets/keymaps/macos/atom.json @@ -25,8 +25,8 @@ "cmd-<": "editor::ScrollCursorCenter", "cmd-g": ["editor::SelectNext", { "replace_newest": true }], "cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }], - "ctrl-shift-down": "editor::AddSelectionBelow", - "ctrl-shift-up": "editor::AddSelectionAbove", + "ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], + "ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], "alt-enter": "editor::Newline", "cmd-shift-d": "editor::DuplicateLineDown", "ctrl-cmd-up": "editor::MoveLineUp", diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json index a1e61bf8859e2e..d1bffca755b611 100644 --- a/assets/keymaps/macos/sublime_text.json +++ b/assets/keymaps/macos/sublime_text.json @@ -28,8 +28,8 @@ { "context": "Editor", "bindings": { - "ctrl-shift-up": "editor::AddSelectionAbove", - "ctrl-shift-down": "editor::AddSelectionBelow", + "ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], + "ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], "cmd-ctrl-up": "editor::MoveLineUp", "cmd-ctrl-down": "editor::MoveLineDown", "cmd-shift-space": "editor::SelectAll", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c90b439c6abb60..d54064436aea7f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -500,8 +500,8 @@ "ctrl-c": "editor::ToggleComments", "d": "vim::HelixDelete", "c": "vim::Substitute", - "shift-c": "editor::AddSelectionBelow", - "alt-shift-c": "editor::AddSelectionAbove" + "shift-c": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], + "alt-shift-c": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }] } }, { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 99fe7557b8f0ab..530a547bb9d28d 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -318,6 +318,24 @@ pub struct GoToPreviousDiagnostic { pub severity: GoToDiagnosticSeverityFilter, } +/// Adds a cursor above the current selection. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct AddSelectionAbove { + #[serde(default = "default_true")] + pub skip_soft_wrap: bool, +} + +/// Adds a cursor below the current selection. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct AddSelectionBelow { + #[serde(default = "default_true")] + pub skip_soft_wrap: bool, +} + actions!( debugger, [ @@ -345,10 +363,6 @@ actions!( /// Accepts a partial edit prediction. #[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])] AcceptPartialEditPrediction, - /// Adds a cursor above the current selection. - AddSelectionAbove, - /// Adds a cursor below the current selection. - AddSelectionBelow, /// Applies all diff hunks in the editor. ApplyAllDiffHunks, /// Applies the diff hunk at the current position. diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 87841a8f7e1356..b3d642f60ea591 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1401,6 +1401,26 @@ impl DisplaySnapshot { pub fn excerpt_header_height(&self) -> u32 { self.block_snapshot.excerpt_header_height } + + /// Given a `DisplayPoint`, returns another `DisplayPoint` corresponding to + /// the start of the buffer row that is a given number of buffer rows away + /// from the provided point. + /// + /// This moves by buffer rows instead of display rows, a distinction that is + /// important when soft wrapping is enabled. + pub fn start_of_relative_buffer_row(&self, point: DisplayPoint, times: isize) -> DisplayPoint { + let start = self.display_point_to_fold_point(point, Bias::Left); + let target = start.row() as isize + times; + let new_row = (target.max(0) as u32).min(self.fold_snapshot().max_point().row()); + + self.clip_point( + self.fold_point_to_display_point( + self.fold_snapshot() + .clip_point(FoldPoint::new(new_row, 0), Bias::Right), + ), + Bias::Right, + ) + } } #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 848e9c76f6fd6e..44a3434af68196 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14235,23 +14235,29 @@ impl Editor { pub fn add_selection_above( &mut self, - _: &AddSelectionAbove, + action: &AddSelectionAbove, window: &mut Window, cx: &mut Context, ) { - self.add_selection(true, window, cx); + self.add_selection(true, action.skip_soft_wrap, window, cx); } pub fn add_selection_below( &mut self, - _: &AddSelectionBelow, + action: &AddSelectionBelow, window: &mut Window, cx: &mut Context, ) { - self.add_selection(false, window, cx); + self.add_selection(false, action.skip_soft_wrap, window, cx); } - fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { + fn add_selection( + &mut self, + above: bool, + skip_soft_wrap: bool, + window: &mut Window, + cx: &mut Context, + ) { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -14338,12 +14344,19 @@ impl Editor { }; let mut maybe_new_selection = None; + let direction = if above { -1 } else { 1 }; + while row != end_row { - if above { + if skip_soft_wrap { + row = display_map + .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction) + .row(); + } else if above { row.0 -= 1; } else { row.0 += 1; } + if let Some(new_selection) = self.selections.build_columnar_selection( &display_map, row, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f6a0406fecf957..014221d58605d5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25671,6 +25671,83 @@ async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppC ); } +#[gpui::test] +async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc!( + r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled + Second line here"# + )); + + cx.update_editor(|editor, window, cx| { + // Enable soft wrapping with a narrow width to force soft wrapping and + // confirm that more than 2 rows are being displayed. + editor.set_wrap_width(Some(100.0.into()), cx); + assert!(editor.display_text(cx).lines().count() > 2); + + editor.add_selection_below( + &AddSelectionBelow { + skip_soft_wrap: true, + }, + window, + cx, + ); + + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0), + ] + ); + + editor.add_selection_above( + &AddSelectionAbove { + skip_soft_wrap: true, + }, + window, + cx, + ); + + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] + ); + + editor.add_selection_below( + &AddSelectionBelow { + skip_soft_wrap: false, + }, + window, + cx, + ); + + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0), + ] + ); + + editor.add_selection_above( + &AddSelectionAbove { + skip_soft_wrap: false, + }, + window, + cx, + ); + + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] + ); + }); +} + #[gpui::test(iterations = 10)] async fn test_document_colors(cx: &mut TestAppContext) { let expected_color = Rgba { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index acdece23d9996a..36491b0d3c9554 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -392,6 +392,11 @@ impl SelectionsCollection { .collect() } + /// Attempts to build a selection in the provided `DisplayRow` within the + /// same range as the provided range of `Pixels`. + /// Returns `None` if the range is not empty but it starts past the line's + /// length, meaning that the line isn't long enough to be contained within + /// part of the provided range. pub fn build_columnar_selection( &mut self, display_map: &DisplaySnapshot, diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 666d2573a53cbf..e44039914f8018 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1525,29 +1525,6 @@ fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayP } } -/// Given a point, returns the start of the buffer row that is a given number of -/// buffer rows away from the current position. -/// -/// This moves by buffer rows instead of display rows, a distinction that is -/// important when soft wrapping is enabled. -pub(crate) fn start_of_relative_buffer_row( - map: &DisplaySnapshot, - point: DisplayPoint, - times: isize, -) -> DisplayPoint { - let start = map.display_point_to_fold_point(point, Bias::Left); - let target = start.row() as isize + times; - let new_row = (target.max(0) as u32).min(map.fold_snapshot().max_point().row()); - - map.clip_point( - map.fold_point_to_display_point( - map.fold_snapshot() - .clip_point(FoldPoint::new(new_row, 0), Bias::Right), - ), - Bias::Right, - ) -} - fn up_down_buffer_rows( map: &DisplaySnapshot, mut point: DisplayPoint, @@ -2127,7 +2104,7 @@ pub(crate) fn end_of_line( times: usize, ) -> DisplayPoint { if times > 1 { - point = start_of_relative_buffer_row(map, point, times as isize - 1); + point = map.start_of_relative_buffer_row(point, times as isize - 1); } if display_lines { map.clip_point( @@ -2732,17 +2709,17 @@ fn sneak_backward( } fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = start_of_relative_buffer_row(map, point, times as isize); + let correct_line = map.start_of_relative_buffer_row(point, times as isize); first_non_whitespace(map, false, correct_line) } fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = start_of_relative_buffer_row(map, point, -(times as isize)); + let correct_line = map.start_of_relative_buffer_row(point, -(times as isize)); first_non_whitespace(map, false, correct_line) } fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = start_of_relative_buffer_row(map, point, 0); + let correct_line = map.start_of_relative_buffer_row(point, 0); right(map, correct_line, times.saturating_sub(1)) } @@ -2752,7 +2729,7 @@ pub(crate) fn next_line_end( times: usize, ) -> DisplayPoint { if times > 1 { - point = start_of_relative_buffer_row(map, point, times as isize - 1); + point = map.start_of_relative_buffer_row(point, times as isize - 1); } end_of_line(map, false, point, 1) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 9386eab58a389b..1f80c78b29b619 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -681,7 +681,7 @@ impl Vim { editor.edit_with_autoindent(edits, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, cursor, _| { - let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1); + let previous_line = map.start_of_relative_buffer_row(cursor, -1); let insert_point = motion::end_of_line(map, false, previous_line, 1); (insert_point, SelectionGoal::None) }); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index f8ef8e32586ca1..fcbfd11bb62b9c 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -15,10 +15,7 @@ use workspace::searchable::Direction; use crate::{ Vim, - motion::{ - Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line, - start_of_relative_buffer_row, - }, + motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line}, object::Object, state::{Mark, Mode, Operator}, }; @@ -406,7 +403,9 @@ impl Vim { // Move to the next or previous buffer row, ensuring that // wrapped lines are handled correctly. let direction = if tail.row() > head.row() { -1 } else { 1 }; - row = start_of_relative_buffer_row(map, DisplayPoint::new(row, 0), direction).row(); + row = map + .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction) + .row(); } s.select(selections); diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index cd18503f61be4a..e9bfb6d92a7101 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -185,8 +185,18 @@ pub fn app_menus(cx: &mut App) -> Vec { editor::actions::SelectPreviousSyntaxNode, ), MenuItem::separator(), - MenuItem::action("Add Cursor Above", editor::actions::AddSelectionAbove), - MenuItem::action("Add Cursor Below", editor::actions::AddSelectionBelow), + MenuItem::action( + "Add Cursor Above", + editor::actions::AddSelectionAbove { + skip_soft_wrap: true, + }, + ), + MenuItem::action( + "Add Cursor Below", + editor::actions::AddSelectionBelow { + skip_soft_wrap: true, + }, + ), MenuItem::action( "Select Next Occurrence", editor::actions::SelectNext { diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index c721e1e8b6e7c8..6c8dc975b567ad 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -266,8 +266,18 @@ impl Render for QuickActionBar { ) .action("Expand Selection", Box::new(SelectLargerSyntaxNode)) .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode)) - .action("Add Cursor Above", Box::new(AddSelectionAbove)) - .action("Add Cursor Below", Box::new(AddSelectionBelow)) + .action( + "Add Cursor Above", + Box::new(AddSelectionAbove { + skip_soft_wrap: true, + }), + ) + .action( + "Add Cursor Below", + Box::new(AddSelectionBelow { + skip_soft_wrap: true, + }), + ) .separator() .action("Go to Symbol", Box::new(ToggleOutline)) .action("Go to Line/Column", Box::new(ToggleGoToLine))