editor: Add MoveUpByLines and MoveDownByLines actions (#7208)

Thorsten Ball created

This adds four new actions:

- `editor::MoveUpByLines`
- `editor::MoveDownByLines`
- `editor::SelectUpByLines`
- `editor::SelectDownByLines`

They all take a count by which to move the cursor up and down.
(Requested by Adam here:
https://twitter.com/adamwathan/status/1753017094248018302)


Example `keymap.json` entries:

```json
{
  "context": "Editor",
  "bindings": [
    "alt-up":         [ "editor::MoveUpByLines",     { "lines": 3 } ],
    "alt-down":       [ "editor::MoveDownByLines",   { "lines": 3 } ],
    "alt-shift-up":   [ "editor::SelectUpByLines",   { "lines": 3 } ],
    "alt-shift-down": [ "editor::SelectDownByLines", { "lines": 3 } ]
  ]
}
```

They are *not* bound by default, so as to not conflict with the
`alt-up/down` bindings that already exist.

Release Notes:

- Added four new actions: `editor::MoveUpByLines`,
`editor::MoveDownByLines`, `editor::SelectUpByLines`,
`editor::SelectDownByLines` that can take a line count configuration and
move the cursor up by the count.

### Demo



https://github.com/zed-industries/zed/assets/1185253/e78d4077-5bd5-4d72-a806-67695698af5d




https://github.com/zed-industries/zed/assets/1185253/0b086ec9-eb90-40a2-9009-844a215e6378

Change summary

crates/editor/src/actions.rs | 30 +++++++++++++
crates/editor/src/editor.rs  | 80 ++++++++++++++++++++++++++++++++++++++
crates/editor/src/element.rs |  4 +
3 files changed, 113 insertions(+), 1 deletion(-)

Detailed changes

crates/editor/src/actions.rs 🔗

@@ -70,6 +70,30 @@ pub struct FoldAt {
 pub struct UnfoldAt {
     pub buffer_row: u32,
 }
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct MoveUpByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct MoveDownByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct SelectUpByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct SelectDownByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+
 impl_actions!(
     editor,
     [
@@ -84,7 +108,11 @@ impl_actions!(
         ConfirmCodeAction,
         ToggleComments,
         FoldAt,
-        UnfoldAt
+        UnfoldAt,
+        MoveUpByLines,
+        MoveDownByLines,
+        SelectUpByLines,
+        SelectDownByLines,
     ]
 );
 

crates/editor/src/editor.rs 🔗

@@ -5379,6 +5379,86 @@ impl Editor {
         })
     }
 
+    pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate();
+            return;
+        }
+
+        let text_layout_details = &self.text_layout_details(cx);
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::up_by_rows(
+                    map,
+                    selection.start,
+                    action.lines,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        })
+    }
+
+    pub fn move_down_by_lines(&mut self, action: &MoveDownByLines, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate();
+            return;
+        }
+
+        let text_layout_details = &self.text_layout_details(cx);
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::down_by_rows(
+                    map,
+                    selection.start,
+                    action.lines,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        })
+    }
+
+    pub fn select_down_by_lines(&mut self, action: &SelectDownByLines, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, goal| {
+                movement::down_by_rows(map, head, action.lines, goal, false, &text_layout_details)
+            })
+        })
+    }
+
+    pub fn select_up_by_lines(&mut self, action: &SelectUpByLines, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, goal| {
+                movement::up_by_rows(map, head, action.lines, goal, false, &text_layout_details)
+            })
+        })
+    }
+
     pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
         if self.take_rename(true, cx).is_some() {
             return;

crates/editor/src/element.rs 🔗

@@ -147,7 +147,11 @@ impl EditorElement {
         register_action(view, cx, Editor::move_left);
         register_action(view, cx, Editor::move_right);
         register_action(view, cx, Editor::move_down);
+        register_action(view, cx, Editor::move_down_by_lines);
+        register_action(view, cx, Editor::select_down_by_lines);
         register_action(view, cx, Editor::move_up);
+        register_action(view, cx, Editor::move_up_by_lines);
+        register_action(view, cx, Editor::select_up_by_lines);
         register_action(view, cx, Editor::cancel);
         register_action(view, cx, Editor::newline);
         register_action(view, cx, Editor::newline_above);