├── .gitignore
├── Cargo.toml
├── README.md
├── Rocket.toml
├── build.rs
├── examples
└── cpp_example.rs
├── frontend
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── components
│ │ └── Viewer.vue
│ └── main.js
└── vue.config.js
├── screenshot1.png
├── screenshot2.png
└── src
└── lib.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tree-sitter-viewer"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | #[lib]
9 | #crate-type = ["cdylib"]
10 |
11 | [[example]]
12 | name = "cpp_example"
13 |
14 | [dependencies]
15 | rust-embed="6.2.0"
16 | tree-sitter = "0.20.0"
17 | serde_json = "1.0"
18 | serde = "*"
19 |
20 | [dependencies.rocket]
21 | version = "0.5.0-rc.1"
22 | features = ["json"]
23 |
24 | [dev-dependencies]
25 | tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp" }
26 | tokio = { version = "1.13.0", features = ["full"] }
27 |
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## a rust lib to visualize tree-sitter syntax tree
2 |
3 | 
4 |
5 | 
6 |
7 | ## prerequisite
8 |
9 | * git clone this project
10 |
11 | * install npm / node.js
12 |
13 | ## build and run example
14 |
15 | * `cd frontend`, run `npm i --save` and `npm run build`
16 |
17 | * `cd ..`, run `cargo run --example cpp_example`
18 |
19 | * visit `http://127.0.0.1:8000/`
20 |
21 | ## usage
22 |
23 | see examples/cpp_example.rs
24 |
25 | ```rust
26 | extern crate tree_sitter_cpp;
27 | extern crate tree_sitter_viewer;
28 |
29 | #[tokio::main]
30 | async fn main() -> Result<(), rocket::Error> {
31 |
32 | // initial code to display
33 | let code = r#"void main() {
34 | printf("test");
35 | }"#;
36 |
37 | // specify the parser's language and the initial code.
38 | let result = tree_sitter_viewer::run(tree_sitter_cpp::language(), code );
39 |
40 | result.await
41 | }
42 |
43 | ```
44 |
45 | ## label / legend explanation
46 |
47 | * black/plain text: A tree node's [kind](https://docs.rs/tree-sitter/0.20.0/tree_sitter/struct.Node.html#method.kind)
48 | * with green background: A tree node's [field_name](https://docs.rs/tree-sitter/0.20.0/tree_sitter/struct.TreeCursor.html#method.field_name), if there is one
49 | * pink, italic: the actual content of a node, only a leaf node / terminal will show this.
--------------------------------------------------------------------------------
/Rocket.toml:
--------------------------------------------------------------------------------
1 | [debug]
2 | address = "0.0.0.0"
3 | port = 8000
4 | workers = 4
5 | keep_alive = 5
6 | read_timeout = 5
7 | write_timeout = 5
8 | limits = {form = 32768, file = "32MiB"}
9 |
10 | [release]
11 | address = "0.0.0.0"
12 | port = 8000
13 | workers = 4
14 | keep_alive = 5
15 | read_timeout = 5
16 | write_timeout = 5
17 | limits = { form = 32768, file = "32MiB"}
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | use std::io::{self, Write};
2 | use std::process::Command;
3 |
4 | fn main() {
5 | println!("cargo:rerun-if-changed=build.rs");
6 | println!("cargo:rerun-if-changed=Cargo.lock");
7 | println!("cargo:rerun-if-changed=frontend/src");
8 | println!("cargo:rerun-if-changed=frontend/package.json");
9 |
10 |
11 | let output = Command::new("npm")
12 | .current_dir("./frontend")
13 | .args(&["i", "--save"])
14 | .output()
15 | .expect("failed to execute process");
16 |
17 | println!("status: {}", output.status);
18 |
19 | io::stdout().write_all(&output.stdout).unwrap();
20 | io::stderr().write_all(&output.stderr).unwrap();
21 |
22 | assert!(output.status.success());
23 |
24 | let output2 = Command::new("npm")
25 | .current_dir("./frontend")
26 | .args(&["run", "build"])
27 | .output()
28 | .expect("failed to execute process");
29 |
30 | println!("status: {}", output2.status);
31 |
32 | io::stdout().write_all(&output2.stdout).unwrap();
33 | io::stderr().write_all(&output2.stderr).unwrap();
34 |
35 | assert!(output2.status.success());
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/examples/cpp_example.rs:
--------------------------------------------------------------------------------
1 | extern crate tree_sitter_cpp;
2 | extern crate tree_sitter_viewer;
3 |
4 | #[tokio::main]
5 | async fn main() -> Result<(), rocket::Error> {
6 |
7 | // initial code to display
8 | let code = r#"void main() {
9 | printf("test");
10 | }"#;
11 |
12 | // specify the parser's language and the initial code.
13 | let result = tree_sitter_viewer::run(tree_sitter_cpp::language(), code );
14 |
15 | result.await
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # frontend
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Customize configuration
19 | See [Configuration Reference](https://cli.vuejs.org/config/).
20 |
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tree-sitter-viewer",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build"
8 | },
9 | "dependencies": {
10 | "core-js": "^3.6.5",
11 | "monaco-editor": "^0.29.1",
12 | "monaco-editor-webpack-plugin": "^5.0.0",
13 | "vue": "^3.0.0",
14 | "vue3-blocks-tree": "^0.5.2"
15 | },
16 | "devDependencies": {
17 | "@vue/cli-plugin-babel": "~4.5.0",
18 | "@vue/cli-service": "~4.5.0",
19 | "@vue/compiler-sfc": "^3.0.0"
20 | },
21 | "browserslist": [
22 | "> 1%",
23 | "last 2 versions",
24 | "not dead"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shi-yan/tree-sitter-viewer/165d30f7d3826fdd93c1a00eebcdf928981d9853/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
257 |
--------------------------------------------------------------------------------
/frontend/src/components/Viewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
22 |
23 |
24 | {{ data.kind }}
25 |
26 | {{ data.name }}
28 |
29 | {{ data.content }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
132 |
133 |
134 |
150 |
151 |
172 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import VueBlocksTree from 'vue3-blocks-tree';
3 |
4 | import App from './App.vue'
5 | let defaultoptions = {treeName:'blocks-tree'}
6 |
7 | createApp(App).use(VueBlocksTree,defaultoptions).mount('#app')
8 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | // vue.config.js
2 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
3 |
4 |
5 | module.exports = {
6 | configureWebpack: {
7 | plugins: [
8 | new MonacoWebpackPlugin()
9 | ]
10 | }
11 | }
--------------------------------------------------------------------------------
/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shi-yan/tree-sitter-viewer/165d30f7d3826fdd93c1a00eebcdf928981d9853/screenshot1.png
--------------------------------------------------------------------------------
/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shi-yan/tree-sitter-viewer/165d30f7d3826fdd93c1a00eebcdf928981d9853/screenshot2.png
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[macro_use]
2 | extern crate rocket;
3 |
4 | use rocket::http::ContentType;
5 | use rocket::http::Status;
6 | use rocket::response::{content, status};
7 | use std::path::{Path, PathBuf};
8 | use std::{thread, time};
9 |
10 | use rocket::serde::json::Json;
11 | use rocket::serde::Deserialize;
12 | use rust_embed::RustEmbed;
13 | use serde_json::json;
14 | use serde_json::{Map, Value};
15 | use std::cell::RefCell;
16 | use std::rc::Rc;
17 | use std::sync::Arc;
18 | use rocket::State;
19 | use std::sync::Mutex;
20 |
21 | #[derive(RustEmbed)]
22 | #[folder = "./frontend/dist/"]
23 | struct Asset;
24 |
25 | #[get("/")]
26 | fn index(file: PathBuf) -> (Status, (ContentType, Vec)) {
27 | println!("{:?}", file.as_path());
28 |
29 | let path = file.as_path();
30 |
31 | if path.to_str() == Some("") {
32 | let index_html = Asset::get("index.html").unwrap();
33 | (
34 | Status::Ok,
35 | (ContentType::HTML, index_html.data.as_ref().to_vec()),
36 | )
37 | } else {
38 | let file_content = Asset::get(path.to_str().unwrap());
39 | match file_content {
40 | Some(content) => {
41 | let extension = path.extension().unwrap().to_str();
42 | match extension {
43 | None => {
44 | println!("{} has no extension", path.to_str().unwrap());
45 | (
46 | Status::NotFound,
47 | (ContentType::Binary, content.data.as_ref().to_vec()),
48 | )
49 | }
50 | Some(extension) => {
51 | let content_type = ContentType::from_extension(extension);
52 | match content_type {
53 | Some(content_type) => {
54 | (Status::Ok, (content_type, content.data.as_ref().to_vec()))
55 | }
56 | None => match extension {
57 | "map" => (
58 | Status::Ok,
59 | (
60 | ContentType::new("application", "json"),
61 | content.data.as_ref().to_vec(),
62 | ),
63 | ),
64 | _ => {
65 | println!("unknown extension {}", extension);
66 |
67 | (
68 | Status::Ok,
69 | (ContentType::Binary, content.data.as_ref().to_vec()),
70 | )
71 | }
72 | },
73 | }
74 | }
75 | }
76 | }
77 | None => (
78 | Status::NotFound,
79 | (ContentType::HTML, "Not Found!
".as_bytes().to_vec()),
80 | ),
81 | }
82 | }
83 | }
84 |
85 | #[derive(Debug, PartialEq, Deserialize)]
86 | #[serde(crate = "rocket::serde")]
87 | struct CodePayload {
88 | code: String,
89 | }
90 |
91 | #[derive(Clone, Debug)]
92 | pub struct Range {
93 | byte_start: usize,
94 | byte_end: usize,
95 | }
96 |
97 | #[derive(Clone, Debug)]
98 | pub struct ASTNode {
99 | pub kind: String,
100 | pub children: Vec>>,
101 | pub range: Range,
102 | pub name: Option,
103 | pub id: usize,
104 | pub content: Option
105 | }
106 |
107 | impl ASTNode {
108 | pub fn add_child(&mut self, c: &Rc>) {
109 | self.children.push(c.clone());
110 | }
111 | }
112 |
113 | fn json_from_ast(ast: &Rc>) -> serde_json::Map {
114 | let mut map: serde_json::Map = serde_json::Map::new();
115 |
116 | map.insert("kind".to_string(), json!(ast.borrow().kind));
117 | map.insert("label".to_string(), json!(ast.borrow().kind));
118 | map.insert("id".to_string(), json!(ast.borrow().id));
119 |
120 | if ast.borrow().content.is_some() {
121 | map.insert("content".to_string(), json!(ast.borrow().content.as_ref().unwrap()));
122 | }
123 |
124 | if ast.borrow().name.is_some() {
125 | map.insert("name".to_string(), json!(ast.borrow().name.as_ref().unwrap()));
126 | }
127 |
128 | let mut vec: Vec = Vec:: ::new();
129 |
130 | if ast.borrow().children.len() > 0 {
131 | for c in &ast.borrow().children {
132 | vec.push(serde_json::Value::Object(json_from_ast(&c)));
133 | }
134 | }
135 |
136 | map.insert("children".to_string(), serde_json::Value::Array(vec));
137 |
138 | map
139 | }
140 |
141 | #[get("/api/initial_code")]
142 | fn initial_code(global: &State>) -> (Status, (ContentType, Vec)) {
143 | let mut map = serde_json::Map::new();
144 | let code = &*global.initial_code.lock().expect("can't lock code");
145 | map.insert("code".to_string(), serde_json::Value::String(code.to_string()));
146 | (
147 | Status::Ok,
148 | (ContentType::JSON, serde_json::to_vec(&map).unwrap()),
149 | )
150 | }
151 |
152 | #[post("/api/update_code", format = "json", data = "")]
153 | fn update_code(payload: Json, global: &State>) -> (Status, (ContentType, Vec)) {
154 | let mut parser = tree_sitter::Parser::new();
155 | parser
156 | .set_language(*global.language.lock().expect("lock language"))
157 | .expect("Error loading grammar");
158 |
159 | let parsed = parser.parse(&payload.code, None).unwrap();
160 |
161 | println!("{}", parsed.root_node().to_sexp());
162 | let bytes = payload.code.as_bytes();
163 | let mut cursor: tree_sitter::TreeCursor = parsed.root_node().walk();
164 |
165 | let curr: Rc> = Rc::new(RefCell::::new(ASTNode {
166 | kind: String::new(),
167 | children: Vec::>>::new(),
168 | range: Range {
169 | byte_start: 0,
170 | byte_end: 0,
171 | },
172 | name: None,
173 | id: 0,
174 | content: None
175 | }));
176 |
177 | let mut stack: Vec::>> = Vec::>>::new();
178 | stack.push(curr.clone());
179 |
180 | let mut reached_root = false;
181 | while reached_root == false {
182 |
183 | let c = Rc::new(RefCell::::new(
184 | ASTNode {
185 | kind: cursor.node().kind().to_string(),
186 | children: Vec::>>::new(),
187 | range: Range {
188 | byte_start: cursor.node().range().start_byte,
189 | byte_end: cursor.node().range().end_byte,
190 | },
191 | name: if let Some(n) = cursor.field_name() {Some(n.to_string())} else {None},
192 | id: cursor.node().id(),
193 | content: None
194 | }
195 | ));
196 |
197 | stack.last_mut().unwrap().borrow_mut().children.push(c.clone());
198 |
199 | if cursor.goto_first_child() {
200 | stack.push(c);
201 | continue;
202 | }
203 | else {
204 | let slice = &bytes[c.borrow().range.byte_start..c.borrow().range.byte_end];
205 | let content = std::str::from_utf8(slice).unwrap();
206 |
207 | if content != cursor.node().kind() {
208 | c.borrow_mut().content = Some(content.to_string());
209 | }
210 |
211 | if cursor.goto_next_sibling() {
212 |
213 |
214 | continue;
215 | }
216 | }
217 |
218 |
219 | let mut retracing = true;
220 | while retracing {
221 | if !cursor.goto_parent() {
222 | retracing = false;
223 | reached_root = true;
224 | } else {
225 | stack.pop();
226 | }
227 | if cursor.goto_next_sibling() {
228 | retracing = false;
229 | }
230 | }
231 | if reached_root {
232 | break;
233 | }
234 | }
235 |
236 | let root: Rc> = curr.borrow().children[0].clone();
237 |
238 | println!("root c len {}", root.borrow().children.len());
239 |
240 |
241 | let map: serde_json::Map = json_from_ast(&root);
242 |
243 |
244 | (
245 | Status::Ok,
246 | (ContentType::JSON, serde_json::to_vec(&map).unwrap()),
247 | )
248 | }
249 |
250 | struct GlobalState {
251 | language: Mutex,
252 | initial_code: Mutex
253 | }
254 |
255 | pub async fn run(language: tree_sitter::Language, initial_code: &str) -> Result<(), rocket::Error> {
256 | println!("Tree-sitter viewer");
257 |
258 | let global = Arc::new(GlobalState{language: Mutex::new(language), initial_code: Mutex::new(String::from(initial_code))});
259 |
260 | let rocket = rocket::build().mount("/", routes![index, update_code, initial_code]).manage(global);
261 |
262 | let result = rocket.launch().await;
263 |
264 | println!("server shutdown");
265 | result
266 | }
267 |
--------------------------------------------------------------------------------