1use anyhow::{Context, Result};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::process::{Child, Command, Stdio};
11use std::sync::Arc;
12use tracing::{debug, info, warn};
13
14pub type RootDetector = Arc<dyn Fn(&Path) -> Option<PathBuf> + Send + Sync>;
16
17pub type SpawnFn = Arc<dyn Fn(&Path) -> Result<LspServerHandle> + Send + Sync>;
19
20pub struct LspServerHandle {
22 pub process: Child,
24 pub initialization: Option<serde_json::Value>,
26}
27
28pub struct LspServerInfo {
30 pub id: String,
32 pub extensions: Vec<String>,
34 pub global: bool,
36 pub root_detector: RootDetector,
38 pub spawn_fn: SpawnFn,
40}
41
42impl LspServerInfo {
43 pub fn new(
45 id: impl Into<String>,
46 extensions: Vec<&str>,
47 root_detector: RootDetector,
48 spawn_fn: SpawnFn,
49 ) -> Self {
50 Self {
51 id: id.into(),
52 extensions: extensions.into_iter().map(String::from).collect(),
53 global: false,
54 root_detector,
55 spawn_fn,
56 }
57 }
58
59 pub fn handles_extension(&self, ext: &str) -> bool {
61 self.extensions.is_empty() || self.extensions.iter().any(|e| e == ext)
62 }
63
64 pub fn detect_root(&self, file: &Path) -> Option<PathBuf> {
66 (self.root_detector)(file)
67 }
68
69 pub fn spawn(&self, root: &Path) -> Result<LspServerHandle> {
71 (self.spawn_fn)(root)
72 }
73}
74
75pub struct LspRegistry {
77 servers: HashMap<String, LspServerInfo>,
78}
79
80impl Default for LspRegistry {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl LspRegistry {
87 pub fn new() -> Self {
89 let mut registry = Self {
90 servers: HashMap::new(),
91 };
92
93 registry.register(Self::rust_analyzer());
95 registry.register(Self::typescript());
96 registry.register(Self::pyright());
97 registry.register(Self::gopls());
98 registry.register(Self::deno());
99 registry.register(Self::clangd());
100 registry.register(Self::lua_ls());
101 registry.register(Self::css());
102 registry.register(Self::html());
103 registry.register(Self::json());
104 registry.register(Self::yaml());
105 registry.register(Self::vue());
106 registry.register(Self::svelte());
107 registry.register(Self::tailwindcss());
108 registry.register(Self::eslint());
109 registry.register(Self::ruby());
110 registry.register(Self::php());
111 registry.register(Self::zig());
112
113 registry
114 }
115
116 pub fn register(&mut self, server: LspServerInfo) {
118 info!(server_id = %server.id, "Registering LSP server");
119 self.servers.insert(server.id.clone(), server);
120 }
121
122 pub fn get(&self, id: &str) -> Option<&LspServerInfo> {
124 self.servers.get(id)
125 }
126
127 pub fn servers_for_extension(&self, ext: &str) -> Vec<&LspServerInfo> {
129 self.servers
130 .values()
131 .filter(|s| s.handles_extension(ext))
132 .collect()
133 }
134
135 pub fn all_servers(&self) -> impl Iterator<Item = &LspServerInfo> {
137 self.servers.values()
138 }
139
140 pub fn server_ids(&self) -> Vec<&str> {
142 self.servers.keys().map(|s| s.as_str()).collect()
143 }
144
145 fn rust_analyzer() -> LspServerInfo {
151 LspServerInfo::new(
152 "rust-analyzer",
153 vec![".rs"],
154 Arc::new(|file| find_project_root(file, &["Cargo.toml"])),
155 Arc::new(|root| {
156 let binary = which::which("rust-analyzer")
157 .context("rust-analyzer not found in PATH")?;
158
159 debug!(binary = ?binary, root = ?root, "Spawning rust-analyzer");
160
161 let process = Command::new(binary)
162 .current_dir(root)
163 .stdin(Stdio::piped())
164 .stdout(Stdio::piped())
165 .stderr(Stdio::null())
166 .spawn()
167 .context("Failed to spawn rust-analyzer")?;
168
169 Ok(LspServerHandle {
170 process,
171 initialization: Some(serde_json::json!({
172 "checkOnSave": {
173 "command": "clippy"
174 }
175 })),
176 })
177 }),
178 )
179 }
180
181 fn typescript() -> LspServerInfo {
183 LspServerInfo::new(
184 "typescript",
185 vec![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
186 Arc::new(|file| {
187 if find_project_root(file, &["deno.json", "deno.jsonc"]).is_some() {
189 return None;
190 }
191 find_project_root(
192 file,
193 &[
194 "package-lock.json",
195 "bun.lockb",
196 "bun.lock",
197 "pnpm-lock.yaml",
198 "yarn.lock",
199 "package.json",
200 ],
201 )
202 }),
203 Arc::new(|root| {
204 let binary = which::which("typescript-language-server")
206 .context("typescript-language-server not found in PATH")?;
207
208 debug!(binary = ?binary, root = ?root, "Spawning typescript-language-server");
209
210 let process = Command::new(binary)
211 .arg("--stdio")
212 .current_dir(root)
213 .stdin(Stdio::piped())
214 .stdout(Stdio::piped())
215 .stderr(Stdio::null())
216 .spawn()
217 .context("Failed to spawn typescript-language-server")?;
218
219 Ok(LspServerHandle {
220 process,
221 initialization: None,
222 })
223 }),
224 )
225 }
226
227 fn pyright() -> LspServerInfo {
229 LspServerInfo::new(
230 "pyright",
231 vec![".py", ".pyi"],
232 Arc::new(|file| {
233 find_project_root(
234 file,
235 &[
236 "pyproject.toml",
237 "setup.py",
238 "setup.cfg",
239 "requirements.txt",
240 "pyrightconfig.json",
241 ],
242 )
243 }),
244 Arc::new(|root| {
245 let binary = which::which("pyright-langserver")
247 .or_else(|_| which::which("pyright"))
248 .context("pyright not found in PATH")?;
249
250 debug!(binary = ?binary, root = ?root, "Spawning pyright");
251
252 let process = Command::new(binary)
253 .arg("--stdio")
254 .current_dir(root)
255 .stdin(Stdio::piped())
256 .stdout(Stdio::piped())
257 .stderr(Stdio::null())
258 .spawn()
259 .context("Failed to spawn pyright")?;
260
261 Ok(LspServerHandle {
262 process,
263 initialization: None,
264 })
265 }),
266 )
267 }
268
269 fn gopls() -> LspServerInfo {
271 LspServerInfo::new(
272 "gopls",
273 vec![".go"],
274 Arc::new(|file| find_project_root(file, &["go.mod", "go.work"])),
275 Arc::new(|root| {
276 let binary =
277 which::which("gopls").context("gopls not found in PATH")?;
278
279 debug!(binary = ?binary, root = ?root, "Spawning gopls");
280
281 let process = Command::new(binary)
282 .current_dir(root)
283 .stdin(Stdio::piped())
284 .stdout(Stdio::piped())
285 .stderr(Stdio::null())
286 .spawn()
287 .context("Failed to spawn gopls")?;
288
289 Ok(LspServerHandle {
290 process,
291 initialization: None,
292 })
293 }),
294 )
295 }
296
297 fn deno() -> LspServerInfo {
299 LspServerInfo::new(
300 "deno",
301 vec![".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"],
302 Arc::new(|file| {
303 find_project_root(file, &["deno.json", "deno.jsonc"])
305 }),
306 Arc::new(|root| {
307 let binary = which::which("deno").context("deno not found in PATH")?;
308
309 debug!(binary = ?binary, root = ?root, "Spawning deno lsp");
310
311 let process = Command::new(binary)
312 .arg("lsp")
313 .current_dir(root)
314 .stdin(Stdio::piped())
315 .stdout(Stdio::piped())
316 .stderr(Stdio::null())
317 .spawn()
318 .context("Failed to spawn deno lsp")?;
319
320 Ok(LspServerHandle {
321 process,
322 initialization: Some(serde_json::json!({
323 "enable": true,
324 "lint": true,
325 "unstable": false
326 })),
327 })
328 }),
329 )
330 }
331
332 fn clangd() -> LspServerInfo {
334 LspServerInfo::new(
335 "clangd",
336 vec![".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".C", ".H"],
337 Arc::new(|file| {
338 find_project_root(
339 file,
340 &["compile_commands.json", "CMakeLists.txt", "Makefile", ".clangd"],
341 )
342 }),
343 Arc::new(|root| {
344 let binary = which::which("clangd").context("clangd not found in PATH")?;
345
346 debug!(binary = ?binary, root = ?root, "Spawning clangd");
347
348 let process = Command::new(binary)
349 .arg("--background-index")
350 .current_dir(root)
351 .stdin(Stdio::piped())
352 .stdout(Stdio::piped())
353 .stderr(Stdio::null())
354 .spawn()
355 .context("Failed to spawn clangd")?;
356
357 Ok(LspServerHandle {
358 process,
359 initialization: None,
360 })
361 }),
362 )
363 }
364
365 fn lua_ls() -> LspServerInfo {
367 LspServerInfo::new(
368 "lua-language-server",
369 vec![".lua"],
370 Arc::new(|file| {
371 find_project_root(file, &[".luarc.json", ".luarc.jsonc", ".luacheckrc"])
372 .or_else(|| file.parent().map(|p| p.to_path_buf()))
373 }),
374 Arc::new(|root| {
375 let binary = which::which("lua-language-server")
376 .context("lua-language-server not found in PATH")?;
377
378 debug!(binary = ?binary, root = ?root, "Spawning lua-language-server");
379
380 let process = Command::new(binary)
381 .current_dir(root)
382 .stdin(Stdio::piped())
383 .stdout(Stdio::piped())
384 .stderr(Stdio::null())
385 .spawn()
386 .context("Failed to spawn lua-language-server")?;
387
388 Ok(LspServerHandle {
389 process,
390 initialization: None,
391 })
392 }),
393 )
394 }
395
396 fn css() -> LspServerInfo {
398 LspServerInfo::new(
399 "css",
400 vec![".css", ".scss", ".sass", ".less"],
401 Arc::new(|file| {
402 find_project_root(file, &["package.json"]).or_else(|| file.parent().map(|p| p.to_path_buf()))
403 }),
404 Arc::new(|root| {
405 let binary = which::which("vscode-css-language-server")
407 .or_else(|_| which::which("css-languageserver"))
408 .context("css language server not found in PATH")?;
409
410 debug!(binary = ?binary, root = ?root, "Spawning CSS language server");
411
412 let process = Command::new(binary)
413 .arg("--stdio")
414 .current_dir(root)
415 .stdin(Stdio::piped())
416 .stdout(Stdio::piped())
417 .stderr(Stdio::null())
418 .spawn()
419 .context("Failed to spawn CSS language server")?;
420
421 Ok(LspServerHandle {
422 process,
423 initialization: None,
424 })
425 }),
426 )
427 }
428
429 fn html() -> LspServerInfo {
431 LspServerInfo::new(
432 "html",
433 vec![".html", ".htm"],
434 Arc::new(|file| {
435 find_project_root(file, &["package.json"]).or_else(|| file.parent().map(|p| p.to_path_buf()))
436 }),
437 Arc::new(|root| {
438 let binary = which::which("vscode-html-language-server")
439 .or_else(|_| which::which("html-languageserver"))
440 .context("HTML language server not found in PATH")?;
441
442 debug!(binary = ?binary, root = ?root, "Spawning HTML language server");
443
444 let process = Command::new(binary)
445 .arg("--stdio")
446 .current_dir(root)
447 .stdin(Stdio::piped())
448 .stdout(Stdio::piped())
449 .stderr(Stdio::null())
450 .spawn()
451 .context("Failed to spawn HTML language server")?;
452
453 Ok(LspServerHandle {
454 process,
455 initialization: None,
456 })
457 }),
458 )
459 }
460
461 fn json() -> LspServerInfo {
463 LspServerInfo::new(
464 "json",
465 vec![".json", ".jsonc"],
466 Arc::new(|file| file.parent().map(|p| p.to_path_buf())),
467 Arc::new(|root| {
468 let binary = which::which("vscode-json-language-server")
469 .or_else(|_| which::which("json-languageserver"))
470 .context("JSON language server not found in PATH")?;
471
472 debug!(binary = ?binary, root = ?root, "Spawning JSON language server");
473
474 let process = Command::new(binary)
475 .arg("--stdio")
476 .current_dir(root)
477 .stdin(Stdio::piped())
478 .stdout(Stdio::piped())
479 .stderr(Stdio::null())
480 .spawn()
481 .context("Failed to spawn JSON language server")?;
482
483 Ok(LspServerHandle {
484 process,
485 initialization: None,
486 })
487 }),
488 )
489 }
490
491 fn yaml() -> LspServerInfo {
493 LspServerInfo::new(
494 "yaml",
495 vec![".yaml", ".yml"],
496 Arc::new(|file| file.parent().map(|p| p.to_path_buf())),
497 Arc::new(|root| {
498 let binary =
499 which::which("yaml-language-server").context("yaml-language-server not found in PATH")?;
500
501 debug!(binary = ?binary, root = ?root, "Spawning YAML language server");
502
503 let process = Command::new(binary)
504 .arg("--stdio")
505 .current_dir(root)
506 .stdin(Stdio::piped())
507 .stdout(Stdio::piped())
508 .stderr(Stdio::null())
509 .spawn()
510 .context("Failed to spawn yaml-language-server")?;
511
512 Ok(LspServerHandle {
513 process,
514 initialization: None,
515 })
516 }),
517 )
518 }
519
520 fn vue() -> LspServerInfo {
522 LspServerInfo::new(
523 "vue",
524 vec![".vue"],
525 Arc::new(|file| find_project_root(file, &["package.json", "vue.config.js", "vite.config.js"])),
526 Arc::new(|root| {
527 let binary = which::which("vue-language-server")
529 .or_else(|_| which::which("volar-server"))
530 .context("Vue language server not found in PATH")?;
531
532 debug!(binary = ?binary, root = ?root, "Spawning Vue language server");
533
534 let process = Command::new(binary)
535 .arg("--stdio")
536 .current_dir(root)
537 .stdin(Stdio::piped())
538 .stdout(Stdio::piped())
539 .stderr(Stdio::null())
540 .spawn()
541 .context("Failed to spawn Vue language server")?;
542
543 Ok(LspServerHandle {
544 process,
545 initialization: None,
546 })
547 }),
548 )
549 }
550
551 fn svelte() -> LspServerInfo {
553 LspServerInfo::new(
554 "svelte",
555 vec![".svelte"],
556 Arc::new(|file| find_project_root(file, &["package.json", "svelte.config.js"])),
557 Arc::new(|root| {
558 let binary = which::which("svelteserver")
559 .or_else(|_| which::which("svelte-language-server"))
560 .context("Svelte language server not found in PATH")?;
561
562 debug!(binary = ?binary, root = ?root, "Spawning Svelte language server");
563
564 let process = Command::new(binary)
565 .arg("--stdio")
566 .current_dir(root)
567 .stdin(Stdio::piped())
568 .stdout(Stdio::piped())
569 .stderr(Stdio::null())
570 .spawn()
571 .context("Failed to spawn Svelte language server")?;
572
573 Ok(LspServerHandle {
574 process,
575 initialization: None,
576 })
577 }),
578 )
579 }
580
581 fn tailwindcss() -> LspServerInfo {
583 LspServerInfo::new(
584 "tailwindcss",
585 vec![".html", ".jsx", ".tsx", ".vue", ".svelte"],
586 Arc::new(|file| {
587 find_project_root(file, &["tailwind.config.js", "tailwind.config.ts", "tailwind.config.cjs"])
589 }),
590 Arc::new(|root| {
591 let binary = which::which("tailwindcss-language-server")
592 .context("tailwindcss-language-server not found in PATH")?;
593
594 debug!(binary = ?binary, root = ?root, "Spawning Tailwind CSS language server");
595
596 let process = Command::new(binary)
597 .arg("--stdio")
598 .current_dir(root)
599 .stdin(Stdio::piped())
600 .stdout(Stdio::piped())
601 .stderr(Stdio::null())
602 .spawn()
603 .context("Failed to spawn tailwindcss-language-server")?;
604
605 Ok(LspServerHandle {
606 process,
607 initialization: None,
608 })
609 }),
610 )
611 }
612
613 fn eslint() -> LspServerInfo {
615 LspServerInfo::new(
616 "eslint",
617 vec![".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"],
618 Arc::new(|file| {
619 find_project_root(
621 file,
622 &[
623 ".eslintrc",
624 ".eslintrc.js",
625 ".eslintrc.json",
626 ".eslintrc.yaml",
627 ".eslintrc.yml",
628 "eslint.config.js",
629 "eslint.config.mjs",
630 ],
631 )
632 }),
633 Arc::new(|root| {
634 let binary = which::which("vscode-eslint-language-server")
635 .or_else(|_| which::which("eslint-language-server"))
636 .context("ESLint language server not found in PATH")?;
637
638 debug!(binary = ?binary, root = ?root, "Spawning ESLint language server");
639
640 let process = Command::new(binary)
641 .arg("--stdio")
642 .current_dir(root)
643 .stdin(Stdio::piped())
644 .stdout(Stdio::piped())
645 .stderr(Stdio::null())
646 .spawn()
647 .context("Failed to spawn ESLint language server")?;
648
649 Ok(LspServerHandle {
650 process,
651 initialization: Some(serde_json::json!({
652 "validate": "on",
653 "packageManager": "npm",
654 "codeActionOnSave": {
655 "enable": false
656 }
657 })),
658 })
659 }),
660 )
661 }
662
663 fn ruby() -> LspServerInfo {
665 LspServerInfo::new(
666 "solargraph",
667 vec![".rb", ".rake", ".gemspec"],
668 Arc::new(|file| {
669 find_project_root(file, &["Gemfile", ".solargraph.yml", "Rakefile"])
670 }),
671 Arc::new(|root| {
672 let binary =
673 which::which("solargraph").context("solargraph not found in PATH")?;
674
675 debug!(binary = ?binary, root = ?root, "Spawning Solargraph");
676
677 let process = Command::new(binary)
678 .arg("stdio")
679 .current_dir(root)
680 .stdin(Stdio::piped())
681 .stdout(Stdio::piped())
682 .stderr(Stdio::null())
683 .spawn()
684 .context("Failed to spawn solargraph")?;
685
686 Ok(LspServerHandle {
687 process,
688 initialization: None,
689 })
690 }),
691 )
692 }
693
694 fn php() -> LspServerInfo {
696 LspServerInfo::new(
697 "intelephense",
698 vec![".php", ".phtml"],
699 Arc::new(|file| {
700 find_project_root(file, &["composer.json", "index.php"])
701 }),
702 Arc::new(|root| {
703 let binary =
704 which::which("intelephense").context("intelephense not found in PATH")?;
705
706 debug!(binary = ?binary, root = ?root, "Spawning Intelephense");
707
708 let process = Command::new(binary)
709 .arg("--stdio")
710 .current_dir(root)
711 .stdin(Stdio::piped())
712 .stdout(Stdio::piped())
713 .stderr(Stdio::null())
714 .spawn()
715 .context("Failed to spawn intelephense")?;
716
717 Ok(LspServerHandle {
718 process,
719 initialization: None,
720 })
721 }),
722 )
723 }
724
725 fn zig() -> LspServerInfo {
727 LspServerInfo::new(
728 "zls",
729 vec![".zig"],
730 Arc::new(|file| {
731 find_project_root(file, &["build.zig", "zls.json"])
732 }),
733 Arc::new(|root| {
734 let binary = which::which("zls").context("zls not found in PATH")?;
735
736 debug!(binary = ?binary, root = ?root, "Spawning ZLS");
737
738 let process = Command::new(binary)
739 .current_dir(root)
740 .stdin(Stdio::piped())
741 .stdout(Stdio::piped())
742 .stderr(Stdio::null())
743 .spawn()
744 .context("Failed to spawn zls")?;
745
746 Ok(LspServerHandle {
747 process,
748 initialization: None,
749 })
750 }),
751 )
752 }
753}
754
755pub fn find_project_root(file: &Path, markers: &[&str]) -> Option<PathBuf> {
761 let start_dir = if file.is_file() {
762 file.parent()?
763 } else {
764 file
765 };
766
767 let mut current = start_dir;
768 loop {
769 for marker in markers {
770 if current.join(marker).exists() {
771 return Some(current.to_path_buf());
772 }
773 }
774
775 match current.parent() {
776 Some(parent) => current = parent,
777 None => break,
778 }
779 }
780
781 None
782}
783
784pub fn find_project_root_with_stop(
786 file: &Path,
787 markers: &[&str],
788 stop_at: &Path,
789) -> Option<PathBuf> {
790 let start_dir = if file.is_file() {
791 file.parent()?
792 } else {
793 file
794 };
795
796 let mut current = start_dir;
797 loop {
798 if current == stop_at {
800 break;
801 }
802
803 for marker in markers {
804 if current.join(marker).exists() {
805 return Some(current.to_path_buf());
806 }
807 }
808
809 match current.parent() {
810 Some(parent) => current = parent,
811 None => break,
812 }
813 }
814
815 None
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821 use std::fs;
822 use tempfile::tempdir;
823
824 #[test]
825 fn test_find_project_root() {
826 let temp = tempdir().unwrap();
827 let project_dir = temp.path().join("my-project");
828 let src_dir = project_dir.join("src");
829 let nested_dir = src_dir.join("nested");
830
831 fs::create_dir_all(&nested_dir).unwrap();
832 fs::write(project_dir.join("Cargo.toml"), "").unwrap();
833
834 let file = nested_dir.join("main.rs");
835 fs::write(&file, "").unwrap();
836
837 let root = find_project_root(&file, &["Cargo.toml"]);
838 assert_eq!(root, Some(project_dir));
839 }
840
841 #[test]
842 fn test_find_project_root_not_found() {
843 let temp = tempdir().unwrap();
844 let file = temp.path().join("orphan.rs");
845 fs::write(&file, "").unwrap();
846
847 let root = find_project_root(&file, &["Cargo.toml"]);
848 assert_eq!(root, None);
849 }
850
851 #[test]
852 fn test_registry_default_servers() {
853 let registry = LspRegistry::new();
854
855 assert!(registry.get("rust-analyzer").is_some());
856 assert!(registry.get("typescript").is_some());
857 assert!(registry.get("pyright").is_some());
858 assert!(registry.get("gopls").is_some());
859 }
860
861 #[test]
862 fn test_servers_for_extension() {
863 let registry = LspRegistry::new();
864
865 let rust_servers = registry.servers_for_extension(".rs");
866 assert_eq!(rust_servers.len(), 1);
867 assert_eq!(rust_servers[0].id, "rust-analyzer");
868
869 let ts_servers = registry.servers_for_extension(".ts");
870 assert_eq!(ts_servers.len(), 1);
871 assert_eq!(ts_servers[0].id, "typescript");
872 }
873}