editor: Ignore soft wrapped lines when adding selection above or below (#40190)

- Add `skip_soft_wrap` field to both `AddSelectionAbove` and
`AddSelectionBelow` actions. When set to `true`, which is now 
the default this will skip soft wrapped lines when extending the 
selections.
- Move the `start_of_relative_buffer_row` function from the
`vim::motion` module to the `editor::display_map::DisplaySnapshot`
implementation as a method.
- Update the default behavior for both `editor: add selection above` and
`editor: add selection below` commands in order to skip over soft
wrapped lines by default, mirroring VS Code's default behavior.
- Update existing keymaps to specify this `skip_soft_wrap` value for
both `AddSelectionAbove` and `AddSelectionBelow` actions.

Closes #16979 

Release Notes:

- Updated both the `editor: add selection above` and `editor: add
selection below` commands to ignore soft wrapped lines. If you wish to
restore the old behavior, add the following to your keymap file:
  ```
  {
    "context": "Editor",
    "bindings": {
"cmd-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false
}],
"cmd-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false
}]
    }
  }
  ```

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Dino
2025-10-16 11:56:57 +01:00
committed by GitHub
co-authored by Smit Barmase
parent 86ce4ef3ab
commit 5c4f1e6b85
18 changed files with 191 additions and 66 deletions
+2 -2
View File
@@ -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",
+4 -4
View File
@@ -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",
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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 }]
}
},
{
+18 -4
View File
@@ -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.
+20
View File
@@ -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)]
+19 -6
View File
@@ -14236,23 +14236,29 @@ impl Editor {
pub fn add_selection_above(
&mut self,
_: &AddSelectionAbove,
action: &AddSelectionAbove,
window: &mut Window,
cx: &mut Context<Self>,
) {
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>,
) {
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<Self>) {
fn add_selection(
&mut self,
above: bool,
skip_soft_wrap: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
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,
+77
View File
@@ -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 {
@@ -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,
+5 -28
View File
@@ -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)
}
+1 -1
View File
@@ -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)
});
+4 -5
View File
@@ -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);
+12 -2
View File
@@ -185,8 +185,18 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
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 {
+12 -2
View File
@@ -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))