use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct LspServerConfig {
pub command: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct LspServerInfo {
pub language: &'static str,
pub binary: &'static str,
pub install_hint: &'static str,
}
pub const KNOWN_SERVERS: &[LspServerInfo] = &[
LspServerInfo {
language: "rust",
binary: "rust-analyzer",
install_hint: "rustup component add rust-analyzer",
},
LspServerInfo {
language: "typescript",
binary: "typescript-language-server",
install_hint: "npm install -g typescript-language-server typescript",
},
LspServerInfo {
language: "python",
binary: "pylsp",
install_hint: "pip install python-lsp-server",
},
LspServerInfo {
language: "go",
binary: "gopls",
install_hint: "go install golang.org/x/tools/gopls@latest",
},
];
pub fn default_servers() -> HashMap<&'static str, LspServerConfig> {
let mut m = HashMap::new();
m.insert(
"rust",
LspServerConfig {
command: "rust-analyzer".into(),
args: vec![],
},
);
m.insert(
"typescript",
LspServerConfig {
command: "typescript-language-server".into(),
args: vec!["--stdio".into()],
},
);
m.insert(
"javascript",
LspServerConfig {
command: "typescript-language-server".into(),
args: vec!["--stdio".into()],
},
);
m.insert(
"python",
LspServerConfig {
command: "pylsp".into(),
args: vec![],
},
);
m.insert(
"go",
LspServerConfig {
command: "gopls".into(),
args: vec!["serve".into()],
},
);
m
}
pub fn language_for_extension(ext: &str) -> Option<&'static str> {
match ext {
"rs" => Some("rust"),
"ts" | "tsx" => Some("typescript"),
"js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
"py" | "pyi" => Some("python"),
"go" => Some("go"),
"java" => Some("java"),
"rb" => Some("ruby"),
"c" | "h" => Some("c"),
"cpp" | "cxx" | "cc" | "hpp" => Some("cpp"),
"cs" => Some("csharp"),
_ => None,
}
}
pub fn find_binary_in_path(binary: &str) -> Option<PathBuf> {
let path_var = std::env::var("PATH").ok()?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(binary);
if candidate.is_file() {
return Some(candidate);
}
if cfg!(windows) {
let exe = dir.join(format!("{binary}.exe"));
if exe.is_file() {
return Some(exe);
}
}
}
None
}
pub fn install_hint_for_language(language: &str) -> &'static str {
for info in KNOWN_SERVERS {
if info.language == language {
return info.install_hint;
}
}
"No install instructions available for this language server."
}
pub fn binary_for_language(language: &str) -> Option<&'static str> {
for info in KNOWN_SERVERS {
if info.language == language {
return Some(info.binary);
}
}
None
}
pub fn check_server_available(language: &str) -> Result<PathBuf, String> {
let servers = default_servers();
let config = servers
.get(language)
.ok_or_else(|| format!("No LSP server configured for '{language}'"))?;
find_binary_in_path(&config.command).ok_or_else(|| {
let hint = install_hint_for_language(language);
format!(
"Language server '{}' not found in PATH.\n\
\n\
ctx_refactor requires an external language server for '{}' files.\n\
Install it with:\n\
\n\
\x20 {}\n\
\n\
Then retry. This is optional — ctx_search and ctx_graph work without it.",
config.command, language, hint
)
})
}