Add option to advance cursor downward when toggling comment

Joseph Lyons and Julia created

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>

Change summary

assets/keymaps/default.json       |   7 +
crates/editor/src/editor.rs       |  40 ++++++++
crates/editor/src/editor_tests.rs | 149 +++++++++++++++++++++++++++++++-
crates/zed/src/menus.rs           |   2 
4 files changed, 185 insertions(+), 13 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -228,7 +228,12 @@
                     "replace_newest": true
                 }
             ],
-            "cmd-/": "editor::ToggleComments",
+            "cmd-/": [
+                "editor::ToggleComments",
+                {
+                    "advance_downwards": false
+                }
+            ],
             "alt-up": "editor::SelectLargerSyntaxNode",
             "alt-down": "editor::SelectSmallerSyntaxNode",
             "cmd-u": "editor::UndoSelection",

crates/editor/src/editor.rs 🔗

@@ -154,6 +154,12 @@ pub struct ConfirmCodeAction {
     pub item_ix: Option<usize>,
 }
 
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ToggleComments {
+    #[serde(default)]
+    pub advance_downwards: bool,
+}
+
 actions!(
     editor,
     [
@@ -216,7 +222,6 @@ actions!(
         AddSelectionBelow,
         Tab,
         TabPrev,
-        ToggleComments,
         ShowCharacterPalette,
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
@@ -251,6 +256,7 @@ impl_actions!(
         MovePageDown,
         ConfirmCompletion,
         ConfirmCodeAction,
+        ToggleComments,
     ]
 );
 
@@ -3804,7 +3810,7 @@ impl Editor {
             }
         }
 
-        if matches!(self.mode, EditorMode::SingleLine) {
+        if self.mode == EditorMode::SingleLine {
             cx.propagate_action();
             return;
         }
@@ -4466,7 +4472,7 @@ impl Editor {
         }
     }
 
-    pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
+    pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             let mut selections = this.selections.all::<Point>(cx);
             let mut edits = Vec::new();
@@ -4685,6 +4691,34 @@ impl Editor {
 
             drop(snapshot);
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+
+            let selections = this.selections.all::<Point>(cx);
+            let selections_on_single_row = selections.windows(2).all(|selections| {
+                selections[0].start.row == selections[1].start.row
+                    && selections[0].end.row == selections[1].end.row
+                    && selections[0].start.row == selections[0].end.row
+            });
+            let selections_selecting = selections
+                .iter()
+                .any(|selection| selection.start != selection.end);
+            let advance_downwards = action.advance_downwards
+                && selections_on_single_row
+                && !selections_selecting
+                && this.mode != EditorMode::SingleLine;
+
+            if advance_downwards {
+                let snapshot = this.buffer.read(cx).snapshot(cx);
+
+                this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_cursors_with(|display_snapshot, display_point, _| {
+                        let mut point = display_point.to_point(display_snapshot);
+                        point.row += 1;
+                        point = snapshot.clip_point(point, Bias::Left);
+                        let display_point = point.to_display_point(display_snapshot);
+                        (display_point, SelectionGoal::Column(display_point.column()))
+                    })
+                });
+            }
         });
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -4451,7 +4451,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
                 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6),
             ])
         });
-        editor.toggle_comments(&ToggleComments, cx);
+        editor.toggle_comments(&ToggleComments::default(), cx);
         assert_eq!(
             editor.text(cx),
             "
@@ -4469,7 +4469,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
         editor.change_selections(None, cx, |s| {
             s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)])
         });
-        editor.toggle_comments(&ToggleComments, cx);
+        editor.toggle_comments(&ToggleComments::default(), cx);
         assert_eq!(
             editor.text(cx),
             "
@@ -4486,7 +4486,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
         editor.change_selections(None, cx, |s| {
             s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)])
         });
-        editor.toggle_comments(&ToggleComments, cx);
+        editor.toggle_comments(&ToggleComments::default(), cx);
         assert_eq!(
             editor.text(cx),
             "
@@ -4501,6 +4501,139 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
+    let mut cx = EditorTestContext::new(cx);
+    cx.update(|cx| cx.set_global(Settings::test(cx)));
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(language), cx);
+    });
+
+    let toggle_comments = &ToggleComments {
+        advance_downwards: true,
+    };
+
+    // Single cursor on one line -> advance
+    // Cursor moves horizontally 3 characters as well on non-blank line
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+             catˇ();
+        }"
+    ));
+
+    // Single selection on one line -> don't advance
+    cx.set_state(indoc!(
+        "fn a() {
+             «dog()ˇ»;
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // «dog()ˇ»;
+             cat();
+        }"
+    ));
+
+    // Multiple cursors on one line -> advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdˇog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+             catˇ(ˇ);
+        }"
+    ));
+
+    // Multiple cursors on one line, with selection -> don't advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdˇog«()ˇ»;
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // ˇdˇog«()ˇ»;
+             cat();
+        }"
+    ));
+
+    // Single cursor on one line -> advance
+    // Cursor moves to column 0 on blank line
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdog();
+
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+        ˇ
+             cat();
+        }"
+    ));
+
+    // Single cursor on one line -> advance
+    // Cursor starts and ends at column 0
+    cx.set_state(indoc!(
+        "fn a() {
+         ˇ    dog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+         ˇ    cat();
+        }"
+    ));
+}
+
 #[gpui::test]
 async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
     let mut cx = EditorTestContext::new(cx);
@@ -4551,7 +4684,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         "#
         .unindent(),
     );
-    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
     cx.assert_editor_state(
         &r#"
             <!-- <p>A</p>ˇ -->
@@ -4560,7 +4693,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         "#
         .unindent(),
     );
-    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
     cx.assert_editor_state(
         &r#"
             <p>A</p>ˇ
@@ -4582,7 +4715,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         .unindent(),
     );
 
-    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
     cx.assert_editor_state(
         &r#"
             <!-- <p>A«</p>
@@ -4592,7 +4725,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         "#
         .unindent(),
     );
-    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
     cx.assert_editor_state(
         &r#"
             <p>A«</p>
@@ -4614,7 +4747,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
         .unindent(),
     );
     cx.foreground().run_until_parked();
-    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
     cx.assert_editor_state(
         &r#"
             <!-- ˇ<script> -->

crates/zed/src/menus.rs 🔗

@@ -146,7 +146,7 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Toggle Line Comment",
-                    action: Box::new(editor::ToggleComments),
+                    action: Box::new(editor::ToggleComments::default()),
                 },
                 MenuItem::Action {
                     name: "Emoji & Symbols",