mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 20:01:33 +00:00
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:
parent
dc7e14f888
commit
2e616f8388
5 changed files with 392 additions and 93 deletions
|
@ -511,7 +511,8 @@
|
|||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets"
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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![
|
||||
|
|
|
@ -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);
|
||||
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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue