Skip to content

Commit

Permalink
Add textobjects queries (#20924)
Browse files Browse the repository at this point in the history
Co-Authored-By: Max <[email protected]>

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 <[email protected]>
  • Loading branch information
ConradIrwin and maxbrunsfeld authored Dec 3, 2024
1 parent c443307 commit 75c9dc1
Show file tree
Hide file tree
Showing 28 changed files with 1,206 additions and 27 deletions.
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions assets/keymaps/vim.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "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
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
Expand Down Expand Up @@ -360,7 +372,8 @@
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators"
"ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
}
},
{
Expand Down Expand Up @@ -389,7 +402,9 @@
">": "vim::AngleBrackets",
"a": "vim::Argument",
"i": "vim::IndentObj",
"shift-i": ["vim::IndentObj", { "includeBelow": true }]
"shift-i": ["vim::IndentObj", { "includeBelow": true }],
"f": "vim::Method",
"c": "vim::Class"
}
},
{
Expand Down
69 changes: 68 additions & 1 deletion crates/language/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions crates/language/src/buffer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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
Expand Down
69 changes: 67 additions & 2 deletions crates/language/src/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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>,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions crates/language/src/language_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)]
Expand Down
Loading

0 comments on commit 75c9dc1

Please sign in to comment.