├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── media ├── screenshot-1.webp └── screenshot-2.webp └── src ├── lang.rs ├── lib.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | .DS_Store 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/ 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "code-graph" 3 | version = "0.0.3" 4 | license = "MIT" 5 | readme = "README.md" 6 | edition = "2021" 7 | repository = "https://github.com/feint123/code-graph" 8 | homepage = "https://github.com/feint123" 9 | description = "An egui app that can display code graphics and find all references" 10 | keywords = ["egui", "ast", "coding"] 11 | categories = ["gui", "command-line-utilities"] 12 | 13 | [[bin]] 14 | name = "code-graph" 15 | path = "src/main.rs" 16 | 17 | [dependencies] 18 | tree-sitter = "0.22.6" 19 | tree-sitter-rust = "0.21.2" 20 | tree-sitter-java = "0.21.0" 21 | tree-sitter-c = "0.21.4" 22 | tree-sitter-javascript = "0.21.4" 23 | egui = "0.28.1" 24 | egui_extras = { version = "0.28.1", features = ["all_loaders"] } 25 | font-kit = "0.14.2" 26 | eframe = { version = "0.28.1", features = ["persistence"] } 27 | rfd = "0.14.1" 28 | serde = { version = "1.0", features = ["derive"] } 29 | serde_json = "1.0" 30 | lazy_static = "1.5.0" 31 | image = { version = "0.25.2", features = ["png"] } 32 | 33 | [dependencies.uuid] 34 | version = "1.10.0" 35 | features = [ 36 | "v4", # Lets you generate random UUIDs 37 | "fast-rng", # Use a faster (but still sufficiently random) RNG 38 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 39 | ] 40 | 41 | [package.metadata.bundle] 42 | name = "Code Graph" 43 | identifier = "com.feint.codegraph" 44 | category = "Developer Tool" 45 | version = "0.0.3" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 feint123 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code-graph 2 | an egui app that can display code graphics and find all references 3 | 4 | 编程语言支持列表 5 | 6 | 1. java 7 | 2. javascript 8 | 3. rust 9 | 4. c 10 | 11 | #### 一、打包 12 | 使用 cargo-bundle 进行打包 [cargo-bundle](https://crates.io/crates/cargo-bundle) 13 | 14 | ```shell 15 | cargo install cargo-bundle 16 | 17 | cargo bundle --release 18 | ``` 19 | 你可以在 `target/release/bundle` 目录中找到打包好的应用 20 | 21 | 22 | #### 二、界面 23 | 24 | | Light | Dark | 25 | |-------|------| 26 | | ![Light mode](media/screenshot-1.webp) | ![Dark mode](media/screenshot-2.webp) | 27 | 28 | #### 三、注意事项 29 | 30 | **使用第三方编辑器** 31 | 32 | 1. VSCode:`shift+command+p` 搜索 `install code command` 33 | 2. Zed:点击菜单`Zed` > `Install CLI` 34 | 3. Idea: 点击菜单 `Tools` > `Create Command-line Launcher...` 35 | 36 | **字体** 37 | 38 | 如果遇到App无法正常开启,请查看系统是否安装以下字体之一: 39 | 1. "Source Han Mono SC" 40 | 2. "PingFang SC" 41 | 3. "Microsoft YaHei" 42 | -------------------------------------------------------------------------------- /media/screenshot-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feint123/code-graph/1cda68b3e9e270ecfc25fd427d2d3c7024f5f4a2/media/screenshot-1.webp -------------------------------------------------------------------------------- /media/screenshot-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feint123/code-graph/1cda68b3e9e270ecfc25fd427d2d3c7024f5f4a2/media/screenshot-2.webp -------------------------------------------------------------------------------- /src/lang.rs: -------------------------------------------------------------------------------- 1 | use tree_sitter::{Language, Node}; 2 | use uuid::Uuid; 3 | 4 | use crate::{CodeBlockType, CodeNode}; 5 | 6 | pub trait SymbolQuery { 7 | fn get_call(&self, code: &str, node: &Node) -> Option; 8 | fn get_lang(&self) -> Language; 9 | fn get_definition(&self, code: &str, node: &Node) -> Option; 10 | } 11 | pub struct RustQuery; 12 | pub struct CQuery; 13 | pub struct JavaQuery; 14 | pub struct JsQuery; 15 | 16 | impl SymbolQuery for JsQuery { 17 | fn get_call(&self, code: &str, node: &Node) -> Option { 18 | let node_type = node.kind(); 19 | 20 | if node_type == "call_expression" { 21 | let block_text = &code[node.byte_range()]; 22 | let fe = node.child_by_field_name("function"); 23 | if let Some(fe) = fe { 24 | let fi = fe.child_by_field_name("property"); 25 | if let Some(fi) = fi { 26 | let label = &code[fi.byte_range()]; 27 | return Some(CodeNode::new( 28 | format!("{}", Uuid::new_v4()).as_str(), 29 | label, 30 | block_text, 31 | fi.start_position().row + 1, 32 | CodeBlockType::CALL, 33 | 0, 34 | )); 35 | } else { 36 | let label = &code[fe.byte_range()]; 37 | return Some(CodeNode::new( 38 | format!("{}", Uuid::new_v4()).as_str(), 39 | label, 40 | block_text, 41 | fe.start_position().row + 1, 42 | CodeBlockType::CALL, 43 | 0, 44 | )); 45 | } 46 | } 47 | } 48 | None 49 | } 50 | 51 | fn get_lang(&self) -> Language { 52 | tree_sitter_javascript::language() 53 | } 54 | 55 | fn get_definition(&self, code: &str, node: &Node) -> Option { 56 | let node_type = node.kind(); 57 | let definition_list = [ 58 | ("function_declaration", "formal_parameters"), 59 | ("class_declaration", "class_body"), 60 | ("method_definition", "formal_parameters"), 61 | ]; 62 | for (root_type, end_type) in definition_list { 63 | if node_type == root_type { 64 | let mut output = String::new(); 65 | for child in node.children(&mut node.walk()) { 66 | if child.kind() == end_type { 67 | break; 68 | } else { 69 | let node_text = &code[child.byte_range()]; 70 | output.push_str(node_text); 71 | output.push(' '); 72 | } 73 | } 74 | let block_type = match root_type { 75 | "function_declaration" => CodeBlockType::FUNCTION, 76 | "method_definition" => CodeBlockType::FUNCTION, 77 | "class_declaration" => CodeBlockType::CLASS, 78 | _ => CodeBlockType::NORMAL, 79 | }; 80 | let block_text = &code[node.byte_range()]; 81 | return Some(CodeNode::new( 82 | format!("{}", Uuid::new_v4()).as_str(), 83 | output.as_str(), 84 | block_text, 85 | node.start_position().row + 1, 86 | block_type, 87 | 0, 88 | )); 89 | } 90 | } 91 | if node_type == "lexical_declaration" { 92 | if node.parent().is_some() && node.parent().unwrap().grammar_name() == "program" { 93 | let mut output = String::new(); 94 | let kind_node = node.child_by_field_name("kind"); 95 | if let Some(kind_node) = kind_node { 96 | output.push_str(&code[kind_node.byte_range()]); 97 | } 98 | for child in node.children(&mut node.walk()) { 99 | if "variable_declarator" == child.kind() { 100 | let name = child.child_by_field_name("name"); 101 | if let Some(name) = name { 102 | output.push_str(" "); 103 | output.push_str(&code[name.byte_range()]); 104 | } 105 | } 106 | } 107 | let block_type = CodeBlockType::CONST; 108 | let block_text = &code[node.byte_range()]; 109 | return Some(CodeNode::new( 110 | format!("{}", Uuid::new_v4()).as_str(), 111 | output.as_str(), 112 | block_text, 113 | node.start_position().row + 1, 114 | block_type, 115 | 0, 116 | )); 117 | } 118 | } 119 | None 120 | } 121 | } 122 | 123 | impl SymbolQuery for CQuery { 124 | fn get_call(&self, code: &str, node: &Node) -> Option { 125 | let node_type = node.kind(); 126 | 127 | if node_type == "call_expression" { 128 | let block_text = &code[node.byte_range()]; 129 | let fe = node.child_by_field_name("function"); 130 | if let Some(fe) = fe { 131 | let fi = fe.child_by_field_name("field"); 132 | if let Some(fi) = fi { 133 | let label = &code[fi.byte_range()]; 134 | return Some(CodeNode::new( 135 | format!("{}", Uuid::new_v4()).as_str(), 136 | label, 137 | block_text, 138 | fi.start_position().row + 1, 139 | CodeBlockType::CALL, 140 | 0, 141 | )); 142 | } else { 143 | let label = &code[fe.byte_range()]; 144 | return Some(CodeNode::new( 145 | format!("{}", Uuid::new_v4()).as_str(), 146 | label, 147 | block_text, 148 | fe.start_position().row + 1, 149 | CodeBlockType::CALL, 150 | 0, 151 | )); 152 | } 153 | } 154 | } 155 | None 156 | } 157 | 158 | fn get_lang(&self) -> Language { 159 | tree_sitter_c::language() 160 | } 161 | 162 | fn get_definition(&self, code: &str, node: &Node) -> Option { 163 | let node_type = node.kind(); 164 | let definition_list = [("function_definition", "compound_statement")]; 165 | for (root_type, end_type) in definition_list { 166 | if node_type == root_type { 167 | let mut output = String::new(); 168 | for child in node.children(&mut node.walk()) { 169 | if child.kind() == end_type { 170 | break; 171 | } else { 172 | let node_text = &code[child.byte_range()]; 173 | output.push_str(node_text); 174 | output.push(' '); 175 | } 176 | } 177 | let block_type = match root_type { 178 | "function_definition" => CodeBlockType::FUNCTION, 179 | "struct_item" => CodeBlockType::STRUCT, 180 | _ => CodeBlockType::NORMAL, 181 | }; 182 | let block_text = &code[node.byte_range()]; 183 | return Some(CodeNode::new( 184 | format!("{}", Uuid::new_v4()).as_str(), 185 | output.as_str().split("(").next().unwrap_or("bad symbol"), 186 | block_text, 187 | node.start_position().row + 1, 188 | block_type, 189 | 0, 190 | )); 191 | } 192 | } 193 | 194 | None 195 | } 196 | } 197 | 198 | impl SymbolQuery for JavaQuery { 199 | fn get_call(&self, code: &str, node: &Node) -> Option { 200 | let node_type = node.kind(); 201 | 202 | if node_type == "method_invocation" { 203 | let block_text = &code[node.byte_range()]; 204 | let fe = node.child_by_field_name("name"); 205 | if let Some(fe) = fe { 206 | let label = &code[fe.byte_range()]; 207 | return Some(CodeNode::new( 208 | format!("{}", Uuid::new_v4()).as_str(), 209 | label, 210 | block_text, 211 | fe.start_position().row + 1, 212 | CodeBlockType::CALL, 213 | 0, 214 | )); 215 | } 216 | } 217 | None 218 | } 219 | 220 | fn get_lang(&self) -> Language { 221 | tree_sitter_java::language() 222 | } 223 | 224 | fn get_definition(&self, code: &str, node: &Node) -> Option { 225 | let node_type = node.kind(); 226 | let definition_list = [ 227 | ("class_declaration", "class_body"), 228 | ("method_declaration", "formal_parameters"), 229 | ("interface_declaration", "interface_body"), 230 | ]; 231 | for (root_type, end_type) in definition_list { 232 | if node_type == root_type { 233 | let mut output = String::new(); 234 | for child in node.children(&mut node.walk()) { 235 | if child.kind() == end_type { 236 | break; 237 | } else { 238 | let node_text = &code[child.byte_range()]; 239 | 240 | output.push_str(node_text); 241 | 242 | output.push(' '); 243 | } 244 | } 245 | let block_type = match root_type { 246 | "method_declaration" => CodeBlockType::FUNCTION, 247 | "class_declaration" => CodeBlockType::CLASS, 248 | "interface_declaration" => CodeBlockType::CLASS, 249 | _ => CodeBlockType::NORMAL, 250 | }; 251 | let block_text = &code[node.byte_range()]; 252 | return Some(CodeNode::new( 253 | format!("{}", Uuid::new_v4()).as_str(), 254 | output.as_str(), 255 | block_text, 256 | node.start_position().row + 1, 257 | block_type, 258 | 0, 259 | )); 260 | } 261 | } 262 | 263 | None 264 | } 265 | } 266 | 267 | impl SymbolQuery for RustQuery { 268 | fn get_lang(&self) -> Language { 269 | tree_sitter_rust::language() 270 | } 271 | 272 | // call_expression 下 identifier 和 field_identifier 273 | fn get_call(&self, code: &str, node: &Node) -> Option { 274 | let node_type = node.kind(); 275 | 276 | if node_type == "call_expression" { 277 | let block_text = &code[node.byte_range()]; 278 | let fe = node.child_by_field_name("function"); 279 | if let Some(fe) = fe { 280 | let fi = fe.child_by_field_name("field"); 281 | if let Some(fi) = fi { 282 | let label = &code[fi.byte_range()]; 283 | return Some(CodeNode::new( 284 | format!("{}", Uuid::new_v4()).as_str(), 285 | label, 286 | block_text, 287 | fi.start_position().row + 1, 288 | CodeBlockType::CALL, 289 | 0, 290 | )); 291 | } else { 292 | let label = &code[fe.byte_range()]; 293 | return Some(CodeNode::new( 294 | format!("{}", Uuid::new_v4()).as_str(), 295 | label, 296 | block_text, 297 | fe.start_position().row + 1, 298 | CodeBlockType::CALL, 299 | 0, 300 | )); 301 | } 302 | } 303 | } 304 | None 305 | } 306 | 307 | fn get_definition(&self, code: &str, node: &Node) -> Option { 308 | let node_type = node.kind(); 309 | let definition_list = [ 310 | ("function_item", "parameters"), 311 | ("impl_item", "declaration_list"), 312 | ("struct_item", "field_declaration_list"), 313 | ("trait_item", "declaration_list"), 314 | ("function_signature_item", "parameters"), 315 | ]; 316 | for (root_type, end_type) in definition_list { 317 | if node_type == root_type { 318 | let mut output = String::new(); 319 | for child in node.children(&mut node.walk()) { 320 | if child.kind() == end_type { 321 | break; 322 | } else { 323 | let node_text = &code[child.byte_range()]; 324 | 325 | output.push_str(node_text); 326 | 327 | output.push(' '); 328 | } 329 | } 330 | let block_type = match root_type { 331 | "function_item" => CodeBlockType::FUNCTION, 332 | "struct_item" => CodeBlockType::STRUCT, 333 | "function_signature_item" => CodeBlockType::FUNCTION, 334 | "trait_item" => CodeBlockType::CLASS, 335 | "impl_item" => CodeBlockType::CLASS, 336 | _ => CodeBlockType::NORMAL, 337 | }; 338 | let block_text = &code[node.byte_range()]; 339 | return Some(CodeNode::new( 340 | format!("{}", Uuid::new_v4()).as_str(), 341 | output.as_str(), 342 | block_text, 343 | node.start_position().row + 1, 344 | block_type, 345 | 0, 346 | )); 347 | } 348 | } 349 | 350 | None 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::path::PathBuf; 3 | use std::{fs::read_dir, path::Path}; 4 | 5 | use eframe::egui::{CollapsingHeader, Ui}; 6 | use egui::{emath, Color32, Pos2, Rect, Stroke, Vec2}; 7 | use lang::{CQuery, JavaQuery, JsQuery, RustQuery, SymbolQuery}; 8 | use lazy_static::lazy_static; 9 | use tree_sitter::Node; 10 | use tree_sitter::Parser; 11 | use uuid::Uuid; 12 | 13 | pub mod lang; 14 | 15 | #[derive(Clone, PartialEq)] 16 | pub enum TreeEvent { 17 | Clicked(String), 18 | None, 19 | } 20 | #[derive(Debug, Clone, PartialEq)] 21 | pub enum TreeType { 22 | File, 23 | Directory, 24 | } 25 | 26 | #[derive(Clone, Default, Debug)] 27 | pub struct Tree { 28 | pub label: String, 29 | full_path: String, 30 | select_path: String, 31 | children: Vec, 32 | tree_type: Option, 33 | clicked: bool, 34 | } 35 | 36 | impl Tree { 37 | pub fn new(name: &str, full_path: &str, tree_type: TreeType) -> Self { 38 | Self { 39 | label: name.to_owned(), 40 | full_path: full_path.to_owned(), 41 | children: vec![], 42 | tree_type: Some(tree_type), 43 | clicked: false, 44 | select_path: "".to_owned(), 45 | } 46 | } 47 | 48 | pub fn ui(&mut self, ui: &mut Ui) -> TreeEvent { 49 | let root_name = self.label.clone(); 50 | self.ui_impl(ui, 0, root_name.as_str()) 51 | } 52 | } 53 | 54 | impl Tree { 55 | fn ui_impl(&mut self, ui: &mut Ui, depth: usize, name: &str) -> TreeEvent { 56 | let tree_type = self.tree_type.clone().unwrap_or(TreeType::File); 57 | if self.children.len() > 0 || tree_type == TreeType::Directory { 58 | return CollapsingHeader::new(name) 59 | .default_open(depth < 1) 60 | .show(ui, |ui| self.children_ui(ui, depth)) 61 | .body_returned 62 | .unwrap_or(TreeEvent::None); 63 | } else { 64 | let full_path = self.full_path.clone(); 65 | if ui 66 | .selectable_value(&mut self.select_path, full_path, name) 67 | .clicked() 68 | { 69 | return TreeEvent::Clicked(self.full_path.to_string()); 70 | } 71 | return TreeEvent::None; 72 | } 73 | } 74 | 75 | pub fn clicked(&self) -> bool { 76 | if self.clicked { 77 | return true; 78 | } else if self.children.len() > 0 { 79 | for child in &self.children { 80 | if child.clicked() { 81 | return true; 82 | } 83 | } 84 | } 85 | return false; 86 | } 87 | 88 | fn children_ui(&mut self, ui: &mut Ui, depth: usize) -> TreeEvent { 89 | for ele in &mut self.children { 90 | let name = ele.label.clone(); 91 | let event = ele.ui_impl(ui, depth + 1, &name); 92 | if let TreeEvent::Clicked(_) = event { 93 | return event; 94 | } 95 | } 96 | TreeEvent::None 97 | } 98 | } 99 | pub fn recursion_dir(root_path: &Path, pathes: &mut Vec, mut root_tree: Tree) -> Tree { 100 | if root_path.is_dir() { 101 | for entry in read_dir(root_path).expect("Error read Dir") { 102 | let dir_entry = entry.expect("Error"); 103 | let path_buf = dir_entry.path(); 104 | let is_dir = path_buf.is_dir(); 105 | let tree_type = if is_dir { 106 | TreeType::Directory 107 | } else { 108 | TreeType::File 109 | }; 110 | let mut tree = Tree::new( 111 | path_buf.file_name().unwrap().to_str().unwrap(), 112 | path_buf.as_os_str().to_str().unwrap(), 113 | tree_type, 114 | ); 115 | if path_buf.is_dir() { 116 | tree = recursion_dir(path_buf.as_path(), pathes, tree); 117 | } else if path_buf.is_file() { 118 | pathes.push(path_buf); 119 | } 120 | root_tree.children.push(tree); 121 | } 122 | } 123 | return root_tree; 124 | } 125 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 126 | pub enum CodeBlockType { 127 | FUNCTION, 128 | METHOD, 129 | STRUCT, 130 | IMPL, 131 | CLASS, 132 | CONST, 133 | NORMAL, 134 | CALL, 135 | } 136 | #[derive(Debug, Clone)] 137 | pub struct CodeNode { 138 | id: String, 139 | // 标签 140 | pub label: String, 141 | // 代码内容 142 | pub block: String, 143 | // 文件定位 LineNumber 144 | pub file_location: usize, 145 | // 文件路径 146 | pub file_path: String, 147 | // 等级 148 | level: usize, 149 | // block 150 | block_type: CodeBlockType, 151 | // position 152 | position: Pos2, 153 | visiable: bool, 154 | } 155 | 156 | impl Default for CodeNode { 157 | fn default() -> Self { 158 | Self { 159 | block_type: CodeBlockType::NORMAL, 160 | id: "".to_owned(), 161 | label: "".to_owned(), 162 | block: "".to_owned(), 163 | file_location: 0, 164 | level: 0, 165 | file_path: "".to_owned(), 166 | position: Pos2::ZERO, 167 | visiable: true, 168 | } 169 | } 170 | } 171 | 172 | impl CodeNode { 173 | pub fn new( 174 | id: &str, 175 | label: &str, 176 | block: &str, 177 | file_location: usize, 178 | block_type: CodeBlockType, 179 | level: usize, 180 | ) -> Self { 181 | Self { 182 | id: id.to_owned(), 183 | label: label.to_owned(), 184 | block: block.to_owned(), 185 | file_location: file_location.to_owned(), 186 | file_path: "".to_owned(), 187 | block_type, 188 | position: Pos2::new(0.0, 0.0), 189 | level, 190 | visiable: true, 191 | } 192 | } 193 | } 194 | #[derive(Clone, Copy)] 195 | pub struct CodeNodeIndex(usize); 196 | 197 | pub struct Edge { 198 | from: usize, 199 | to: usize, 200 | } 201 | 202 | pub struct Graph { 203 | nodes: Vec, 204 | edges: Vec, 205 | focus_node: Option, 206 | } 207 | 208 | lazy_static! { 209 | static ref GRAPH_THEME: HashMap> = { 210 | let mut dark_block_type_map = HashMap::new(); 211 | dark_block_type_map.insert(CodeBlockType::NORMAL, egui::Color32::DARK_GRAY); 212 | dark_block_type_map.insert(CodeBlockType::FUNCTION, egui::Color32::DARK_BLUE); 213 | dark_block_type_map.insert(CodeBlockType::STRUCT, egui::Color32::from_rgb(204, 112, 0)); 214 | dark_block_type_map.insert(CodeBlockType::CONST, egui::Color32::from_rgb(204, 112, 0)); 215 | dark_block_type_map.insert(CodeBlockType::CLASS, egui::Color32::DARK_GREEN); 216 | let mut light_block_type_map = HashMap::new(); 217 | light_block_type_map.insert(CodeBlockType::NORMAL, egui::Color32::LIGHT_GRAY); 218 | light_block_type_map.insert(CodeBlockType::FUNCTION, egui::Color32::LIGHT_BLUE); 219 | light_block_type_map.insert(CodeBlockType::STRUCT, egui::Color32::LIGHT_YELLOW); 220 | light_block_type_map.insert(CodeBlockType::CONST, egui::Color32::LIGHT_YELLOW); 221 | light_block_type_map.insert(CodeBlockType::CLASS, egui::Color32::LIGHT_GREEN); 222 | let mut m = HashMap::new(); 223 | m.insert(eframe::Theme::Dark, dark_block_type_map); 224 | m.insert(eframe::Theme::Light, light_block_type_map); 225 | m 226 | }; 227 | } 228 | 229 | impl Graph { 230 | pub fn new() -> Self { 231 | Self { 232 | nodes: vec![], 233 | edges: vec![], 234 | focus_node: None, 235 | } 236 | } 237 | 238 | pub fn get_focus_idx(&mut self) -> Option { 239 | return self.focus_node; 240 | } 241 | 242 | pub fn add_node(&mut self, node: CodeNode) -> CodeNodeIndex { 243 | let index = self.nodes.len(); 244 | self.nodes.push(node); 245 | return CodeNodeIndex(index); 246 | } 247 | 248 | pub fn add_edge(&mut self, from: CodeNodeIndex, to: CodeNodeIndex) { 249 | self.edges.push(Edge { 250 | from: from.0, 251 | to: to.0, 252 | }) 253 | } 254 | 255 | pub fn clear(&mut self) { 256 | self.nodes.clear(); 257 | self.edges.clear(); 258 | self.focus_node = None; 259 | } 260 | /** 261 | * 对节点进行布局 262 | */ 263 | pub fn layout(&mut self, ui: &mut Ui, start_point: Option) { 264 | let (_, painter) = ui.allocate_painter(ui.available_size(), egui::Sense::click()); 265 | let mut sum_height = 0.0; 266 | let mut start_p = Vec2::new(ui.available_width() / 2.0, 32.0); 267 | if let Some(point) = start_point { 268 | start_p = point; 269 | } 270 | // 直线布局 271 | for (index, node) in self 272 | .nodes 273 | .iter_mut() 274 | .filter(|node| node.visiable) 275 | .enumerate() 276 | { 277 | let text_size = painter 278 | .layout_no_wrap( 279 | node.label.clone(), 280 | egui::FontId::default(), 281 | egui::Color32::WHITE, 282 | ) 283 | .size(); 284 | node.position = Pos2::new( 285 | start_p.x + node.level as f32 * 20.0, 286 | index as f32 * 16.0 + sum_height + start_p.y, 287 | ); 288 | sum_height += text_size.y; 289 | } 290 | } 291 | 292 | pub fn ui(&mut self, ui: &mut Ui) -> egui::Response { 293 | let (response, painter) = 294 | ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag()); 295 | 296 | let focus_stroke_color; 297 | let stroke_color; 298 | let text_color; 299 | let grid_color; 300 | let block_type_map; 301 | 302 | if ui.ctx().style().visuals.dark_mode { 303 | stroke_color = egui::Color32::LIGHT_GRAY; 304 | text_color = egui::Color32::WHITE; 305 | focus_stroke_color = egui::Color32::LIGHT_BLUE; 306 | grid_color = Color32::from_gray(50); 307 | block_type_map = GRAPH_THEME.get(&eframe::Theme::Dark).unwrap(); 308 | } else { 309 | focus_stroke_color = egui::Color32::BLUE; 310 | stroke_color = egui::Color32::DARK_GRAY; 311 | text_color = egui::Color32::DARK_GRAY; 312 | grid_color = Color32::from_gray(220); 313 | block_type_map = GRAPH_THEME.get(&eframe::Theme::Light).unwrap(); 314 | } 315 | 316 | // 获取可用区域 317 | let rect = ui.max_rect(); 318 | 319 | // 定义网格参数 320 | let cell_size = 10.0; // 网格单元格大小 321 | let stroke = Stroke::new(0.5, grid_color); // 线条宽度和颜色 322 | 323 | // 绘制垂直线 324 | let mut x = rect.left(); 325 | while x <= rect.right() { 326 | let line = [Pos2::new(x, rect.top()), Pos2::new(x, rect.bottom())]; 327 | painter.line_segment(line, stroke); 328 | x += cell_size; 329 | } 330 | 331 | // 绘制水平线 332 | let mut y = rect.top(); 333 | while y <= rect.bottom() { 334 | let line = [Pos2::new(rect.left(), y), Pos2::new(rect.right(), y)]; 335 | painter.line_segment(line, stroke); 336 | y += cell_size; 337 | } 338 | 339 | let to_screen = emath::RectTransform::from_to( 340 | Rect::from_min_size(Pos2::ZERO, response.rect.size()), 341 | response.rect, 342 | ); 343 | let mut node_size_list = vec![]; 344 | 345 | // 绘制节点 346 | for (index, node) in self.nodes.iter_mut().enumerate() { 347 | let node_pos = to_screen.transform_pos(node.position); 348 | let text_size = painter 349 | .layout_no_wrap( 350 | node.label.clone(), 351 | egui::FontId::default(), 352 | egui::Color32::WHITE, 353 | ) 354 | .size(); 355 | 356 | node_size_list.push(text_size + Vec2::new(16.0, 8.0)); 357 | if node.visiable { 358 | let rect = egui::Rect::from_min_size( 359 | node_pos, 360 | egui::vec2(text_size.x + 16.0, text_size.y + 8.0), 361 | ); 362 | let fill_color = block_type_map 363 | .get(&node.block_type) 364 | .copied() 365 | .unwrap_or(egui::Color32::DARK_GRAY); 366 | 367 | painter.rect(rect, 5.0, fill_color, Stroke::new(1.0, stroke_color)); 368 | 369 | painter.text( 370 | node_pos + Vec2::new(8.0, 4.0), 371 | egui::Align2::LEFT_TOP, 372 | &node.label, 373 | egui::FontId::default(), 374 | text_color, 375 | ); 376 | 377 | let point_id = response.id.with(&node.id); 378 | 379 | let node_response = ui.interact(rect, point_id, egui::Sense::click_and_drag()); 380 | if node_response.dragged() { 381 | // 更新节点位置 382 | node.position += node_response.drag_delta(); 383 | } 384 | if node_response.clicked() { 385 | self.focus_node = Some(CodeNodeIndex(index)); 386 | } 387 | if let Some(f_node) = self.focus_node { 388 | if f_node.0 == index { 389 | // ui.ctx().request_repaint(); 390 | // let time = ui.input(|i| i.time); 391 | painter.rect( 392 | rect, 393 | 5.0, 394 | egui::Color32::TRANSPARENT, 395 | Stroke::new(2.5, focus_stroke_color), 396 | ); 397 | } 398 | } 399 | } 400 | 401 | if response.dragged() { 402 | // 更新节点位置 403 | node.position += response.drag_delta(); 404 | } 405 | } 406 | 407 | // 绘制边 408 | for edge in &self.edges { 409 | if !self.nodes[edge.to].visiable || !self.nodes[edge.from].visiable { 410 | continue; 411 | } 412 | let from = to_screen.transform_pos(self.nodes[edge.from].position) 413 | + Vec2::new(0.0, node_size_list[edge.from].y / 2.0); 414 | let to = to_screen.transform_pos(self.nodes[edge.to].position) 415 | + Vec2::new(0.0, node_size_list[edge.to].y / 2.0); 416 | painter.line_segment( 417 | [from, from + Vec2::new(-10.0, 0.0)], 418 | (1.0, egui::Color32::GRAY), 419 | ); 420 | painter.line_segment( 421 | [from + Vec2::new(-10.0, 0.0), Pos2::new(from.x - 10.0, to.y)], 422 | (1.0, egui::Color32::GRAY), 423 | ); 424 | painter.line_segment( 425 | [Pos2::new(from.x - 10.0, to.y), to], 426 | (1.0, egui::Color32::GRAY), 427 | ); 428 | } 429 | // 绘制伸缩 430 | if self.nodes.len() > 0 { 431 | let mut level_queue = VecDeque::new(); 432 | level_queue.push_back(0); 433 | while let Some(node_index) = level_queue.pop_front() { 434 | let mut sub_nodes = vec![]; 435 | for edge in &self.edges { 436 | if edge.from == node_index { 437 | level_queue.push_back(edge.to); 438 | sub_nodes.push(edge.to); 439 | } 440 | } 441 | if !sub_nodes.is_empty() && self.nodes[node_index].visiable { 442 | let from = to_screen.transform_pos(self.nodes[node_index].position) 443 | + Vec2::new(0.0, node_size_list[node_index].y / 2.0); 444 | let tree_point = from + Vec2::new(-10.0, 0.0); 445 | painter.circle_filled(tree_point, 5.0, stroke_color); 446 | let point_id = response 447 | .id 448 | .with(format!("edge-{}", self.nodes[node_index].id)); 449 | 450 | let node_response = ui.interact( 451 | egui::Rect::from_center_size(tree_point, Vec2::new(10.0, 10.0)), 452 | point_id, 453 | egui::Sense::click(), 454 | ); 455 | if !self.nodes[sub_nodes[0]].visiable { 456 | painter.circle_stroke( 457 | tree_point, 458 | 7.0, 459 | Stroke::new(2.0, focus_stroke_color), 460 | ); 461 | } 462 | if node_response.clicked() { 463 | let mut change_visiable_queue = VecDeque::new(); 464 | let visiable = !self.nodes[sub_nodes[0]].visiable; 465 | for index in sub_nodes { 466 | change_visiable_queue.push_back(index); 467 | } 468 | while let Some(visiable_index) = change_visiable_queue.pop_front() { 469 | self.nodes[visiable_index].visiable = visiable; 470 | for edge in &self.edges { 471 | if edge.from == visiable_index { 472 | change_visiable_queue.push_back(edge.to); 473 | } 474 | } 475 | } 476 | self.layout(ui, Some(self.nodes[0].position.to_vec2())); 477 | } 478 | } 479 | } 480 | } 481 | self.draw_minimap(ui, &node_size_list, &response, block_type_map); 482 | response 483 | } 484 | 485 | fn draw_minimap( 486 | &self, 487 | ui: &mut Ui, 488 | rect_size: &Vec, 489 | response: &egui::Response, 490 | color_map: &HashMap, 491 | ) { 492 | let minimap_size = Vec2::new(200.0, 150.0); // 缩略图大小 493 | let minimap_margin = 10.0; // 缩略图与画布边缘的间距 494 | 495 | // 计算缩略图位置(右下角) 496 | let minimap_pos = Pos2::new( 497 | response.rect.right() - minimap_size.x - minimap_margin, 498 | response.rect.bottom() - minimap_size.y - minimap_margin, 499 | ); 500 | 501 | let minimap_rect = Rect::from_min_size(minimap_pos, minimap_size); 502 | 503 | // 绘制缩略图背景 504 | ui.painter() 505 | .rect_filled(minimap_rect, 0.0, ui.visuals().extreme_bg_color); 506 | 507 | // 计算缩放比例 508 | let scale_x = minimap_size.x / response.rect.width(); 509 | let scale_y = minimap_size.y / response.rect.height(); 510 | let scale = scale_x.min(scale_y); 511 | 512 | for (index, node) in self.nodes.iter().enumerate() { 513 | if node.visiable { 514 | // 检查节点是否在可视区域内 515 | 516 | let mut minimap_node_pos = minimap_pos + (node.position.to_vec2() * scale); 517 | let mut node_size = rect_size[index] * scale; 518 | if minimap_node_pos.x < minimap_rect.min.x { 519 | node_size.x = node_size.x - (minimap_rect.min.x - minimap_node_pos.x); 520 | minimap_node_pos.x = minimap_rect.min.x; 521 | } 522 | if minimap_node_pos.y < minimap_rect.min.y { 523 | node_size.y = node_size.y - (minimap_rect.min.y - minimap_node_pos.y); 524 | minimap_node_pos.y = minimap_rect.min.y; 525 | } 526 | if minimap_node_pos.x + node_size.x > minimap_rect.max.x { 527 | node_size.x = minimap_rect.max.x - minimap_node_pos.x; 528 | } 529 | if minimap_node_pos.y + node_size.y > minimap_rect.max.y { 530 | node_size.y = minimap_rect.max.y - minimap_node_pos.y; 531 | } 532 | let node_rect = Rect::from_min_size(minimap_node_pos, node_size); 533 | 534 | let fill_color = color_map 535 | .get(&node.block_type) 536 | .copied() 537 | .unwrap_or(egui::Color32::DARK_GRAY); 538 | 539 | ui.painter().rect_filled(node_rect, 0.0, fill_color); 540 | } 541 | } 542 | // 绘制缩略图边框 543 | ui.painter().rect_stroke( 544 | minimap_rect, 545 | 0.0, 546 | Stroke::new(1.0, ui.visuals().text_color()), 547 | ); 548 | } 549 | 550 | pub fn get_node(&mut self, index: CodeNodeIndex) -> CodeNode { 551 | let default_node = CodeNode::default(); 552 | let node = self.nodes.get(index.0).unwrap_or(&default_node); 553 | return node.clone(); 554 | } 555 | 556 | pub fn node_index(&mut self, node_id: &str) -> CodeNodeIndex { 557 | for (index, node) in self.nodes.iter().enumerate() { 558 | if node.id == node_id { 559 | return CodeNodeIndex(index); 560 | } 561 | } 562 | CodeNodeIndex(0) 563 | } 564 | } 565 | 566 | pub fn valid_file_extention(extension: &str) -> bool { 567 | return vec!["rs", "c", "h", "java", "js", "jsx"].contains(&extension); 568 | } 569 | 570 | pub fn get_symbol_query(extention: &str) -> Box { 571 | match extention { 572 | "rs" => Box::new(RustQuery), 573 | "java" => Box::new(JavaQuery), 574 | "c" | "h" => Box::new(CQuery), 575 | "js" | "jsx" => Box::new(JsQuery), 576 | _ => Box::new(RustQuery), 577 | } 578 | } 579 | 580 | pub fn fetch_calls(path: &str, code: &str, symbol_query: Box) -> Vec { 581 | let mut parser = Parser::new(); 582 | parser 583 | .set_language(&symbol_query.get_lang()) 584 | .expect("Error load Rust grammer"); 585 | let tree = parser.parse(code, None).unwrap(); 586 | let root_node = tree.root_node(); 587 | recursion_call(root_node, path, code, &symbol_query) 588 | } 589 | 590 | pub fn recursion_call( 591 | node: Node, 592 | path: &str, 593 | code: &str, 594 | symbol_query: &Box, 595 | ) -> Vec { 596 | let mut nodes = vec![]; 597 | let code_node = symbol_query.get_call(code, &node); 598 | if let Some(mut node) = code_node { 599 | node.file_path = path.to_string(); 600 | nodes.push(node); 601 | } 602 | 603 | for child in node.children(&mut node.walk()) { 604 | let sub_nodes = recursion_call(child, path, code, symbol_query); 605 | if sub_nodes.len() > 0 { 606 | for sub_node in sub_nodes { 607 | nodes.push(sub_node); 608 | } 609 | } 610 | } 611 | return nodes; 612 | } 613 | /** 614 | * 打印大纲 615 | */ 616 | pub fn fetch_symbols( 617 | path: &str, 618 | code: &str, 619 | symbol_query: Box, 620 | graph: &mut Graph, 621 | ) { 622 | let mut parser = Parser::new(); 623 | parser 624 | .set_language(&symbol_query.get_lang()) 625 | .expect("Error load Rust grammer"); 626 | let tree = parser.parse(code, None).unwrap(); 627 | let root_node = tree.root_node(); 628 | let root_code_node = CodeNode::new( 629 | format!("{}", Uuid::new_v4()).as_str(), 630 | path, 631 | code, 632 | 0, 633 | CodeBlockType::NORMAL, 634 | 0, 635 | ); 636 | graph.add_node(root_code_node); 637 | recursion_outline( 638 | root_node, 639 | CodeNodeIndex(0), 640 | path, 641 | code, 642 | 1, 643 | &symbol_query, 644 | graph, 645 | ); 646 | } 647 | 648 | pub fn recursion_outline( 649 | node: Node, 650 | parent_id: CodeNodeIndex, 651 | path: &str, 652 | code: &str, 653 | level: usize, 654 | symbol_query: &Box, 655 | graph: &mut Graph, 656 | ) { 657 | let mut current_id = parent_id; 658 | let code_node = symbol_query.get_definition(code, &node); 659 | let mut level = level; 660 | if let Some(mut node) = code_node { 661 | node.file_path = path.to_string(); 662 | node.level = level; 663 | let index = graph.add_node(node); 664 | current_id = index; 665 | graph.add_edge(parent_id, index); 666 | level += 1; 667 | } 668 | 669 | for child in node.children(&mut node.walk()) { 670 | recursion_outline(child, current_id, path, code, level, symbol_query, graph) 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsStr, 3 | fs::{self}, 4 | path::{Path, PathBuf}, 5 | process::Command, 6 | sync::mpsc::{self, Receiver}, 7 | thread::{self}, 8 | }; 9 | 10 | use code_graph::{ 11 | fetch_calls, fetch_symbols, get_symbol_query, recursion_dir, valid_file_extention, CodeNode, 12 | Graph, Tree, TreeEvent, TreeType, 13 | }; 14 | use eframe::egui::{self}; 15 | use egui::{text::LayoutJob, FontId, Rounding, TextFormat, Ui, Vec2, Widget}; 16 | use font_kit::{family_name::FamilyName, properties::Properties, source::SystemSource}; 17 | use rfd::{FileDialog, MessageDialog}; 18 | use serde::{Deserialize, Serialize}; 19 | 20 | fn main() -> eframe::Result { 21 | let mut options = eframe::NativeOptions::default(); 22 | options.persist_window = true; 23 | eframe::run_native( 24 | "Code Graph", 25 | options, 26 | Box::new(|cc| { 27 | egui_extras::install_image_loaders(&cc.egui_ctx); 28 | // 修改一些基本配置 29 | cc.egui_ctx.style_mut(|style| { 30 | style.spacing.button_padding = Vec2::new(8.0, 2.0); 31 | }); 32 | let system_source = SystemSource::new(); 33 | let mut fonts = egui::FontDefinitions::default(); 34 | // 尝试加载系统默认字体 35 | if let Ok(font) = system_source.select_best_match( 36 | &[ 37 | FamilyName::Title("Source Han Mono SC".to_string()), 38 | FamilyName::Title("PingFang SC".to_string()), 39 | FamilyName::Title("Microsoft YaHei".to_string()), 40 | ], 41 | &Properties::new(), 42 | ) { 43 | if let Ok(font_data) = font.load() { 44 | fonts.font_data.insert( 45 | "system_font".to_owned(), 46 | egui::FontData::from_owned(font_data.copy_font_data().unwrap().to_vec()), 47 | ); 48 | } 49 | } 50 | fonts 51 | .families 52 | .entry(egui::FontFamily::Proportional) 53 | .or_default() 54 | .insert(0, "system_font".to_owned()); 55 | 56 | fonts 57 | .families 58 | .entry(egui::FontFamily::Monospace) 59 | .or_default() 60 | .push("system_font".to_owned()); 61 | // cc.egui_ctx.set_debug_on_hover(true); 62 | cc.egui_ctx.set_fonts(fonts); 63 | let mut my_app = MyApp::default(); 64 | if let Some(storage) = cc.storage { 65 | if let Some(app_state) = storage.get_string("app_state") { 66 | let app_state = serde_json::from_str::(&app_state); 67 | if let Ok(app_state) = app_state { 68 | my_app.project_root_path = 69 | Some(Path::new(&app_state.root_path).to_path_buf()); 70 | my_app.editor = app_state.editor; 71 | } 72 | } 73 | } 74 | Ok(Box::new(my_app)) 75 | }), 76 | ) 77 | } 78 | 79 | #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] 80 | enum Editor { 81 | VSCode, 82 | Zed, 83 | Idea, 84 | } 85 | #[derive(Debug, Serialize, Deserialize)] 86 | struct AppState { 87 | editor: Editor, 88 | root_path: String, 89 | } 90 | struct MyApp { 91 | tree: Tree, 92 | code: String, 93 | current_node: CodeNode, 94 | call_nodes: Vec, 95 | filter_call_nodes: Vec, 96 | project_root_path: Option, 97 | root_path: String, 98 | graph: Graph, 99 | editor: Editor, 100 | rx: Option)>>, 101 | debug: DebugInfo, 102 | } 103 | #[derive(Default, Debug)] 104 | struct DebugInfo { 105 | fps: f32, 106 | enable: bool, 107 | } 108 | 109 | impl Default for MyApp { 110 | fn default() -> Self { 111 | Self { 112 | code: "".to_owned(), 113 | current_node: CodeNode::default(), 114 | call_nodes: vec![], 115 | filter_call_nodes: vec![], 116 | tree: Tree::new("", "", TreeType::File), 117 | project_root_path: None, 118 | root_path: "".to_owned(), 119 | graph: Graph::new(), 120 | editor: Editor::VSCode, 121 | rx: None, 122 | debug: DebugInfo::default(), 123 | } 124 | } 125 | } 126 | 127 | impl MyApp { 128 | fn side_panel(&mut self, ui: &mut Ui) { 129 | if self.tree.label.is_empty() { 130 | ui.label("这里什么也没有"); 131 | } else { 132 | if let TreeEvent::Clicked(name) = self.tree.ui(ui) { 133 | let path = Path::new(&name); 134 | let ext = path.extension().unwrap_or(OsStr::new("")).to_str().unwrap(); 135 | if valid_file_extention(ext) { 136 | self.code = fs::read_to_string(path).unwrap(); 137 | self.current_node = CodeNode::default(); 138 | self.graph.clear(); 139 | // 解析代码,生成图 140 | fetch_symbols(&name, &self.code, get_symbol_query(ext), &mut self.graph); 141 | // 布局 142 | self.graph.layout(ui, None); 143 | } else { 144 | MessageDialog::new() 145 | .set_title("提示") 146 | .set_description("不受支持的文件类型") 147 | .show(); 148 | } 149 | } 150 | } 151 | } 152 | fn open_editor(&self, file_path: &str, line_number: usize) { 153 | let command = match self.editor { 154 | Editor::Zed => "zed", 155 | Editor::VSCode => "code", 156 | Editor::Idea => "idea", 157 | }; 158 | let args = match self.editor { 159 | Editor::Zed => vec![format!("{}:{}", file_path, line_number)], 160 | Editor::VSCode => vec!["-g".to_owned(), format!("{}:{}", file_path, line_number)], 161 | Editor::Idea => vec![ 162 | "-l".to_owned(), 163 | format!("{}", line_number), 164 | format!("{}", file_path), 165 | ], 166 | }; 167 | // 执行shell 命令 168 | let _ = Command::new(command).args(args).output().is_err_and(|err| { 169 | let message_dialog = rfd::MessageDialog::new(); 170 | message_dialog 171 | .set_title("打开失败") 172 | .set_description(err.to_string()) 173 | .show(); 174 | return true; 175 | }); // 传递命令行参数 176 | } 177 | fn right_panel(&mut self, ui: &mut Ui) { 178 | ui.add_space(10.0); 179 | egui::Grid::new("param_grid") 180 | .num_columns(2) 181 | .spacing([10.0, 10.0]) 182 | .show(ui, |ui| { 183 | ui.label("选择编辑器"); 184 | ui.horizontal(|ui| { 185 | egui::ComboBox::from_id_source("choose editor") 186 | .selected_text(format!("{:?}", self.editor)) 187 | .show_ui(ui, |ui| { 188 | ui.selectable_value(&mut self.editor, Editor::Idea, "Idea"); 189 | ui.selectable_value(&mut self.editor, Editor::VSCode, "VSCode"); 190 | ui.selectable_value(&mut self.editor, Editor::Zed, "Zed"); 191 | }); 192 | ui.add_space(4.0); 193 | if self.get_normal_button("打开").ui(ui).clicked() { 194 | self.open_editor( 195 | &self.current_node.file_path, 196 | self.current_node.file_location, 197 | ); 198 | } 199 | }); 200 | 201 | ui.end_row(); 202 | }); 203 | 204 | ui.add_space(10.0); 205 | egui::CollapsingHeader::new("调用列表") 206 | .default_open(true) 207 | .show(ui, |ui| { 208 | for node in &self.filter_call_nodes { 209 | let mut job = LayoutJob::default(); 210 | job.append( 211 | node.block.replace("\n", " ").replace(" ", "").as_str(), 212 | 0.0, 213 | TextFormat { 214 | color: ui.style().visuals.text_color(), 215 | ..Default::default() 216 | }, 217 | ); 218 | job.append( 219 | format!("\n{}:{}", node.file_path, node.file_location).as_str(), 220 | 0.0, 221 | TextFormat { 222 | font_id: FontId::monospace(8.0), 223 | ..Default::default() 224 | }, 225 | ); 226 | if egui::Button::new(job) 227 | .rounding(Rounding::same(8.0)) 228 | .min_size(egui::Vec2::new(ui.available_width(), 0.0)) 229 | .ui(ui) 230 | .clicked() 231 | { 232 | self.open_editor(&node.file_path, node.file_location); 233 | } 234 | } 235 | }); 236 | 237 | ui.add_space(10.0); 238 | egui::CollapsingHeader::new("代码预览") 239 | .default_open(true) 240 | .show(ui, |ui| { 241 | let language = "rs"; 242 | let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); 243 | egui::ScrollArea::vertical().show(ui, |ui| { 244 | egui_extras::syntax_highlighting::code_view_ui( 245 | ui, 246 | &theme, 247 | &self.current_node.block, 248 | language, 249 | ); 250 | }); 251 | }); 252 | ui.add_space(10.0); 253 | } 254 | 255 | fn draw_debug_info(&self, ctx: &egui::Context) { 256 | let painter = ctx.debug_painter(); 257 | 258 | // 绘制帧率 259 | painter.text( 260 | egui::pos2(10.0, 10.0), 261 | egui::Align2::LEFT_TOP, 262 | format!("FPS: {:.1}", self.debug.fps), 263 | egui::FontId::default(), 264 | egui::Color32::GREEN, 265 | ); 266 | 267 | // 可以添加更多调试信息... 268 | // 例如,内存使用、对象数量等 269 | } 270 | 271 | fn get_normal_button(&mut self, text: &str) -> egui::Button { 272 | return egui::Button::new(text).rounding(Rounding::same(5.0)); 273 | } 274 | } 275 | 276 | impl eframe::App for MyApp { 277 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 278 | storage.set_string( 279 | "app_state", 280 | serde_json::to_string(&AppState { 281 | editor: self.editor.clone(), 282 | root_path: self.root_path.clone(), 283 | }) 284 | .unwrap(), 285 | ); 286 | } 287 | 288 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 289 | if self.debug.enable { 290 | let time = ctx.input(|i| i.unstable_dt); 291 | self.debug.fps = 1.0 / time; 292 | self.draw_debug_info(ctx); 293 | } 294 | egui::SidePanel::left("side_panel") 295 | .resizable(true) 296 | .show_separator_line(false) 297 | .show(ctx, |ui| { 298 | ui.add_space(10.0); 299 | ui.horizontal(|ui| { 300 | ui.label("文件列表"); 301 | // let file_icon = egui::include_image!("../assets/folder.png"); 302 | let open_file_button = ui.add(self.get_normal_button("选择")); 303 | if open_file_button.clicked() { 304 | // 打开系统目录 305 | if let Some(path) = FileDialog::new().pick_folder() { 306 | self.root_path = path.as_os_str().to_str().unwrap().to_string(); 307 | self.project_root_path = Some(path); 308 | } 309 | } 310 | open_file_button.on_hover_text("选择项目目录"); 311 | }); 312 | if let Some(dir_path) = &self.project_root_path { 313 | // 清除图里的数据 314 | self.graph.clear(); 315 | let new_tree = Tree::new( 316 | dir_path.as_os_str().to_str().unwrap(), 317 | dir_path.as_os_str().to_str().unwrap(), 318 | TreeType::Directory, 319 | ); 320 | let dir_path = dir_path.clone(); 321 | let (tx, rx) = mpsc::channel(); 322 | self.rx = Some(rx); 323 | // 在后台线程中执行耗时任务 324 | thread::spawn(move || { 325 | let mut pathes = vec![]; 326 | let result = recursion_dir(&dir_path, &mut pathes, new_tree); 327 | let call_node_list = pathes 328 | .iter() 329 | .map(|path_buffer| { 330 | let ext = path_buffer 331 | .extension() 332 | .unwrap_or(OsStr::new("")) 333 | .to_str() 334 | .unwrap(); 335 | let name = path_buffer.as_os_str().to_str().unwrap(); 336 | if valid_file_extention(ext) { 337 | let code = fs::read_to_string(path_buffer).unwrap_or("".into()); 338 | return fetch_calls(&name, &code, get_symbol_query(ext)); 339 | } 340 | return vec![]; 341 | }) 342 | .flatten() 343 | .collect::>(); 344 | // 解析获取文件中说有使用了符号的代码 345 | tx.send((result, call_node_list)).unwrap(); 346 | }); 347 | self.project_root_path = None 348 | } 349 | 350 | if let Some(rx) = &self.rx { 351 | if let Ok(result) = rx.try_recv() { 352 | self.tree = result.0; 353 | self.call_nodes = result.1; 354 | self.rx = None; 355 | } else { 356 | ui.spinner(); 357 | } 358 | } 359 | 360 | ui.add_space(10.0); 361 | egui::ScrollArea::both().show(ui, |ui| { 362 | ui.set_min_height(ui.available_height()); 363 | self.side_panel(ui); 364 | }); 365 | }); 366 | egui::SidePanel::right("right_panel") 367 | .min_width(240.0) 368 | .resizable(true) 369 | .show_separator_line(false) 370 | .show(ctx, |ui| { 371 | egui::ScrollArea::both().show(ui, |ui| { 372 | ui.set_min_height(ui.available_height()); 373 | self.right_panel(ui); 374 | }); 375 | }); 376 | 377 | egui::CentralPanel::default().show(ctx, |ui| { 378 | egui::Frame::canvas(ui.style()).show(ui, |ui| { 379 | let response = self.graph.ui(ui); 380 | if let Some(focue_node) = self.graph.get_focus_idx() { 381 | self.current_node = self.graph.get_node(focue_node); 382 | self.filter_call_nodes.clear(); 383 | for node in &self.call_nodes { 384 | let current_label = &self.current_node.label; 385 | for ele in current_label.split(" ") { 386 | if ele == node.label { 387 | self.filter_call_nodes.push(node.clone()); 388 | } 389 | } 390 | } 391 | } 392 | response 393 | }); 394 | }); 395 | } 396 | } 397 | --------------------------------------------------------------------------------