Rust · 22005 bytes Raw Blame History
1 //! LSP Server Manager
2 //!
3 //! Detects available language servers and helps users install them.
4 //!
5 //! Note: Some fields are for planned UI features.
6 #![allow(dead_code)]
7
8 use std::collections::HashSet;
9 use std::process::Command;
10 use std::sync::mpsc::{self, Receiver, Sender};
11 use std::thread;
12
13 /// Result of an install operation
14 pub struct InstallResult {
15 pub server_index: usize,
16 pub server_name: String,
17 pub check_cmd: String,
18 pub success: bool,
19 pub message: String,
20 }
21
22 /// A known language server with installation instructions
23 #[derive(Debug, Clone)]
24 pub struct KnownServer {
25 pub name: &'static str,
26 pub language: &'static str,
27 pub check_cmd: &'static str,
28 pub install_cmd: &'static str,
29 pub description: &'static str,
30 pub is_installed: bool,
31 }
32
33 impl KnownServer {
34 const fn new(
35 name: &'static str,
36 language: &'static str,
37 check_cmd: &'static str,
38 install_cmd: &'static str,
39 description: &'static str,
40 ) -> Self {
41 Self {
42 name,
43 language,
44 check_cmd,
45 install_cmd,
46 description,
47 is_installed: false,
48 }
49 }
50 }
51
52 /// All known language servers
53 pub fn get_known_servers() -> Vec<KnownServer> {
54 vec![
55 // Python
56 KnownServer::new(
57 "pyright",
58 "Python",
59 "pyright-langserver",
60 "pip install pyright",
61 "Python type checker and language server",
62 ),
63 KnownServer::new(
64 "ruff",
65 "Python",
66 "ruff",
67 "pip install ruff",
68 "Fast Python linter with LSP support",
69 ),
70 KnownServer::new(
71 "pylsp",
72 "Python",
73 "pylsp",
74 "pip install python-lsp-server",
75 "Python LSP server with plugin support",
76 ),
77 // Rust
78 KnownServer::new(
79 "rust-analyzer",
80 "Rust",
81 "rust-analyzer",
82 "rustup component add rust-analyzer",
83 "Rust language server",
84 ),
85 // C/C++
86 KnownServer::new(
87 "clangd",
88 "C/C++",
89 "clangd",
90 "# Install via package manager (apt/brew/etc)",
91 "C/C++ language server from LLVM",
92 ),
93 // Go
94 KnownServer::new(
95 "gopls",
96 "Go",
97 "gopls",
98 "go install golang.org/x/tools/gopls@latest",
99 "Go language server",
100 ),
101 // TypeScript/JavaScript
102 KnownServer::new(
103 "typescript-language-server",
104 "JS/TS",
105 "typescript-language-server",
106 "npm i -g typescript-language-server typescript",
107 "TypeScript and JavaScript language server",
108 ),
109 KnownServer::new(
110 "vtsls",
111 "JS/TS",
112 "vtsls",
113 "npm i -g @vtsls/language-server",
114 "Fast TypeScript language server",
115 ),
116 // Lua
117 KnownServer::new(
118 "lua-language-server",
119 "Lua",
120 "lua-language-server",
121 "# Install via package manager",
122 "Lua language server",
123 ),
124 // Ruby
125 KnownServer::new(
126 "solargraph",
127 "Ruby",
128 "solargraph",
129 "gem install solargraph",
130 "Ruby language server",
131 ),
132 // Java
133 KnownServer::new(
134 "jdtls",
135 "Java",
136 "jdtls",
137 "# Install via package manager",
138 "Eclipse JDT Language Server",
139 ),
140 // HTML/CSS/JSON
141 KnownServer::new(
142 "vscode-langservers",
143 "HTML/CSS/JSON",
144 "vscode-html-language-server",
145 "npm i -g vscode-langservers-extracted",
146 "HTML, CSS, JSON language servers",
147 ),
148 // Bash
149 KnownServer::new(
150 "bash-language-server",
151 "Bash",
152 "bash-language-server",
153 "npm i -g bash-language-server",
154 "Bash/Shell language server",
155 ),
156 // YAML
157 KnownServer::new(
158 "yaml-language-server",
159 "YAML",
160 "yaml-language-server",
161 "npm i -g yaml-language-server",
162 "YAML language server",
163 ),
164 // Docker
165 KnownServer::new(
166 "dockerfile-langserver",
167 "Docker",
168 "docker-langserver",
169 "npm i -g dockerfile-language-server-nodejs",
170 "Dockerfile language server",
171 ),
172 // Zig
173 KnownServer::new(
174 "zls",
175 "Zig",
176 "zls",
177 "# Install via package manager or zigtools",
178 "Zig language server",
179 ),
180 // Haskell
181 KnownServer::new(
182 "haskell-language-server",
183 "Haskell",
184 "haskell-language-server-wrapper",
185 "ghcup install hls",
186 "Haskell language server",
187 ),
188 // Terraform
189 KnownServer::new(
190 "terraform-ls",
191 "Terraform",
192 "terraform-ls",
193 "# Install from HashiCorp",
194 "Terraform language server",
195 ),
196 // Fortran
197 KnownServer::new(
198 "fortls",
199 "Fortran",
200 "fortls",
201 "pip install fortls",
202 "Fortran language server",
203 ),
204 // Elixir
205 KnownServer::new(
206 "elixir-ls",
207 "Elixir",
208 "elixir-ls",
209 "# Download from GitHub releases",
210 "Elixir language server",
211 ),
212 // Markdown
213 KnownServer::new(
214 "marksman",
215 "Markdown",
216 "marksman",
217 "# Install from GitHub releases or brew install marksman",
218 "Markdown language server with wiki-links support",
219 ),
220 // Kotlin
221 KnownServer::new(
222 "kotlin-language-server",
223 "Kotlin",
224 "kotlin-language-server",
225 "# Install from GitHub releases",
226 "Kotlin language server",
227 ),
228 // Swift
229 KnownServer::new(
230 "sourcekit-lsp",
231 "Swift",
232 "sourcekit-lsp",
233 "# Included with Xcode or Swift toolchain",
234 "Swift/Objective-C language server",
235 ),
236 // PHP
237 KnownServer::new(
238 "intelephense",
239 "PHP",
240 "intelephense",
241 "npm i -g intelephense",
242 "PHP language server",
243 ),
244 // C#
245 KnownServer::new(
246 "omnisharp",
247 "C#",
248 "OmniSharp",
249 "# Install from GitHub releases",
250 "C# language server",
251 ),
252 // Scala
253 KnownServer::new(
254 "metals",
255 "Scala",
256 "metals",
257 "# Install via coursier: cs install metals",
258 "Scala language server",
259 ),
260 // OCaml
261 KnownServer::new(
262 "ocamllsp",
263 "OCaml",
264 "ocamllsp",
265 "opam install ocaml-lsp-server",
266 "OCaml language server",
267 ),
268 // Nim
269 KnownServer::new(
270 "nimlangserver",
271 "Nim",
272 "nimlangserver",
273 "nimble install nimlangserver",
274 "Nim language server",
275 ),
276 // Julia
277 KnownServer::new(
278 "julia-lsp",
279 "Julia",
280 "julia",
281 "# Install LanguageServer.jl package in Julia",
282 "Julia language server",
283 ),
284 // Erlang
285 KnownServer::new(
286 "erlang_ls",
287 "Erlang",
288 "erlang_ls",
289 "# Install from GitHub releases",
290 "Erlang language server",
291 ),
292 // Clojure
293 KnownServer::new(
294 "clojure-lsp",
295 "Clojure",
296 "clojure-lsp",
297 "# Install from GitHub releases or brew",
298 "Clojure language server",
299 ),
300 // Perl
301 KnownServer::new(
302 "perlnavigator",
303 "Perl",
304 "perlnavigator",
305 "npm i -g perlnavigator-server",
306 "Perl language server",
307 ),
308 // R
309 KnownServer::new(
310 "r-languageserver",
311 "R",
312 "R",
313 "# Install languageserver package in R",
314 "R language server",
315 ),
316 // Dart/Flutter
317 KnownServer::new(
318 "dart-language-server",
319 "Dart",
320 "dart",
321 "# Included with Dart SDK",
322 "Dart language server",
323 ),
324 // Vue
325 KnownServer::new(
326 "vue-language-server",
327 "Vue",
328 "vue-language-server",
329 "npm i -g @vue/language-server",
330 "Vue.js language server",
331 ),
332 // Svelte
333 KnownServer::new(
334 "svelte-language-server",
335 "Svelte",
336 "svelteserver",
337 "npm i -g svelte-language-server",
338 "Svelte language server",
339 ),
340 // TOML
341 KnownServer::new(
342 "taplo",
343 "TOML",
344 "taplo",
345 "cargo install taplo-cli --features lsp",
346 "TOML language server",
347 ),
348 // Nix
349 KnownServer::new(
350 "nil",
351 "Nix",
352 "nil",
353 "nix profile install nixpkgs#nil",
354 "Nix language server",
355 ),
356 // GraphQL
357 KnownServer::new(
358 "graphql-lsp",
359 "GraphQL",
360 "graphql-lsp",
361 "npm i -g graphql-language-service-cli",
362 "GraphQL language server",
363 ),
364 // SQL
365 KnownServer::new(
366 "sqls",
367 "SQL",
368 "sqls",
369 "go install github.com/sqls-server/sqls@latest",
370 "SQL language server",
371 ),
372 // LaTeX
373 KnownServer::new(
374 "texlab",
375 "LaTeX",
376 "texlab",
377 "# Install from GitHub releases or cargo install texlab",
378 "LaTeX language server",
379 ),
380 // CMake
381 KnownServer::new(
382 "cmake-language-server",
383 "CMake",
384 "cmake-language-server",
385 "pip install cmake-language-server",
386 "CMake language server",
387 ),
388 // D
389 KnownServer::new(
390 "serve-d",
391 "D",
392 "serve-d",
393 "# Install from GitHub releases",
394 "D language server",
395 ),
396 // V
397 KnownServer::new(
398 "v-analyzer",
399 "V",
400 "v-analyzer",
401 "# Install from GitHub releases",
402 "V language server",
403 ),
404 // Odin
405 KnownServer::new(
406 "ols",
407 "Odin",
408 "ols",
409 "# Install from GitHub releases",
410 "Odin language server",
411 ),
412 // F#
413 KnownServer::new(
414 "fsautocomplete",
415 "F#",
416 "fsautocomplete",
417 "dotnet tool install -g fsautocomplete",
418 "F# language server",
419 ),
420 // Groovy
421 KnownServer::new(
422 "groovy-language-server",
423 "Groovy",
424 "groovy-language-server",
425 "# Install from GitHub releases",
426 "Groovy language server",
427 ),
428 ]
429 }
430
431 /// Check if a command exists in PATH
432 pub fn check_command_exists(cmd: &str) -> bool {
433 if cmd.is_empty() {
434 return false;
435 }
436
437 Command::new("which")
438 .arg(cmd)
439 .stdout(std::process::Stdio::null())
440 .stderr(std::process::Stdio::null())
441 .status()
442 .map(|s| s.success())
443 .unwrap_or(false)
444 }
445
446 /// Detect which servers are installed
447 pub fn detect_installed_servers() -> Vec<KnownServer> {
448 let mut servers = get_known_servers();
449 for server in &mut servers {
450 server.is_installed = check_command_exists(server.check_cmd);
451 }
452 servers
453 }
454
455 /// Sanitize a string for display - remove ANSI escape codes and control characters
456 fn sanitize_for_display(s: &str) -> String {
457 let mut result = String::new();
458 let mut chars = s.chars().peekable();
459
460 while let Some(c) = chars.next() {
461 // Skip ANSI escape sequences (ESC [ ... m and similar)
462 if c == '\x1b' {
463 // Skip until we hit a letter (end of escape sequence)
464 while let Some(&next) = chars.peek() {
465 chars.next();
466 if next.is_ascii_alphabetic() {
467 break;
468 }
469 }
470 continue;
471 }
472
473 // Skip other control characters except space
474 if c.is_control() {
475 if c == '\n' || c == '\t' {
476 result.push(' '); // Replace newlines/tabs with space
477 }
478 continue;
479 }
480
481 result.push(c);
482 }
483
484 // Collapse multiple spaces
485 let mut prev_space = false;
486 let collapsed: String = result.chars().filter(|&c| {
487 if c == ' ' {
488 if prev_space {
489 return false;
490 }
491 prev_space = true;
492 } else {
493 prev_space = false;
494 }
495 true
496 }).collect();
497
498 collapsed.trim().to_string()
499 }
500
501 /// Run an install command
502 pub fn run_install_command(cmd: &str) -> Result<String, String> {
503 // Skip comments
504 if cmd.starts_with('#') {
505 return Err("Manual installation required. See instructions.".to_string());
506 }
507
508 let output = Command::new("sh")
509 .arg("-c")
510 .arg(cmd)
511 .output()
512 .map_err(|e| format!("Failed to run command: {}", e))?;
513
514 if output.status.success() {
515 Ok(String::from_utf8_lossy(&output.stdout).to_string())
516 } else {
517 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
518 Err(sanitize_for_display(&stderr))
519 }
520 }
521
522 /// LSP Server Manager Panel state
523 pub struct ServerManagerPanel {
524 pub visible: bool,
525 pub servers: Vec<KnownServer>,
526 pub selected_index: usize,
527 pub scroll_offset: usize,
528 pub confirm_mode: bool,
529 pub confirm_index: usize,
530 /// Set of server indices currently being installed
531 pub installing_indices: HashSet<usize>,
532 pub status_message: Option<String>,
533 /// Show manual install info dialog
534 pub manual_info_mode: bool,
535 pub manual_info_index: usize,
536 /// Text that was copied to clipboard (for status message)
537 pub copied_to_clipboard: bool,
538 /// Channel to receive install completion results
539 install_rx: Option<Receiver<InstallResult>>,
540 install_tx: Option<Sender<InstallResult>>,
541 }
542
543 impl Default for ServerManagerPanel {
544 fn default() -> Self {
545 Self::new()
546 }
547 }
548
549 impl ServerManagerPanel {
550 pub fn new() -> Self {
551 let (tx, rx) = mpsc::channel();
552 Self {
553 visible: false,
554 servers: Vec::new(),
555 selected_index: 0,
556 scroll_offset: 0,
557 confirm_mode: false,
558 confirm_index: 0,
559 installing_indices: HashSet::new(),
560 status_message: None,
561 manual_info_mode: false,
562 manual_info_index: 0,
563 copied_to_clipboard: false,
564 install_rx: Some(rx),
565 install_tx: Some(tx),
566 }
567 }
568
569 /// Check if a specific server is currently being installed
570 pub fn is_installing(&self, index: usize) -> bool {
571 self.installing_indices.contains(&index)
572 }
573
574 /// Check if any installs are in progress
575 pub fn has_active_installs(&self) -> bool {
576 !self.installing_indices.is_empty()
577 }
578
579 /// Poll for completed installs (non-blocking)
580 /// Returns true if there was an update (caller should re-render)
581 pub fn poll_installs(&mut self) -> bool {
582 let rx = match &self.install_rx {
583 Some(rx) => rx,
584 None => return false,
585 };
586
587 let mut had_update = false;
588
589 // Drain all available results
590 while let Ok(result) = rx.try_recv() {
591 had_update = true;
592 self.installing_indices.remove(&result.server_index);
593
594 if result.success {
595 // Update the server's installed status
596 if let Some(server) = self.servers.get_mut(result.server_index) {
597 // Re-check if it's actually installed now
598 server.is_installed = check_command_exists(&result.check_cmd);
599 if server.is_installed {
600 self.status_message = Some(format!("✓ {} installed successfully", result.server_name));
601 } else {
602 self.status_message = Some(format!("Installed {} (may need shell restart)", result.server_name));
603 }
604 }
605 } else {
606 self.status_message = Some(result.message);
607 }
608 }
609
610 had_update
611 }
612
613 pub fn show(&mut self) {
614 self.visible = true;
615 self.selected_index = 0;
616 self.scroll_offset = 0;
617 self.confirm_mode = false;
618 self.manual_info_mode = false;
619 self.status_message = None;
620 self.copied_to_clipboard = false;
621
622 // Detect servers if not already done
623 if self.servers.is_empty() {
624 self.refresh();
625 }
626 }
627
628 pub fn hide(&mut self) {
629 self.visible = false;
630 self.confirm_mode = false;
631 self.manual_info_mode = false;
632 }
633
634 pub fn refresh(&mut self) {
635 self.servers = detect_installed_servers();
636 self.status_message = Some("Server status refreshed".to_string());
637 }
638
639 pub fn move_up(&mut self) {
640 if self.selected_index > 0 {
641 self.selected_index -= 1;
642 if self.selected_index < self.scroll_offset {
643 self.scroll_offset = self.selected_index;
644 }
645 }
646 }
647
648 pub fn move_down(&mut self, max_visible: usize) {
649 if self.selected_index < self.servers.len().saturating_sub(1) {
650 self.selected_index += 1;
651 if self.selected_index >= self.scroll_offset + max_visible {
652 self.scroll_offset = self.selected_index - max_visible + 1;
653 }
654 }
655 }
656
657 pub fn enter_confirm_mode(&mut self) {
658 if self.selected_index < self.servers.len() {
659 let server = &self.servers[self.selected_index];
660 if server.is_installed {
661 self.status_message = Some(format!("{} is already installed", server.name));
662 } else if server.install_cmd.starts_with('#') {
663 // Show manual install info dialog
664 self.manual_info_mode = true;
665 self.manual_info_index = self.selected_index;
666 self.copied_to_clipboard = false;
667 } else {
668 self.confirm_mode = true;
669 self.confirm_index = self.selected_index;
670 }
671 }
672 }
673
674 pub fn cancel_confirm(&mut self) {
675 self.confirm_mode = false;
676 self.manual_info_mode = false;
677 self.status_message = None;
678 self.copied_to_clipboard = false;
679 }
680
681 /// Get the manual install info for clipboard
682 pub fn get_manual_install_text(&self) -> Option<String> {
683 self.servers.get(self.manual_info_index).map(|s| {
684 // Remove the leading "# " from the install command
685 let cmd = s.install_cmd.trim_start_matches('#').trim();
686 format!("{} - {}\n{}", s.name, s.language, cmd)
687 })
688 }
689
690 /// Mark that text was copied to clipboard
691 pub fn mark_copied(&mut self) {
692 self.copied_to_clipboard = true;
693 self.status_message = Some("Copied to clipboard".to_string());
694 }
695
696 /// Get the server for manual info dialog
697 pub fn manual_info_server(&self) -> Option<&KnownServer> {
698 self.servers.get(self.manual_info_index)
699 }
700
701 /// Start the install process - spawns a background thread
702 pub fn start_install(&mut self) {
703 if self.confirm_index >= self.servers.len() {
704 self.confirm_mode = false;
705 return;
706 }
707
708 // Don't allow installing the same server twice
709 if self.installing_indices.contains(&self.confirm_index) {
710 self.status_message = Some("Already installing...".to_string());
711 self.confirm_mode = false;
712 return;
713 }
714
715 let server = &self.servers[self.confirm_index];
716 let name = server.name.to_string();
717 let cmd = server.install_cmd.to_string();
718 let check_cmd = server.check_cmd.to_string();
719 let server_index = self.confirm_index;
720
721 // Mark as installing
722 self.installing_indices.insert(server_index);
723 self.confirm_mode = false;
724 self.status_message = Some(format!("Installing {}...", name));
725
726 // Clone the sender for the thread
727 let tx = match &self.install_tx {
728 Some(tx) => tx.clone(),
729 None => return,
730 };
731
732 // Spawn install thread
733 thread::spawn(move || {
734 let result = run_install_command(&cmd);
735 let (success, message) = match &result {
736 Ok(_) => (true, String::new()),
737 Err(e) => {
738 let err_msg = if e.len() > 50 { format!("{}...", &e[..50]) } else { e.clone() };
739 (false, format!("Install failed: {}", err_msg))
740 }
741 };
742
743 let _ = tx.send(InstallResult {
744 server_index,
745 server_name: name,
746 check_cmd,
747 success,
748 message,
749 });
750 });
751 }
752
753 pub fn selected_server(&self) -> Option<&KnownServer> {
754 self.servers.get(self.selected_index)
755 }
756
757 pub fn confirm_server(&self) -> Option<&KnownServer> {
758 self.servers.get(self.confirm_index)
759 }
760 }
761