Skip to main content

vtcode_core/utils/
common.rs

1//! Utility functions for the VT Code agent
2//!
3//! This module contains common utility functions that are used across different parts
4//! of the VT Code agent, helping to reduce code duplication and improve maintainability.
5
6use crate::utils::colors::style;
7use anyhow::Result;
8use std::collections::BTreeMap;
9use std::path::Path;
10
11pub use vtcode_commons::project::{ProjectOverview, build_project_overview};
12pub use vtcode_commons::utils::{
13    current_timestamp, extract_readme_excerpt, extract_toml_str, safe_replace_text,
14};
15
16/// Merge a base list of patterns with patterns loaded from an environment variable.
17/// The environment variable, if set, is expected to be a comma-separated list of values.
18pub fn merge_env_patterns(base: &[String], env_var: &str) -> Vec<String> {
19    let extra_val = std::env::var(env_var).ok();
20    let extra_count = extra_val
21        .as_ref()
22        .map(|s| s.split(',').count())
23        .unwrap_or(0);
24
25    let mut combined = Vec::with_capacity(base.len() + extra_count);
26
27    for entry in base {
28        let trimmed = entry.trim();
29        if !trimmed.is_empty() {
30            combined.push(trimmed.to_owned());
31        }
32    }
33
34    if let Some(extra) = extra_val {
35        for item in extra.split(',') {
36            let trimmed = item.trim();
37            if !trimmed.is_empty() {
38                combined.push(trimmed.to_owned());
39            }
40        }
41    }
42
43    combined
44}
45
46const WORKSPACE_LANGUAGE_SCAN_LIMIT: usize = 5_000;
47
48/// Render PTY output in a terminal-like interface
49pub fn render_pty_output_fn(output: &str, title: &str, command: Option<&str>) -> Result<()> {
50    use std::io::Write;
51
52    let stdout = std::io::stdout();
53    let mut handle = stdout.lock();
54
55    writeln!(handle, "{}", style("=".repeat(80)).dim())?;
56    writeln!(handle, "{} {}", style("==").bold(), style(title).bold())?;
57
58    if let Some(cmd) = command {
59        writeln!(handle, "{}", style(format!("> {}", cmd)).dim())?;
60    }
61
62    writeln!(handle, "{}", style("-".repeat(80)).dim())?;
63    write!(handle, "{}", output)?;
64    writeln!(handle, "{}", style("-".repeat(80)).dim())?;
65    writeln!(handle, "{}", style("==").bold())?;
66    writeln!(handle, "{}", style("=".repeat(80)).dim())?;
67    handle.flush()?;
68
69    Ok(())
70}
71
72/// Summarize workspace languages using file extension heuristics
73pub fn summarize_workspace_languages(root: &Path) -> Option<String> {
74    let counts = collect_workspace_language_counts(root);
75    if counts.is_empty() {
76        return None;
77    }
78
79    Some(
80        counts
81            .into_iter()
82            .map(|(language, count)| format!("{language}:{count}"))
83            .collect::<Vec<_>>()
84            .join(", "),
85    )
86}
87
88/// Detect the dominant workspace languages using file extension heuristics.
89pub fn detect_workspace_languages(root: &Path) -> Vec<String> {
90    let mut counts = collect_workspace_language_counts(root)
91        .into_iter()
92        .collect::<Vec<_>>();
93    counts.sort_by(|(left_lang, left_count), (right_lang, right_count)| {
94        right_count
95            .cmp(left_count)
96            .then_with(|| left_lang.cmp(right_lang))
97    });
98    counts
99        .into_iter()
100        .map(|(language, _)| language)
101        .take(5)
102        .collect()
103}
104
105pub fn display_language_from_path(path: &Path) -> Option<&'static str> {
106    let extension = path.extension()?.to_str()?;
107    display_language_from_extension(extension)
108}
109
110pub fn display_language_from_editor_language_id(language_id: &str) -> Option<&'static str> {
111    match language_id.trim().to_ascii_lowercase().as_str() {
112        "rust" => Some("Rust"),
113        "python" => Some("Python"),
114        "javascript" | "javascriptreact" => Some("JavaScript"),
115        "typescript" | "typescriptreact" => Some("TypeScript"),
116        "go" => Some("Go"),
117        "java" => Some("Java"),
118        "shellscript" | "bash" | "shell" | "zsh" | "sh" => Some("Bash"),
119        "swift" => Some("Swift"),
120        "c" => Some("C"),
121        "cpp" | "c++" => Some("C++"),
122        "ruby" => Some("Ruby"),
123        "php" => Some("PHP"),
124        _ => None,
125    }
126}
127
128fn collect_workspace_language_counts(root: &Path) -> BTreeMap<String, usize> {
129    let mut counts = BTreeMap::new();
130    let mut total = 0usize;
131
132    for entry in vtcode_commons::walk::build_walker_single_threaded(root)
133        .max_depth(Some(4))
134        .build()
135        .filter_map(|entry| entry.ok())
136    {
137        let path = entry.path();
138        if path.is_file()
139            && let Some(language) = display_language_from_path(path)
140        {
141            *counts.entry(language.to_string()).or_insert(0) += 1;
142            total += 1;
143        }
144
145        if total > WORKSPACE_LANGUAGE_SCAN_LIMIT {
146            break;
147        }
148    }
149
150    counts
151}
152
153fn display_language_from_extension(extension: &str) -> Option<&'static str> {
154    match extension {
155        "rs" => Some("Rust"),
156        "py" => Some("Python"),
157        "js" | "jsx" => Some("JavaScript"),
158        "ts" | "tsx" => Some("TypeScript"),
159        "go" => Some("Go"),
160        "java" => Some("Java"),
161        "sh" | "bash" => Some("Bash"),
162        "swift" => Some("Swift"),
163        "c" | "h" => Some("C"),
164        "cpp" | "cc" | "cxx" | "hpp" => Some("C++"),
165        "rb" => Some("Ruby"),
166        "php" => Some("PHP"),
167        _ => None,
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::{
174        detect_workspace_languages, display_language_from_editor_language_id,
175        display_language_from_path, summarize_workspace_languages,
176    };
177    use std::fs;
178    use std::path::Path;
179    use tempfile::TempDir;
180
181    #[test]
182    fn detect_workspace_languages_returns_top_languages() {
183        let workspace = TempDir::new().expect("workspace tempdir");
184        fs::create_dir_all(workspace.path().join("src")).expect("create src");
185        fs::create_dir_all(workspace.path().join("web")).expect("create web");
186        fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
187        fs::write(workspace.path().join("src/main.rs"), "fn main() {}\n").expect("write rust");
188        fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
189
190        let languages = detect_workspace_languages(workspace.path());
191        assert_eq!(
192            languages,
193            vec!["Rust".to_string(), "TypeScript".to_string()]
194        );
195    }
196
197    #[test]
198    fn summarize_workspace_languages_reports_counts() {
199        let workspace = TempDir::new().expect("workspace tempdir");
200        fs::create_dir_all(workspace.path().join("src")).expect("create src");
201        fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
202        fs::write(workspace.path().join("src/main.rs"), "fn main() {}\n").expect("write rust");
203
204        let summary = summarize_workspace_languages(workspace.path()).expect("summary");
205        assert_eq!(summary, "Rust:2");
206    }
207
208    #[test]
209    fn display_language_helpers_cover_paths_and_editor_language_ids() {
210        assert_eq!(
211            display_language_from_path(Path::new("src/lib.rs")),
212            Some("Rust")
213        );
214        assert_eq!(
215            display_language_from_editor_language_id("typescriptreact"),
216            Some("TypeScript")
217        );
218        assert_eq!(
219            display_language_from_editor_language_id("shellscript"),
220            Some("Bash")
221        );
222        assert_eq!(display_language_from_editor_language_id("unknown"), None);
223    }
224}