From 52fd104311061f1273c33aaf12300e99594cf3d1 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Wed, 8 Sep 2021 15:23:07 +0530 Subject: [PATCH] Add tree-sitter textobjects Only has functions and class objects as of now. --- helix-core/src/indent.rs | 2 +- helix-core/src/syntax.rs | 45 +++++++++++++++++++++++++++++-- helix-core/src/textobject.rs | 51 ++++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 22 +++++++++++++++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 558020599d52..e9c87f7329f2 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -473,7 +473,7 @@ where let language_config = loader.language_config_for_scope("source.rust").unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap(); - let syntax = Syntax::new(&doc, highlight_config.clone()); + let syntax = Syntax::new(&doc, highlight_config.clone(), "rust"); let text = doc.slice(..); let tab_width = 4; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 1afe0e25a27b..5a760bea39a7 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -31,7 +31,7 @@ pub struct Configuration { #[serde(rename_all = "kebab-case")] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub(crate) language_id: String, + pub language_id: String, pub scope: String, // source.rust pub file_types: Vec, // filename ends_with? pub roots: Vec, // these indicate project roots <.git, Cargo.toml> @@ -55,6 +55,8 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) indent_query: OnceCell>, + #[serde(skip)] + pub(crate) textobject_query: OnceCell>, } #[derive(Debug, Serialize, Deserialize)] @@ -84,6 +86,32 @@ pub struct IndentQuery { pub outdent: HashSet, } +#[derive(Debug)] +pub struct TextObjectQuery { + pub query: Query, +} + +impl TextObjectQuery { + /// Run the query on the given node and return sub nodes which match given + /// capture ("function.inside", "class.around", etc). + pub fn capture_nodes<'a>( + &'a self, + capture_name: &str, + node: Node<'a>, + slice: RopeSlice<'a>, + cursor: &'a mut QueryCursor, + ) -> Option>> { + let capture_idx = self.query.capture_index_for_name(capture_name)?; + let captures = cursor.captures(&self.query, node, RopeProvider(slice)); + + captures + .filter_map(move |(mat, idx)| { + (mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node) + }) + .into() + } +} + fn load_runtime_file(language: &str, filename: &str) -> Result { let path = crate::RUNTIME_DIR .join("queries") @@ -132,7 +160,6 @@ impl LanguageConfiguration { // highlights_query += "\n(ERROR) @error"; let injections_query = read_query(&language, "injections.scm"); - let locals_query = read_query(&language, "locals.scm"); if highlights_query.is_empty() { @@ -182,6 +209,20 @@ impl LanguageConfiguration { .as_ref() } + pub fn textobject_query(&self) -> Option<&TextObjectQuery> { + self.textobject_query + .get_or_init(|| { + let language = self.language_id.to_ascii_lowercase(); + let query = read_query(&language, "textobjects.scm"); + self.highlight_config + .get() + .and_then(|config| config.as_ref().map(|c| c.language)) + .and_then(move |lang| Query::new(lang, &query).ok()) + .map(|query| TextObjectQuery { query }) + }) + .as_ref() + } + pub fn scope(&self) -> &str { &self.scope } diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index b965f6dfccc0..975ed115bc03 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -1,9 +1,13 @@ +use std::fmt::Display; + use ropey::RopeSlice; +use tree_sitter::{Node, QueryCursor}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::graphemes::next_grapheme_boundary; use crate::movement::Direction; use crate::surround; +use crate::syntax::LanguageConfiguration; use crate::Range; fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { @@ -51,6 +55,15 @@ pub enum TextObject { Inside, } +impl Display for TextObject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Around => "around", + Self::Inside => "inside", + }) + } +} + // count doesn't do anything yet pub fn textobject_word( slice: RopeSlice, @@ -108,6 +121,44 @@ pub fn textobject_surround( .unwrap_or(range) } +/// Transform the given range to select text objects based on tree-sitter. +/// `object_name` is a query capture base name like "function", "class", etc. +/// `slice_tree` is the tree-sitter node corresponding to given text slice. +pub fn textobject_treesitter( + slice: RopeSlice, + range: Range, + textobject: TextObject, + object_name: &str, + slice_tree: Node, + lang_config: &LanguageConfiguration, + _count: usize, +) -> Range { + let get_range = move || -> Option { + let byte_pos = slice.char_to_byte(range.cursor(slice)); + + let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner + let mut cursor = QueryCursor::new(); + let node = lang_config + .textobject_query()? + .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)? + .filter(|node| node.byte_range().contains(&byte_pos)) + .min_by_key(|node| node.byte_range().len())?; + + let len = slice.len_bytes(); + let start_byte = node.start_byte(); + let end_byte = node.end_byte(); + if start_byte >= len || end_byte >= len { + return None; + } + + let start_char = slice.byte_to_char(start_byte); + let end_char = slice.byte_to_char(end_byte); + + Some(Range::new(start_char, end_char)) + }; + get_range().unwrap_or(range) +} + #[cfg(test)] mod test { use super::TextObject::*; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a7a715763e05..6be6cb33d5fa 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4167,7 +4167,27 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ch if !ch.is_ascii_alphanumeric() => { textobject::textobject_surround(text, range, objtype, ch, count) } - _ => range, + ch => { + // tree-sitter objects + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => return range, + }; + let obj_name = match ch { + 'f' => "function", + 'c' => "class", + _ => return range, + }; + textobject::textobject_treesitter( + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) + } } }); doc.set_selection(view.id, selection);