Add new `argument` vim text object (#7791)

vultix created

This PR adds a new `argument` vim text object, inspired by
[targets.vim](https://github.com/wellle/targets.vim).

As it's the first vim text object to use the syntax tree, it needed to
operate on the `Buffer` level, not the `MultiBuffer` level, then map the
buffer coordinates to `DisplayPoint` as necessary.

This required two main changes:
1. `innermost_enclosing_bracket_ranges` and `enclosing_bracket_ranges`
were moved into `Buffer`. The `MultiBuffer` implementations were updated
to map to/from these.
2. `MultiBuffer::excerpt_containing` was made public, returning a new
`MultiBufferExcerpt` type that contains a reference to the excerpt and
methods for mapping to/from `Buffer` and `MultiBuffer` offsets and
ranges.

Release Notes:
- Added new `argument` vim text object, inspired by
[targets.vim](https://github.com/wellle/targets.vim).

Change summary

assets/keymaps/vim.json                         |   3 
crates/editor/src/highlight_matching_bracket.rs |   2 
crates/language/src/buffer.rs                   |  48 +++
crates/multi_buffer/src/multi_buffer.rs         | 209 ++++++++++-------
crates/vim/src/object.rs                        | 225 ++++++++++++++++++
5 files changed, 393 insertions(+), 94 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -511,7 +511,8 @@
       "}": "vim::CurlyBrackets",
       "shift-b": "vim::CurlyBrackets",
       "<": "vim::AngleBrackets",
-      ">": "vim::AngleBrackets"
+      ">": "vim::AngleBrackets",
+      "a": "vim::Argument"
     }
   },
   {

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
     let snapshot = editor.snapshot(cx);
     if let Some((opening_range, closing_range)) = snapshot
         .buffer_snapshot
-        .innermost_enclosing_bracket_ranges(head..head)
+        .innermost_enclosing_bracket_ranges(head..head, None)
     {
         editor.highlight_background::<MatchingBracketHighlight>(
             vec![

crates/language/src/buffer.rs 🔗

@@ -2492,7 +2492,7 @@ impl BufferSnapshot {
         self.syntax.layers_for_range(0..self.len(), &self.text)
     }
 
-    fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer> {
+    pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer> {
         let offset = position.to_offset(self);
         self.syntax
             .layers_for_range(offset..offset, &self.text)
@@ -2886,6 +2886,52 @@ impl BufferSnapshot {
         })
     }
 
+    /// Returns enclosing bracket ranges containing the given range
+    pub fn enclosing_bracket_ranges<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + '_ {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+
+        self.bracket_ranges(range.clone())
+            .filter(move |(open, close)| open.start <= range.start && close.end >= range.end)
+    }
+
+    /// Returns the smallest enclosing bracket ranges containing the given range or None if no brackets contain range
+    ///
+    /// Can optionally pass a range_filter to filter the ranges of brackets to consider
+    pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
+        &self,
+        range: Range<T>,
+        range_filter: Option<&dyn Fn(Range<usize>, Range<usize>) -> bool>,
+    ) -> Option<(Range<usize>, Range<usize>)> {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+
+        // Get the ranges of the innermost pair of brackets.
+        let mut result: Option<(Range<usize>, Range<usize>)> = None;
+
+        for (open, close) in self.enclosing_bracket_ranges(range.clone()) {
+            if let Some(range_filter) = range_filter {
+                if !range_filter(open.clone(), close.clone()) {
+                    continue;
+                }
+            }
+
+            let len = close.end - open.start;
+
+            if let Some((existing_open, existing_close)) = &result {
+                let existing_len = existing_close.end - existing_open.start;
+                if len > existing_len {
+                    continue;
+                }
+            }
+
+            result = Some((open, close));
+        }
+
+        result
+    }
+
     /// Returns anchor ranges for any matches of the redaction query.
     /// The buffer can be associated with multiple languages, and the redaction query associated with each
     /// will be run on the relevant section of the buffer.

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -191,6 +191,16 @@ struct Excerpt {
     has_trailing_newline: bool,
 }
 
+/// A public view into an [`Excerpt`] in a [`MultiBuffer`].
+///
+/// Contains methods for getting the [`Buffer`] of the excerpt,
+/// as well as mapping offsets to/from buffer and multibuffer coordinates.
+#[derive(Copy, Clone)]
+pub struct MultiBufferExcerpt<'a> {
+    excerpt: &'a Excerpt,
+    excerpt_offset: usize,
+}
+
 #[derive(Clone, Debug)]
 struct ExcerptIdMapping {
     id: ExcerptId,
@@ -2912,33 +2922,36 @@ impl MultiBufferSnapshot {
     /// Returns the smallest enclosing bracket ranges containing the given range or
     /// None if no brackets contain range or the range is not contained in a single
     /// excerpt
+    ///
+    /// Can optionally pass a range_filter to filter the ranges of brackets to consider
     pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
         &self,
         range: Range<T>,
+        range_filter: Option<&dyn Fn(Range<usize>, Range<usize>) -> bool>,
     ) -> Option<(Range<usize>, Range<usize>)> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
-
-        // Get the ranges of the innermost pair of brackets.
-        let mut result: Option<(Range<usize>, Range<usize>)> = None;
-
-        let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else {
-            return None;
+        let excerpt = self.excerpt_containing(range.clone())?;
+
+        // Filter to ranges contained in the excerpt
+        let range_filter = |open: Range<usize>, close: Range<usize>| -> bool {
+            excerpt.contains_buffer_range(open.start..close.end)
+                && range_filter.map_or(true, |filter| {
+                    filter(
+                        excerpt.map_range_from_buffer(open),
+                        excerpt.map_range_from_buffer(close),
+                    )
+                })
         };
 
-        for (open, close) in enclosing_bracket_ranges {
-            let len = close.end - open.start;
+        let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges(
+            excerpt.map_range_to_buffer(range),
+            Some(&range_filter),
+        )?;
 
-            if let Some((existing_open, existing_close)) = &result {
-                let existing_len = existing_close.end - existing_open.start;
-                if len > existing_len {
-                    continue;
-                }
-            }
-
-            result = Some((open, close));
-        }
-
-        result
+        Some((
+            excerpt.map_range_from_buffer(open),
+            excerpt.map_range_from_buffer(close),
+        ))
     }
 
     /// Returns enclosing bracket ranges containing the given range or returns None if the range is
@@ -2948,11 +2961,14 @@ impl MultiBufferSnapshot {
         range: Range<T>,
     ) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let excerpt = self.excerpt_containing(range.clone())?;
 
-        self.bracket_ranges(range.clone()).map(|range_pairs| {
-            range_pairs
-                .filter(move |(open, close)| open.start <= range.start && close.end >= range.end)
-        })
+        Some(
+            excerpt
+                .buffer()
+                .enclosing_bracket_ranges(excerpt.map_range_to_buffer(range))
+                .filter(move |(open, close)| excerpt.contains_buffer_range(open.start..close.end)),
+        )
     }
 
     /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is
@@ -2962,38 +2978,24 @@ impl MultiBufferSnapshot {
         range: Range<T>,
     ) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
-        let excerpt = self.excerpt_containing(range.clone());
-        excerpt.map(|(excerpt, excerpt_offset)| {
-            let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
-            let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
-
-            let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
-            let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
+        let excerpt = self.excerpt_containing(range.clone())?;
 
+        Some(
             excerpt
-                .buffer
-                .bracket_ranges(start_in_buffer..end_in_buffer)
-                .filter_map(move |(start_bracket_range, end_bracket_range)| {
-                    if start_bracket_range.start < excerpt_buffer_start
-                        || end_bracket_range.end > excerpt_buffer_end
-                    {
-                        return None;
+                .buffer()
+                .bracket_ranges(excerpt.map_range_to_buffer(range))
+                .filter_map(move |(start_bracket_range, close_bracket_range)| {
+                    let buffer_range = start_bracket_range.start..close_bracket_range.end;
+                    if excerpt.contains_buffer_range(buffer_range) {
+                        Some((
+                            excerpt.map_range_from_buffer(start_bracket_range),
+                            excerpt.map_range_from_buffer(close_bracket_range),
+                        ))
+                    } else {
+                        None
                     }
-
-                    let mut start_bracket_range = start_bracket_range.clone();
-                    start_bracket_range.start =
-                        excerpt_offset + (start_bracket_range.start - excerpt_buffer_start);
-                    start_bracket_range.end =
-                        excerpt_offset + (start_bracket_range.end - excerpt_buffer_start);
-
-                    let mut end_bracket_range = end_bracket_range.clone();
-                    end_bracket_range.start =
-                        excerpt_offset + (end_bracket_range.start - excerpt_buffer_start);
-                    end_bracket_range.end =
-                        excerpt_offset + (end_bracket_range.end - excerpt_buffer_start);
-                    Some((start_bracket_range, end_bracket_range))
-                })
-        })
+                }),
+        )
     }
 
     pub fn redacted_ranges<'a, T: ToOffset>(
@@ -3260,26 +3262,13 @@ impl MultiBufferSnapshot {
 
     pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let excerpt = self.excerpt_containing(range.clone())?;
 
-        self.excerpt_containing(range.clone())
-            .and_then(|(excerpt, excerpt_offset)| {
-                let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
-                let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
+        let ancestor_buffer_range = excerpt
+            .buffer()
+            .range_for_syntax_ancestor(excerpt.map_range_to_buffer(range))?;
 
-                let start_in_buffer =
-                    excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
-                let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
-                let mut ancestor_buffer_range = excerpt
-                    .buffer
-                    .range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
-                ancestor_buffer_range.start =
-                    cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
-                ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end);
-
-                let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start);
-                let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start);
-                Some(start..end)
-            })
+        Some(excerpt.map_range_from_buffer(ancestor_buffer_range))
     }
 
     pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
