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",
|
"}": "vim::CurlyBrackets",
|
||||||
"shift-b": "vim::CurlyBrackets",
|
"shift-b": "vim::CurlyBrackets",
|
||||||
"<": "vim::AngleBrackets",
|
"<": "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);
|
let snapshot = editor.snapshot(cx);
|
||||||
if let Some((opening_range, closing_range)) = snapshot
|
if let Some((opening_range, closing_range)) = snapshot
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.innermost_enclosing_bracket_ranges(head..head)
|
.innermost_enclosing_bracket_ranges(head..head, None)
|
||||||
{
|
{
|
||||||
editor.highlight_background::<MatchingBracketHighlight>(
|
editor.highlight_background::<MatchingBracketHighlight>(
|
||||||
vec![
|
vec![
|
||||||
|
|
|
@ -2492,7 +2492,7 @@ impl BufferSnapshot {
|
||||||
self.syntax.layers_for_range(0..self.len(), &self.text)
|
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);
|
let offset = position.to_offset(self);
|
||||||
self.syntax
|
self.syntax
|
||||||
.layers_for_range(offset..offset, &self.text)
|
.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.
|
/// 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
|
/// 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.
|
/// will be run on the relevant section of the buffer.
|
||||||
|
|
|
@ -191,6 +191,16 @@ struct Excerpt {
|
||||||
has_trailing_newline: bool,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
struct ExcerptIdMapping {
|
struct ExcerptIdMapping {
|
||||||
id: ExcerptId,
|
id: ExcerptId,
|
||||||
|
@ -2912,33 +2922,36 @@ impl MultiBufferSnapshot {
|
||||||
/// Returns the smallest enclosing bracket ranges containing the given range or
|
/// 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
|
/// None if no brackets contain range or the range is not contained in a single
|
||||||
/// excerpt
|
/// excerpt
|
||||||
|
///
|
||||||
|
/// Can optionally pass a range_filter to filter the ranges of brackets to consider
|
||||||
pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
|
pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
|
||||||
&self,
|
&self,
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
|
range_filter: Option<&dyn Fn(Range<usize>, Range<usize>) -> bool>,
|
||||||
) -> Option<(Range<usize>, Range<usize>)> {
|
) -> Option<(Range<usize>, Range<usize>)> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
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.
|
// Filter to ranges contained in the excerpt
|
||||||
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
let range_filter = |open: Range<usize>, close: Range<usize>| -> bool {
|
||||||
|
excerpt.contains_buffer_range(open.start..close.end)
|
||||||
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else {
|
&& range_filter.map_or(true, |filter| {
|
||||||
return None;
|
filter(
|
||||||
|
excerpt.map_range_from_buffer(open),
|
||||||
|
excerpt.map_range_from_buffer(close),
|
||||||
|
)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
for (open, close) in enclosing_bracket_ranges {
|
let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges(
|
||||||
let len = close.end - open.start;
|
excerpt.map_range_to_buffer(range),
|
||||||
|
Some(&range_filter),
|
||||||
|
)?;
|
||||||
|
|
||||||
if let Some((existing_open, existing_close)) = &result {
|
Some((
|
||||||
let existing_len = existing_close.end - existing_open.start;
|
excerpt.map_range_from_buffer(open),
|
||||||
if len > existing_len {
|
excerpt.map_range_from_buffer(close),
|
||||||
continue;
|
))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = Some((open, close));
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns enclosing bracket ranges containing the given range or returns None if the range is
|
/// Returns enclosing bracket ranges containing the given range or returns None if the range is
|
||||||
|
@ -2948,11 +2961,14 @@ impl MultiBufferSnapshot {
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
|
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
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| {
|
Some(
|
||||||
range_pairs
|
excerpt
|
||||||
.filter(move |(open, close)| open.start <= range.start && close.end >= range.end)
|
.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
|
/// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is
|
||||||
|
@ -2962,38 +2978,24 @@ impl MultiBufferSnapshot {
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
|
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
let excerpt = self.excerpt_containing(range.clone());
|
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);
|
|
||||||
|
|
||||||
|
Some(
|
||||||
excerpt
|
excerpt
|
||||||
.buffer
|
.buffer()
|
||||||
.bracket_ranges(start_in_buffer..end_in_buffer)
|
.bracket_ranges(excerpt.map_range_to_buffer(range))
|
||||||
.filter_map(move |(start_bracket_range, end_bracket_range)| {
|
.filter_map(move |(start_bracket_range, close_bracket_range)| {
|
||||||
if start_bracket_range.start < excerpt_buffer_start
|
let buffer_range = start_bracket_range.start..close_bracket_range.end;
|
||||||
|| end_bracket_range.end > excerpt_buffer_end
|
if excerpt.contains_buffer_range(buffer_range) {
|
||||||
{
|
Some((
|
||||||
return None;
|
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>(
|
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>> {
|
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 range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
let excerpt = self.excerpt_containing(range.clone())?;
|
||||||
|
|
||||||
self.excerpt_containing(range.clone())
|
let ancestor_buffer_range = excerpt
|
||||||
.and_then(|(excerpt, excerpt_offset)| {
|
.buffer()
|
||||||
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
.range_for_syntax_ancestor(excerpt.map_range_to_buffer(range))?;
|
||||||
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
|
|
||||||
|
|
||||||
let start_in_buffer =
|
Some(excerpt.map_range_from_buffer(ancestor_buffer_range))
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
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
|
/// 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>(
|
pub fn excerpt_containing<T: ToOffset>(&self, range: Range<T>) -> Option<MultiBufferExcerpt> {
|
||||||
&'a self,
|
|
||||||
range: Range<T>,
|
|
||||||
) -> Option<(&'a Excerpt, usize)> {
|
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
let mut cursor = self.excerpts.cursor::<usize>();
|
let mut cursor = self.excerpts.cursor::<usize>();
|
||||||
cursor.seek(&range.start, Bias::Right, &());
|
cursor.seek(&range.start, Bias::Right, &());
|
||||||
let start_excerpt = cursor.item();
|
let start_excerpt = cursor.item()?;
|
||||||
|
|
||||||
if range.start == range.end {
|
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, &());
|
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 {
|
if start_excerpt.id != end_excerpt.id {
|
||||||
return None;
|
None
|
||||||
|
} else {
|
||||||
|
Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start()))
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((start_excerpt, *cursor.start()))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote_selections_in_range<'a>(
|
pub fn remote_selections_in_range<'a>(
|
||||||
|
@ -3768,6 +3750,61 @@ impl Excerpt {
|
||||||
.cmp(&anchor.text_anchor, &self.buffer)
|
.cmp(&anchor.text_anchor, &self.buffer)
|
||||||
.is_ge()
|
.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 {
|
impl ExcerptId {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use editor::{
|
||||||
Bias, DisplayPoint,
|
Bias, DisplayPoint,
|
||||||
};
|
};
|
||||||
use gpui::{actions, impl_actions, ViewContext, WindowContext};
|
use gpui::{actions, impl_actions, ViewContext, WindowContext};
|
||||||
use language::{char_kind, CharKind, Selection};
|
use language::{char_kind, BufferSnapshot, CharKind, Selection};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ pub enum Object {
|
||||||
SquareBrackets,
|
SquareBrackets,
|
||||||
CurlyBrackets,
|
CurlyBrackets,
|
||||||
AngleBrackets,
|
AngleBrackets,
|
||||||
|
Argument,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
@ -49,7 +50,8 @@ actions!(
|
||||||
Parentheses,
|
Parentheses,
|
||||||
SquareBrackets,
|
SquareBrackets,
|
||||||
CurlyBrackets,
|
CurlyBrackets,
|
||||||
AngleBrackets
|
AngleBrackets,
|
||||||
|
Argument
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -82,6 +84,8 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
|
workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| {
|
||||||
object(Object::VerticalBars, cx)
|
object(Object::VerticalBars, cx)
|
||||||
});
|
});
|
||||||
|
workspace
|
||||||
|
.register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn object(object: Object, cx: &mut WindowContext) {
|
fn object(object: Object, cx: &mut WindowContext) {
|
||||||
|
@ -106,13 +110,14 @@ impl Object {
|
||||||
| Object::Parentheses
|
| Object::Parentheses
|
||||||
| Object::AngleBrackets
|
| Object::AngleBrackets
|
||||||
| Object::CurlyBrackets
|
| Object::CurlyBrackets
|
||||||
| Object::SquareBrackets => true,
|
| Object::SquareBrackets
|
||||||
|
| Object::Argument => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn always_expands_both_ways(self) -> bool {
|
pub fn always_expands_both_ways(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Object::Word { .. } | Object::Sentence => false,
|
Object::Word { .. } | Object::Sentence | Object::Argument => false,
|
||||||
Object::Quotes
|
Object::Quotes
|
||||||
| Object::BackQuotes
|
| Object::BackQuotes
|
||||||
| Object::DoubleQuotes
|
| Object::DoubleQuotes
|
||||||
|
@ -136,7 +141,8 @@ impl Object {
|
||||||
| Object::Parentheses
|
| Object::Parentheses
|
||||||
| Object::SquareBrackets
|
| Object::SquareBrackets
|
||||||
| Object::CurlyBrackets
|
| Object::CurlyBrackets
|
||||||
| Object::AngleBrackets => Mode::Visual,
|
| Object::AngleBrackets
|
||||||
|
| Object::Argument => Mode::Visual,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +185,7 @@ impl Object {
|
||||||
Object::AngleBrackets => {
|
Object::AngleBrackets => {
|
||||||
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
|
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)
|
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(
|
fn sentence(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
relative_to: DisplayPoint,
|
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]
|
#[gpui::test]
|
||||||
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue