Add new argument vim text object (#7791)

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).
This commit is contained in:
vultix 2024-02-23 20:37:13 -06:00 committed by GitHub
parent dc7e14f888
commit 2e616f8388
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 392 additions and 93 deletions

View file

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

View file

@ -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![

View file

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

View file

@ -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);
let excerpt = self.excerpt_containing(range.clone())?;
// 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;
// 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();
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<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 {

View file

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