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

@ -14,7 +14,8 @@ use crate::{
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
},
task_context::RunnableRange,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
TreeSitterOptions,
};
use anyhow::{anyhow, Context, Result};
use async_watch as watch;
@ -3412,6 +3413,72 @@ impl BufferSnapshot {
})
}
pub fn text_object_ranges<T: ToOffset>(
&self,
range: Range<T>,
options: TreeSitterOptions,
) -> impl Iterator<Item = (Range<usize>, TextObject)> + '_ {
let range = range.start.to_offset(self).saturating_sub(1)
..self.len().min(range.end.to_offset(self) + 1);
let mut matches =
self.syntax
.matches_with_options(range.clone(), &self.text, options, |grammar| {
grammar.text_object_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|grammar| grammar.text_object_config.as_ref())
.collect::<Vec<_>>();
let mut captures = Vec::<(Range<usize>, TextObject)>::new();
iter::from_fn(move || loop {
while let Some(capture) = captures.pop() {
if capture.0.overlaps(&range) {
return Some(capture);
}
}
let mat = matches.peek()?;
let Some(config) = configs[mat.grammar_index].as_ref() else {
matches.advance();
continue;
};
for capture in mat.captures {
let Some(ix) = config
.text_objects_by_capture_ix
.binary_search_by_key(&capture.index, |e| e.0)
.ok()
else {
continue;
};
let text_object = config.text_objects_by_capture_ix[ix].1;
let byte_range = capture.node.byte_range();
let mut found = false;
for (range, existing) in captures.iter_mut() {
if existing == &text_object {
range.start = range.start.min(byte_range.start);
range.end = range.end.max(byte_range.end);
found = true;
break;
}
}
if !found {
captures.push((byte_range, text_object));
}
}
matches.advance();
})
}
/// Returns enclosing bracket ranges containing the given range
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,

View file

