vim: Add Separator and RemoveIndent in Join Lines, fix gJ use space join (#22496)

0x2CA and Conrad Irwin created

Closes #22492

Release Notes:

- Added Join Lines Separator And RemoveIndent

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

assets/keymaps/vim.json                   |  2 +
crates/editor/src/actions.rs              |  2 
crates/editor/src/editor.rs               | 17 +++++---
crates/vim/src/normal.rs                  | 47 ++++++++++++++----------
crates/vim/src/test.rs                    | 40 +++++++++++++++++++++
crates/vim/test_data/test_join_lines.json | 16 ++++++++
6 files changed, 98 insertions(+), 26 deletions(-)

Detailed changes

assets/keymaps/vim.json đź”—

@@ -197,6 +197,7 @@
       "d": ["vim::PushOperator", "Delete"],
       "shift-d": "vim::DeleteToEndOfLine",
       "shift-j": "vim::JoinLines",
+      "g shift-j": "vim::JoinLinesNoWhitespace",
       "y": ["vim::PushOperator", "Yank"],
       "shift-y": "vim::YankLine",
       "i": "vim::InsertBefore",
@@ -278,6 +279,7 @@
       "g shift-i": "vim::VisualInsertFirstNonWhiteSpace",
       "g shift-a": "vim::VisualInsertEndOfLine",
       "shift-j": "vim::JoinLines",
+      "g shift-j": "vim::JoinLinesNoWhitespace",
       "r": ["vim::PushOperator", "Replace"],
       "ctrl-c": ["vim::SwitchMode", "Normal"],
       "escape": ["vim::SwitchMode", "Normal"],

crates/editor/src/editor.rs đź”—

@@ -5851,7 +5851,7 @@ impl Editor {
         });
     }
 
-    pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
+    pub fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext<Self>) {
         if self.read_only(cx) {
             return;
         }
@@ -5893,11 +5893,12 @@ impl Editor {
                     let indent = snapshot.indent_size_for_line(next_line_row);
                     let start_of_next_line = Point::new(next_line_row.0, indent.len);
 
-                    let replace = if snapshot.line_len(next_line_row) > indent.len {
-                        " "
-                    } else {
-                        ""
-                    };
+                    let replace =
+                        if snapshot.line_len(next_line_row) > indent.len && insert_whitespace {
+                            " "
+                        } else {
+                            ""
+                        };
 
                     this.buffer.update(cx, |buffer, cx| {
                         buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
@@ -5911,6 +5912,10 @@ impl Editor {
         });
     }
 
+    pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
+        self.join_lines_impl(true, cx);
+    }
+
     pub fn sort_lines_case_sensitive(
         &mut self,
         _: &SortLinesCaseSensitive,

crates/vim/src/normal.rs đź”—

@@ -44,6 +44,8 @@ actions!(
         InsertLineAbove,
         InsertLineBelow,
         InsertAtPrevious,
+        JoinLines,
+        JoinLinesNoWhitespace,
         DeleteLeft,
         DeleteRight,
         ChangeToEndOfLine,
@@ -53,7 +55,6 @@ actions!(
         ChangeCase,
         ConvertToUpperCase,
         ConvertToLowerCase,
-        JoinLines,
         ToggleComments,
         Undo,
         Redo,
@@ -108,25 +109,11 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
         );
     });
     Vim::action(editor, cx, |vim, _: &JoinLines, cx| {
-        vim.record_current_action(cx);
-        let mut times = Vim::take_count(cx).unwrap_or(1);
-        if vim.mode.is_visual() {
-            times = 1;
-        } else if times > 1 {
-            // 2J joins two lines together (same as J or 1J)
-            times -= 1;
-        }
+        vim.join_lines_impl(true, cx);
+    });
 
-        vim.update_editor(cx, |_, editor, cx| {
-            editor.transact(cx, |editor, cx| {
-                for _ in 0..times {
-                    editor.join_lines(&Default::default(), cx)
-                }
-            })
-        });
-        if vim.mode.is_visual() {
-            vim.switch_mode(Mode::Normal, true, cx)
-        }
+    Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, cx| {
+        vim.join_lines_impl(false, cx);
     });
 
     Vim::action(editor, cx, |vim, _: &Undo, cx| {
@@ -401,6 +388,28 @@ impl Vim {
         });
     }
 
+    fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext<Self>) {
+        self.record_current_action(cx);
+        let mut times = Vim::take_count(cx).unwrap_or(1);
+        if self.mode.is_visual() {
+            times = 1;
+        } else if times > 1 {
+            // 2J joins two lines together (same as J or 1J)
+            times -= 1;
+        }
+
+        self.update_editor(cx, |_, editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                for _ in 0..times {
+                    editor.join_lines_impl(insert_whitespace, cx)
+                }
+            })
+        });
+        if self.mode.is_visual() {
+            self.switch_mode(Mode::Normal, true, cx)
+        }
+    }
+
     fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext<Self>) {
         let count = Vim::take_count(cx);
         self.yank_motion(motion::Motion::CurrentLine, count, cx)

crates/vim/src/test.rs đź”—

@@ -367,6 +367,46 @@ async fn test_join_lines(cx: &mut gpui::TestAppContext) {
       two three fourˇ five
       six
       "});
+
+    cx.set_shared_state(indoc! {"
+      ˇone
+      two
+      three
+      four
+      five
+      six
+      "})
+        .await;
+    cx.simulate_shared_keystrokes("g shift-j").await;
+    cx.shared_state().await.assert_eq(indoc! {"
+          oneˇtwo
+          three
+          four
+          five
+          six
+          "});
+    cx.simulate_shared_keystrokes("3 g shift-j").await;
+    cx.shared_state().await.assert_eq(indoc! {"
+          onetwothreeˇfour
+          five
+          six
+          "});
+
+    cx.set_shared_state(indoc! {"
+      ˇone
+      two
+      three
+      four
+      five
+      six
+      "})
+        .await;
+    cx.simulate_shared_keystrokes("j v 3 j g shift-j").await;
+    cx.shared_state().await.assert_eq(indoc! {"
+      one
+      twothreefourˇfive
+      six
+      "});
 }
 
 #[cfg(target_os = "macos")]

crates/vim/test_data/test_join_lines.json đź”—

@@ -11,3 +11,19 @@
 {"Key":"j"}
 {"Key":"shift-j"}
 {"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
+{"Key":"g"}
+{"Key":"shift-j"}
+{"Get":{"state":"oneˇtwo\nthree\nfour\nfive\nsix\n","mode":"Normal"}}
+{"Key":"3"}
+{"Key":"g"}
+{"Key":"shift-j"}
+{"Get":{"state":"onetwothreeˇfour\nfive\nsix\n","mode":"Normal"}}
+{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
+{"Key":"j"}
+{"Key":"v"}
+{"Key":"3"}
+{"Key":"j"}
+{"Key":"g"}
+{"Key":"shift-j"}
+{"Get":{"state":"one\ntwothreefourˇfive\nsix\n","mode":"Normal"}}