1use std::collections::HashMap;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone)]
5pub struct LspServerConfig {
6 pub command: String,
7 pub args: Vec<String>,
8}
9
10#[derive(Debug, Clone)]
11pub struct LspServerInfo {
12 pub language: &'static str,
13 pub binary: &'static str,
14 pub install_hint: &'static str,
15}
16
17pub const KNOWN_SERVERS: &[LspServerInfo] = &[
18 LspServerInfo {
19 language: "rust",
20 binary: "rust-analyzer",
21 install_hint: "rustup component add rust-analyzer",
22 },
23 LspServerInfo {
24 language: "typescript",
25 binary: "typescript-language-server",
26 install_hint: "npm install -g typescript-language-server typescript",
27 },
28 LspServerInfo {
29 language: "python",
30 binary: "pylsp",
31 install_hint: "pip install python-lsp-server",
32 },
33 LspServerInfo {
34 language: "go",
35 binary: "gopls",
36 install_hint: "go install golang.org/x/tools/gopls@latest",
37 },
38];
39
40pub fn default_servers() -> HashMap<&'static str, LspServerConfig> {
41 let mut m = HashMap::new();
42 m.insert(
43 "rust",
44 LspServerConfig {
45 command: "rust-analyzer".into(),
46 args: vec![],
47 },
48 );
49 m.insert(
50 "typescript",
51 LspServerConfig {
52 command: "typescript-language-server".into(),
53 args: vec!["--stdio".into()],
54 },
55 );
56 m.insert(
57 "javascript",
58 LspServerConfig {
59 command: "typescript-language-server".into(),
60 args: vec!["--stdio".into()],
61 },
62 );
63 m.insert(
64 "python",
65 LspServerConfig {
66 command: "pylsp".into(),
67 args: vec![],
68 },
69 );
70 m.insert(
71 "go",
72 LspServerConfig {
73 command: "gopls".into(),
74 args: vec!["serve".into()],
75 },
76 );
77 m
78}
79
80pub fn language_for_extension(ext: &str) -> Option<&'static str> {
81 match ext {
82 "rs" => Some("rust"),
83 "ts" | "tsx" => Some("typescript"),
84 "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
85 "py" | "pyi" => Some("python"),
86 "go" => Some("go"),
87 "java" => Some("java"),
88 "kt" | "kts" => Some("kotlin"),
89 "rb" => Some("ruby"),
90 "c" | "h" => Some("c"),
91 "cpp" | "cxx" | "cc" | "hpp" => Some("cpp"),
92 "cs" => Some("csharp"),
93 _ => None,
94 }
95}
96
97pub fn find_binary_in_path(binary: &str) -> Option<PathBuf> {
98 let path_var = std::env::var("PATH").ok()?;
99 for dir in std::env::split_paths(&path_var) {
100 let candidate = dir.join(binary);
101 if candidate.is_file() {
102 return Some(candidate);
103 }
104 if cfg!(windows) {
105 let exe = dir.join(format!("{binary}.exe"));
106 if exe.is_file() {
107 return Some(exe);
108 }
109 }
110 }
111 None
112}
113
114pub fn install_hint_for_language(language: &str) -> &'static str {
115 for info in KNOWN_SERVERS {
116 if info.language == language {
117 return info.install_hint;
118 }
119 }
120 "No install instructions available for this language server."
121}
122
123pub fn binary_for_language(language: &str) -> Option<&'static str> {
124 for info in KNOWN_SERVERS {
125 if info.language == language {
126 return Some(info.binary);
127 }
128 }
129 None
130}
131
132pub fn check_server_available(language: &str) -> Result<PathBuf, String> {
133 let servers = default_servers();
134 let config = servers
135 .get(language)
136 .ok_or_else(|| format!("No LSP server configured for '{language}'"))?;
137
138 find_binary_in_path(&config.command).ok_or_else(|| {
139 let hint = install_hint_for_language(language);
140 format!(
141 "Language server '{}' not found in PATH.\n\
142 \n\
143 ctx_refactor requires an external language server for '{}' files.\n\
144 Install it with:\n\
145 \n\
146 \x20 {}\n\
147 \n\
148 Then retry. This is optional — ctx_search and ctx_graph work without it.",
149 config.command, language, hint
150 )
151 })
152}