@@ -3366,32 +3355,25 @@ impl MultiBufferSnapshot {
     }
 
     /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts
-    fn excerpt_containing<'a, T: ToOffset>(
-        &'a self,
-        range: Range<T>,
-    ) -> Option<(&'a Excerpt, usize)> {
+    pub fn excerpt_containing<T: ToOffset>(&self, range: Range<T>) -> Option<MultiBufferExcerpt> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
 
         let mut cursor = self.excerpts.cursor::<usize>();
         cursor.seek(&range.start, Bias::Right, &());
-        let start_excerpt = cursor.item();
+        let start_excerpt = cursor.item()?;
 
         if range.start == range.end {
-            return start_excerpt.map(|excerpt| (excerpt, *cursor.start()));
+            return Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start()));
         }
 
         cursor.seek(&range.end, Bias::Right, &());
-        let end_excerpt = cursor.item();
-
-        start_excerpt
-            .zip(end_excerpt)
-            .and_then(|(start_excerpt, end_excerpt)| {
-                if start_excerpt.id != end_excerpt.id {
-                    return None;
-                }
+        let end_excerpt = cursor.item()?;
 
-                Some((start_excerpt, *cursor.start()))
-            })
+        if start_excerpt.id != end_excerpt.id {
+            None
+        } else {
+            Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start()))
+        }
     }
 
     pub fn remote_selections_in_range<'a>(
