diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d94adb95a3..88ae9ae0ed 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -511,7 +511,8 @@ "}": "vim::CurlyBrackets", "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", - ">": "vim::AngleBrackets" + ">": "vim::AngleBrackets", + "a": "vim::Argument" } }, { diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 787be1999e..1d0b304913 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/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::( vec![ diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index be66e64cd9..428e4b33f9 100644 --- a/crates/language/src/buffer.rs +++ b/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(&self, position: D) -> Option { + pub fn syntax_layer_at(&self, position: D) -> Option { 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( + &self, + range: Range, + ) -> impl Iterator, Range)> + '_ { + 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( + &self, + range: Range, + range_filter: Option<&dyn Fn(Range, Range) -> bool>, + ) -> Option<(Range, Range)> { + 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, Range)> = 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. diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f6aaded23e..eedced9c2a 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/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( &self, range: Range, + range_filter: Option<&dyn Fn(Range, Range) -> bool>, ) -> Option<(Range, Range)> { let range = range.start.to_offset(self)..range.end.to_offset(self); + let excerpt = self.excerpt_containing(range.clone())?; - // Get the ranges of the innermost pair of brackets. - let mut result: Option<(Range, Range)> = None; - - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { - return None; + // Filter to ranges contained in the excerpt + let range_filter = |open: Range, close: Range| -> 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, ) -> Option, Range)> + '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, ) -> Option, Range)> + '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(&self, range: Range) -> Option> { 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> { @@ -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, - ) -> Option<(&'a Excerpt, usize)> { + pub fn excerpt_containing(&self, range: Range) -> Option { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut cursor = self.excerpts.cursor::(); 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(); + 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; - } - - 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) -> Range { + 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) -> Range { + 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) -> bool { + range.start >= self.excerpt.buffer_start_offset() + && range.end <= self.excerpt.buffer_end_offset() + } } impl ExcerptId { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index a3106e923e..f7718cd1b7 100644 --- a/crates/vim/src/object.rs +++ b/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.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> { + 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> { + // Seek to the first non-whitespace character + offset += buffer + .chars_at(offset) + .take_while(|c| c.is_whitespace()) + .map(char::len_utf8) + .sum::(); + + let bracket_filter = |open: Range, close: Range| { + // 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() {}", 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;