Add textobjects queries (#20924)

Co-Authored-By: Max <max@zed.dev>

Release Notes:

- vim: Added motions `[[`, `[]`, `]]`, `][` for navigating by section,
`[m`, `]m`, `[M`, `]M` for navigating by method, and `[*`, `]*`, `[/`,
`]/` for comments. These currently only work for languages built in to
Zed, as they are powered by new tree-sitter queries.
- vim: Added new text objects: `ic`, `ac` for inside/around classes,
`if`,`af` for functions/methods, and `g c` for comments. These currently
only work for languages built in to Zed, as they are powered by new
tree-sitter queries.

---------

Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Conrad Irwin 2024-12-03 09:37:01 -08:00 committed by GitHub
parent c443307c19
commit 75c9dc179b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1205 additions and 26 deletions

View file

@ -1,6 +1,10 @@
use std::ops::Range;
use crate::{motion::right, state::Mode, Vim};
use crate::{
motion::right,
state::{Mode, Operator},
Vim,
};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement::{self, FindRange},
@ -10,7 +14,7 @@ use editor::{
use itertools::Itertools;
use gpui::{actions, impl_actions, ViewContext};
use language::{BufferSnapshot, CharKind, Point, Selection};
use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
use multi_buffer::MultiBufferRow;
use serde::Deserialize;
@ -30,6 +34,9 @@ pub enum Object {
Argument,
IndentObj { include_below: bool },
Tag,
Method,
Class,
Comment,
}
#[derive(Clone, Deserialize, PartialEq)]
@ -61,7 +68,10 @@ actions!(
CurlyBrackets,
AngleBrackets,
Argument,
Tag
Tag,
Method,
Class,
Comment
]
);
@ -107,6 +117,18 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Argument, cx| {
vim.object(Object::Argument, cx)
});
Vim::action(editor, cx, |vim, _: &Method, cx| {
vim.object(Object::Method, cx)
});
Vim::action(editor, cx, |vim, _: &Class, cx| {
vim.object(Object::Class, cx)
});
Vim::action(editor, cx, |vim, _: &Comment, cx| {
if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
vim.push_operator(Operator::Object { around: true }, cx);
}
vim.object(Object::Comment, cx)
});
Vim::action(
editor,
cx,
@ -144,6 +166,9 @@ impl Object {
| Object::CurlyBrackets
| Object::SquareBrackets
| Object::Argument
| Object::Method
| Object::Class
| Object::Comment
| Object::IndentObj { .. } => true,
}
}
@ -162,12 +187,15 @@ impl Object {
| Object::Parentheses
| Object::SquareBrackets
| Object::Tag
| Object::Method
| Object::Class
| Object::Comment
| Object::CurlyBrackets
| Object::AngleBrackets => true,
}
}
pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
match self {
Object::Word { .. }
| Object::Sentence
@ -186,8 +214,16 @@ impl Object {
| Object::AngleBrackets
| Object::VerticalBars
| Object::Tag
| Object::Comment
| Object::Argument
| Object::IndentObj { .. } => Mode::Visual,
Object::Method | Object::Class => {
if around {
Mode::VisualLine
} else {
Mode::Visual
}
}
Object::Paragraph => Mode::VisualLine,
}
}
@ -238,6 +274,33 @@ impl Object {
Object::AngleBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
}
Object::Method => text_object(
map,
relative_to,
if around {
TextObject::AroundFunction
} else {
TextObject::InsideFunction
},
),
Object::Comment => text_object(
map,
relative_to,
if around {
TextObject::AroundComment
} else {
TextObject::InsideComment
},
),
Object::Class => text_object(
map,
relative_to,
if around {
TextObject::AroundClass
} else {
TextObject::InsideClass
},
),
Object::Argument => argument(map, relative_to, around),
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
}
@ -441,6 +504,47 @@ fn around_next_word(
Some(start..end)
}
fn text_object(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
target: TextObject,
) -> Option<Range<DisplayPoint>> {
let snapshot = &map.buffer_snapshot;
let offset = relative_to.to_offset(map, Bias::Left);
let excerpt = snapshot.excerpt_containing(offset..offset)?;
let buffer = excerpt.buffer();
let mut matches: Vec<Range<usize>> = buffer
.text_object_ranges(offset..offset, TreeSitterOptions::default())
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
.collect();
matches.sort_by_key(|r| (r.end - r.start));
if let Some(range) = matches.first() {
return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
}
let around = target.around()?;
let mut matches: Vec<Range<usize>> = buffer
.text_object_ranges(offset..offset, TreeSitterOptions::default())
.filter_map(|(r, m)| if m == around { Some(r) } else { None })
.collect();
matches.sort_by_key(|r| (r.end - r.start));
let around_range = matches.first()?;
let mut matches: Vec<Range<usize>> = buffer
.text_object_ranges(around_range.clone(), TreeSitterOptions::default())
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
.collect();
matches.sort_by_key(|r| r.start);
if let Some(range) = matches.first() {
if !range.is_empty() {
return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
}
}
return Some(around_range.start.to_display_point(map)..around_range.end.to_display_point(map));
}
fn argument(
map: &DisplaySnapshot,
relative_to: DisplayPoint,