zarz 0.3.1-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2815 -0
- package/Cargo.toml +30 -0
- package/QUICKSTART.md +326 -0
- package/README.md +72 -0
- package/bin/zarz.js +80 -0
- package/package.json +53 -0
- package/scripts/postinstall.js +91 -0
- package/src/cli.rs +201 -0
- package/src/config.rs +249 -0
- package/src/conversation_store.rs +183 -0
- package/src/executor.rs +164 -0
- package/src/fs_ops.rs +117 -0
- package/src/intelligence/context.rs +143 -0
- package/src/intelligence/mod.rs +60 -0
- package/src/intelligence/rust_parser.rs +141 -0
- package/src/intelligence/symbol_search.rs +97 -0
- package/src/main.rs +867 -0
- package/src/mcp/client.rs +316 -0
- package/src/mcp/config.rs +133 -0
- package/src/mcp/manager.rs +186 -0
- package/src/mcp/mod.rs +12 -0
- package/src/mcp/types.rs +170 -0
- package/src/providers/anthropic.rs +214 -0
- package/src/providers/glm.rs +209 -0
- package/src/providers/mod.rs +90 -0
- package/src/providers/openai.rs +197 -0
- package/src/repl.rs +1910 -0
- package/src/session.rs +173 -0
package/src/executor.rs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
use anyhow::{Context, Result};
|
|
2
|
+
use std::process::Stdio;
|
|
3
|
+
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
4
|
+
use tokio::process::Command;
|
|
5
|
+
|
|
6
|
+
pub struct CommandExecutor;
|
|
7
|
+
|
|
8
|
+
#[derive(Debug)]
|
|
9
|
+
pub struct CommandResult {
|
|
10
|
+
pub stdout: String,
|
|
11
|
+
pub stderr: String,
|
|
12
|
+
pub exit_code: i32,
|
|
13
|
+
pub success: bool,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl CommandExecutor {
|
|
17
|
+
pub async fn execute(command: &str) -> Result<CommandResult> {
|
|
18
|
+
let (shell, flag) = if cfg!(target_os = "windows") {
|
|
19
|
+
("cmd", "/C")
|
|
20
|
+
} else {
|
|
21
|
+
("sh", "-c")
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let mut child = Command::new(shell)
|
|
25
|
+
.arg(flag)
|
|
26
|
+
.arg(command)
|
|
27
|
+
.stdout(Stdio::piped())
|
|
28
|
+
.stderr(Stdio::piped())
|
|
29
|
+
.spawn()
|
|
30
|
+
.with_context(|| format!("Failed to execute command: {}", command))?;
|
|
31
|
+
|
|
32
|
+
let stdout = child
|
|
33
|
+
.stdout
|
|
34
|
+
.take()
|
|
35
|
+
.context("Failed to capture stdout")?;
|
|
36
|
+
|
|
37
|
+
let stderr = child
|
|
38
|
+
.stderr
|
|
39
|
+
.take()
|
|
40
|
+
.context("Failed to capture stderr")?;
|
|
41
|
+
|
|
42
|
+
let mut stdout_lines = BufReader::new(stdout).lines();
|
|
43
|
+
let mut stderr_lines = BufReader::new(stderr).lines();
|
|
44
|
+
|
|
45
|
+
let stdout_handle = tokio::spawn(async move {
|
|
46
|
+
let mut output = String::new();
|
|
47
|
+
while let Ok(Some(line)) = stdout_lines.next_line().await {
|
|
48
|
+
output.push_str(&line);
|
|
49
|
+
output.push('\n');
|
|
50
|
+
}
|
|
51
|
+
output
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let stderr_handle = tokio::spawn(async move {
|
|
55
|
+
let mut output = String::new();
|
|
56
|
+
while let Ok(Some(line)) = stderr_lines.next_line().await {
|
|
57
|
+
output.push_str(&line);
|
|
58
|
+
output.push('\n');
|
|
59
|
+
}
|
|
60
|
+
output
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let stdout_output = stdout_handle
|
|
64
|
+
.await
|
|
65
|
+
.context("Failed to join stdout task")?;
|
|
66
|
+
|
|
67
|
+
let stderr_output = stderr_handle
|
|
68
|
+
.await
|
|
69
|
+
.context("Failed to join stderr task")?;
|
|
70
|
+
|
|
71
|
+
let status = child
|
|
72
|
+
.wait()
|
|
73
|
+
.await
|
|
74
|
+
.context("Failed to wait for command")?;
|
|
75
|
+
|
|
76
|
+
let exit_code = status.code().unwrap_or(-1);
|
|
77
|
+
let success = status.success();
|
|
78
|
+
|
|
79
|
+
Ok(CommandResult {
|
|
80
|
+
stdout: stdout_output,
|
|
81
|
+
stderr: stderr_output,
|
|
82
|
+
exit_code,
|
|
83
|
+
success,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[allow(dead_code)]
|
|
88
|
+
pub async fn execute_streaming<F>(command: &str, mut on_output: F) -> Result<CommandResult>
|
|
89
|
+
where
|
|
90
|
+
F: FnMut(&str) + Send,
|
|
91
|
+
{
|
|
92
|
+
let (shell, flag) = if cfg!(target_os = "windows") {
|
|
93
|
+
("cmd", "/C")
|
|
94
|
+
} else {
|
|
95
|
+
("sh", "-c")
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let mut child = Command::new(shell)
|
|
99
|
+
.arg(flag)
|
|
100
|
+
.arg(command)
|
|
101
|
+
.stdout(Stdio::piped())
|
|
102
|
+
.stderr(Stdio::piped())
|
|
103
|
+
.spawn()
|
|
104
|
+
.with_context(|| format!("Failed to execute command: {}", command))?;
|
|
105
|
+
|
|
106
|
+
let stdout = child
|
|
107
|
+
.stdout
|
|
108
|
+
.take()
|
|
109
|
+
.context("Failed to capture stdout")?;
|
|
110
|
+
|
|
111
|
+
let stderr = child
|
|
112
|
+
.stderr
|
|
113
|
+
.take()
|
|
114
|
+
.context("Failed to capture stderr")?;
|
|
115
|
+
|
|
116
|
+
let mut stdout_lines = BufReader::new(stdout).lines();
|
|
117
|
+
let mut stderr_lines = BufReader::new(stderr).lines();
|
|
118
|
+
|
|
119
|
+
let mut stdout_output = String::new();
|
|
120
|
+
let mut stderr_output = String::new();
|
|
121
|
+
|
|
122
|
+
loop {
|
|
123
|
+
tokio::select! {
|
|
124
|
+
result = stdout_lines.next_line() => {
|
|
125
|
+
match result {
|
|
126
|
+
Ok(Some(line)) => {
|
|
127
|
+
on_output(&line);
|
|
128
|
+
stdout_output.push_str(&line);
|
|
129
|
+
stdout_output.push('\n');
|
|
130
|
+
}
|
|
131
|
+
Ok(None) => break,
|
|
132
|
+
Err(_) => break,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
result = stderr_lines.next_line() => {
|
|
136
|
+
match result {
|
|
137
|
+
Ok(Some(line)) => {
|
|
138
|
+
on_output(&line);
|
|
139
|
+
stderr_output.push_str(&line);
|
|
140
|
+
stderr_output.push('\n');
|
|
141
|
+
}
|
|
142
|
+
Ok(None) => {},
|
|
143
|
+
Err(_) => {},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let status = child
|
|
150
|
+
.wait()
|
|
151
|
+
.await
|
|
152
|
+
.context("Failed to wait for command")?;
|
|
153
|
+
|
|
154
|
+
let exit_code = status.code().unwrap_or(-1);
|
|
155
|
+
let success = status.success();
|
|
156
|
+
|
|
157
|
+
Ok(CommandResult {
|
|
158
|
+
stdout: stdout_output,
|
|
159
|
+
stderr: stderr_output,
|
|
160
|
+
exit_code,
|
|
161
|
+
success,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/fs_ops.rs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
use anyhow::{Context, Result};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use tokio::fs;
|
|
4
|
+
use walkdir::WalkDir;
|
|
5
|
+
|
|
6
|
+
pub struct FileSystemOps;
|
|
7
|
+
|
|
8
|
+
impl FileSystemOps {
|
|
9
|
+
pub async fn create_file(path: &Path, content: &str) -> Result<()> {
|
|
10
|
+
if let Some(parent) = path.parent() {
|
|
11
|
+
fs::create_dir_all(parent)
|
|
12
|
+
.await
|
|
13
|
+
.with_context(|| format!("Failed to create parent directories for {}", path.display()))?;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fs::write(path, content)
|
|
17
|
+
.await
|
|
18
|
+
.with_context(|| format!("Failed to write file {}", path.display()))?;
|
|
19
|
+
|
|
20
|
+
Ok(())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[allow(dead_code)]
|
|
24
|
+
pub async fn delete_file(path: &Path) -> Result<()> {
|
|
25
|
+
fs::remove_file(path)
|
|
26
|
+
.await
|
|
27
|
+
.with_context(|| format!("Failed to delete file {}", path.display()))?;
|
|
28
|
+
|
|
29
|
+
Ok(())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[allow(dead_code)]
|
|
33
|
+
pub async fn rename_file(from: &Path, to: &Path) -> Result<()> {
|
|
34
|
+
if let Some(parent) = to.parent() {
|
|
35
|
+
fs::create_dir_all(parent)
|
|
36
|
+
.await
|
|
37
|
+
.with_context(|| format!("Failed to create parent directories for {}", to.display()))?;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs::rename(from, to)
|
|
41
|
+
.await
|
|
42
|
+
.with_context(|| format!("Failed to rename {} to {}", from.display(), to.display()))?;
|
|
43
|
+
|
|
44
|
+
Ok(())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[allow(dead_code)]
|
|
48
|
+
pub async fn create_directory(path: &Path) -> Result<()> {
|
|
49
|
+
fs::create_dir_all(path)
|
|
50
|
+
.await
|
|
51
|
+
.with_context(|| format!("Failed to create directory {}", path.display()))?;
|
|
52
|
+
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub async fn read_file(path: &Path) -> Result<String> {
|
|
57
|
+
fs::read_to_string(path)
|
|
58
|
+
.await
|
|
59
|
+
.with_context(|| format!("Failed to read file {}", path.display()))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub async fn file_exists(path: &Path) -> bool {
|
|
63
|
+
fs::metadata(path).await.is_ok()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[allow(dead_code)]
|
|
67
|
+
pub fn list_files(root: &Path, pattern: Option<&str>) -> Result<Vec<PathBuf>> {
|
|
68
|
+
let mut files = Vec::new();
|
|
69
|
+
|
|
70
|
+
for entry in WalkDir::new(root)
|
|
71
|
+
.follow_links(false)
|
|
72
|
+
.into_iter()
|
|
73
|
+
.filter_map(|e| e.ok())
|
|
74
|
+
{
|
|
75
|
+
if entry.file_type().is_file() {
|
|
76
|
+
let path = entry.path();
|
|
77
|
+
|
|
78
|
+
if let Some(pattern) = pattern {
|
|
79
|
+
if let Some(file_name) = path.file_name() {
|
|
80
|
+
if file_name.to_string_lossy().contains(pattern) {
|
|
81
|
+
files.push(path.to_path_buf());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
files.push(path.to_path_buf());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Ok(files)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[allow(dead_code)]
|
|
94
|
+
pub fn get_directory_structure(root: &Path, max_depth: Option<usize>) -> Result<String> {
|
|
95
|
+
let mut output = String::new();
|
|
96
|
+
let max_depth = max_depth.unwrap_or(3);
|
|
97
|
+
|
|
98
|
+
for entry in WalkDir::new(root)
|
|
99
|
+
.max_depth(max_depth)
|
|
100
|
+
.follow_links(false)
|
|
101
|
+
.into_iter()
|
|
102
|
+
.filter_map(|e| e.ok())
|
|
103
|
+
{
|
|
104
|
+
let depth = entry.depth();
|
|
105
|
+
let indent = " ".repeat(depth);
|
|
106
|
+
let name = entry.file_name().to_string_lossy();
|
|
107
|
+
|
|
108
|
+
if entry.file_type().is_dir() {
|
|
109
|
+
output.push_str(&format!("{}{}/\n", indent, name));
|
|
110
|
+
} else {
|
|
111
|
+
output.push_str(&format!("{}{}\n", indent, name));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Ok(output)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use regex::Regex;
|
|
3
|
+
use std::collections::HashSet;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
use walkdir::WalkDir;
|
|
6
|
+
|
|
7
|
+
pub struct ContextBuilder;
|
|
8
|
+
|
|
9
|
+
impl ContextBuilder {
|
|
10
|
+
pub fn build_context(root: &Path, query: &str) -> Result<Vec<PathBuf>> {
|
|
11
|
+
let keywords = Self::extract_keywords(query);
|
|
12
|
+
let mut relevant_files = Vec::new();
|
|
13
|
+
let mut scores: Vec<(PathBuf, usize)> = Vec::new();
|
|
14
|
+
|
|
15
|
+
for entry in WalkDir::new(root)
|
|
16
|
+
.max_depth(10)
|
|
17
|
+
.follow_links(false)
|
|
18
|
+
.into_iter()
|
|
19
|
+
.filter_map(|e| e.ok())
|
|
20
|
+
{
|
|
21
|
+
if entry.file_type().is_file() {
|
|
22
|
+
let path = entry.path();
|
|
23
|
+
|
|
24
|
+
if Self::should_skip(path) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if let Ok(content) = std::fs::read_to_string(path) {
|
|
29
|
+
let score = Self::calculate_relevance(&content, &keywords);
|
|
30
|
+
|
|
31
|
+
if score > 0 {
|
|
32
|
+
scores.push((path.to_path_buf(), score));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
scores.sort_by(|a, b| b.1.cmp(&a.1));
|
|
39
|
+
|
|
40
|
+
for (path, _) in scores.iter().take(5) {
|
|
41
|
+
relevant_files.push(path.clone());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Ok(relevant_files)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn extract_keywords(query: &str) -> HashSet<String> {
|
|
48
|
+
let re = Regex::new(r"\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b").unwrap();
|
|
49
|
+
let mut keywords = HashSet::new();
|
|
50
|
+
|
|
51
|
+
for cap in re.find_iter(query) {
|
|
52
|
+
let word = cap.as_str().to_lowercase();
|
|
53
|
+
if !Self::is_common_word(&word) {
|
|
54
|
+
keywords.insert(word);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
keywords
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn calculate_relevance(content: &str, keywords: &HashSet<String>) -> usize {
|
|
62
|
+
let content_lower = content.to_lowercase();
|
|
63
|
+
let mut score = 0;
|
|
64
|
+
|
|
65
|
+
for keyword in keywords {
|
|
66
|
+
let count = content_lower.matches(keyword.as_str()).count();
|
|
67
|
+
score += count * 10;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
score
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn should_skip(path: &Path) -> bool {
|
|
74
|
+
let path_str = path.to_string_lossy();
|
|
75
|
+
|
|
76
|
+
if path_str.contains("target/")
|
|
77
|
+
|| path_str.contains(".git/")
|
|
78
|
+
|| path_str.contains("node_modules/")
|
|
79
|
+
|| path_str.contains(".vscode/")
|
|
80
|
+
{
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if let Some(ext) = path.extension() {
|
|
85
|
+
let ext_str = ext.to_string_lossy();
|
|
86
|
+
if ext_str == "lock"
|
|
87
|
+
|| ext_str == "json"
|
|
88
|
+
|| ext_str == "md"
|
|
89
|
+
|| ext_str == "txt"
|
|
90
|
+
|| ext_str == "yml"
|
|
91
|
+
|| ext_str == "yaml"
|
|
92
|
+
{
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn is_common_word(word: &str) -> bool {
|
|
101
|
+
matches!(
|
|
102
|
+
word,
|
|
103
|
+
"the" | "and"
|
|
104
|
+
| "for"
|
|
105
|
+
| "are"
|
|
106
|
+
| "but"
|
|
107
|
+
| "not"
|
|
108
|
+
| "you"
|
|
109
|
+
| "all"
|
|
110
|
+
| "can"
|
|
111
|
+
| "her"
|
|
112
|
+
| "was"
|
|
113
|
+
| "one"
|
|
114
|
+
| "our"
|
|
115
|
+
| "out"
|
|
116
|
+
| "day"
|
|
117
|
+
| "get"
|
|
118
|
+
| "has"
|
|
119
|
+
| "him"
|
|
120
|
+
| "his"
|
|
121
|
+
| "how"
|
|
122
|
+
| "let"
|
|
123
|
+
| "may"
|
|
124
|
+
| "new"
|
|
125
|
+
| "now"
|
|
126
|
+
| "old"
|
|
127
|
+
| "see"
|
|
128
|
+
| "try"
|
|
129
|
+
| "use"
|
|
130
|
+
| "way"
|
|
131
|
+
| "who"
|
|
132
|
+
| "boy"
|
|
133
|
+
| "did"
|
|
134
|
+
| "its"
|
|
135
|
+
| "say"
|
|
136
|
+
| "she"
|
|
137
|
+
| "too"
|
|
138
|
+
| "any"
|
|
139
|
+
| "add"
|
|
140
|
+
| "set"
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
mod rust_parser;
|
|
2
|
+
mod symbol_search;
|
|
3
|
+
mod context;
|
|
4
|
+
|
|
5
|
+
pub use rust_parser::RustParser;
|
|
6
|
+
pub use symbol_search::SymbolSearcher;
|
|
7
|
+
pub use context::ContextBuilder;
|
|
8
|
+
|
|
9
|
+
use anyhow::Result;
|
|
10
|
+
use std::path::{Path, PathBuf};
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Clone)]
|
|
13
|
+
pub struct Symbol {
|
|
14
|
+
pub name: String,
|
|
15
|
+
pub kind: SymbolKind,
|
|
16
|
+
pub file: PathBuf,
|
|
17
|
+
#[allow(dead_code)]
|
|
18
|
+
pub line: usize,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
22
|
+
pub enum SymbolKind {
|
|
23
|
+
Function,
|
|
24
|
+
Struct,
|
|
25
|
+
Enum,
|
|
26
|
+
Trait,
|
|
27
|
+
Impl,
|
|
28
|
+
Module,
|
|
29
|
+
Constant,
|
|
30
|
+
Static,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[derive(Debug)]
|
|
34
|
+
pub struct ProjectIntelligence {
|
|
35
|
+
root: PathBuf,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl ProjectIntelligence {
|
|
39
|
+
pub fn new(root: PathBuf) -> Self {
|
|
40
|
+
Self { root }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn find_symbol(&self, name: &str) -> Result<Vec<Symbol>> {
|
|
44
|
+
SymbolSearcher::search(&self.root, name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[allow(dead_code)]
|
|
48
|
+
pub fn get_file_symbols(&self, file: &Path) -> Result<Vec<Symbol>> {
|
|
49
|
+
RustParser::parse_file(file)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn get_relevant_context(&self, query: &str) -> Result<Vec<PathBuf>> {
|
|
53
|
+
ContextBuilder::build_context(&self.root, query)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[allow(dead_code)]
|
|
57
|
+
pub fn analyze_dependencies(&self) -> Result<Vec<String>> {
|
|
58
|
+
RustParser::extract_dependencies(&self.root)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
use super::{Symbol, SymbolKind};
|
|
2
|
+
use anyhow::{Context, Result};
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
use syn::{visit::Visit, Item};
|
|
5
|
+
|
|
6
|
+
pub struct RustParser;
|
|
7
|
+
|
|
8
|
+
impl RustParser {
|
|
9
|
+
pub fn parse_file(path: &Path) -> Result<Vec<Symbol>> {
|
|
10
|
+
let content = std::fs::read_to_string(path)
|
|
11
|
+
.with_context(|| format!("Failed to read file {}", path.display()))?;
|
|
12
|
+
|
|
13
|
+
let syntax = syn::parse_file(&content)
|
|
14
|
+
.with_context(|| format!("Failed to parse Rust file {}", path.display()))?;
|
|
15
|
+
|
|
16
|
+
let mut visitor = SymbolVisitor {
|
|
17
|
+
symbols: Vec::new(),
|
|
18
|
+
file: path.to_path_buf(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
visitor.visit_file(&syntax);
|
|
22
|
+
|
|
23
|
+
Ok(visitor.symbols)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[allow(dead_code)]
|
|
27
|
+
pub fn extract_dependencies(root: &Path) -> Result<Vec<String>> {
|
|
28
|
+
let cargo_path = root.join("Cargo.toml");
|
|
29
|
+
|
|
30
|
+
if !cargo_path.exists() {
|
|
31
|
+
return Ok(Vec::new());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let content = std::fs::read_to_string(&cargo_path)
|
|
35
|
+
.context("Failed to read Cargo.toml")?;
|
|
36
|
+
|
|
37
|
+
let toml: toml::Value = toml::from_str(&content)
|
|
38
|
+
.context("Failed to parse Cargo.toml")?;
|
|
39
|
+
|
|
40
|
+
let mut deps = Vec::new();
|
|
41
|
+
|
|
42
|
+
if let Some(dependencies) = toml.get("dependencies") {
|
|
43
|
+
if let Some(table) = dependencies.as_table() {
|
|
44
|
+
for (name, _) in table {
|
|
45
|
+
deps.push(name.clone());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Ok(deps)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct SymbolVisitor {
|
|
55
|
+
symbols: Vec<Symbol>,
|
|
56
|
+
file: PathBuf,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl<'ast> Visit<'ast> for SymbolVisitor {
|
|
60
|
+
fn visit_item(&mut self, item: &'ast Item) {
|
|
61
|
+
match item {
|
|
62
|
+
Item::Fn(func) => {
|
|
63
|
+
let name = func.sig.ident.to_string();
|
|
64
|
+
self.symbols.push(Symbol {
|
|
65
|
+
name,
|
|
66
|
+
kind: SymbolKind::Function,
|
|
67
|
+
file: self.file.clone(),
|
|
68
|
+
line: 0,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
Item::Struct(s) => {
|
|
72
|
+
let name = s.ident.to_string();
|
|
73
|
+
self.symbols.push(Symbol {
|
|
74
|
+
name,
|
|
75
|
+
kind: SymbolKind::Struct,
|
|
76
|
+
file: self.file.clone(),
|
|
77
|
+
line: 0,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
Item::Enum(e) => {
|
|
81
|
+
let name = e.ident.to_string();
|
|
82
|
+
self.symbols.push(Symbol {
|
|
83
|
+
name,
|
|
84
|
+
kind: SymbolKind::Enum,
|
|
85
|
+
file: self.file.clone(),
|
|
86
|
+
line: 0,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
Item::Trait(t) => {
|
|
90
|
+
let name = t.ident.to_string();
|
|
91
|
+
self.symbols.push(Symbol {
|
|
92
|
+
name,
|
|
93
|
+
kind: SymbolKind::Trait,
|
|
94
|
+
file: self.file.clone(),
|
|
95
|
+
line: 0,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
Item::Impl(impl_item) => {
|
|
99
|
+
if let Some((_, path, _)) = &impl_item.trait_ {
|
|
100
|
+
let name = quote::quote!(#path).to_string();
|
|
101
|
+
self.symbols.push(Symbol {
|
|
102
|
+
name,
|
|
103
|
+
kind: SymbolKind::Impl,
|
|
104
|
+
file: self.file.clone(),
|
|
105
|
+
line: 0,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
Item::Mod(m) => {
|
|
110
|
+
let name = m.ident.to_string();
|
|
111
|
+
self.symbols.push(Symbol {
|
|
112
|
+
name,
|
|
113
|
+
kind: SymbolKind::Module,
|
|
114
|
+
file: self.file.clone(),
|
|
115
|
+
line: 0,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
Item::Const(c) => {
|
|
119
|
+
let name = c.ident.to_string();
|
|
120
|
+
self.symbols.push(Symbol {
|
|
121
|
+
name,
|
|
122
|
+
kind: SymbolKind::Constant,
|
|
123
|
+
file: self.file.clone(),
|
|
124
|
+
line: 0,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
Item::Static(s) => {
|
|
128
|
+
let name = s.ident.to_string();
|
|
129
|
+
self.symbols.push(Symbol {
|
|
130
|
+
name,
|
|
131
|
+
kind: SymbolKind::Static,
|
|
132
|
+
file: self.file.clone(),
|
|
133
|
+
line: 0,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
_ => {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
syn::visit::visit_item(self, item);
|
|
140
|
+
}
|
|
141
|
+
}
|