Add surround aliases (#20104)

Mike Lloyd created

Closes #19417

Release Notes:

- vim : Added `r` and `a` as aliases for `[` and `<` text objects
(copying vim-surround).
- vim: (breaking change) rebound the function argument text object to
`g`.
- vim: Fixed surrounds to allow `b`/`B`/`r`/`a` anywhere you could use
`(`, `{`, `[`, `<`.

---


- vim: Added `b`, `B`, `r`, `s`, `a` as aliases for `()`, `{}`, `[]`,
`<>` in vim surround mode.
- Adds a new `surround_alias` function where aliases are defined.
- This function is used in `find_surround_pairs` to substitute the
chosen text with the alias
- The keymap is also modified to add support for Square and Angle
brackets when changing surrounds. These two were added to follow the
example of Tim Pope's ubiquitous `vim-surround` plugin.
- I had to overwrite the `vim::Argument` keybind in order to do this. I
moved it to use the `g` modifier. I realize this is a breaking change
and will happily move the `vim::AngleBracket` keymap to a different
letter if you'd like to avoid this. I'm just trying to keep with
convention. Ideally, Users would be able to define surround aliases
themselves in the config file but that's a much bigger task than I'm
able to do right now.
- I also added tests for the new aliases.

Thanks for making such a clean and organized codebase. I was able to
find the relevant section of code rather quickly thanks to this.

Change summary

assets/keymaps/vim.json     |   4 
crates/vim/src/object.rs    |  18 +-
crates/vim/src/surrounds.rs | 250 ++++++++++++++++++++++++++++++++++++++
3 files changed, 259 insertions(+), 13 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -364,12 +364,14 @@
       "b": "vim::Parentheses",
       "[": "vim::SquareBrackets",
       "]": "vim::SquareBrackets",
+      "r": "vim::SquareBrackets",
       "{": "vim::CurlyBrackets",
       "}": "vim::CurlyBrackets",
       "shift-b": "vim::CurlyBrackets",
       "<": "vim::AngleBrackets",
       ">": "vim::AngleBrackets",
-      "a": "vim::Argument"
+      "a": "vim::AngleBrackets",
+      "g": "vim::Argument"
     }
   },
   {

crates/vim/src/object.rs 🔗

@@ -1402,7 +1402,7 @@ mod test {
 
         // Generic arguments
         cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
-        cx.simulate_keystrokes("v i a");
+        cx.simulate_keystrokes("v i g");
         cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
 
         // Function arguments
@@ -1410,11 +1410,11 @@ mod test {
             "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
             Mode::Normal,
         );
-        cx.simulate_keystrokes("d a a");
+        cx.simulate_keystrokes("d a g");
         cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
 
         cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
-        cx.simulate_keystrokes("v a a");
+        cx.simulate_keystrokes("v a g");
         cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
 
         // Tuple, vec, and array arguments
@@ -1422,34 +1422,34 @@ mod test {
             "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
             Mode::Normal,
         );
-        cx.simulate_keystrokes("c i a");
+        cx.simulate_keystrokes("c i g");
         cx.assert_state(
             "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
             Mode::Insert,
         );
 
         cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
-        cx.simulate_keystrokes("c a a");
+        cx.simulate_keystrokes("c a g");
         cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
 
         cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
-        cx.simulate_keystrokes("c i a");
+        cx.simulate_keystrokes("c i g");
         cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
 
         cx.set_state(
             "let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
             Mode::Normal,
         );
-        cx.simulate_keystrokes("c a a");
+        cx.simulate_keystrokes("c a g");
         cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
 
         // Cursor immediately before / after brackets
         cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
-        cx.simulate_keystrokes("v i a");
+        cx.simulate_keystrokes("v i g");
         cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
 
         cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
-        cx.simulate_keystrokes("v i a");
+        cx.simulate_keystrokes("v i g");
         cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
     }
 

crates/vim/src/surrounds.rs 🔗

@@ -52,7 +52,7 @@ impl Vim {
                         newline: false,
                     },
                 };
-                let surround = pair.end != *text;
+                let surround = pair.end != surround_alias((*text).as_ref());
                 let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
                 let mut edits = Vec::new();
                 let mut anchors = Vec::new();
@@ -245,7 +245,7 @@ impl Vim {
                             newline: false,
                         },
                     };
-                    let surround = pair.end != *text;
+                    let surround = pair.end != surround_alias((*text).as_ref());
                     let (display_map, selections) = editor.selections.all_adjusted_display(cx);
                     let mut edits = Vec::new();
                     let mut anchors = Vec::new();
@@ -393,7 +393,19 @@ impl Vim {
 }
 
 fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
-    pairs.iter().find(|pair| pair.start == ch || pair.end == ch)
+    pairs
+        .iter()
+        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
+}
+
+fn surround_alias(ch: &str) -> &str {
+    match ch {
+        "b" => ")",
+        "B" => "}",
+        "a" => ">",
+        "r" => "]",
+        _ => ch,
+    }
 }
 
 fn all_support_surround_pair() -> Vec<BracketPair> {
@@ -1171,4 +1183,236 @@ mod test {
             Mode::Normal,
         );
     }
+
+    #[gpui::test]
+    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // add aliases
+        cx.set_state(
+            indoc! {"
+            The quˇick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("y s i w b");
+        cx.assert_state(
+            indoc! {"
+            The ˇ(quick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The quˇick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("y s i w B");
+        cx.assert_state(
+            indoc! {"
+            The ˇ{quick} brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The quˇick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("y s i w a");
+        cx.assert_state(
+            indoc! {"
+            The ˇ<quick> brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The quˇick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("y s i w r");
+        cx.assert_state(
+            indoc! {"
+            The ˇ[quick] brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        // change aliases
+        cx.set_state(
+            indoc! {"
+            The {quˇick} brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s { b");
+        cx.assert_state(
+            indoc! {"
+            The ˇ(quick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The (quˇick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s ( B");
+        cx.assert_state(
+            indoc! {"
+            The ˇ{quick} brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The (quˇick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s ( a");
+        cx.assert_state(
+            indoc! {"
+            The ˇ<quick> brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The <quˇick> brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s < b");
+        cx.assert_state(
+            indoc! {"
+            The ˇ(quick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The (quˇick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s ( r");
+        cx.assert_state(
+            indoc! {"
+            The ˇ[quick] brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The [quˇick] brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s [ b");
+        cx.assert_state(
+            indoc! {"
+            The ˇ(quick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        // delete alias
+        cx.set_state(
+            indoc! {"
+            The {quˇick} brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("d s B");
+        cx.assert_state(
+            indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The (quˇick) brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("d s b");
+        cx.assert_state(
+            indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The [quˇick] brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("d s r");
+        cx.assert_state(
+            indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+
+        cx.set_state(
+            indoc! {"
+            The <quˇick> brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("d s a");
+        cx.assert_state(
+            indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Normal,
+        );
+    }
 }