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