diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 01a09e0cba..cab4333f74 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -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", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 12f5b3b872..814435059e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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) { if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9a21429301..180de155e9 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, |_| {}); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6c9bd6cb4f..523a0af964 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -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