@@ -67,10 +67,12 @@
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"up": "editor::MoveUp",
+ "ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
+ "ctrl-down": "editor::MoveToEndOfParagraph",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"end": "editor::MoveToEndOfLine",
@@ -103,6 +105,8 @@
"alt-shift-b": "editor::SelectToPreviousWordStart",
"alt-shift-right": "editor::SelectToNextWordEnd",
"alt-shift-f": "editor::SelectToNextWordEnd",
+ "ctrl-shift-up": "editor::SelectToStartOfParagraph",
+ "ctrl-shift-down": "editor::SelectToEndOfParagraph",
"cmd-shift-up": "editor::SelectToBeginning",
"cmd-shift-down": "editor::SelectToEnd",
"cmd-a": "editor::SelectAll",
@@ -216,6 +216,8 @@ actions!(
MoveToNextSubwordEnd,
MoveToBeginningOfLine,
MoveToEndOfLine,
+ MoveToStartOfParagraph,
+ MoveToEndOfParagraph,
MoveToBeginning,
MoveToEnd,
SelectUp,
@@ -226,6 +228,8 @@ actions!(
SelectToPreviousSubwordStart,
SelectToNextWordEnd,
SelectToNextSubwordEnd,
+ SelectToStartOfParagraph,
+ SelectToEndOfParagraph,
SelectToBeginning,
SelectToEnd,
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_beginning_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_end);
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_beginning_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_end);
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>) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
@@ -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]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -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
/// 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