Skip to main content

codelens_engine/lsp/
registry.rs

1use serde::Serialize;
2use std::path::Path;
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    // Phase 6a: new languages
134    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    // Perl deferred until tree-sitter 0.26 upgrade
216];
217
218/// Return `true` when the given LSP binary is resolvable either via the
219/// current `PATH` or via a conservative allow-list of common install
220/// locations. This keeps runtime capability reporting and `check_lsp_status`
221/// aligned even when the daemon inherits a minimal launchd/systemd PATH.
222pub fn lsp_binary_exists(command: &str) -> bool {
223    if std::process::Command::new("which")
224        .arg(command)
225        .output()
226        .map(|output| output.status.success())
227        .unwrap_or(false)
228    {
229        return true;
230    }
231
232    let home = std::env::var("HOME").unwrap_or_default();
233    let fallback_dirs = [
234        "/opt/homebrew/bin".to_owned(),
235        "/usr/local/bin".to_owned(),
236        format!("{home}/.cargo/bin"),
237        format!("{home}/.fnm/aliases/default/bin"),
238        format!("{home}/.nvm/versions/node/current/bin"),
239    ];
240    for dir in fallback_dirs.iter().filter(|dir| !dir.is_empty()) {
241        if Path::new(dir).join(command).exists() {
242            return true;
243        }
244    }
245
246    if let Ok(extra) = std::env::var("CODELENS_LSP_PATH_EXTRA") {
247        for dir in extra.split(':').filter(|dir| !dir.is_empty()) {
248            if Path::new(dir).join(command).exists() {
249                return true;
250            }
251        }
252    }
253
254    false
255}
256
257/// Check which LSP servers are installed and which are missing.
258pub fn check_lsp_status() -> Vec<LspStatus> {
259    LSP_RECIPES
260        .iter()
261        .map(|recipe| LspStatus {
262            language: recipe.language,
263            server_name: recipe.server_name,
264            installed: lsp_binary_exists(recipe.binary_name),
265            install_command: recipe.install_command,
266        })
267        .collect()
268}
269
270#[derive(Debug, Clone, Serialize)]
271pub struct LspStatus {
272    pub language: &'static str,
273    pub server_name: &'static str,
274    pub installed: bool,
275    pub install_command: &'static str,
276}
277
278/// Get the recipe for a file extension.
279pub fn get_lsp_recipe(extension: &str) -> Option<&'static LspRecipe> {
280    let ext = extension.to_ascii_lowercase();
281    LSP_RECIPES
282        .iter()
283        .find(|r| r.extensions.contains(&ext.as_str()))
284}
285
286pub fn default_lsp_command_for_extension(extension: &str) -> Option<&'static str> {
287    get_lsp_recipe(extension).map(|recipe| recipe.binary_name)
288}
289
290pub fn default_lsp_command_for_path(file_path: &str) -> Option<&'static str> {
291    Path::new(file_path)
292        .extension()
293        .and_then(|ext| ext.to_str())
294        .and_then(default_lsp_command_for_extension)
295}
296
297pub fn default_lsp_args_for_command(command: &str) -> Option<&'static [&'static str]> {
298    LSP_RECIPES
299        .iter()
300        .find(|recipe| recipe.binary_name == command)
301        .map(|recipe| recipe.args)
302}