diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index d85aec85d9c15442222b2cd1c934be20d440fe1a..279f9af968d532a2f55220c47eac07dace48e185 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 95c959b086a306e37b2069cf869e9cb6dcd98e89..91797cedd84617873ea17fdab81e9bf8b1a25ab7 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 ae754f2e60b8c7fbfaa84aef996f72bc00cace21..b7d8ab0aa3eb1c3ae7fbcdbc38cb7bbbe1698608 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 86ee068b06ef38ccec8215e4296c718dd873c824..98992b19fac72055807063edae8b7b23652062d3 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 f526db45ff29e0828ce58df6ca9816bd71a4cbe5..eefd59e5bd1aa48125d0c6e3d662f3cb4e270be7 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 df48e51767e54524c6645630d1fcb6b1cdeba599..ca015b667faa05db53d8fdc3bd82352d9bcc62aa 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 a1e61bf8859e2e4ea227ed3dbe22ec29eb35a149..d1bffca755b611d9046d4b7e794d2303835227a2 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 542492a0d4803bfbdaaa98bbe98caa4a43e7c35b..adfdd4883640eb8a3ee6c193e1bd26ea479f55b6 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -498,8 +498,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 99fe7557b8f0abe12a093d4dd540ead30b600e78..530a547bb9d28d9dadeb95f5e9e98425eb967a55 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 87841a8f7e135663df14b4bb82e18b61cf36907e..b3d642f60ea591fcf8b1987c723b4ba025dc110a 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 43f2e2ba5d2d1cb1b0a808eee388a75ccbfb02ab..8e3f594f185685f919ca68c33755c7d9710d4a72 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14236,23 +14236,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)); @@ -14339,12 +14345,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 dab89557cf137b2c75eb5690011d8914d29a358f..c627f1852cf4562fa366ab7caae695e8495700a9 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25681,6 +25681,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 54b8edcdc0b90de3079853d20240b68d2249f976..9ca2d1cb4ee116fb77bcae41fcb9d6a435629cff 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 666d2573a53cbf74ed1c2edee02c8561167038c3..e44039914f801848ce362b081f6e1bcd18b3c1fa 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 c79fd7984fb5ad0194a0c4cdd7ba5bb1ac11198c..2757a927aaa8ad7d565e56ccbda4042e5f3c56b3 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -679,7 +679,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 f8ef8e32586ca11800b5d3872aafaead3275bd37..fcbfd11bb62b9c6cf1e4df54f7521b4ba4810f69 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 cd18503f61be4a712caf5e4399f794b12c6bf889..e9bfb6d92a710177308dbd87b0fdc1129343a6a4 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 c721e1e8b6e7c8d1a3caf5c9a57e0159a5a3c031..6c8dc975b567ada889737c3f5def064b9b50e9fe 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))