Add a new and improved syntax tree viewer

This commit is contained in:
Giga Bowser 2024-12-20 19:12:04 -05:00
parent c0eaff7dd1
commit 5ffe45d8cd
14 changed files with 759 additions and 4 deletions

View file

@ -56,6 +56,7 @@ mod view_hir;
mod view_item_tree;
mod view_memory_layout;
mod view_mir;
mod view_syntax_tree;
use std::{iter, panic::UnwindSafe};
@ -339,6 +340,10 @@ impl Analysis {
self.with_db(|db| syntax_tree::syntax_tree(db, file_id, text_range))
}
pub fn view_syntax_tree(&self, file_id: FileId) -> Cancellable<String> {
self.with_db(|db| view_syntax_tree::view_syntax_tree(db, file_id))
}
pub fn view_hir(&self, position: FilePosition) -> Cancellable<String> {
self.with_db(|db| view_hir::view_hir(db, position))
}

View file

@ -4,9 +4,9 @@ use syntax::{
AstNode, NodeOrToken, SourceFile, SyntaxKind::STRING, SyntaxToken, TextRange, TextSize,
};
// Feature: Show Syntax Tree
// Feature: Show Debug Syntax Tree
//
// Shows the parse tree of the current file. It exists mostly for debugging
// Shows the textual parse tree of the current file. It exists mostly for debugging
// rust-analyzer itself.
//
// |===

View file

