Skip to content

Commit

Permalink
Use TreeCursor to pretty-print :tree-sitter-subtree
Browse files Browse the repository at this point in the history
The current `:tree-sitter-subtree` has a bug for field-names when the
field name belongs to an unnamed child node. Take this ruby example:

    def self.method_name
      true
    end

The subtree given by tree-sitter-cli is:

    (singleton_method [2, 0] - [4, 3]
      object: (self [2, 4] - [2, 8])
      name: (identifier [2, 9] - [2, 20])
      body: (body_statement [3, 2] - [3, 6]
        (true [3, 2] - [3, 6])))

But the `:tree-sitter-subtree` output was

    (singleton_method
      object: (self)
      body: (identifier)
      (body_statement (true)))

The `singleton_method` rule defines the `name` and `body` fields in an
unnamed helper rule `_method_rest` and the old implementation of
`pretty_print_tree_impl` would pass the `field_name` down from the
named `singleton_method` node.

To fix it we switch to the [TreeCursor] API which is recommended by
the tree-sitter docs for traversing the tree. `TreeCursor::field_name`
accurately determines the field name for the current cursor position
even when the node is unnamed.

[TreeCursor]: https://docs.rs/tree-sitter/0.20.9/tree_sitter/struct.TreeCursor.html
  • Loading branch information
the-mikedavis committed Nov 5, 2022
1 parent 3814987 commit a48ec87
Showing 1 changed file with 65 additions and 30 deletions.
95 changes: 65 additions & 30 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, TextProvider, Tree,
QueryMatch, Range, TextProvider, Tree, TreeCursor,
};

const CANCELLATION_CHECK_INTERVAL: usize = 100;
Expand Down Expand Up @@ -2130,57 +2130,67 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
}
}

fn node_is_visible(node: &Node) -> bool {
node.is_missing() || (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}

pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result {
pretty_print_tree_impl(fmt, node, true, None, 0)
if node.child_count() == 0 {
if node_is_visible(&node) {
write!(fmt, "({})", node.kind())
} else {
write!(fmt, "\"{}\"", node.kind())
}
} else {
pretty_print_tree_impl(fmt, &mut node.walk(), 0)
}
}

fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W,
node: Node,
is_root: bool,
field_name: Option<&str>,
cursor: &mut TreeCursor,
depth: usize,
) -> fmt::Result {
fn is_visible(node: Node) -> bool {
node.is_missing()
|| (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}
let node = cursor.node();
let visible = node_is_visible(&node);

if is_visible(node) {
if visible {
let indentation_columns = depth * 2;
write!(fmt, "{:indentation_columns$}", "")?;

if let Some(field_name) = field_name {
if let Some(field_name) = cursor.field_name() {
write!(fmt, "{}: ", field_name)?;
}

write!(fmt, "({}", node.kind())?;
} else if is_root {
write!(fmt, "(\"{}\")", node.kind())?;
}

for child_idx in 0..node.child_count() {
if let Some(child) = node.child(child_idx) {
if is_visible(child) {
// Handle children.
if cursor.goto_first_child() {
loop {
if node_is_visible(&cursor.node()) {
fmt.write_char('\n')?;
}

pretty_print_tree_impl(
fmt,
child,
false,
node.field_name_for_child(child_idx as u32),
depth + 1,
)?;
pretty_print_tree_impl(fmt, cursor, depth + 1)?;

if !cursor.goto_next_sibling() {
break;
}
}

// The parent of the first child must exist, and must be `node`.
debug_assert!(cursor.goto_parent());
debug_assert!(cursor.node() == node);
}

if is_visible(node) {
write!(fmt, ")")?;
if visible {
fmt.write_char(')')?;
}

Ok(())
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -2353,11 +2363,17 @@ mod test {
}

#[track_caller]
fn assert_pretty_print(source: &str, expected: &str, start: usize, end: usize) {
fn assert_pretty_print(
language_name: &str,
source: &str,
expected: &str,
start: usize,
end: usize,
) {
let source = Rope::from_str(source);

let loader = Loader::new(Configuration { language: vec![] });
let language = get_language("rust").unwrap();
let language = get_language(language_name).unwrap();

let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
Expand All @@ -2377,13 +2393,14 @@ mod test {
#[test]
fn test_pretty_print() {
let source = r#"/// Hello"#;
assert_pretty_print(source, "(line_comment)", 0, source.len());
assert_pretty_print("rust", source, "(line_comment)", 0, source.len());

// A large tree should be indented with fields:
let source = r#"fn main() {
println!("Hello, World!");
}"#;
assert_pretty_print(
"rust",
source,
concat!(
"(function_item\n",
Expand All @@ -2402,11 +2419,29 @@ mod test {

// Selecting a token should print just that token:
let source = r#"fn main() {}"#;
assert_pretty_print(source, r#"("fn")"#, 0, 1);
assert_pretty_print("rust", source, r#""fn""#, 0, 1);

// Error nodes are printed as errors:
let source = r#"}{"#;
assert_pretty_print(source, "(ERROR)", 0, source.len());
assert_pretty_print("rust", source, "(ERROR)", 0, source.len());

// Fields broken under unnamed nodes are determined correctly.
let source = "def self.method_name
true
end";
assert_pretty_print(
"ruby",
source,
concat!(
"(singleton_method\n",
" object: (self)\n",
" name: (identifier)\n",
" body: (body_statement\n",
" (true)))"
),
0,
source.len(),
);
}

#[test]
Expand Down

0 comments on commit a48ec87

Please sign in to comment.