1use serde::Serialize;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, Serialize)]
5pub struct LspRecipe {
6 pub language: &'static str,
7 pub extensions: &'static [&'static str],
8 pub server_name: &'static str,
9 pub install_command: &'static str,
10 pub binary_name: &'static str,
11 pub args: &'static [&'static str],
12 pub package_manager: &'static str,
13}
14
15pub const LSP_RECIPES: &[LspRecipe] = &[
16 LspRecipe {
17 language: "python",
18 extensions: &["py"],
19 server_name: "pyright",
20 install_command: "npm install -g pyright",
21 binary_name: "pyright-langserver",
22 args: &["--stdio"],
23 package_manager: "npm",
24 },
25 LspRecipe {
26 language: "typescript",
27 extensions: &["ts", "tsx", "js", "jsx", "mjs", "cjs"],
28 server_name: "typescript-language-server",
29 install_command: "npm install -g typescript-language-server typescript",
30 binary_name: "typescript-language-server",
31 args: &["--stdio"],
32 package_manager: "npm",
33 },
34 LspRecipe {
35 language: "rust",
36 extensions: &["rs"],
37 server_name: "rust-analyzer",
38 install_command: "rustup component add rust-analyzer",
39 binary_name: "rust-analyzer",
40 args: &[],
41 package_manager: "rustup",
42 },
43 LspRecipe {
44 language: "go",
45 extensions: &["go"],
46 server_name: "gopls",
47 install_command: "go install golang.org/x/tools/gopls@latest",
48 binary_name: "gopls",
49 args: &["serve"],
50 package_manager: "go",
51 },
52 LspRecipe {
53 language: "java",
54 extensions: &["java"],
55 server_name: "jdtls",
56 install_command: "brew install jdtls",
57 binary_name: "jdtls",
58 args: &[],
59 package_manager: "brew",
60 },
61 LspRecipe {
62 language: "kotlin",
63 extensions: &["kt", "kts"],
64 server_name: "kotlin-language-server",
65 install_command: "brew install kotlin-language-server",
66 binary_name: "kotlin-language-server",
67 args: &[],
68 package_manager: "brew",
69 },
70 LspRecipe {
71 language: "c_cpp",
72 extensions: &["c", "h", "cpp", "cc", "cxx", "hpp"],
73 server_name: "clangd",
74 install_command: "brew install llvm",
75 binary_name: "clangd",
76 args: &["--background-index"],
77 package_manager: "brew",
78 },
79 LspRecipe {
80 language: "ruby",
81 extensions: &["rb"],
82 server_name: "solargraph",
83 install_command: "gem install solargraph",
84 binary_name: "solargraph",
85 args: &["stdio"],
86 package_manager: "gem",
87 },
88 LspRecipe {
89 language: "php",
90 extensions: &["php"],
91 server_name: "intelephense",
92 install_command: "npm install -g intelephense",
93 binary_name: "intelephense",
94 args: &["--stdio"],
95 package_manager: "npm",
96 },
97 LspRecipe {
98 language: "scala",
99 extensions: &["scala", "sc"],
100 server_name: "metals",
101 install_command: "cs install metals",
102 binary_name: "metals",
103 args: &[],
104 package_manager: "coursier",
105 },
106 LspRecipe {
107 language: "swift",
108 extensions: &["swift"],
109 server_name: "sourcekit-lsp",
110 install_command: "xcode-select --install",
111 binary_name: "sourcekit-lsp",
112 args: &[],
113 package_manager: "xcode",
114 },
115 LspRecipe {
116 language: "csharp",
117 extensions: &["cs"],
118 server_name: "omnisharp",
119 install_command: "dotnet tool install -g csharp-ls",
120 binary_name: "csharp-ls",
121 args: &[],
122 package_manager: "dotnet",
123 },
124 LspRecipe {
125 language: "dart",
126 extensions: &["dart"],
127 server_name: "dart-language-server",
128 install_command: "dart pub global activate dart_language_server",
129 binary_name: "dart",
130 args: &["language-server", "--protocol=lsp"],
131 package_manager: "dart",
132 },
133 LspRecipe {
135 language: "lua",
136 extensions: &["lua"],
137 server_name: "lua-language-server",
138 install_command: "brew install lua-language-server",
139 binary_name: "lua-language-server",
140 args: &[],
141 package_manager: "brew",
142 },
143 LspRecipe {
144 language: "zig",
145 extensions: &["zig"],
146 server_name: "zls",
147 install_command: "brew install zls",
148 binary_name: "zls",
149 args: &[],
150 package_manager: "brew",
151 },
152 LspRecipe {
153 language: "elixir",
154 extensions: &["ex", "exs"],
155 server_name: "next-ls",
156 install_command: "mix escript.install hex next_ls",
157 binary_name: "nextls",
158 args: &["--stdio"],
159 package_manager: "mix",
160 },
161 LspRecipe {
162 language: "haskell",
163 extensions: &["hs"],
164 server_name: "haskell-language-server",
165 install_command: "ghcup install hls",
166 binary_name: "haskell-language-server-wrapper",
167 args: &["--lsp"],
168 package_manager: "ghcup",
169 },
170 LspRecipe {
171 language: "ocaml",
172 extensions: &["ml", "mli"],
173 server_name: "ocamllsp",
174 install_command: "opam install ocaml-lsp-server",
175 binary_name: "ocamllsp",
176 args: &[],
177 package_manager: "opam",
178 },
179 LspRecipe {
180 language: "erlang",
181 extensions: &["erl", "hrl"],
182 server_name: "erlang_ls",
183 install_command: "brew install erlang_ls",
184 binary_name: "erlang_ls",
185 args: &[],
186 package_manager: "brew",
187 },
188 LspRecipe {
189 language: "r",
190 extensions: &["r", "R"],
191 server_name: "languageserver",
192 install_command: "R -e 'install.packages(\"languageserver\")'",
193 binary_name: "R",
194 args: &["--slave", "-e", "languageserver::run()"],
195 package_manager: "R",
196 },
197 LspRecipe {
198 language: "shellscript",
199 extensions: &["sh", "bash"],
200 server_name: "bash-language-server",
201 install_command: "npm install -g bash-language-server",
202 binary_name: "bash-language-server",
203 args: &["start"],
204 package_manager: "npm",
205 },
206 LspRecipe {
207 language: "julia",
208 extensions: &["jl"],
209 server_name: "julia-lsp",
210 install_command: "julia -e 'using Pkg; Pkg.add(\"LanguageServer\")'",
211 binary_name: "julia",
212 args: &["--project=@.", "-e", "using LanguageServer; runserver()"],
213 package_manager: "julia",
214 },
215 ];
217
218fn command_candidates(command: &str) -> Vec<String> {
219 #[cfg(windows)]
220 let mut candidates = vec![command.to_owned()];
221 #[cfg(not(windows))]
222 let candidates = vec![command.to_owned()];
223 #[cfg(windows)]
224 if Path::new(command).extension().is_none() {
225 let pathext = std::env::var_os("PATHEXT")
226 .and_then(|value| value.into_string().ok())
227 .unwrap_or_else(|| ".COM;.EXE;.BAT;.CMD".to_owned());
228 for ext in pathext.split(';').filter(|ext| !ext.is_empty()) {
229 let normalized = if ext.starts_with('.') {
230 ext.to_owned()
231 } else {
232 format!(".{ext}")
233 };
234 let candidate = format!("{command}{normalized}");
235 if !candidates
236 .iter()
237 .any(|existing| existing.eq_ignore_ascii_case(&candidate))
238 {
239 candidates.push(candidate);
240 }
241 }
242 }
243 candidates
244}
245
246fn resolve_in_dir(dir: &Path, command: &str) -> Option<PathBuf> {
247 command_candidates(command)
248 .into_iter()
249 .map(|candidate| dir.join(candidate))
250 .find(|candidate| candidate.is_file())
251}
252
253fn fallback_search_dirs() -> Vec<PathBuf> {
254 let mut dirs = Vec::new();
255 #[cfg(not(windows))]
256 {
257 let home = std::env::var("HOME").unwrap_or_default();
258 for dir in [
259 "/opt/homebrew/bin".to_owned(),
260 "/usr/local/bin".to_owned(),
261 format!("{home}/.cargo/bin"),
262 format!("{home}/.fnm/aliases/default/bin"),
263 format!("{home}/.nvm/versions/node/current/bin"),
264 ] {
265 if !dir.is_empty() {
266 dirs.push(PathBuf::from(dir));
267 }
268 }
269 }
270 #[cfg(windows)]
271 {
272 let user_profile = std::env::var("USERPROFILE").unwrap_or_default();
273 let app_data = std::env::var("APPDATA").unwrap_or_default();
274 for dir in [
275 format!("{user_profile}\\.cargo\\bin"),
276 format!("{app_data}\\npm"),
277 ] {
278 if !dir.is_empty() {
279 dirs.push(PathBuf::from(dir));
280 }
281 }
282 }
283 dirs
284}
285
286pub(crate) fn resolve_lsp_binary(command: &str) -> Option<PathBuf> {
287 let command_path = Path::new(command);
288 if command_path.components().count() > 1 {
289 return if command_path.is_file() {
290 Some(command_path.to_path_buf())
291 } else if let Some(parent) = command_path.parent() {
292 resolve_in_dir(parent, command_path.file_name()?.to_str()?)
293 } else {
294 None
295 };
296 }
297
298 if let Some(path_dirs) = std::env::var_os("PATH") {
299 for dir in std::env::split_paths(&path_dirs) {
300 if let Some(path) = resolve_in_dir(&dir, command) {
301 return Some(path);
302 }
303 }
304 }
305
306 for dir in fallback_search_dirs() {
307 if let Some(path) = resolve_in_dir(&dir, command) {
308 return Some(path);
309 }
310 }
311
312 if let Some(extra) = std::env::var_os("CODELENS_LSP_PATH_EXTRA") {
313 for dir in std::env::split_paths(&extra) {
314 if let Some(path) = resolve_in_dir(&dir, command) {
315 return Some(path);
316 }
317 }
318 }
319
320 None
321}
322
323pub fn lsp_binary_exists(command: &str) -> bool {
328 resolve_lsp_binary(command).is_some()
329}
330
331pub fn check_lsp_status() -> Vec<LspStatus> {
333 LSP_RECIPES
334 .iter()
335 .map(|recipe| LspStatus {
336 language: recipe.language,
337 server_name: recipe.server_name,
338 installed: lsp_binary_exists(recipe.binary_name),
339 install_command: recipe.install_command,
340 })
341 .collect()
342}
343
344#[derive(Debug, Clone, Serialize)]
345pub struct LspStatus {
346 pub language: &'static str,
347 pub server_name: &'static str,
348 pub installed: bool,
349 pub install_command: &'static str,
350}
351
352pub fn get_lsp_recipe(extension: &str) -> Option<&'static LspRecipe> {
354 let ext = extension.to_ascii_lowercase();
355 LSP_RECIPES
356 .iter()
357 .find(|r| r.extensions.contains(&ext.as_str()))
358}
359
360pub fn default_lsp_command_for_extension(extension: &str) -> Option<&'static str> {
361 get_lsp_recipe(extension).map(|recipe| recipe.binary_name)
362}
363
364pub fn default_lsp_command_for_path(file_path: &str) -> Option<&'static str> {
365 Path::new(file_path)
366 .extension()
367 .and_then(|ext| ext.to_str())
368 .and_then(default_lsp_command_for_extension)
369}
370
371pub fn default_lsp_args_for_command(command: &str) -> Option<&'static [&'static str]> {
372 LSP_RECIPES
373 .iter()
374 .find(|recipe| recipe.binary_name == command)
375 .map(|recipe| recipe.args)
376}