@@ -3768,6 +3750,61 @@ impl Excerpt {
                 .cmp(&anchor.text_anchor, &self.buffer)
                 .is_ge()
     }
+
+    /// The [`Excerpt`]'s start offset in its [`Buffer`]
+    fn buffer_start_offset(&self) -> usize {
+        self.range.context.start.to_offset(&self.buffer)
+    }
+
+    /// The [`Excerpt`]'s end offset in its [`Buffer`]
+    fn buffer_end_offset(&self) -> usize {
+        self.buffer_start_offset() + self.text_summary.len
+    }
+}
+
+impl<'a> MultiBufferExcerpt<'a> {
+    fn new(excerpt: &'a Excerpt, excerpt_offset: usize) -> Self {
+        MultiBufferExcerpt {
+            excerpt,
+            excerpt_offset,
+        }
+    }
+
+    pub fn buffer(&self) -> &'a BufferSnapshot {
+        &self.excerpt.buffer
+    }
+
+    /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`]
+    pub fn map_offset_to_buffer(&self, offset: usize) -> usize {
+        self.excerpt.buffer_start_offset() + offset.saturating_sub(self.excerpt_offset)
+    }
+
+    /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`]
+    pub fn map_range_to_buffer(&self, range: Range<usize>) -> Range<usize> {
+        self.map_offset_to_buffer(range.start)..self.map_offset_to_buffer(range.end)
+    }
+
+    /// Map an offset within the [`Buffer`] to an offset within the [`MultiBuffer`]
+    pub fn map_offset_from_buffer(&self, buffer_offset: usize) -> usize {
+        let mut buffer_offset_in_excerpt =
+            buffer_offset.saturating_sub(self.excerpt.buffer_start_offset());
+        buffer_offset_in_excerpt =
+            cmp::min(buffer_offset_in_excerpt, self.excerpt.text_summary.len);
+
+        self.excerpt_offset + buffer_offset_in_excerpt
+    }
+
+    /// Map a range within the [`Buffer`] to a range within the [`MultiBuffer`]
+    pub fn map_range_from_buffer(&self, buffer_range: Range<usize>) -> Range<usize> {
+        self.map_offset_from_buffer(buffer_range.start)
+            ..self.map_offset_from_buffer(buffer_range.end)
+    }
+
+    /// Returns true if the entirety of the given range is in the buffer's excerpt
+    pub fn contains_buffer_range(&self, range: Range<usize>) -> bool {
+        range.start >= self.excerpt.buffer_start_offset()
+            && range.end <= self.excerpt.buffer_end_offset()
+    }
 }
 
 impl ExcerptId {

crates/vim/src/object.rs 🔗

@@ -6,7 +6,7 @@ use editor::{
     Bias, DisplayPoint,
 };
 use gpui::{actions, impl_actions, ViewContext, WindowContext};
-use language::{char_kind, CharKind, Selection};
+use language::{char_kind, BufferSnapshot, CharKind, Selection};
 use serde::Deserialize;
 use workspace::Workspace;
 
@@ -27,6 +27,7 @@ pub enum Object {
     SquareBrackets,
     CurlyBrackets,
     AngleBrackets,
+    Argument,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -49,7 +50,8 @@ actions!(
         Parentheses,
         SquareBrackets,
         CurlyBrackets,
-        AngleBrackets
+        AngleBrackets,
+        Argument
     ]
 );
 
@@ -82,6 +84,8 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
     workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
         object(Object::VerticalBars, cx)
     });
