Skip to content

Commit

Permalink
Add tree-sitter textobjects
Browse files Browse the repository at this point in the history
Only has functions and class objects as of now.
  • Loading branch information
sudormrfbin committed Sep 9, 2021
1 parent 81d5e5d commit 52fd104
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 4 deletions.
2 changes: 1 addition & 1 deletion helix-core/src/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
45 changes: 43 additions & 2 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
Expand All @@ -55,6 +55,8 @@ pub struct LanguageConfiguration {

#[serde(skip)]
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
#[serde(skip)]
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -84,6 +86,32 @@ pub struct IndentQuery {
pub outdent: HashSet<String>,
}

#[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<impl Iterator<Item = Node<'a>>> {
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<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
}
Expand Down
51 changes: 51 additions & 0 deletions helix-core/src/textobject.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Range> {
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::*;
Expand Down
22 changes: 21 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 52fd104

Please sign in to comment.