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

12
Cargo.lock generated
View file

@ -3416,9 +3416,9 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.2.9" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.87", "syn 2.0.87",
@ -6789,9 +6789,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.164" version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]] [[package]]
name = "libdbus-sys" name = "libdbus-sys"
@ -10956,9 +10956,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.133" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [ dependencies = [
"indexmap 2.6.0", "indexmap 2.6.0",
"itoa", "itoa",

View file

@ -33,6 +33,18 @@
"(": "vim::SentenceBackward", "(": "vim::SentenceBackward",
")": "vim::SentenceForward", ")": "vim::SentenceForward",
"|": "vim::GoToColumn", "|": "vim::GoToColumn",
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] M": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ M": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
// Word motions // Word motions
"w": "vim::NextWordStart", "w": "vim::NextWordStart",
"e": "vim::NextWordEnd", "e": "vim::NextWordEnd",
@ -360,7 +372,8 @@
"bindings": { "bindings": {
"escape": "vim::ClearOperators", "escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators" "ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
} }
}, },
{ {
@ -389,7 +402,9 @@
">": "vim::AngleBrackets", ">": "vim::AngleBrackets",
"a": "vim::Argument", "a": "vim::Argument",
"i": "vim::IndentObj", "i": "vim::IndentObj",
"shift-i": ["vim::IndentObj", { "includeBelow": true }] "shift-i": ["vim::IndentObj", { "includeBelow": true }],
"f": "vim::Method",
"c": "vim::Class"
} }
}, },
{ {

View file

@ -14,7 +14,8 @@ use crate::{
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint, SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
}, },
task_context::RunnableRange, task_context::RunnableRange,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
TreeSitterOptions,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_watch as watch; 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 /// Returns enclosing bracket ranges containing the given range
pub fn enclosing_bracket_ranges<T: ToOffset>( pub fn enclosing_bracket_ranges<T: ToOffset>(
&self, &self,

View file

@ -20,6 +20,7 @@ use std::{
sync::LazyLock, sync::LazyLock,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use syntax_map::TreeSitterOptions;
use text::network::Network; use text::network::Network;
use text::{BufferId, LineEnding, LineIndent}; use text::{BufferId, LineEnding, LineIndent};
use text::{Point, ToPoint}; 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] #[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut AppContext) { fn test_enclosing_bracket_ranges(cx: &mut AppContext) {
let mut assert = |selection_text, range_markers| { let mut assert = |selection_text, range_markers| {
@ -3182,6 +3216,20 @@ fn rust_lang() -> Language {
"#, "#,
) )
.unwrap() .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( .with_outline_query(
r#" r#"
(line_comment) @annotation (line_comment) @annotation

View file

@ -78,7 +78,7 @@ pub use language_registry::{
}; };
pub use lsp::LanguageServerId; pub use lsp::LanguageServerId;
pub use outline::*; pub use outline::*;
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer}; pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions};
pub use text::{AnchorRangeExt, LineEnding}; pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor}; pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
@ -848,6 +848,7 @@ pub struct Grammar {
pub(crate) runnable_config: Option<RunnableConfig>, pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) indents_config: Option<IndentConfig>, pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>, pub outline_config: Option<OutlineConfig>,
pub text_object_config: Option<TextObjectConfig>,
pub embedding_config: Option<EmbeddingConfig>, pub embedding_config: Option<EmbeddingConfig>,
pub(crate) injection_config: Option<InjectionConfig>, pub(crate) injection_config: Option<InjectionConfig>,
pub(crate) override_config: Option<OverrideConfig>, pub(crate) override_config: Option<OverrideConfig>,
@ -873,6 +874,44 @@ pub struct OutlineConfig {
pub annotation_capture_ix: Option<u32>, 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)] #[derive(Debug)]
pub struct EmbeddingConfig { pub struct EmbeddingConfig {
pub query: Query, pub query: Query,
@ -950,6 +989,7 @@ impl Language {
highlights_query: None, highlights_query: None,
brackets_config: None, brackets_config: None,
outline_config: None, outline_config: None,
text_object_config: None,
embedding_config: None, embedding_config: None,
indents_config: None, indents_config: None,
injection_config: None, injection_config: None,
@ -1020,7 +1060,12 @@ impl Language {
if let Some(query) = queries.runnables { if let Some(query) = queries.runnables {
self = self self = self
.with_runnable_query(query.as_ref()) .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) Ok(self)
} }
@ -1097,6 +1142,26 @@ impl Language {
Ok(self) 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> { pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
let grammar = self let grammar = self
.grammar_mut() .grammar_mut()

View file

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

View file

@ -814,6 +814,23 @@ impl SyntaxSnapshot {
buffer.as_rope(), buffer.as_rope(),
self.layers_for_range(range, buffer, true), self.layers_for_range(range, buffer, true),
query, 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> { impl<'a> SyntaxMapMatches<'a> {
fn new( fn new(
range: Range<usize>, range: Range<usize>,
text: &'a Rope, text: &'a Rope,
layers: impl Iterator<Item = SyntaxLayer<'a>>, layers: impl Iterator<Item = SyntaxLayer<'a>>,
query: fn(&Grammar) -> Option<&Query>, query: fn(&Grammar) -> Option<&Query>,
options: TreeSitterOptions,
) -> Self { ) -> Self {
let mut result = Self::default(); let mut result = Self::default();
for layer in layers { for layer in layers {
@ -1027,6 +1057,7 @@ impl<'a> SyntaxMapMatches<'a> {
query_cursor.deref_mut(), query_cursor.deref_mut(),
) )
}; };
cursor.set_max_start_depth(options.max_start_depth);
cursor.set_byte_range(range.clone()); cursor.set_byte_range(range.clone());
let matches = cursor.matches(query, layer.node(), TextProvider(text)); 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 (visibility_modifier)? @context
name: (_) @name) @item name: (_) @name) @item
(impl_item (function_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name
body: (_ "{" @open (_)* "}" @close)) @item body: (_ "{" @open (_)* "}" @close)) @item
(trait_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>( pub fn excerpt_boundaries_in_range<R, T>(
&self, &self,
range: R, 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 { pub fn buffer(&self) -> &'a BufferSnapshot {
&self.excerpt.buffer &self.excerpt.buffer
} }

View file

@ -11,6 +11,7 @@ use language::{CharKind, Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use serde::Deserialize; use serde::Deserialize;
use std::ops::Range; use std::ops::Range;
use workspace::searchable::Direction;
use crate::{ use crate::{
normal::mark, normal::mark,
@ -104,6 +105,16 @@ pub enum Motion {
WindowTop, WindowTop,
WindowMiddle, WindowMiddle,
WindowBottom, 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 don't have a good way to run a search synchronously, so
// we handle search motions by running the search async and then // we handle search motions by running the search async and then
@ -269,6 +280,16 @@ actions!(
WindowTop, WindowTop,
WindowMiddle, WindowMiddle,
WindowBottom, 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::action(editor, cx, |vim, &WindowBottom, cx| {
vim.motion(Motion::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 { impl Vim {
@ -536,6 +588,16 @@ impl Motion {
| WindowTop | WindowTop
| WindowMiddle | WindowMiddle
| WindowBottom | WindowBottom
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| Jump { line: true, .. } => true, | Jump { line: true, .. } => true,
EndOfLine { .. } EndOfLine { .. }
| Matching | Matching
@ -607,6 +669,16 @@ impl Motion {
| NextLineStart | NextLineStart
| PreviousLineStart | PreviousLineStart
| ZedSearchResult { .. } | ZedSearchResult { .. }
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| Jump { .. } => false, | Jump { .. } => false,
} }
} }
@ -652,6 +724,16 @@ impl Motion {
| FirstNonWhitespace { .. } | FirstNonWhitespace { .. }
| FindBackward { .. } | FindBackward { .. }
| Jump { .. } | Jump { .. }
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| ZedSearchResult { .. } => false, | ZedSearchResult { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => { RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
motion.inclusive() motion.inclusive()
@ -867,6 +949,47 @@ impl Motion {
return None; 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)) (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)] #[cfg(test)]
mod test { mod test {

View file

@ -1,6 +1,10 @@
use std::ops::Range; use std::ops::Range;
use crate::{motion::right, state::Mode, Vim}; use crate::{
motion::right,
state::{Mode, Operator},
Vim,
};
use editor::{ use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
movement::{self, FindRange}, movement::{self, FindRange},
@ -10,7 +14,7 @@ use editor::{
use itertools::Itertools; use itertools::Itertools;
use gpui::{actions, impl_actions, ViewContext}; 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 multi_buffer::MultiBufferRow;
use serde::Deserialize; use serde::Deserialize;
@ -30,6 +34,9 @@ pub enum Object {
Argument, Argument,
IndentObj { include_below: bool }, IndentObj { include_below: bool },
Tag, Tag,
Method,
Class,
Comment,
} }
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
@ -61,7 +68,10 @@ actions!(
CurlyBrackets, CurlyBrackets,
AngleBrackets, AngleBrackets,
Argument, 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::action(editor, cx, |vim, _: &Argument, cx| {
vim.object(Object::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( Vim::action(
editor, editor,
cx, cx,
@ -144,6 +166,9 @@ impl Object {
| Object::CurlyBrackets | Object::CurlyBrackets
| Object::SquareBrackets | Object::SquareBrackets
| Object::Argument | Object::Argument
| Object::Method
| Object::Class
| Object::Comment
| Object::IndentObj { .. } => true, | Object::IndentObj { .. } => true,
} }
} }
@ -162,12 +187,15 @@ impl Object {
| Object::Parentheses | Object::Parentheses
| Object::SquareBrackets | Object::SquareBrackets
| Object::Tag | Object::Tag
| Object::Method
| Object::Class
| Object::Comment
| Object::CurlyBrackets | Object::CurlyBrackets
| Object::AngleBrackets => true, | 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 { match self {
Object::Word { .. } Object::Word { .. }
| Object::Sentence | Object::Sentence
@ -186,8 +214,16 @@ impl Object {
| Object::AngleBrackets | Object::AngleBrackets
| Object::VerticalBars | Object::VerticalBars
| Object::Tag | Object::Tag
| Object::Comment
| Object::Argument | Object::Argument
| Object::IndentObj { .. } => Mode::Visual, | Object::IndentObj { .. } => Mode::Visual,
Object::Method | Object::Class => {
if around {
Mode::VisualLine
} else {
Mode::Visual
}
}
Object::Paragraph => Mode::VisualLine, Object::Paragraph => Mode::VisualLine,
} }
} }
@ -238,6 +274,33 @@ 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::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::Argument => argument(map, relative_to, around),
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below), Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
} }
@ -441,6 +504,47 @@ fn around_next_word(
Some(start..end) 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( fn argument(
map: &DisplaySnapshot, map: &DisplaySnapshot,
relative_to: DisplayPoint, relative_to: DisplayPoint,

View file

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

View file

@ -69,6 +69,7 @@ several features:
- Syntax overrides - Syntax overrides
- Text redactions - Text redactions
- Runnable code detection - Runnable code detection
- Selecting classes, functions, etc.
The following sections elaborate on how [Tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) enable these The following sections elaborate on how [Tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) enable these
features in Zed, using [JSON syntax](https://www.json.org/json-en.html) as a guiding example. features in Zed, using [JSON syntax](https://www.json.org/json-en.html) as a guiding example.
@ -259,6 +260,44 @@ For example, in JavaScript, we also disable auto-closing of single quotes within
(comment) @comment.inclusive (comment) @comment.inclusive
``` ```
### Text objects
The `textobjects.scm` file defines rules for navigating by text objects. This was added in Zed v0.165 and is currently used only in Vim mode.
Vim provides two levels of granularity for navigating around files. Section-by-section with `[]` etc., and method-by-method with `]m` etc. Even languages that don't support functions and classes can work well by defining similar concepts. For example CSS defines a rule-set as a method, and a media-query as a class.
For languages with closures, these typically should not count as functions in Zed. This is best-effort however, as languages like Javascript do not syntactically differentiate syntactically between closures and top-level function declarations.
For languages with declarations like C, provide queries that match `@class.around` or `@function.around`. The `if` and `ic` text objects will default to these if there is no inside.
If you are not sure what to put in textobjects.scm, both [nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects), and the [Helix editor](https://github.com/helix-editor/helix) have queries for many languages. You can refer to the Zed [built-in languages](https://github.com/zed-industries/zed/tree/main/crates/languages/src) to see how to adapt these.
| Capture | Description | Vim mode |
| ---------------- | ----------------------------------------------------------------------- | ------------------------------------------------ |
| @function.around | An entire function definition or equivalent small section of a file. | `[m`, `]m`, `[M`,`]M` motions. `af` text object |
| @function.inside | The function body (the stuff within the braces). | `if` text object |
| @class.around | An entire class definition or equivalent large section of a file. | `[[`, `]]`, `[]`, `][` motions. `ac` text object |
| @class.inside | The contents of a class definition. | `ic` text object |
| @comment.around | An entire comment (e.g. all adjacent line comments, or a block comment) | `gc` text object |
| @comment.inside | The contents of a comment | `igc` text object (rarely supported) |
For example:
```scheme
; include only the content of the method in the function
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
; match function.around for declarations with no body
(function_signature_item) @function.around
; join all adjacent comments into one
(comment)+ @comment.around
```
### Text redactions ### Text redactions
The `redactions.scm` file defines text redaction rules. When collaborating and sharing your screen, it makes sure that certain syntax nodes are rendered in a redacted mode to avoid them from leaking. The `redactions.scm` file defines text redaction rules. When collaborating and sharing your screen, it makes sure that certain syntax nodes are rendered in a redacted mode to avoid them from leaking.

View file

@ -79,12 +79,41 @@ The following commands use the language server to help you navigate and refactor
### Treesitter ### Treesitter
Treesitter is a powerful tool that Zed uses to understand the structure of your code. These commands help you navigate your code semantically. Treesitter is a powerful tool that Zed uses to understand the structure of your code. Zed provides motions that change the current cursor position, and text objects that can be used as the target of actions.
| Command | Default Shortcut | | Command | Default Shortcut |
| ---------------------------- | ---------------- | | ------------------------------- | --------------------------- |
| Select a smaller syntax node | `] x` | | Go to next/previous method | `] m` / `[ m` |
| Select a larger syntax node | `[ x` | | Go to next/previous method end | `] M` / `[ M` |
| Go to next/previous section | `] ]` / `[ [` |
| Go to next/previous section end | `] [` / `[ ]` |
| Go to next/previous comment | `] /`, `] *` / `[ /`, `[ *` |
| Select a larger syntax node | `[ x` |
| Select a larger syntax node | `[ x` |
| Text Objects | Default Shortcut |
| ---------------------------------------------------------- | ---------------- |
| Around a class, definition, etc. | `a c` |
| Inside a class, definition, etc. | `i c` |
| Around a function, method etc. | `a f` |
| Inside a function, method, etc. | `i f` |
| A comment | `g c` |
| An argument, or list item, etc. | `i a` |
| An argument, or list item, etc. (including trailing comma) | `a a` |
| Around an HTML-like tag | `i a` |
| Inside an HTML-like tag | `i a` |
| The current indent level, and one line before and after | `a I` |
| The current indent level, and one line before | `a i` |
| The current indent level | `i i` |
Note that the definitions for the targets of the `[m` family of motions are the same as the
boundaries defined by `af`. The targets of the `[[` are the same as those defined by `ac`, though
if there are no classes, then functions are also used. Similarly `gc` is used to find `[ /`. `g c`
The definition of functions, classes and comments is language dependent, and support can be added
to extensions by adding a [`textobjects.scm`]. The definition of arguments and tags operates at
the tree-sitter level, but looks for certain patterns in the parse tree and is not currently configurable
per language.
### Multi cursor ### Multi cursor