@ -20,6 +20,7 @@ use std::{
sync::LazyLock,
time::{Duration, Instant},
};
use syntax_map::TreeSitterOptions;
use text::network::Network;
use text::{BufferId, LineEnding, LineIndent};
use text::{Point, ToPoint};
@ -915,6 +916,39 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
}
}
#[gpui::test]
fn test_text_objects(cx: &mut AppContext) {
let (text, ranges) = marked_text_ranges(
indoc! {r#"
impl Hello {
fn say() -> u8 { return /* ˇhi */ 1 }
}"#
},
false,
);
let buffer =
cx.new_model(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let matches = snapshot
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
.map(|(range, text_object)| (&text[range], text_object))
.collect::<Vec<_>>();
assert_eq!(
matches,
&[
("/* hi */", TextObject::AroundComment),
("return /* hi */ 1", TextObject::InsideFunction),
(
"fn say() -> u8 { return /* hi */ 1 }",
TextObject::AroundFunction
),
],
)
}
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut AppContext) {
let mut assert = |selection_text, range_markers| {
@ -3182,6 +3216,20 @@ fn rust_lang() -> Language {
"#,
)
.unwrap()
.with_text_object_query(
r#"
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(line_comment)+ @comment.around
(block_comment) @comment.around
"#,
)
.unwrap()
.with_outline_query(
r#"
(line_comment) @annotation

View file

@ -78,7 +78,7 @@ pub use language_registry::{
};
pub use lsp::LanguageServerId;
pub use outline::*;
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions};
pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
@ -848,6 +848,7 @@ pub struct Grammar {
pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub text_object_config: Option<TextObjectConfig>,
pub embedding_config: Option<EmbeddingConfig>,
pub(crate) injection_config: Option<InjectionConfig>,
pub(crate) override_config: Option<OverrideConfig>,
@ -873,6 +874,44 @@ pub struct OutlineConfig {
pub annotation_capture_ix: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextObject {
InsideFunction,
AroundFunction,
InsideClass,
AroundClass,
InsideComment,
AroundComment,
}
impl TextObject {
pub fn from_capture_name(name: &str) -> Option<TextObject> {
match name {
"function.inside" => Some(TextObject::InsideFunction),
"function.around" => Some(TextObject::AroundFunction),
"class.inside" => Some(TextObject::InsideClass),
"class.around" => Some(TextObject::AroundClass),
"comment.inside" => Some(TextObject::InsideComment),
"comment.around" => Some(TextObject::AroundComment),
_ => None,
}
}
pub fn around(&self) -> Option<Self> {
match self {
TextObject::InsideFunction => Some(TextObject::AroundFunction),
TextObject::InsideClass => Some(TextObject::AroundClass),
TextObject::InsideComment => Some(TextObject::AroundComment),
_ => None,
}
}
}
pub struct TextObjectConfig {
pub query: Query,
pub text_objects_by_capture_ix: Vec<(u32, TextObject)>,
}
#[derive(Debug)]
pub struct EmbeddingConfig {
pub query: Query,
@ -950,6 +989,7 @@ impl Language {
highlights_query: None,
brackets_config: None,
outline_config: None,
text_object_config: None,
embedding_config: None,
indents_config: None,
injection_config: None,
@ -1020,7 +1060,12 @@ impl Language {
if let Some(query) = queries.runnables {
self = self
.with_runnable_query(query.as_ref())
.context("Error loading tests query")?;
.context("Error loading runnables query")?;
}
if let Some(query) = queries.text_objects {
self = self
.with_text_object_query(query.as_ref())
.context("Error loading textobject query")?;
}
Ok(self)
}
@ -1097,6 +1142,26 @@ impl Language {
Ok(self)
}
pub fn with_text_object_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
let query = Query::new(&grammar.ts_language, source)?;
let mut text_objects_by_capture_ix = Vec::new();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(text_object) = TextObject::from_capture_name(name) {
text_objects_by_capture_ix.push((ix as u32, text_object));
}
}
grammar.text_object_config = Some(TextObjectConfig {
query,
text_objects_by_capture_ix,
});
Ok(self)
}
pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()

View file

@ -181,6 +181,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
("runnables", |q| &mut q.runnables),
("textobjects", |q| &mut q.text_objects),
];
/// Tree-sitter language queries for a given language.
@ -195,6 +196,7 @@ pub struct LanguageQueries {
pub overrides: Option<Cow<'static, str>>,
pub redactions: Option<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
pub text_objects: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]

View file

@ -814,6 +814,23 @@ impl SyntaxSnapshot {
buffer.as_rope(),
self.layers_for_range(range, buffer, true),
query,
TreeSitterOptions::default(),
)
}
pub fn matches_with_options<'a>(
&'a self,
range: Range<usize>,
buffer: &'a BufferSnapshot,
options: TreeSitterOptions,
query: fn(&Grammar) -> Option<&Query>,
) -> SyntaxMapMatches<'a> {
SyntaxMapMatches::new(
range.clone(),
buffer.as_rope(),
self.layers_for_range(range, buffer, true),
query,
options,
)
}
@ -1001,12 +1018,25 @@ impl<'a> SyntaxMapCaptures<'a> {
}
}
#[derive(Default)]
pub struct TreeSitterOptions {
max_start_depth: Option<u32>,
}
impl TreeSitterOptions {
pub fn max_start_depth(max_start_depth: u32) -> Self {
Self {
max_start_depth: Some(max_start_depth),
}
}
}
impl<'a> SyntaxMapMatches<'a> {
fn new(
range: Range<usize>,
text: &'a Rope,
layers: impl Iterator<Item = SyntaxLayer<'a>>,
query: fn(&Grammar) -> Option<&Query>,
options: TreeSitterOptions,
) -> Self {
let mut result = Self::default();
for layer in layers {
@ -1027,6 +1057,7 @@ impl<'a> SyntaxMapMatches<'a> {
query_cursor.deref_mut(),
)
};
cursor.set_max_start_depth(options.max_start_depth);
cursor.set_byte_range(range.clone());
let matches = cursor.matches(query, layer.node(), TextProvider(text));

View file

@ -0,0 +1,7 @@
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(comment) @comment.around

View file

@ -0,0 +1,25 @@
(declaration
declarator: (function_declarator)) @function.around
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(preproc_function_def
value: (_) @function.inside) @function.around
(comment) @comment.around
(struct_specifier
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(enum_specifier
body: (_
"{"
[(_) ","?]* @class.inside
"}")) @class.around

View file

@ -0,0 +1,31 @@
(declaration
declarator: (function_declarator)) @function.around
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(preproc_function_def
value: (_) @function.inside) @function.around
(comment) @comment.around
(struct_specifier
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(enum_specifier
body: (_
"{"
[(_) ","?]* @class.inside
"}")) @class.around
(class_specifier
body: (_
"{"
[(_) ":"? ";"?]* @class.inside
"}"?)) @class.around

View file

@ -0,0 +1,30 @@
(comment) @comment.around
(rule_set
(block (
"{"
(_)* @function.inside
"}" ))) @function.around
(keyframe_block
(block (
"{"
(_)* @function.inside
"}" ))) @function.around
(media_statement
(block (
"{"
(_)* @class.inside
"}" ))) @class.around
(supports_statement
(block (
"{"
(_)* @class.inside
"}" ))) @class.around
(keyframes_statement
(keyframe_block_list (
"{"
(_)* @class.inside
"}" ))) @class.around

View file

@ -0,0 +1,25 @@
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(type_declaration
(type_spec (struct_type (field_declaration_list (
"{"
(_)* @class.inside
"}")?)))) @class.around
(type_declaration
(type_spec (interface_type
(_)* @class.inside))) @class.around
(type_declaration) @class.around
(comment)+ @comment.around

View file

@ -0,0 +1,51 @@
(comment)+ @comment.around
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(function_expression
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function) @function.around
(generator_function
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(generator_function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(class
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around

View file

@ -0,0 +1 @@
(comment)+ @comment.around

View file

@ -0,0 +1 @@
(comment)+ @comment.around

View file

@ -0,0 +1,3 @@
(section
(atx_heading)
(_)* @class.inside) @class.around

View file

@ -0,0 +1,7 @@
(comment)+ @comment.around
(function_definition
body: (_) @function.inside) @function.around
(class_definition
body: (_) @class.inside) @class.around

View file

@ -15,11 +15,7 @@
(visibility_modifier)? @context
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name
(function_item
body: (_ "{" @open (_)* "}" @close)) @item
(trait_item

View file

@ -0,0 +1,51 @@
; functions
(function_signature_item) @function.around
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
; classes
(struct_item
body: (_
["{" "("]?
[(_) ","?]* @class.inside
["}" ")"]? )) @class.around
(enum_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(union_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(trait_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(impl_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(mod_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
; comments
(line_comment)+ @comment.around
(block_comment) @comment.around

View file

@ -0,0 +1,79 @@
(comment)+ @comment.around
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(function_expression
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function) @function.around
(function_signature) @function.around
(generator_function
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(generator_function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(class
body: (_
"{"
(_)* @class.inside
"}" )) @class.around
(interface_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(enum_declaration
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(ambient_declaration
(module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" ))) @class.around
(internal_module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(type_alias_declaration) @class.around

View file

@ -0,0 +1,79 @@
(comment)+ @comment.around
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(function_expression
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function) @function.around
(function_signature) @function.around
(generator_function
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(generator_function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(class
body: (_
"{"
(_)* @class.inside
"}" )) @class.around
(interface_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(enum_declaration
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(ambient_declaration
(module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" ))) @class.around
(internal_module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(type_alias_declaration) @class.around

View file

@ -0,0 +1 @@
(comment)+ @comment

View file

@ -3441,6 +3441,30 @@ impl MultiBufferSnapshot {
})
}
pub fn excerpt_before(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
let start_locator = self.excerpt_locator_for_id(id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
cursor.seek(&Some(start_locator), Bias::Left, &());
cursor.prev(&());
let excerpt = cursor.item()?;
Some(MultiBufferExcerpt {
excerpt,
excerpt_offset: 0,
})
}
pub fn excerpt_after(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
let start_locator = self.excerpt_locator_for_id(id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
cursor.seek(&Some(start_locator), Bias::Left, &());
cursor.next(&());
let excerpt = cursor.item()?;
Some(MultiBufferExcerpt {
excerpt,
excerpt_offset: 0,
})
}
pub fn excerpt_boundaries_in_range<R, T>(
&self,
range: R,
@ -4689,6 +4713,26 @@ impl<'a> MultiBufferExcerpt<'a> {
}
}
pub fn id(&self) -> ExcerptId {
self.excerpt.id
}
pub fn start_anchor(&self) -> Anchor {
Anchor {
buffer_id: Some(self.excerpt.buffer_id),
excerpt_id: self.excerpt.id,
text_anchor: self.excerpt.range.context.start,
}
}
pub fn end_anchor(&self) -> Anchor {
Anchor {
buffer_id: Some(self.excerpt.buffer_id),
excerpt_id: self.excerpt.id,
text_anchor: self.excerpt.range.context.end,
}
}
pub fn buffer(&self) -> &'a BufferSnapshot {
&self.excerpt.buffer
}

View file

@ -11,6 +11,7 @@ use language::{CharKind, Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow;
use serde::Deserialize;
use std::ops::Range;
use workspace::searchable::Direction;
use crate::{
normal::mark,
@ -104,6 +105,16 @@ pub enum Motion {
WindowTop,
WindowMiddle,
WindowBottom,
NextSectionStart,
NextSectionEnd,
PreviousSectionStart,
PreviousSectionEnd,
NextMethodStart,
NextMethodEnd,
PreviousMethodStart,
PreviousMethodEnd,
NextComment,
PreviousComment,
// we don't have a good way to run a search synchronously, so
// we handle search motions by running the search async and then
@ -269,6 +280,16 @@ actions!(
WindowTop,
WindowMiddle,
WindowBottom,
NextSectionStart,
NextSectionEnd,
PreviousSectionStart,
PreviousSectionEnd,
NextMethodStart,
NextMethodEnd,
PreviousMethodStart,
PreviousMethodEnd,
NextComment,
PreviousComment,
]
);
@ -454,6 +475,37 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, &WindowBottom, cx| {
vim.motion(Motion::WindowBottom, cx)
});
Vim::action(editor, cx, |vim, &PreviousSectionStart, cx| {
vim.motion(Motion::PreviousSectionStart, cx)
});
Vim::action(editor, cx, |vim, &NextSectionStart, cx| {
vim.motion(Motion::NextSectionStart, cx)
});
Vim::action(editor, cx, |vim, &PreviousSectionEnd, cx| {
vim.motion(Motion::PreviousSectionEnd, cx)
});
Vim::action(editor, cx, |vim, &NextSectionEnd, cx| {
vim.motion(Motion::NextSectionEnd, cx)
});
Vim::action(editor, cx, |vim, &PreviousMethodStart, cx| {
vim.motion(Motion::PreviousMethodStart, cx)
});
Vim::action(editor, cx, |vim, &NextMethodStart, cx| {
vim.motion(Motion::NextMethodStart, cx)
});
Vim::action(editor, cx, |vim, &PreviousMethodEnd, cx| {
vim.motion(Motion::PreviousMethodEnd, cx)
});
Vim::action(editor, cx, |vim, &NextMethodEnd, cx| {
vim.motion(Motion::NextMethodEnd, cx)
});
Vim::action(editor, cx, |vim, &NextComment, cx| {
vim.motion(Motion::NextComment, cx)
});
Vim::action(editor, cx, |vim, &PreviousComment, cx| {
vim.motion(Motion::PreviousComment, cx)
});
}
impl Vim {
@ -536,6 +588,16 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| Jump { line: true, .. } => true,
EndOfLine { .. }
| Matching
@ -607,6 +669,16 @@ impl Motion {
| NextLineStart
| PreviousLineStart
| ZedSearchResult { .. }
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| Jump { .. } => false,
}
}
@ -652,6 +724,16 @@ impl Motion {
| FirstNonWhitespace { .. }
| FindBackward { .. }
| Jump { .. }
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| ZedSearchResult { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
motion.inclusive()
@ -867,6 +949,47 @@ impl Motion {
return None;
}
}
NextSectionStart => (
section_motion(map, point, times, Direction::Next, true),
SelectionGoal::None,
),
NextSectionEnd => (
section_motion(map, point, times, Direction::Next, false),
SelectionGoal::None,
),
PreviousSectionStart => (
section_motion(map, point, times, Direction::Prev, true),
SelectionGoal::None,
),
PreviousSectionEnd => (
section_motion(map, point, times, Direction::Prev, false),
SelectionGoal::None,
),
NextMethodStart => (
method_motion(map, point, times, Direction::Next, true),
SelectionGoal::None,
),
NextMethodEnd => (
method_motion(map, point, times, Direction::Next, false),
SelectionGoal::None,
),
PreviousMethodStart => (
method_motion(map, point, times, Direction::Prev, true),
SelectionGoal::None,
),
PreviousMethodEnd => (
method_motion(map, point, times, Direction::Prev, false),
SelectionGoal::None,
),
NextComment => (
comment_motion(map, point, times, Direction::Next),
SelectionGoal::None,
),
PreviousComment => (
comment_motion(map, point, times, Direction::Prev),
SelectionGoal::None,
),
};
(new_point != point || infallible).then_some((new_point, goal))
@ -2129,6 +2252,231 @@ fn window_bottom(
}
}
fn method_motion(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
times: usize,
direction: Direction,
is_start: bool,
) -> DisplayPoint {
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
return display_point;
};
for _ in 0..times {
let point = map.display_point_to_point(display_point, Bias::Left);
let offset = point.to_offset(&map.buffer_snapshot);
let range = if direction == Direction::Prev {
0..offset
} else {
offset..buffer.len()
};
let possibilities = buffer
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
.filter_map(|(range, object)| {
if !matches!(object, language::TextObject::AroundFunction) {
return None;
}
let relevant = if is_start { range.start } else { range.end };
if direction == Direction::Prev && relevant < offset {
Some(relevant)
} else if direction == Direction::Next && relevant > offset + 1 {
Some(relevant)
} else {
None
}
});
let dest = if direction == Direction::Prev {
possibilities.max().unwrap_or(offset)
} else {
possibilities.min().unwrap_or(offset)
};
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
if new_point == display_point {
break;
}
display_point = new_point;
}
display_point
}
fn comment_motion(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
times: usize,
direction: Direction,
) -> DisplayPoint {
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
return display_point;
};
for _ in 0..times {
let point = map.display_point_to_point(display_point, Bias::Left);
let offset = point.to_offset(&map.buffer_snapshot);
let range = if direction == Direction::Prev {
0..offset
} else {
offset..buffer.len()
};
let possibilities = buffer
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
.filter_map(|(range, object)| {
if !matches!(object, language::TextObject::AroundComment) {
return None;
}
let relevant = if direction == Direction::Prev {
range.start
} else {
range.end
};
if direction == Direction::Prev && relevant < offset {
Some(relevant)
} else if direction == Direction::Next && relevant > offset + 1 {
Some(relevant)
} else {
None
}
});
let dest = if direction == Direction::Prev {
possibilities.max().unwrap_or(offset)
} else {
possibilities.min().unwrap_or(offset)
};
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
if new_point == display_point {
break;
}
display_point = new_point;
}
display_point
}
fn section_motion(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
times: usize,
direction: Direction,
is_start: bool,
) -> DisplayPoint {
if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
for _ in 0..times {
let offset = map
.display_point_to_point(display_point, Bias::Left)
.to_offset(&map.buffer_snapshot);
let range = if direction == Direction::Prev {
0..offset
} else {
offset..buffer.len()
};
// we set a max start depth here because we want a section to only be "top level"
// similar to vim's default of '{' in the first column.
// (and without it, ]] at the start of editor.rs is -very- slow)
let mut possibilities = buffer
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
.filter(|(_, object)| {
matches!(
object,
language::TextObject::AroundClass | language::TextObject::AroundFunction
)
})
.collect::<Vec<_>>();
possibilities.sort_by_key(|(range_a, _)| range_a.start);
let mut prev_end = None;
let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
if t == language::TextObject::AroundFunction
&& prev_end.is_some_and(|prev_end| prev_end > range.start)
{
return None;
}
prev_end = Some(range.end);
let relevant = if is_start { range.start } else { range.end };
if direction == Direction::Prev && relevant < offset {
Some(relevant)
} else if direction == Direction::Next && relevant > offset + 1 {
Some(relevant)
} else {
None
}
});
let offset = if direction == Direction::Prev {
possibilities.max().unwrap_or(0)
} else {
possibilities.min().unwrap_or(buffer.len())
};
let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
if new_point == display_point {
break;
}
display_point = new_point;
}
return display_point;
};
for _ in 0..times {
let point = map.display_point_to_point(display_point, Bias::Left);
let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
return display_point;
};
let next_point = match (direction, is_start) {
(Direction::Prev, true) => {
let mut start = excerpt.start_anchor().to_display_point(&map);
if start >= display_point && start.row() > DisplayRow(0) {
let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
return display_point;
};
start = excerpt.start_anchor().to_display_point(&map);
}
start
}
(Direction::Prev, false) => {
let mut start = excerpt.start_anchor().to_display_point(&map);
if start.row() > DisplayRow(0) {
*start.row_mut() -= 1;
}
map.clip_point(start, Bias::Left)
}
(Direction::Next, true) => {
let mut end = excerpt.end_anchor().to_display_point(&map);
*end.row_mut() += 1;
map.clip_point(end, Bias::Right)
}
(Direction::Next, false) => {
let mut end = excerpt.end_anchor().to_display_point(&map);
*end.column_mut() = 0;
if end <= display_point {
*end.row_mut() += 1;
let point_end = map.display_point_to_point(end, Bias::Right);
let Some(excerpt) =
map.buffer_snapshot.excerpt_containing(point_end..point_end)
else {
return display_point;
};
end = excerpt.end_anchor().to_display_point(&map);
*end.column_mut() = 0;
}
end
}
};
if next_point == display_point {
break;
}
display_point = next_point;
}
display_point
}
#[cfg(test)]
mod test {

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,

View file

@ -308,7 +308,7 @@ impl Vim {
if let Some(Operator::Object { around }) = self.active_operator() {
self.pop_operator(cx);
let current_mode = self.mode;
let target_mode = object.target_visual_mode(current_mode);
let target_mode = object.target_visual_mode(current_mode, around);
if target_mode != current_mode {
self.switch_mode(target_mode, true, cx);
}