vtcode_core/utils/
utils.rs1use anyhow::Result;
7use console::style;
8use regex::Regex;
9use std::fs;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12
13pub fn render_pty_output_fn(output: &str, title: &str, command: Option<&str>) -> Result<()> {
15 println!("{}", style("=".repeat(80)).dim());
17
18 println!(
20 "{} {}",
21 style("==").blue().bold(),
22 style(title).blue().bold()
23 );
24
25 if let Some(cmd) = command {
27 println!("{}", style(format!("> {}", cmd)).dim());
28 }
29
30 println!("{}", style("-".repeat(80)).dim());
32
33 print!("{}", output);
35 std::io::stdout().flush()?;
36
37 println!("{}", style("-".repeat(80)).dim());
39 println!("{}", style("==").blue().bold());
40 println!("{}", style("=".repeat(80)).dim());
41
42 Ok(())
43}
44
45pub struct ProjectOverview {
47 pub name: Option<String>,
48 pub version: Option<String>,
49 pub description: Option<String>,
50 pub readme_excerpt: Option<String>,
51 pub root: PathBuf,
52}
53
54impl ProjectOverview {
55 pub fn short_for_display(&self) -> String {
56 let mut out = String::new();
57 if let Some(name) = &self.name {
58 out.push_str(&format!("Project: {}", name));
59 }
60 if let Some(ver) = &self.version {
61 if !out.is_empty() {
62 out.push(' ');
63 }
64 out.push_str(&format!("v{}", ver));
65 }
66 if !out.is_empty() {
67 out.push('\n');
68 }
69 if let Some(desc) = &self.description {
70 out.push_str(desc);
71 out.push('\n');
72 }
73 out.push_str(&format!("Root: {}", self.root.display()));
74 out
75 }
76
77 pub fn as_prompt_block(&self) -> String {
78 let mut s = String::new();
79 if let Some(name) = &self.name {
80 s.push_str(&format!("- Name: {}\n", name));
81 }
82 if let Some(ver) = &self.version {
83 s.push_str(&format!("- Version: {}\n", ver));
84 }
85 if let Some(desc) = &self.description {
86 s.push_str(&format!("- Description: {}\n", desc));
87 }
88 s.push_str(&format!("- Workspace Root: {}\n", self.root.display()));
89 if let Some(excerpt) = &self.readme_excerpt {
90 s.push_str("- README Excerpt: \n");
91 s.push_str(excerpt);
92 if !excerpt.ends_with('\n') {
93 s.push('\n');
94 }
95 }
96 s
97 }
98}
99
100pub fn build_project_overview(root: &Path) -> Option<ProjectOverview> {
102 let mut overview = ProjectOverview {
103 name: None,
104 version: None,
105 description: None,
106 readme_excerpt: None,
107 root: root.to_path_buf(),
108 };
109
110 let cargo_toml_path = root.join("Cargo.toml");
112 if let Ok(cargo_toml) = fs::read_to_string(&cargo_toml_path) {
113 overview.name = extract_toml_str(&cargo_toml, "name");
114 overview.version = extract_toml_str(&cargo_toml, "version");
115 overview.description = extract_toml_str(&cargo_toml, "description");
116 }
117
118 let readme_path = root.join("README.md");
120 if let Ok(readme) = fs::read_to_string(&readme_path) {
121 overview.readme_excerpt = Some(extract_readme_excerpt(&readme, 1200));
122 } else {
123 for alt in [
125 "QUICKSTART.md",
126 "user-context.md",
127 "docs/project/ROADMAP.md",
128 ] {
129 let path = root.join(alt);
130 if let Ok(txt) = fs::read_to_string(&path) {
131 overview.readme_excerpt = Some(extract_readme_excerpt(&txt, 800));
132 break;
133 }
134 }
135 }
136
137 if overview.name.is_none() && overview.readme_excerpt.is_none() {
139 return None;
140 }
141 Some(overview)
142}
143
144pub fn extract_toml_str(content: &str, key: &str) -> Option<String> {
146 let pkg_section = if let Some(start) = content.find("[package]") {
148 let rest = &content[start + "[package]".len()..];
149 if let Some(_next) = rest.find('\n') {
151 &content[start..]
152 } else {
153 &content[start..]
154 }
155 } else {
156 content
157 };
158
159 let pattern = format!(r#"(?m)^\s*{}\s*=\s*"([^"]+)"\s*$"#, regex::escape(key));
161 let re = Regex::new(&pattern).ok()?;
162 re.captures(pkg_section)
163 .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
164}
165
166pub fn extract_readme_excerpt(md: &str, max_len: usize) -> String {
168 let mut excerpt = String::new();
170 for line in md.lines() {
171 if excerpt.len() > max_len {
173 break;
174 }
175 excerpt.push_str(line);
176 excerpt.push('\n');
177 if line.trim().starts_with("## ") && excerpt.len() > (max_len / 2) {
179 break;
180 }
181 }
182 if excerpt.len() > max_len {
183 excerpt.truncate(max_len);
184 excerpt.push_str("...\n");
185 }
186 excerpt
187}
188
189pub fn summarize_workspace_languages(root: &std::path::Path) -> Option<String> {
191 use indexmap::IndexMap;
192 let analyzer = match crate::tools::tree_sitter::analyzer::TreeSitterAnalyzer::new() {
193 Ok(a) => a,
194 Err(_) => return None,
195 };
196 let mut counts: IndexMap<String, usize> = IndexMap::new();
197 let mut total = 0usize;
198 for entry in walkdir::WalkDir::new(root)
199 .max_depth(4)
200 .into_iter()
201 .filter_map(|e| e.ok())
202 {
203 let path = entry.path();
204 if path.is_file()
205 && let Ok(lang) = analyzer.detect_language_from_path(path)
206 {
207 *counts.entry(format!("{:?}", lang)).or_insert(0) += 1;
208 total += 1;
209 }
210 if total > 5000 {
211 break;
212 }
213 }
214 if counts.is_empty() {
215 None
216 } else {
217 let mut parts: Vec<String> = counts
218 .into_iter()
219 .map(|(k, v)| format!("{}:{}", k, v))
220 .collect();
221 parts.sort();
222 Some(parts.join(", "))
223 }
224}
225
226pub fn safe_replace_text(
228 content: &str,
229 old_str: &str,
230 new_str: &str,
231) -> Result<String, anyhow::Error> {
232 if old_str.is_empty() {
233 return Err(anyhow::anyhow!("old_string cannot be empty"));
234 }
235
236 if !content.contains(old_str) {
237 return Err(anyhow::anyhow!("Text '{}' not found in file", old_str));
238 }
239
240 Ok(content.replace(old_str, new_str))
241}