+    workspace
+        .register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx));
 }
 
 fn object(object: Object, cx: &mut WindowContext) {
@@ -106,13 +110,14 @@ impl Object {
             | Object::Parentheses
             | Object::AngleBrackets
             | Object::CurlyBrackets
-            | Object::SquareBrackets => true,
+            | Object::SquareBrackets
+            | Object::Argument => true,
         }
     }
 
     pub fn always_expands_both_ways(self) -> bool {
         match self {
-            Object::Word { .. } | Object::Sentence => false,
+            Object::Word { .. } | Object::Sentence | Object::Argument => false,
             Object::Quotes
             | Object::BackQuotes
             | Object::DoubleQuotes
@@ -136,7 +141,8 @@ impl Object {
             | Object::Parentheses
             | Object::SquareBrackets
             | Object::CurlyBrackets
-            | Object::AngleBrackets => Mode::Visual,
+            | Object::AngleBrackets
+            | Object::Argument => Mode::Visual,
         }
     }
 
@@ -179,6 +185,7 @@ impl Object {
             Object::AngleBrackets => {
                 surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
             }
+            Object::Argument => argument(map, relative_to, around),
         }
     }
 
@@ -308,6 +315,157 @@ fn around_next_word(
     Some(start..end)
 }
 
+fn argument(
+    map: &DisplaySnapshot,
+    relative_to: DisplayPoint,
+    around: bool,
+) -> Option<Range<DisplayPoint>> {
+    let snapshot = &map.buffer_snapshot;
+    let offset = relative_to.to_offset(map, Bias::Left);
+
+    // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
+    let excerpt = snapshot.excerpt_containing(offset..offset)?;
+    let buffer = excerpt.buffer();
+
+    fn comma_delimited_range_at(
+        buffer: &BufferSnapshot,
+        mut offset: usize,
+        include_comma: bool,
+    ) -> Option<Range<usize>> {
+        // Seek to the first non-whitespace character
+        offset += buffer
+            .chars_at(offset)
+            .take_while(|c| c.is_whitespace())
+            .map(char::len_utf8)
+            .sum::<usize>();
+
+        let bracket_filter = |open: Range<usize>, close: Range<usize>| {
+            // Filter out empty ranges
+            if open.end == close.start {
+                return false;
+            }
+
+            // If the cursor is outside the brackets, ignore them
+            if open.start == offset || close.end == offset {
+                return false;
+            }
+
+            // TODO: Is there any better way to filter out string brackets?
+            // Used to filter out string brackets
+            return matches!(
+                buffer.chars_at(open.start).next(),
+                Some('(' | '[' | '{' | '<' | '|')
+            );
+        };
+
+        // Find the brackets containing the cursor
+        let (open_bracket, close_bracket) =
+            buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
+
+        let inner_bracket_range = open_bracket.end..close_bracket.start;
+
+        let layer = buffer.syntax_layer_at(offset)?;
+        let node = layer.node();
+        let mut cursor = node.walk();
+
+        // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list
+        let mut parent_covers_bracket_range = false;
+        loop {
+            let node = cursor.node();
+            let range = node.byte_range();
+            let covers_bracket_range =
+                range.start == open_bracket.start && range.end == close_bracket.end;
+            if parent_covers_bracket_range && !covers_bracket_range {
+                break;
+            }
+            parent_covers_bracket_range = covers_bracket_range;
+
+            // Unable to find a child node with a parent that covers the bracket range, so no argument to select
+            if !cursor.goto_first_child_for_byte(offset).is_some() {
+                return None;
+            }
+        }
+
+        let mut argument_node = cursor.node();
+
+        // If the child node is the open bracket, move to the next sibling.
+        if argument_node.byte_range() == open_bracket {
+            if !cursor.goto_next_sibling() {
+                return Some(inner_bracket_range);
+            }
+            argument_node = cursor.node();
+        }
+        // While the child node is the close bracket or a comma, move to the previous sibling
+        while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
+            if !cursor.goto_previous_sibling() {
+                return Some(inner_bracket_range);
+            }
+            argument_node = cursor.node();
+            if argument_node.byte_range() == open_bracket {
+                return Some(inner_bracket_range);
+            }
+        }
+
+        // The start and end of the argument range, defaulting to the start and end of the argument node
+        let mut start = argument_node.start_byte();
+        let mut end = argument_node.end_byte();
+
+        let mut needs_surrounding_comma = include_comma;
+
+        // Seek backwards to find the start of the argument - either the previous comma or the opening bracket.
+        // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]`
+        while cursor.goto_previous_sibling() {
+            let prev = cursor.node();
+
+            if prev.start_byte() < open_bracket.end {
+                start = open_bracket.end;
+                break;
+            } else if prev.kind() == "," {
+                if needs_surrounding_comma {
+                    start = prev.start_byte();
+                    needs_surrounding_comma = false;
+                }
+                break;
+            } else if prev.start_byte() < start {
+                start = prev.start_byte();
+            }
+        }
+
+        // Do the same for the end of the argument, extending to next comma or the end of the argument list
+        while cursor.goto_next_sibling() {
+            let next = cursor.node();
+
+            if next.end_byte() > close_bracket.start {
+                end = close_bracket.start;
+                break;
+            } else if next.kind() == "," {
+                if needs_surrounding_comma {
+                    // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma
+                    if let Some(next_arg) = next.next_sibling() {
+                        end = next_arg.start_byte();
+                    } else {
+                        end = next.end_byte();
+                    }
+                }
+                break;
+            } else if next.end_byte() > end {
+                end = next.end_byte();
+            }
+        }
+
+        Some(start..end)
+    }
+
+    let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
+
+    if excerpt.contains_buffer_range(result.clone()) {
+        let result = excerpt.map_range_from_buffer(result);
+        Some(result.start.to_display_point(map)..result.end.to_display_point(map))
+    } else {
+        None
+    }
+}
+
 fn sentence(
     map: &DisplaySnapshot,
     relative_to: DisplayPoint,
@@ -1007,6 +1165,63 @@ mod test {
         );
     }
 
+    #[gpui::test]
+    async fn test_argument_object(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // Generic arguments
+        cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
+        cx.simulate_keystrokes(["v", "i", "a"]);
+        cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
+
+        // Function arguments
+        cx.set_state(
+            "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["d", "a", "a"]);
+        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.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
+
+        // Tuple, vec, and array arguments
+        cx.set_state(
+            "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["c", "i", "a"]);
+        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.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.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.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.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.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
+    }
+
     #[gpui::test]
     async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;