use std::collections::HashMap;
use std::path::Path;
use walkdir::WalkDir;
fn ext_to_lang(ext: &str) -> Option<&'static str> {
let ext = ext.to_lowercase();
match ext.as_str() {
"rs" => Some("Rust"),
"c" => Some("C"),
"cpp" | "cc" | "cxx" => Some("C++"),
"h" | "hpp" | "hxx" => Some("C/C++ Header"),
"go" => Some("Go"),
"zig" => Some("Zig"),
"java" => Some("Java"),
"kt" | "kts" => Some("Kotlin"),
"scala" => Some("Scala"),
"cs" => Some("C#"),
"fs" | "fsx" => Some("F#"),
"py" | "pyw" => Some("Python"),
"rb" => Some("Ruby"),
"pl" => Some("Perl"),
"php" => Some("PHP"),
"lua" => Some("Lua"),
"r" => Some("R"),
"js" | "mjs" | "cjs" => Some("JavaScript"),
"ts" => Some("TypeScript"),
"jsx" => Some("JSX"),
"tsx" => Some("TSX"),
"html" | "htm" => Some("HTML"),
"css" => Some("CSS"),
"scss" | "sass" => Some("SCSS"),
"less" => Some("Less"),
"vue" => Some("Vue"),
"svelte" => Some("Svelte"),
"sh" | "bash" | "zsh" => Some("Shell"),
"ps1" | "psm1" | "psd1" => Some("PowerShell"),
"bat" | "cmd" => Some("Batch"),
"toml" => Some("TOML"),
"yaml" | "yml" => Some("YAML"),
"xml" => Some("XML"),
"json" => Some("JSON"),
"md" | "mdx" => Some("Markdown"),
"rst" => Some("reST"),
"tex" => Some("LaTeX"),
"typ" => Some("Typst"),
"sql" => Some("SQL"),
"proto" => Some("Protobuf"),
"graphql" | "gql" => Some("GraphQL"),
"dockerfile" => Some("Docker"),
"nix" => Some("Nix"),
"jl" => Some("Julia"),
"swift" => Some("Swift"),
"dart" => Some("Dart"),
"ex" | "exs" => Some("Elixir"),
"erl" | "hrl" => Some("Erlang"),
"hs" => Some("Haskell"),
"ml" | "mli" => Some("OCaml"),
"elm" => Some("Elm"),
"clj" | "cljs" | "edn" => Some("Clojure"),
"tf" | "tfvars" => Some("Terraform"),
"cmake" | "CMakeLists.txt" => Some("CMake"),
"Makefile" | "makefile" => Some("Makefile"),
"ipynb" => Some("Jupyter"),
_ => None,
}
}
const SKIP_DIRS: &[&str] = &[
".git", ".claude", "node_modules", "target", "__pycache__",
"venv", ".venv", "dist", "build", ".next", ".nuxt",
"vendor", "bower_components", ".tox", ".eggs", ".mypy_cache",
".pytest_cache", ".ruff_cache", "coverage", "htmlcov",
];
const SKIP_EXTS: &[&str] = &[
"lock", "txt", "log", "png", "jpg", "jpeg", "gif", "svg", "ico",
"pdf", "exe", "dll", "so", "dylib", "o", "a", "class",
"jar", "war", "zip", "tar", "gz", "bz2", "7z", "rar",
"mp3", "mp4", "avi", "mov", "wav", "flac",
"ttf", "otf", "woff", "woff2", "eot",
"db", "sqlite", "sqlite3",
"pem", "crt", "key", "keystore",
"bin", "dat", "pkl", "pickle",
"pyc", "pyo", "rlib", "rmeta",
];
pub fn detect_languages(project_paths: &[(String, String, bool)]) -> HashMap<String, f64> {
let mut lang_counts: HashMap<String, u64> = HashMap::new();
let mut total = 0u64;
for (_display_name, full_path, path_resolved) in project_paths {
if !path_resolved || full_path.is_empty() {
continue;
}
let path = Path::new(full_path);
if !path.exists() {
continue;
}
let walker = WalkDir::new(path)
.max_depth(3)
.into_iter()
.filter_entry(|e| {
if let Some(name) = e.file_name().to_str() {
!SKIP_DIRS.contains(&name)
} else {
false
}
});
let mut count = 0u64;
for entry in walker {
if count >= 10_000 {
break;
}
if let Ok(entry) = entry {
if !entry.file_type().is_file() {
continue;
}
if let Some(ext) = entry.path().extension() {
let ext_str = ext.to_string_lossy();
if SKIP_EXTS.contains(&ext_str.as_ref()) {
continue;
}
if let Some(lang) = ext_to_lang(&ext_str) {
*lang_counts.entry(lang.to_string()).or_insert(0) += 1;
total += 1;
}
} else if let Some(name) = entry.path().file_name().and_then(|n| n.to_str()) {
if let Some(lang) = ext_to_lang(name) {
*lang_counts.entry(lang.to_string()).or_insert(0) += 1;
total += 1;
}
}
count += 1;
}
}
}
if total == 0 {
return HashMap::new();
}
let mut result: HashMap<String, f64> = HashMap::new();
for (lang, count) in &lang_counts {
result.insert(lang.clone(), (*count as f64 / total as f64) * 100.0);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ext_to_lang_common_languages() {
assert_eq!(ext_to_lang("rs"), Some("Rust"));
assert_eq!(ext_to_lang("py"), Some("Python"));
assert_eq!(ext_to_lang("js"), Some("JavaScript"));
assert_eq!(ext_to_lang("ts"), Some("TypeScript"));
assert_eq!(ext_to_lang("go"), Some("Go"));
assert_eq!(ext_to_lang("java"), Some("Java"));
assert_eq!(ext_to_lang("rb"), Some("Ruby"));
assert_eq!(ext_to_lang("c"), Some("C"));
assert_eq!(ext_to_lang("cpp"), Some("C++"));
assert_eq!(ext_to_lang("cc"), Some("C++"));
}
#[test]
fn test_ext_to_lang_case_insensitive() {
assert_eq!(ext_to_lang("RS"), Some("Rust"));
assert_eq!(ext_to_lang("Py"), Some("Python"));
assert_eq!(ext_to_lang("JS"), Some("JavaScript"));
assert_eq!(ext_to_lang("Ts"), Some("TypeScript"));
}
#[test]
fn test_ext_to_lang_web_extensions() {
assert_eq!(ext_to_lang("tsx"), Some("TSX"));
assert_eq!(ext_to_lang("jsx"), Some("JSX"));
assert_eq!(ext_to_lang("vue"), Some("Vue"));
assert_eq!(ext_to_lang("svelte"), Some("Svelte"));
assert_eq!(ext_to_lang("html"), Some("HTML"));
assert_eq!(ext_to_lang("htm"), Some("HTML"));
assert_eq!(ext_to_lang("css"), Some("CSS"));
assert_eq!(ext_to_lang("scss"), Some("SCSS"));
}
#[test]
fn test_ext_to_lang_shell_and_config() {
assert_eq!(ext_to_lang("sh"), Some("Shell"));
assert_eq!(ext_to_lang("bash"), Some("Shell"));
assert_eq!(ext_to_lang("zsh"), Some("Shell"));
assert_eq!(ext_to_lang("toml"), Some("TOML"));
assert_eq!(ext_to_lang("yaml"), Some("YAML"));
assert_eq!(ext_to_lang("yml"), Some("YAML"));
assert_eq!(ext_to_lang("json"), Some("JSON"));
}
#[test]
fn test_ext_to_lang_special_filenames() {
assert_eq!(ext_to_lang("dockerfile"), Some("Docker"));
assert_eq!(ext_to_lang("Makefile"), Some("Makefile"));
assert_eq!(ext_to_lang("makefile"), Some("Makefile"));
}
#[test]
fn test_ext_to_lang_unknown() {
assert_eq!(ext_to_lang("xyz"), None);
assert_eq!(ext_to_lang(""), None);
assert_eq!(ext_to_lang("foobar"), None);
}
#[test]
fn test_ext_to_lang_functional_languages() {
assert_eq!(ext_to_lang("hs"), Some("Haskell"));
assert_eq!(ext_to_lang("ml"), Some("OCaml"));
assert_eq!(ext_to_lang("elm"), Some("Elm"));
assert_eq!(ext_to_lang("clj"), Some("Clojure"));
assert_eq!(ext_to_lang("ex"), Some("Elixir"));
assert_eq!(ext_to_lang("erl"), Some("Erlang"));
}
#[test]
fn test_ext_to_lang_microsoft_languages() {
assert_eq!(ext_to_lang("cs"), Some("C#"));
assert_eq!(ext_to_lang("fs"), Some("F#"));
assert_eq!(ext_to_lang("ps1"), Some("PowerShell"));
assert_eq!(ext_to_lang("bat"), Some("Batch"));
assert_eq!(ext_to_lang("cmd"), Some("Batch"));
}
#[test]
fn test_detect_languages_empty_input() {
let result = detect_languages(&[]);
assert!(result.is_empty());
}
#[test]
fn test_detect_languages_unresolved_path_skipped() {
let projects = vec![
("test".to_string(), "/some/path".to_string(), false),
];
let result = detect_languages(&projects);
assert!(result.is_empty());
}
#[test]
fn test_detect_languages_nonexistent_path() {
let projects = vec![
("test".to_string(), "/nonexistent/path/xyz".to_string(), true),
];
let result = detect_languages(&projects);
assert!(result.is_empty());
}
#[test]
fn test_detect_languages_with_real_files() {
let dir = std::env::temp_dir().join("claude-dash-test-lang-detect");
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(dir.join("main.rs"), "fn main() {}");
let _ = std::fs::write(dir.join("lib.py"), "pass");
let _ = std::fs::write(dir.join("app.js"), "");
let projects = vec![
("test".to_string(), dir.to_string_lossy().to_string(), true),
];
let result = detect_languages(&projects);
assert!(!result.is_empty());
assert!(result.contains_key("Rust"));
assert!(result.contains_key("Python"));
assert!(result.contains_key("JavaScript"));
let total: f64 = result.values().sum();
assert!((total - 100.0).abs() < 0.01);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_detect_languages_skips_node_modules() {
let dir = std::env::temp_dir().join("claude-dash-test-lang-skip");
let _ = std::fs::remove_dir_all(&dir);
let nm_dir = dir.join("node_modules");
let _ = std::fs::create_dir_all(&nm_dir);
let _ = std::fs::write(dir.join("index.ts"), "");
let _ = std::fs::write(nm_dir.join("dep.js"), "");
let projects = vec![
("test".to_string(), dir.to_string_lossy().to_string(), true),
];
let result = detect_languages(&projects);
assert!(result.contains_key("TypeScript"));
assert!(result.get("TypeScript").unwrap() > &0.0);
let _ = std::fs::remove_dir_all(&dir);
}
}