Add paragraph based vertical movements (#2502)

Very selfish patch I worked on yesterday, I kept saying I wanted these
and finally decided to just add them. Feedback on the keybindings
welcome

Release Notes:

* Added `MoveToStartOfParagraph` and `MoveToEndOfParagraph` movements
for paragraph based vertical navigation
This commit is contained in:
Julia 2023-05-22 14:31:34 -04:00 committed by GitHub
commit a69144911f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 236 additions and 0 deletions

View file

@ -67,10 +67,12 @@
"cmd-z": "editor::Undo", "cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo", "cmd-shift-z": "editor::Redo",
"up": "editor::MoveUp", "up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::PageUp", "pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp", "shift-pageup": "editor::MovePageUp",
"home": "editor::MoveToBeginningOfLine", "home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown", "down": "editor::MoveDown",
"ctrl-down": "editor::MoveToEndOfParagraph",
"pagedown": "editor::PageDown", "pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown", "shift-pagedown": "editor::MovePageDown",
"end": "editor::MoveToEndOfLine", "end": "editor::MoveToEndOfLine",
@ -103,6 +105,8 @@
"alt-shift-b": "editor::SelectToPreviousWordStart", "alt-shift-b": "editor::SelectToPreviousWordStart",
"alt-shift-right": "editor::SelectToNextWordEnd", "alt-shift-right": "editor::SelectToNextWordEnd",
"alt-shift-f": "editor::SelectToNextWordEnd", "alt-shift-f": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
"cmd-shift-up": "editor::SelectToBeginning", "cmd-shift-up": "editor::SelectToBeginning",
"cmd-shift-down": "editor::SelectToEnd", "cmd-shift-down": "editor::SelectToEnd",
"cmd-a": "editor::SelectAll", "cmd-a": "editor::SelectAll",

View file

@ -216,6 +216,8 @@ actions!(
MoveToNextSubwordEnd, MoveToNextSubwordEnd,
MoveToBeginningOfLine, MoveToBeginningOfLine,
MoveToEndOfLine, MoveToEndOfLine,
MoveToStartOfParagraph,
MoveToEndOfParagraph,
MoveToBeginning, MoveToBeginning,
MoveToEnd, MoveToEnd,
SelectUp, SelectUp,
@ -226,6 +228,8 @@ actions!(
SelectToPreviousSubwordStart, SelectToPreviousSubwordStart,
SelectToNextWordEnd, SelectToNextWordEnd,
SelectToNextSubwordEnd, SelectToNextSubwordEnd,
SelectToStartOfParagraph,
SelectToEndOfParagraph,
SelectToBeginning, SelectToBeginning,
SelectToEnd, SelectToEnd,
SelectAll, SelectAll,
@ -337,6 +341,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::move_to_next_subword_end); cx.add_action(Editor::move_to_next_subword_end);
cx.add_action(Editor::move_to_beginning_of_line); cx.add_action(Editor::move_to_beginning_of_line);
cx.add_action(Editor::move_to_end_of_line); cx.add_action(Editor::move_to_end_of_line);
cx.add_action(Editor::move_to_start_of_paragraph);
cx.add_action(Editor::move_to_end_of_paragraph);
cx.add_action(Editor::move_to_beginning); cx.add_action(Editor::move_to_beginning);
cx.add_action(Editor::move_to_end); cx.add_action(Editor::move_to_end);
cx.add_action(Editor::select_up); cx.add_action(Editor::select_up);
@ -349,6 +355,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::select_to_next_subword_end); cx.add_action(Editor::select_to_next_subword_end);
cx.add_action(Editor::select_to_beginning_of_line); cx.add_action(Editor::select_to_beginning_of_line);
cx.add_action(Editor::select_to_end_of_line); cx.add_action(Editor::select_to_end_of_line);
cx.add_action(Editor::select_to_start_of_paragraph);
cx.add_action(Editor::select_to_end_of_paragraph);
cx.add_action(Editor::select_to_beginning); cx.add_action(Editor::select_to_beginning);
cx.add_action(Editor::select_to_end); cx.add_action(Editor::select_to_end);
cx.add_action(Editor::select_all); cx.add_action(Editor::select_all);
@ -4759,6 +4767,80 @@ impl Editor {
}); });
} }
pub fn move_to_start_of_paragraph(
&mut self,
_: &MoveToStartOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::start_of_paragraph(map, selection.head()),
SelectionGoal::None,
)
});
})
}
pub fn move_to_end_of_paragraph(
&mut self,
_: &MoveToEndOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::end_of_paragraph(map, selection.head()),
SelectionGoal::None,
)
});
})
}
pub fn select_to_start_of_paragraph(
&mut self,
_: &SelectToStartOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(movement::start_of_paragraph(map, head), SelectionGoal::None)
});
})
}
pub fn select_to_end_of_paragraph(
&mut self,
_: &SelectToEndOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(movement::end_of_paragraph(map, head), SelectionGoal::None)
});
})
}
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) { pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) { if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action(); cx.propagate_action();

View file

@ -1243,6 +1243,118 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
cx.set_state(
&r#"ˇone
two
three
fourˇ
five
six"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
ˇ
three
four
five
ˇ
six"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
three
four
five
ˇ
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
two
three
four
five
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
two
ˇ
three
four
five
six"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
two
three
four
five
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
three
four
five
ˇ
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
ˇ
three
four
five
ˇ
six"#
.unindent(),
);
}
#[gpui::test] #[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});

View file

@ -193,6 +193,44 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
}) })
} }
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return map.max_point();
}
let mut found_non_blank_line = false;
for row in (0..point.row + 1).rev() {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
return Point::new(row, 0).to_display_point(map);
}
found_non_blank_line |= !blank;
}
DisplayPoint::zero()
}
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return DisplayPoint::zero();
}
let mut found_non_blank_line = false;
for row in point.row..map.max_buffer_row() + 1 {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
return Point::new(row, 0).to_display_point(map);
}
found_non_blank_line |= !blank;
}
map.max_point()
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the /// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right /// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start /// of the candidate boundary location, and will be called with `\n` characters indicating the start