@@ -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.
@@ -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 {
@@ -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;