pub struct LanguageMapping {
pub language: &'static str,
pub extensions: &'static [&'static str],
pub backend: &'static str,
pub spawn_args: &'static [&'static str],
pub default_args: &'static [&'static str],
}
pub const DEFAULT_LANGUAGES: &[LanguageMapping] = &[
LanguageMapping {
language: "rust",
extensions: &["rs"],
backend: "rust-analyzer",
spawn_args: &[],
default_args: &[],
},
LanguageMapping {
language: "go",
extensions: &["go"],
backend: "gopls",
spawn_args: &[],
default_args: &[],
},
LanguageMapping {
language: "typescript",
extensions: &["ts", "tsx"],
backend: "typescript-language-server",
spawn_args: &["--stdio"],
default_args: &[],
},
LanguageMapping {
language: "javascript",
extensions: &["js", "jsx"],
backend: "typescript-language-server",
spawn_args: &["--stdio"],
default_args: &[],
},
LanguageMapping {
language: "python",
extensions: &["py"],
backend: "basedpyright-langserver",
spawn_args: &["--stdio"],
default_args: &[],
},
LanguageMapping {
language: "c",
extensions: &["c", "h"],
backend: "clangd",
spawn_args: &[],
default_args: &[],
},
LanguageMapping {
language: "cpp",
extensions: &["cpp", "cc", "cxx", "hpp", "hxx"],
backend: "clangd",
spawn_args: &[],
default_args: &[],
},
];
pub fn lookup_by_extension(ext: &str) -> Option<(&'static str, String)> {
for mapping in DEFAULT_LANGUAGES {
if mapping.extensions.contains(&ext) {
let cmd = spawn_cmd(mapping);
return Some((mapping.language, cmd));
}
}
None
}
pub fn default_args_by_extension(ext: &str) -> &'static [&'static str] {
for mapping in DEFAULT_LANGUAGES {
if mapping.extensions.contains(&ext) {
return mapping.default_args;
}
}
&[]
}
fn spawn_cmd(mapping: &LanguageMapping) -> String {
if mapping.spawn_args.is_empty() {
mapping.backend.to_string()
} else {
format!("{} {}", mapping.backend, mapping.spawn_args.join(" "))
}
}
pub fn generate_language_table() -> String {
let mut rows = vec!["| Extensions | Language | Backend | Status |".to_string()];
rows.push("|---|---|---|---|".to_string());
for mapping in DEFAULT_LANGUAGES {
let available = which(mapping.backend);
let exts: Vec<String> = mapping.extensions.iter().map(|e| format!(".{e}")).collect();
let status = if available { "ok" } else { "not installed" };
rows.push(format!(
"| {} | `{}` | `{}` | {} |",
exts.join(", "),
mapping.language,
mapping.backend,
status,
));
}
rows.join("\n")
}
fn which(cmd: &str) -> bool {
crate::tool_path::resolve_tool(cmd).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
fn touch_exe(path: &std::path::Path) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, b"#!/bin/sh\n").unwrap();
let mut perms = fs::metadata(path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms).unwrap();
}
#[test]
fn test_which_finds_local_bin_with_lean_path() {
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let root = tempfile::tempdir().unwrap();
let home = root.path().join("home");
let empty = root.path().join("empty");
fs::create_dir_all(&empty).unwrap();
let backend = "basedpyright-langserver";
touch_exe(&home.join(".local").join("bin").join(backend));
let prev_home = std::env::var_os("HOME");
let prev_path = std::env::var_os("PATH");
unsafe {
std::env::set_var("HOME", &home);
std::env::set_var("PATH", &empty);
}
let ok = which(backend);
unsafe {
match prev_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match prev_path {
Some(v) => std::env::set_var("PATH", v),
None => std::env::remove_var("PATH"),
}
}
assert!(ok, "lean PATH should still resolve ~/.local/bin backend");
}
#[test]
fn test_generate_language_table_contains_status_column() {
let table = generate_language_table();
assert!(table.contains("| Extensions | Language | Backend | Status |"));
assert!(table.contains("`rust`"));
}
}