@ -0,0 +1,226 @@
use hir::Semantics;
use ide_db::{FileId, RootDatabase};
use span::TextRange;
use stdx::format_to;
use syntax::{
ast::{self, IsString},
AstNode, AstToken, NodeOrToken, SourceFile, SyntaxNode, SyntaxToken, WalkEvent,
};
// Feature: Show Syntax Tree
//
// Shows a tree view with the syntax tree of the current file
//
// |===
// | Editor | Panel Name
//
// | VS Code | **Rust Syntax Tree**
// |===
pub(crate) fn view_syntax_tree(db: &RootDatabase, file_id: FileId) -> String {
let sema = Semantics::new(db);
let parse = sema.parse_guess_edition(file_id);
syntax_node_to_json(parse.syntax(), None)
}
fn syntax_node_to_json(node: &SyntaxNode, ctx: Option<InStringCtx>) -> String {
let mut result = String::new();
for event in node.preorder_with_tokens() {
match event {
WalkEvent::Enter(it) => {
let kind = it.kind();
let (text_range, inner_range_str) = match &ctx {
Some(ctx) => {
let inner_start: u32 = it.text_range().start().into();
let inner_end: u32 = it.text_range().end().into();
let mut true_start = inner_start + ctx.offset;
let mut true_end = inner_end + ctx.offset;
for pos in &ctx.marker_positions {
if *pos >= inner_end {
break;
}
// We conditionally add to true_start in case
// the marker is between the start and end.
true_start += 2 * (*pos < inner_start) as u32;
true_end += 2;
}
let true_range = TextRange::new(true_start.into(), true_end.into());
(
true_range,
format!(
r#","istart":{:?},"iend":{:?}"#,
it.text_range().start(),
it.text_range().end()
),
)
}
None => (it.text_range(), "".to_owned()),
};
let start = text_range.start();
let end = text_range.end();
match it {
NodeOrToken::Node(_) => {
format_to!(
result,
r#"{{"type":"Node","kind":"{kind:?}","start":{start:?},"end":{end:?}{inner_range_str},"children":["#
);
}
NodeOrToken::Token(token) => {
let comma = if token.next_sibling_or_token().is_some() { "," } else { "" };
match parse_rust_string(token) {
Some(parsed) => {
format_to!(
result,
r#"{{"type":"Node","kind":"{kind:?}","start":{start:?},"end":{end:?}{inner_range_str},"children":[{parsed}]}}{comma}"#
);
}
None => format_to!(
result,
r#"{{"type":"Token","kind":"{kind:?}","start":{start:?},"end":{end:?}{inner_range_str}}}{comma}"#
),
}
}
}
}
WalkEvent::Leave(it) => match it {
NodeOrToken::Node(node) => {
let comma = if node.next_sibling_or_token().is_some() { "," } else { "" };
format_to!(result, "]}}{comma}")
}
NodeOrToken::Token(_) => (),
},
}
}
result
}
fn parse_rust_string(token: SyntaxToken) -> Option<String> {
let string_node = ast::String::cast(token)?;
let text = string_node.value().ok()?;
let mut trim_result = String::new();
let mut marker_positions = Vec::new();
let mut skipped = 0;
let mut last_end = 0;
for (start, part) in text.match_indices("$0") {
marker_positions.push((start - skipped) as u32);
trim_result.push_str(&text[last_end..start]);
skipped += part.len();
last_end = start + part.len();
}
trim_result.push_str(&text[last_end..text.len()]);
let parsed = SourceFile::parse(&trim_result, span::Edition::CURRENT);
if !parsed.errors().is_empty() {
return None;
}
let node: &SyntaxNode = &parsed.syntax_node();
if node.children().count() == 0 {
// C'mon, you should have at least one node other than SOURCE_FILE
return None;
}
Some(syntax_node_to_json(
node,
Some(InStringCtx {
offset: string_node.text_range_between_quotes()?.start().into(),
marker_positions,
}),
))
}
struct InStringCtx {
offset: u32,
marker_positions: Vec<u32>,
}
#[cfg(test)]
mod tests {
use expect_test::expect;
use crate::fixture;
fn check(ra_fixture: &str, expect: expect_test::Expect) {
let (analysis, file_id) = fixture::file(ra_fixture);
let syn = analysis.view_syntax_tree(file_id).unwrap();
expect.assert_eq(&syn)
}
#[test]
fn view_syntax_tree() {
// Basic syntax
check(
r#"fn foo() {}"#,
expect![[
r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":11,"children":[{"type":"Node","kind":"FN","start":0,"end":11,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":6,"children":[{"type":"Token","kind":"IDENT","start":3,"end":6}]},{"type":"Node","kind":"PARAM_LIST","start":6,"end":8,"children":[{"type":"Token","kind":"L_PAREN","start":6,"end":7},{"type":"Token","kind":"R_PAREN","start":7,"end":8}]},{"type":"Token","kind":"WHITESPACE","start":8,"end":9},{"type":"Node","kind":"BLOCK_EXPR","start":9,"end":11,"children":[{"type":"Node","kind":"STMT_LIST","start":9,"end":11,"children":[{"type":"Token","kind":"L_CURLY","start":9,"end":10},{"type":"Token","kind":"R_CURLY","start":10,"end":11}]}]}]}]}"#
]],
);
check(
r#"
fn test() {
assert!("
fn foo() {
}
", "");
}"#,
expect![[
r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":60,"children":[{"type":"Node","kind":"FN","start":0,"end":60,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":60,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":60,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":58,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":57,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":57,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":57,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":52,"children":[{"type":"Node","kind":"SOURCE_FILE","start":25,"end":51,"istart":0,"iend":26,"children":[{"type":"Token","kind":"WHITESPACE","start":25,"end":30,"istart":0,"iend":5},{"type":"Node","kind":"FN","start":30,"end":46,"istart":5,"iend":21,"children":[{"type":"Token","kind":"FN_KW","start":30,"end":32,"istart":5,"iend":7},{"type":"Token","kind":"WHITESPACE","start":32,"end":33,"istart":7,"iend":8},{"type":"Node","kind":"NAME","start":33,"end":36,"istart":8,"iend":11,"children":[{"type":"Token","kind":"IDENT","start":33,"end":36,"istart":8,"iend":11}]},{"type":"Node","kind":"PARAM_LIST","start":36,"end":38,"istart":11,"iend":13,"children":[{"type":"Token","kind":"L_PAREN","start":36,"end":37,"istart":11,"iend":12},{"type":"Token","kind":"R_PAREN","start":37,"end":38,"istart":12,"iend":13}]},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":13,"iend":14},{"type":"Node","kind":"BLOCK_EXPR","start":39,"end":46,"istart":14,"iend":21,"children":[{"type":"Node","kind":"STMT_LIST","start":39,"end":46,"istart":14,"iend":21,"children":[{"type":"Token","kind":"L_CURLY","start":39,"end":40,"istart":14,"iend":15},{"type":"Token","kind":"WHITESPACE","start":40,"end":45,"istart":15,"iend":20},{"type":"Token","kind":"R_CURLY","start":45,"end":46,"istart":20,"iend":21}]}]}]},{"type":"Token","kind":"WHITESPACE","start":46,"end":51,"istart":21,"iend":26}]}]},{"type":"Token","kind":"COMMA","start":52,"end":53},{"type":"Token","kind":"WHITESPACE","start":53,"end":54},{"type":"Token","kind":"STRING","start":54,"end":56},{"type":"Token","kind":"R_PAREN","start":56,"end":57}]}]}]},{"type":"Token","kind":"SEMICOLON","start":57,"end":58}]},{"type":"Token","kind":"WHITESPACE","start":58,"end":59},{"type":"Token","kind":"R_CURLY","start":59,"end":60}]}]}]}]}"#
]],
)
}
#[test]
fn view_syntax_tree_inside_string() {
check(
r#"fn test() {
assert!("
$0fn foo() {
}$0
fn bar() {
}
", "");
}"#,
expect![[
r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":65,"children":[{"type":"Node","kind":"FN","start":0,"end":65,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":65,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":65,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":63,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":62,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":62,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":62,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":57,"children":[{"type":"Node","kind":"SOURCE_FILE","start":25,"end":56,"istart":0,"iend":31,"children":[{"type":"Token","kind":"WHITESPACE","start":25,"end":26,"istart":0,"iend":1},{"type":"Node","kind":"FN","start":26,"end":38,"istart":1,"iend":13,"children":[{"type":"Token","kind":"FN_KW","start":26,"end":28,"istart":1,"iend":3},{"type":"Token","kind":"WHITESPACE","start":28,"end":29,"istart":3,"iend":4},{"type":"Node","kind":"NAME","start":29,"end":32,"istart":4,"iend":7,"children":[{"type":"Token","kind":"IDENT","start":29,"end":32,"istart":4,"iend":7}]},{"type":"Node","kind":"PARAM_LIST","start":32,"end":34,"istart":7,"iend":9,"children":[{"type":"Token","kind":"L_PAREN","start":32,"end":33,"istart":7,"iend":8},{"type":"Token","kind":"R_PAREN","start":33,"end":34,"istart":8,"iend":9}]},{"type":"Token","kind":"WHITESPACE","start":34,"end":35,"istart":9,"iend":10},{"type":"Node","kind":"BLOCK_EXPR","start":35,"end":38,"istart":10,"iend":13,"children":[{"type":"Node","kind":"STMT_LIST","start":35,"end":38,"istart":10,"iend":13,"children":[{"type":"Token","kind":"L_CURLY","start":35,"end":36,"istart":10,"iend":11},{"type":"Token","kind":"WHITESPACE","start":36,"end":37,"istart":11,"iend":12},{"type":"Token","kind":"R_CURLY","start":37,"end":38,"istart":12,"iend":13}]}]}]},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":13,"iend":14},{"type":"Node","kind":"FN","start":39,"end":51,"istart":14,"iend":26,"children":[{"type":"Token","kind":"FN_KW","start":39,"end":41,"istart":14,"iend":16},{"type":"Token","kind":"WHITESPACE","start":41,"end":42,"istart":16,"iend":17},{"type":"Node","kind":"NAME","start":42,"end":45,"istart":17,"iend":20,"children":[{"type":"Token","kind":"IDENT","start":42,"end":45,"istart":17,"iend":20}]},{"type":"Node","kind":"PARAM_LIST","start":45,"end":47,"istart":20,"iend":22,"children":[{"type":"Token","kind":"L_PAREN","start":45,"end":46,"istart":20,"iend":21},{"type":"Token","kind":"R_PAREN","start":46,"end":47,"istart":21,"iend":22}]},{"type":"Token","kind":"WHITESPACE","start":47,"end":48,"istart":22,"iend":23},{"type":"Node","kind":"BLOCK_EXPR","start":48,"end":51,"istart":23,"iend":26,"children":[{"type":"Node","kind":"STMT_LIST","start":48,"end":51,"istart":23,"iend":26,"children":[{"type":"Token","kind":"L_CURLY","start":48,"end":49,"istart":23,"iend":24},{"type":"Token","kind":"WHITESPACE","start":49,"end":50,"istart":24,"iend":25},{"type":"Token","kind":"R_CURLY","start":50,"end":51,"istart":25,"iend":26}]}]}]},{"type":"Token","kind":"WHITESPACE","start":51,"end":56,"istart":26,"iend":31}]}]},{"type":"Token","kind":"COMMA","start":57,"end":58},{"type":"Token","kind":"WHITESPACE","start":58,"end":59},{"type":"Token","kind":"STRING","start":59,"end":61},{"type":"Token","kind":"R_PAREN","start":61,"end":62}]}]}]},{"type":"Token","kind":"SEMICOLON","start":62,"end":63}]},{"type":"Token","kind":"WHITESPACE","start":63,"end":64},{"type":"Token","kind":"R_CURLY","start":64,"end":65}]}]}]}]}"#
]],
);
// With a raw string
check(
r###"fn test() {
assert!(r#"
$0fn foo() {
}$0
fn bar() {
}
"#, "");
}"###,
expect![[
r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":68,"children":[{"type":"Node","kind":"FN","start":0,"end":68,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":68,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":68,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":66,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":65,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":65,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":65,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":60,"children":[{"type":"Node","kind":"SOURCE_FILE","start":27,"end":58,"istart":0,"iend":31,"children":[{"type":"Token","kind":"WHITESPACE","start":27,"end":28,"istart":0,"iend":1},{"type":"Node","kind":"FN","start":28,"end":40,"istart":1,"iend":13,"children":[{"type":"Token","kind":"FN_KW","start":28,"end":30,"istart":1,"iend":3},{"type":"Token","kind":"WHITESPACE","start":30,"end":31,"istart":3,"iend":4},{"type":"Node","kind":"NAME","start":31,"end":34,"istart":4,"iend":7,"children":[{"type":"Token","kind":"IDENT","start":31,"end":34,"istart":4,"iend":7}]},{"type":"Node","kind":"PARAM_LIST","start":34,"end":36,"istart":7,"iend":9,"children":[{"type":"Token","kind":"L_PAREN","start":34,"end":35,"istart":7,"iend":8},{"type":"Token","kind":"R_PAREN","start":35,"end":36,"istart":8,"iend":9}]},{"type":"Token","kind":"WHITESPACE","start":36,"end":37,"istart":9,"iend":10},{"type":"Node","kind":"BLOCK_EXPR","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Node","kind":"STMT_LIST","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Token","kind":"L_CURLY","start":37,"end":38,"istart":10,"iend":11},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":11,"iend":12},{"type":"Token","kind":"R_CURLY","start":39,"end":40,"istart":12,"iend":13}]}]}]},{"type":"Token","kind":"WHITESPACE","start":40,"end":41,"istart":13,"iend":14},{"type":"Node","kind":"FN","start":41,"end":53,"istart":14,"iend":26,"children":[{"type":"Token","kind":"FN_KW","start":41,"end":43,"istart":14,"iend":16},{"type":"Token","kind":"WHITESPACE","start":43,"end":44,"istart":16,"iend":17},{"type":"Node","kind":"NAME","start":44,"end":47,"istart":17,"iend":20,"children":[{"type":"Token","kind":"IDENT","start":44,"end":47,"istart":17,"iend":20}]},{"type":"Node","kind":"PARAM_LIST","start":47,"end":49,"istart":20,"iend":22,"children":[{"type":"Token","kind":"L_PAREN","start":47,"end":48,"istart":20,"iend":21},{"type":"Token","kind":"R_PAREN","start":48,"end":49,"istart":21,"iend":22}]},{"type":"Token","kind":"WHITESPACE","start":49,"end":50,"istart":22,"iend":23},{"type":"Node","kind":"BLOCK_EXPR","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Node","kind":"STMT_LIST","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Token","kind":"L_CURLY","start":50,"end":51,"istart":23,"iend":24},{"type":"Token","kind":"WHITESPACE","start":51,"end":52,"istart":24,"iend":25},{"type":"Token","kind":"R_CURLY","start":52,"end":53,"istart":25,"iend":26}]}]}]},{"type":"Token","kind":"WHITESPACE","start":53,"end":58,"istart":26,"iend":31}]}]},{"type":"Token","kind":"COMMA","start":60,"end":61},{"type":"Token","kind":"WHITESPACE","start":61,"end":62},{"type":"Token","kind":"STRING","start":62,"end":64},{"type":"Token","kind":"R_PAREN","start":64,"end":65}]}]}]},{"type":"Token","kind":"SEMICOLON","start":65,"end":66}]},{"type":"Token","kind":"WHITESPACE","start":66,"end":67},{"type":"Token","kind":"R_CURLY","start":67,"end":68}]}]}]}]}"#
]],
);
// With a raw string
check(
r###"fn test() {
assert!(r$0#"
fn foo() {
}
fn bar() {
}"$0#, "");
}"###,
expect![[
r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":63,"children":[{"type":"Node","kind":"FN","start":0,"end":63,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":63,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":63,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":61,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":60,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":60,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":60,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":55,"children":[{"type":"Node","kind":"SOURCE_FILE","start":27,"end":53,"istart":0,"iend":26,"children":[{"type":"Token","kind":"WHITESPACE","start":27,"end":28,"istart":0,"iend":1},{"type":"Node","kind":"FN","start":28,"end":40,"istart":1,"iend":13,"children":[{"type":"Token","kind":"FN_KW","start":28,"end":30,"istart":1,"iend":3},{"type":"Token","kind":"WHITESPACE","start":30,"end":31,"istart":3,"iend":4},{"type":"Node","kind":"NAME","start":31,"end":34,"istart":4,"iend":7,"children":[{"type":"Token","kind":"IDENT","start":31,"end":34,"istart":4,"iend":7}]},{"type":"Node","kind":"PARAM_LIST","start":34,"end":36,"istart":7,"iend":9,"children":[{"type":"Token","kind":"L_PAREN","start":34,"end":35,"istart":7,"iend":8},{"type":"Token","kind":"R_PAREN","start":35,"end":36,"istart":8,"iend":9}]},{"type":"Token","kind":"WHITESPACE","start":36,"end":37,"istart":9,"iend":10},{"type":"Node","kind":"BLOCK_EXPR","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Node","kind":"STMT_LIST","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Token","kind":"L_CURLY","start":37,"end":38,"istart":10,"iend":11},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":11,"iend":12},{"type":"Token","kind":"R_CURLY","start":39,"end":40,"istart":12,"iend":13}]}]}]},{"type":"Token","kind":"WHITESPACE","start":40,"end":41,"istart":13,"iend":14},{"type":"Node","kind":"FN","start":41,"end":53,"istart":14,"iend":26,"children":[{"type":"Token","kind":"FN_KW","start":41,"end":43,"istart":14,"iend":16},{"type":"Token","kind":"WHITESPACE","start":43,"end":44,"istart":16,"iend":17},{"type":"Node","kind":"NAME","start":44,"end":47,"istart":17,"iend":20,"children":[{"type":"Token","kind":"IDENT","start":44,"end":47,"istart":17,"iend":20}]},{"type":"Node","kind":"PARAM_LIST","start":47,"end":49,"istart":20,"iend":22,"children":[{"type":"Token","kind":"L_PAREN","start":47,"end":48,"istart":20,"iend":21},{"type":"Token","kind":"R_PAREN","start":48,"end":49,"istart":21,"iend":22}]},{"type":"Token","kind":"WHITESPACE","start":49,"end":50,"istart":22,"iend":23},{"type":"Node","kind":"BLOCK_EXPR","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Node","kind":"STMT_LIST","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Token","kind":"L_CURLY","start":50,"end":51,"istart":23,"iend":24},{"type":"Token","kind":"WHITESPACE","start":51,"end":52,"istart":24,"iend":25},{"type":"Token","kind":"R_CURLY","start":52,"end":53,"istart":25,"iend":26}]}]}]}]}]},{"type":"Token","kind":"COMMA","start":55,"end":56},{"type":"Token","kind":"WHITESPACE","start":56,"end":57},{"type":"Token","kind":"STRING","start":57,"end":59},{"type":"Token","kind":"R_PAREN","start":59,"end":60}]}]}]},{"type":"Token","kind":"SEMICOLON","start":60,"end":61}]},{"type":"Token","kind":"WHITESPACE","start":61,"end":62},{"type":"Token","kind":"R_CURLY","start":62,"end":63}]}]}]}]}"#
]],
);
}
}

View file

@ -148,6 +148,16 @@ pub(crate) fn handle_syntax_tree(
Ok(res)
}
pub(crate) fn handle_view_syntax_tree(
snap: GlobalStateSnapshot,
params: lsp_ext::ViewSyntaxTreeParams,
) -> anyhow::Result<String> {
let _p = tracing::info_span!("handle_view_syntax_tree").entered();
let id = from_proto::file_id(&snap, &params.text_document.uri)?;
let res = snap.analysis.view_syntax_tree(id)?;
Ok(res)
}
pub(crate) fn handle_view_hir(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,

View file

@ -123,6 +123,20 @@ pub struct SyntaxTreeParams {
pub range: Option<Range>,
}
pub enum ViewSyntaxTree {}
impl Request for ViewSyntaxTree {
type Params = ViewSyntaxTreeParams;
type Result = String;
const METHOD: &'static str = "rust-analyzer/viewSyntaxTree";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ViewSyntaxTreeParams {
pub text_document: TextDocumentIdentifier,
}
pub enum ViewHir {}
impl Request for ViewHir {

View file

@ -1146,6 +1146,7 @@ impl GlobalState {
.on::<NO_RETRY, lsp_ext::Ssr>(handlers::handle_ssr)
.on::<NO_RETRY, lsp_ext::ViewRecursiveMemoryLayout>(handlers::handle_view_recursive_memory_layout)
.on::<NO_RETRY, lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
.on::<NO_RETRY, lsp_ext::ViewSyntaxTree>(handlers::handle_view_syntax_tree)
.on::<NO_RETRY, lsp_ext::ViewHir>(handlers::handle_view_hir)
.on::<NO_RETRY, lsp_ext::ViewMir>(handlers::handle_view_mir)
.on::<NO_RETRY, lsp_ext::InterpretFunction>(handlers::handle_interpret_function)

View file

@ -1,5 +1,5 @@
<!---
lsp/ext.rs hash: 6dd762ae19630ec0
lsp/ext.rs hash: a85494375e528064
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@ -710,6 +710,23 @@ interface SyntaxTreeParams {
Returns textual representation of a parse tree for the file/selected region.
Primarily for debugging, but very useful for all people working on rust-analyzer itself.
## View Syntax Tree
**Method:** `rust-analyzer/viewSyntaxTree`
**Request:**
```typescript
interface ViewSyntaxTreeParams {
textDocument: TextDocumentIdentifier,
}
```
**Response:** `string`
Returns json representation of the file's syntax tree.
Used to create a treeView for debugging and working on rust-analyzer itself.
## View Hir
**Method:** `rust-analyzer/viewHir`

View file

@ -288,6 +288,24 @@
"title": "Reveal File",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.syntaxTreeReveal",
"title": "Reveal Syntax Element",
"icon": "$(search)",
"category": "rust-analyzer (syntax tree)"
},
{
"command": "rust-analyzer.syntaxTreeHideWhitespace",
"title": "Hide Whitespace",
"icon": "$(filter)",
"category": "rust-analyzer (syntax tree)"
},
{
"command": "rust-analyzer.syntaxTreeShowWhitespace",
"title": "Show Whitespace",
"icon": "$(filter-filled)",
"category": "rust-analyzer (syntax tree)"
},
{
"command": "rust-analyzer.viewMemoryLayout",
"title": "View Memory Layout",
@ -345,6 +363,11 @@
"default": true,
"type": "boolean"
},
"rust-analyzer.showSyntaxTree": {
"markdownDescription": "Whether to show the syntax tree view.",
"default": true,
"type": "boolean"
},
"rust-analyzer.testExplorer": {
"markdownDescription": "Whether to show the test explorer.",
"default": false,
@ -3362,6 +3385,18 @@
},
{
"command": "rust-analyzer.openWalkthrough"
},
{
"command": "rust-analyzer.syntaxTreeReveal",
"when": "false"
},
{
"command": "rust-analyzer.syntaxTreeHideWhitespace",
"when": "false"
},
{
"command": "rust-analyzer.syntaxTreeShowWhitespace",
"when": "false"
}
],
"editor/context": [
@ -3375,6 +3410,25 @@
"when": "inRustProject && editorTextFocus && editorLangId == rust",
"group": "navigation@1001"
}
],
"view/title": [
{
"command": "rust-analyzer.syntaxTreeHideWhitespace",
"group": "navigation",
"when": "view == rustSyntaxTree && !rustSyntaxTree.hideWhitespace"
},
{
"command": "rust-analyzer.syntaxTreeShowWhitespace",
"group": "navigation",
"when": "view == rustSyntaxTree && rustSyntaxTree.hideWhitespace"
}
],
"view/item/context": [
{
"command": "rust-analyzer.syntaxTreeReveal",
"group": "inline",
"when": "view == rustSyntaxTree"
}
]
},
"views": {
@ -3384,6 +3438,22 @@
"name": "Rust Dependencies",
"when": "inRustProject && config.rust-analyzer.showDependenciesExplorer"
}
],
"rustSyntaxTreeContainer": [
{
"id": "rustSyntaxTree",
"name": "Rust Syntax Tree",
"when": "inRustProject && config.rust-analyzer.showSyntaxTree"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "rustSyntaxTreeContainer",
"title": "Rust Syntax Tree",
"icon": "$(list-tree)"
}
]
},
"jsonValidation": [

View file

@ -31,6 +31,7 @@ import type { LanguageClient } from "vscode-languageclient/node";
import { HOVER_REFERENCE_COMMAND } from "./client";
import type { DependencyId } from "./dependencies_provider";
import { log } from "./util";
import type { SyntaxElement } from "./syntax_tree_provider";
export * from "./ast_inspector";
export * from "./run";
@ -357,6 +358,38 @@ export async function execRevealDependency(e: RustEditor): Promise<void> {
await vscode.commands.executeCommand("rust-analyzer.revealDependency", e);
}
export function syntaxTreeReveal(): Cmd {
return async (element: SyntaxElement) => {
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor !== undefined) {
const start = activeEditor.document.positionAt(element.start);
const end = activeEditor.document.positionAt(element.end);
const newSelection = new vscode.Selection(start, end);
activeEditor.selection = newSelection;
activeEditor.revealRange(newSelection);
}
};
}
export function syntaxTreeHideWhitespace(ctx: CtxInit): Cmd {
return async () => {
if (ctx.syntaxTreeProvider !== undefined) {
await ctx.syntaxTreeProvider.toggleWhitespace();
}
};
}
export function syntaxTreeShowWhitespace(ctx: CtxInit): Cmd {
return async () => {
if (ctx.syntaxTreeProvider !== undefined) {
await ctx.syntaxTreeProvider.toggleWhitespace();
}
};
}
export function ssr(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;

View file

@ -351,6 +351,10 @@ export class Config {
return this.get<boolean>("showDependenciesExplorer");
}
get showSyntaxTree() {
return this.get<boolean>("showSyntaxTree");
}
get statusBarClickAction() {
return this.get<string>("statusBar.clickAction");
}

View file

@ -19,6 +19,7 @@ import {
RustDependenciesProvider,
type DependencyId,
} from "./dependencies_provider";
import { SyntaxTreeProvider, type SyntaxElement } from "./syntax_tree_provider";
import { execRevealDependency } from "./commands";
import { PersistentState } from "./persistent_state";
import { bootstrap } from "./bootstrap";
@ -85,7 +86,11 @@ export class Ctx implements RustAnalyzerExtensionApi {
private commandDisposables: Disposable[];
private unlinkedFiles: vscode.Uri[];
private _dependenciesProvider: RustDependenciesProvider | undefined;
private _dependencyTreeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined;
private _dependencyTreeView:
| vscode.TreeView<Dependency | DependencyFile | DependencyId>
| undefined;
private _syntaxTreeProvider: SyntaxTreeProvider | undefined;
private _syntaxTreeView: vscode.TreeView<SyntaxElement> | undefined;
private lastStatus: ServerStatusParams | { health: "stopped" } = { health: "stopped" };
private _serverVersion: string;
private statusBarActiveEditorListener: Disposable;
@ -110,6 +115,14 @@ export class Ctx implements RustAnalyzerExtensionApi {
return this._dependenciesProvider;
}
get syntaxTreeView() {
return this._syntaxTreeView;
}
get syntaxTreeProvider() {
return this._syntaxTreeProvider;
}
constructor(
readonly extCtx: vscode.ExtensionContext,
commandFactories: Record<string, CommandFactory>,
@ -278,6 +291,9 @@ export class Ctx implements RustAnalyzerExtensionApi {
if (this.config.showDependenciesExplorer) {
this.prepareTreeDependenciesView(client);
}
if (this.config.showSyntaxTree) {
this.prepareSyntaxTreeView(client);
}
}
private prepareTreeDependenciesView(client: lc.LanguageClient) {
@ -326,6 +342,56 @@ export class Ctx implements RustAnalyzerExtensionApi {
);
}
private prepareSyntaxTreeView(client: lc.LanguageClient) {
const ctxInit: CtxInit = {
...this,
client: client,
};
this._syntaxTreeProvider = new SyntaxTreeProvider(ctxInit);
this._syntaxTreeView = vscode.window.createTreeView("rustSyntaxTree", {
treeDataProvider: this._syntaxTreeProvider,
showCollapseAll: true,
});
this.pushExtCleanup(this._syntaxTreeView);
vscode.window.onDidChangeActiveTextEditor(async () => {
if (this.syntaxTreeView?.visible) {
await this.syntaxTreeProvider?.refresh();
}
});
vscode.workspace.onDidChangeTextDocument(async () => {
if (this.syntaxTreeView?.visible) {
await this.syntaxTreeProvider?.refresh();
}
});
vscode.window.onDidChangeTextEditorSelection(async (e) => {
if (!this.syntaxTreeView?.visible || !isRustEditor(e.textEditor)) {
return;
}
const selection = e.selections[0];
if (selection === undefined) {
return;
}
const start = e.textEditor.document.offsetAt(selection.start);
const end = e.textEditor.document.offsetAt(selection.end);
const result = this.syntaxTreeProvider?.getElementByRange(start, end);
if (result !== undefined) {
await this.syntaxTreeView?.reveal(result);
}
});
this._syntaxTreeView.onDidChangeVisibility(async (e) => {
if (e.visible) {
await this.syntaxTreeProvider?.refresh();
}
});
}
async restart() {
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
await this.stopAndDispose();
@ -424,6 +490,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
statusBar.command = "rust-analyzer.openLogs";
}
this.dependenciesProvider?.refresh();
void this.syntaxTreeProvider?.refresh();
break;
case "warning":
statusBar.color = new vscode.ThemeColor("statusBarItem.warningForeground");

View file

@ -48,6 +48,9 @@ export const runFlycheck = new lc.NotificationType<{
export const syntaxTree = new lc.RequestType<SyntaxTreeParams, string, void>(
"rust-analyzer/syntaxTree",
);
export const viewSyntaxTree = new lc.RequestType<ViewSyntaxTreeParams, string, void>(
"rust-analyzer/viewSyntaxTree",
);
export const viewCrateGraph = new lc.RequestType<ViewCrateGraphParams, string, void>(
"rust-analyzer/viewCrateGraph",
);
@ -157,6 +160,7 @@ export type SyntaxTreeParams = {
textDocument: lc.TextDocumentIdentifier;
range: lc.Range | null;
};
export type ViewSyntaxTreeParams = { textDocument: lc.TextDocumentIdentifier };
export type ViewCrateGraphParams = { full: boolean };
export type ViewItemTreeParams = { textDocument: lc.TextDocumentIdentifier };

View file

@ -199,6 +199,9 @@ function createCommands(): Record<string, CommandFactory> {
rename: { enabled: commands.rename },
openLogs: { enabled: commands.openLogs },
revealDependency: { enabled: commands.revealDependency },
syntaxTreeReveal: { enabled: commands.syntaxTreeReveal },
syntaxTreeHideWhitespace: { enabled: commands.syntaxTreeHideWhitespace },
syntaxTreeShowWhitespace: { enabled: commands.syntaxTreeShowWhitespace },
};
}

View file

@ -0,0 +1,301 @@
import * as vscode from "vscode";
import { isRustEditor, setContextValue } from "./util";
import type { CtxInit } from "./ctx";
import * as ra from "./lsp_ext";
export class SyntaxTreeProvider implements vscode.TreeDataProvider<SyntaxElement> {
private _onDidChangeTreeData: vscode.EventEmitter<SyntaxElement | undefined | void> =
new vscode.EventEmitter<SyntaxElement | undefined | void>();
readonly onDidChangeTreeData: vscode.Event<SyntaxElement | undefined | void> =
this._onDidChangeTreeData.event;
ctx: CtxInit;
root: SyntaxNode | undefined;
hideWhitespace: boolean = false;
constructor(ctx: CtxInit) {
this.ctx = ctx;
}
getTreeItem(element: SyntaxElement): vscode.TreeItem {
return new SyntaxTreeItem(element);
}
getChildren(element?: SyntaxElement): vscode.ProviderResult<SyntaxElement[]> {
return this.getRawChildren(element);
}
getParent(element: SyntaxElement): vscode.ProviderResult<SyntaxElement> {
return element.parent;
}
resolveTreeItem(
item: SyntaxTreeItem,
element: SyntaxElement,
_token: vscode.CancellationToken,
): vscode.ProviderResult<SyntaxTreeItem> {
const editor = vscode.window.activeTextEditor;
if (editor !== undefined) {
const start = editor.document.positionAt(element.start);
const end = editor.document.positionAt(element.end);
const range = new vscode.Range(start, end);
const text = editor.document.getText(range);
item.tooltip = new vscode.MarkdownString().appendCodeblock(text, "rust");
}
return item;
}
private getRawChildren(element?: SyntaxElement): SyntaxElement[] {
if (element?.type === "Node") {
if (this.hideWhitespace) {
return element.children.filter((e) => e.kind !== "WHITESPACE");
}
return element.children;
}
if (element?.type === "Token") {
return [];
}
if (element === undefined && this.root !== undefined) {
return [this.root];
}
return [];
}
async refresh(): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (editor && isRustEditor(editor)) {
const params = { textDocument: { uri: editor.document.uri.toString() }, range: null };
const fileText = await this.ctx.client.sendRequest(ra.viewSyntaxTree, params);
this.root = JSON.parse(fileText, (_key, value: SyntaxElement) => {
if (value.type === "Node") {
for (const child of value.children) {
child.parent = value;
}
}
return value;
});
} else {
this.root = undefined;
}
this._onDidChangeTreeData.fire();
}
getElementByRange(start: number, end: number): SyntaxElement | undefined {
if (this.root === undefined) {
return undefined;
}
let result: SyntaxElement = this.root;
if (this.root.start === start && this.root.end === end) {
return result;
}
let children = this.getRawChildren(this.root);
outer: while (true) {
for (const child of children) {
if (child.start <= start && child.end >= end) {
result = child;
if (start === end && start === child.end) {
// When the cursor is on the very end of a token,
// we assume the user wants the next token instead.
continue;
}
if (child.type === "Token") {
return result;
} else {
children = this.getRawChildren(child);
continue outer;
}
}
}
return result;
}
}
async toggleWhitespace() {
this.hideWhitespace = !this.hideWhitespace;
this._onDidChangeTreeData.fire();
await setContextValue("rustSyntaxTree.hideWhitespace", this.hideWhitespace);
}
}
export type SyntaxNode = {
type: "Node";
kind: string;
start: number;
end: number;
istart?: number;
iend?: number;
children: SyntaxElement[];
parent?: SyntaxElement;
};
type SyntaxToken = {
type: "Token";
kind: string;
start: number;
end: number;
istart?: number;
iend?: number;
parent?: SyntaxElement;
};
export type SyntaxElement = SyntaxNode | SyntaxToken;
export class SyntaxTreeItem extends vscode.TreeItem {
constructor(private readonly element: SyntaxElement) {
super(element.kind);
const icon = getIcon(element.kind);
if (element.type === "Node") {
this.contextValue = "syntaxNode";
this.iconPath = icon ?? new vscode.ThemeIcon("list-tree");
this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded;
} else {
this.contextValue = "syntaxToken";
this.iconPath = icon ?? new vscode.ThemeIcon("symbol-string");
this.collapsibleState = vscode.TreeItemCollapsibleState.None;
}
if (element.istart !== undefined && element.iend !== undefined) {
this.description = `${this.element.istart}..${this.element.iend}`;
} else {
this.description = `${this.element.start}..${this.element.end}`;
}
}
}
function getIcon(kind: string): vscode.ThemeIcon | undefined {
const icon = iconTable[kind];
if (icon !== undefined) {
return icon;
}
if (kind.endsWith("_KW")) {
return new vscode.ThemeIcon(
"symbol-keyword",
new vscode.ThemeColor("symbolIcon.keywordForeground"),
);
}
if (operators.includes(kind)) {
return new vscode.ThemeIcon(
"symbol-operator",
new vscode.ThemeColor("symbolIcon.operatorForeground"),
);
}
return undefined;
}
const iconTable: Record<string, vscode.ThemeIcon> = {
CALL_EXPR: new vscode.ThemeIcon("call-outgoing"),
COMMENT: new vscode.ThemeIcon("comment"),
ENUM: new vscode.ThemeIcon("symbol-enum", new vscode.ThemeColor("symbolIcon.enumForeground")),
FN: new vscode.ThemeIcon(
"symbol-function",
new vscode.ThemeColor("symbolIcon.functionForeground"),
),
FLOAT_NUMBER: new vscode.ThemeIcon(
"symbol-number",
new vscode.ThemeColor("symbolIcon.numberForeground"),
),
INDEX_EXPR: new vscode.ThemeIcon(
"symbol-array",
new vscode.ThemeColor("symbolIcon.arrayForeground"),
),
INT_NUMBER: new vscode.ThemeIcon(
"symbol-number",
new vscode.ThemeColor("symbolIcon.numberForeground"),
),
LITERAL: new vscode.ThemeIcon(
"symbol-misc",
new vscode.ThemeColor("symbolIcon.miscForeground"),
),
MODULE: new vscode.ThemeIcon(
"symbol-module",
new vscode.ThemeColor("symbolIcon.moduleForeground"),
),
METHOD_CALL_EXPR: new vscode.ThemeIcon("call-outgoing"),
PARAM: new vscode.ThemeIcon(
"symbol-parameter",
new vscode.ThemeColor("symbolIcon.parameterForeground"),
),
RECORD_FIELD: new vscode.ThemeIcon(
"symbol-field",
new vscode.ThemeColor("symbolIcon.fieldForeground"),
),
SOURCE_FILE: new vscode.ThemeIcon("file-code"),
STRING: new vscode.ThemeIcon("quote"),
STRUCT: new vscode.ThemeIcon(
"symbol-struct",
new vscode.ThemeColor("symbolIcon.structForeground"),
),
TRAIT: new vscode.ThemeIcon(
"symbol-interface",
new vscode.ThemeColor("symbolIcon.interfaceForeground"),
),
TYPE_PARAM: new vscode.ThemeIcon(
"symbol-type-parameter",
new vscode.ThemeColor("symbolIcon.typeParameterForeground"),
),
VARIANT: new vscode.ThemeIcon(
"symbol-enum-member",
new vscode.ThemeColor("symbolIcon.enumMemberForeground"),
),
WHITESPACE: new vscode.ThemeIcon("whitespace"),
};
const operators = [
"PLUS",
"PLUSEQ",
"MINUS",
"MINUSEQ",
"STAR",
"STAREQ",
"SLASH",
"SLASHEQ",
"PERCENT",
"PERCENTEQ",
"CARET",
"CARETEQ",
"AMP",
"AMPEQ",
"AMP2",
"PIPE",
"PIPEEQ",
"PIPE2",
"SHL",
"SHLEQ",
"SHR",
"SHREQ",
"EQ",
"EQ2",
"BANG",
"NEQ",
"L_ANGLE",
"LTEQ",
"R_ANGLE",
"GTEQ",
"COLON2",
"THIN_ARROW",
"FAT_ARROW",
"DOT",
"DOT2",
"DOT2EQ",
"AT",
];