├── .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 | ![Screenshot 1](screenshot1.png) 4 | 5 | ![Screenshot 2](screenshot2.png) 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 | 4 | 5 | 15 | 16 | 257 | -------------------------------------------------------------------------------- /frontend/src/components/Viewer.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------