1use crate::services::async_bridge::AsyncBridge;
10use crate::services::lsp::async_handler::LspHandle;
11use crate::types::{FeatureFilter, LspFeature, LspServerConfig};
12use lsp_types::{SemanticTokensLegend, Uri};
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15use std::time::{Duration, Instant};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum LspSpawnResult {
20 Spawned,
22 NotAutoStart,
25 NotConfigured,
27 Failed,
29}
30
31const MAX_RESTARTS_IN_WINDOW: usize = 5;
33const RESTART_WINDOW_SECS: u64 = 180; const RESTART_BACKOFF_BASE_MS: u64 = 1000; fn path_to_uri(path: &Path) -> Option<Uri> {
38 let abs = if path.is_absolute() {
39 path.to_path_buf()
40 } else {
41 std::env::current_dir().ok()?.join(path)
42 };
43 let encoded: String = abs
45 .components()
46 .filter_map(|c| match c {
47 std::path::Component::RootDir => None, std::path::Component::Normal(s) => {
49 let s = s.to_str()?;
50 let mut out = String::with_capacity(s.len() + 1);
51 out.push('/');
52 for b in s.bytes() {
53 if b.is_ascii_alphanumeric()
54 || matches!(
55 b,
56 b'-' | b'.'
57 | b'_'
58 | b'~'
59 | b'@'
60 | b'!'
61 | b'$'
62 | b'&'
63 | b'\''
64 | b'('
65 | b')'
66 | b'+'
67 | b','
68 | b';'
69 | b'='
70 )
71 {
72 out.push(b as char);
73 } else {
74 out.push_str(&format!("%{:02X}", b));
75 }
76 }
77 Some(out)
78 }
79 _ => None,
80 })
81 .collect();
82 format!("file://{}", encoded).parse().ok()
83}
84
85pub fn detect_workspace_root(file_path: &Path, root_markers: &[String]) -> std::path::PathBuf {
90 let file_dir = file_path.parent().unwrap_or(file_path).to_path_buf();
91
92 if root_markers.is_empty() {
93 return file_dir;
94 }
95
96 let mut dir = Some(file_dir.as_path());
97 while let Some(d) = dir {
98 for marker in root_markers {
99 if d.join(marker).exists() {
100 return d.to_path_buf();
101 }
102 }
103 dir = d.parent();
104 }
105
106 file_dir
107}
108
109#[derive(Debug, Clone, Default)]
116pub struct ServerCapabilitySummary {
117 pub initialized: bool,
120 pub hover: bool,
121 pub completion: bool,
122 pub completion_resolve: bool,
123 pub completion_trigger_characters: Vec<String>,
124 pub definition: bool,
125 pub references: bool,
126 pub document_formatting: bool,
127 pub document_range_formatting: bool,
128 pub rename: bool,
129 pub signature_help: bool,
130 pub inlay_hints: bool,
131 pub folding_ranges: bool,
132 pub semantic_tokens_full: bool,
133 pub semantic_tokens_full_delta: bool,
134 pub semantic_tokens_range: bool,
135 pub semantic_tokens_legend: Option<SemanticTokensLegend>,
136 pub document_highlight: bool,
137 pub code_action: bool,
138 pub code_action_resolve: bool,
139 pub document_symbols: bool,
140 pub workspace_symbols: bool,
141 pub diagnostics: bool,
142}
143
144pub struct ServerHandle {
148 pub name: String,
150 pub handle: LspHandle,
152 pub feature_filter: FeatureFilter,
154 pub capabilities: ServerCapabilitySummary,
156}
157
158impl ServerHandle {
159 pub fn has_capability(&self, feature: LspFeature) -> bool {
168 if !self.capabilities.initialized {
169 return false;
170 }
171 match feature {
172 LspFeature::Hover => self.capabilities.hover,
173 LspFeature::Completion => self.capabilities.completion,
174 LspFeature::Definition => self.capabilities.definition,
175 LspFeature::References => self.capabilities.references,
176 LspFeature::Format => {
177 self.capabilities.document_formatting || self.capabilities.document_range_formatting
178 }
179 LspFeature::Rename => self.capabilities.rename,
180 LspFeature::SignatureHelp => self.capabilities.signature_help,
181 LspFeature::InlayHints => self.capabilities.inlay_hints,
182 LspFeature::FoldingRange => self.capabilities.folding_ranges,
183 LspFeature::SemanticTokens => {
184 self.capabilities.semantic_tokens_full || self.capabilities.semantic_tokens_range
185 }
186 LspFeature::DocumentHighlight => self.capabilities.document_highlight,
187 LspFeature::CodeAction => self.capabilities.code_action,
188 LspFeature::DocumentSymbols => self.capabilities.document_symbols,
189 LspFeature::WorkspaceSymbols => self.capabilities.workspace_symbols,
190 LspFeature::Diagnostics => self.capabilities.diagnostics,
191 }
192 }
193}
194
195pub struct LspManager {
197 handles: HashMap<String, Vec<ServerHandle>>,
199
200 config: HashMap<String, Vec<LspServerConfig>>,
202
203 root_uri: Option<Uri>,
205
206 per_language_root_uris: HashMap<String, Uri>,
208
209 runtime: Option<tokio::runtime::Handle>,
211
212 async_bridge: Option<AsyncBridge>,
214
215 restart_attempts: HashMap<String, Vec<Instant>>,
217
218 restart_cooldown: HashSet<String>,
220
221 pending_restarts: HashMap<String, Instant>,
223
224 allowed_languages: HashSet<String>,
227
228 disabled_languages: HashSet<String>,
231}
232
233impl LspManager {
234 pub fn new(root_uri: Option<Uri>) -> Self {
236 Self {
237 handles: HashMap::new(),
238 config: HashMap::new(),
239 root_uri,
240 per_language_root_uris: HashMap::new(),
241 runtime: None,
242 async_bridge: None,
243 restart_attempts: HashMap::new(),
244 restart_cooldown: HashSet::new(),
245 pending_restarts: HashMap::new(),
246 allowed_languages: HashSet::new(),
247 disabled_languages: HashSet::new(),
248 }
249 }
250
251 pub fn is_language_allowed(&self, language: &str) -> bool {
253 self.allowed_languages.contains(language)
254 }
255
256 pub fn allow_language(&mut self, language: &str) {
258 self.allowed_languages.insert(language.to_string());
259 tracing::info!("LSP language '{}' manually enabled", language);
260 }
261
262 pub fn allowed_languages(&self) -> &HashSet<String> {
264 &self.allowed_languages
265 }
266
267 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
269 self.config.get(language).map(|v| v.as_slice())
270 }
271
272 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
274 self.config.get(language).and_then(|v| v.first())
275 }
276
277 pub fn set_server_capabilities(
279 &mut self,
280 language: &str,
281 server_name: &str,
282 mut capabilities: ServerCapabilitySummary,
283 ) {
284 capabilities.initialized = true;
285 if let Some(handles) = self.handles.get_mut(language) {
286 if let Some(sh) = handles.iter_mut().find(|sh| sh.name == server_name) {
287 sh.capabilities = capabilities;
288 }
289 }
290 }
291
292 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
294 self.handles.get(language)?.iter().find_map(|sh| {
295 if sh.feature_filter.allows(LspFeature::SemanticTokens)
296 && sh.has_capability(LspFeature::SemanticTokens)
297 {
298 sh.capabilities.semantic_tokens_legend.as_ref()
299 } else {
300 None
301 }
302 })
303 }
304
305 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
307 self.handles.get(language).is_some_and(|handles| {
308 handles.iter().any(|sh| {
309 sh.feature_filter.allows(LspFeature::SemanticTokens)
310 && sh.capabilities.semantic_tokens_full
311 })
312 })
313 }
314
315 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
317 self.handles.get(language).is_some_and(|handles| {
318 handles.iter().any(|sh| {
319 sh.feature_filter.allows(LspFeature::SemanticTokens)
320 && sh.capabilities.semantic_tokens_full_delta
321 })
322 })
323 }
324
325 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
327 self.handles.get(language).is_some_and(|handles| {
328 handles.iter().any(|sh| {
329 sh.feature_filter.allows(LspFeature::SemanticTokens)
330 && sh.capabilities.semantic_tokens_range
331 })
332 })
333 }
334
335 pub fn folding_ranges_supported(&self, language: &str) -> bool {
337 self.handles.get(language).is_some_and(|handles| {
338 handles.iter().any(|sh| {
339 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
340 })
341 })
342 }
343
344 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
346 let ch_str = ch.to_string();
347 self.handles.get(language).is_some_and(|handles| {
348 handles.iter().any(|sh| {
349 sh.feature_filter.allows(LspFeature::Completion)
350 && sh
351 .capabilities
352 .completion_trigger_characters
353 .contains(&ch_str)
354 })
355 })
356 }
357
358 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
372 if self.handles.get(language).is_some_and(|v| !v.is_empty()) {
374 return LspSpawnResult::Spawned;
375 }
376
377 let configs = match self.config.get(language) {
379 Some(configs) if !configs.is_empty() => configs,
380 _ => return LspSpawnResult::NotConfigured,
381 };
382
383 if !configs.iter().any(|c| c.enabled) {
385 return LspSpawnResult::Failed;
386 }
387
388 if self.runtime.is_none() || self.async_bridge.is_none() {
390 return LspSpawnResult::Failed;
391 }
392
393 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
395 if !any_auto_start && !self.allowed_languages.contains(language) {
396 return LspSpawnResult::NotAutoStart;
397 }
398
399 if self.force_spawn(language, file_path).is_some() {
401 LspSpawnResult::Spawned
402 } else {
403 LspSpawnResult::Failed
404 }
405 }
406
407 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
411 self.runtime = Some(runtime);
412 self.async_bridge = Some(async_bridge);
413 }
414
415 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
417 self.config.insert(language, vec![config]);
418 }
419
420 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
422 self.config.insert(language, configs);
423 }
424
425 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
430 self.root_uri = root_uri;
431 }
432
433 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
439 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
440 self.per_language_root_uris
441 .insert(language.to_string(), uri.clone());
442
443 if self.handles.contains_key(language) {
445 tracing::info!(
446 "Restarting {} LSP server with new root: {}",
447 language,
448 uri.as_str()
449 );
450 self.shutdown_server(language);
451 return true;
453 }
454 false
455 }
456
457 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
464 if let Some(uri) = self.per_language_root_uris.get(language) {
466 return Some(uri.clone());
467 }
468
469 if let Some(path) = file_path {
471 let markers = self
472 .config
473 .get(language)
474 .and_then(|configs| configs.first())
475 .map(|c| c.root_markers.as_slice())
476 .unwrap_or(&[]);
477 let root = detect_workspace_root(path, markers);
478 if let Some(uri) = path_to_uri(&root) {
479 return Some(uri);
480 }
481 }
482
483 self.root_uri.clone()
485 }
486
487 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
491 self.resolve_root_uri(language, None)
492 }
493
494 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
499 self.shutdown_all();
501
502 self.root_uri = new_root_uri;
504
505 self.restart_attempts.clear();
507 self.restart_cooldown.clear();
508 self.pending_restarts.clear();
509
510 tracing::info!(
514 "LSP manager reset for new project: {:?}",
515 self.root_uri.as_ref().map(|u| u.as_str())
516 );
517 }
518
519 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
521 self.handles
522 .get(language)
523 .and_then(|v| v.first())
524 .map(|sh| &sh.handle)
525 }
526
527 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
529 self.handles
530 .get_mut(language)
531 .and_then(|v| v.first_mut())
532 .map(|sh| &mut sh.handle)
533 }
534
535 pub fn get_handles(&self, language: &str) -> &[ServerHandle] {
537 self.handles
538 .get(language)
539 .map(|v| v.as_slice())
540 .unwrap_or(&[])
541 }
542
543 pub fn has_server_named(&self, server_name: &str) -> bool {
545 self.handles
546 .values()
547 .any(|handles| handles.iter().any(|sh| sh.name == server_name))
548 }
549
550 pub fn get_handles_mut(&mut self, language: &str) -> &mut [ServerHandle] {
552 self.handles
553 .get_mut(language)
554 .map(|v| v.as_mut_slice())
555 .unwrap_or(&mut [])
556 }
557
558 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
563 self.handles
564 .get(language)?
565 .iter()
566 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
567 }
568
569 pub fn handle_for_feature_mut(
572 &mut self,
573 language: &str,
574 feature: LspFeature,
575 ) -> Option<&mut ServerHandle> {
576 self.handles
577 .get_mut(language)?
578 .iter_mut()
579 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
580 }
581
582 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
585 self.handles
586 .get(language)
587 .map(|v| {
588 v.iter()
589 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
590 .collect()
591 })
592 .unwrap_or_default()
593 }
594
595 pub fn handles_for_feature_mut(
598 &mut self,
599 language: &str,
600 feature: LspFeature,
601 ) -> Vec<&mut ServerHandle> {
602 self.handles
603 .get_mut(language)
604 .map(|v| {
605 v.iter_mut()
606 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
607 .collect()
608 })
609 .unwrap_or_default()
610 }
611
612 pub fn force_spawn(
619 &mut self,
620 language: &str,
621 file_path: Option<&Path>,
622 ) -> Option<&mut LspHandle> {
623 tracing::debug!("force_spawn called for language: {}", language);
624
625 if self.handles.get(language).is_some_and(|v| !v.is_empty()) {
627 tracing::debug!("force_spawn: returning existing handle for {}", language);
628 return self
629 .handles
630 .get_mut(language)
631 .and_then(|v| v.first_mut())
632 .map(|sh| &mut sh.handle);
633 }
634
635 if self.disabled_languages.contains(language) {
637 tracing::debug!(
638 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
639 language
640 );
641 return None;
642 }
643
644 let configs = match self.config.get(language) {
646 Some(configs) if !configs.is_empty() => configs.clone(),
647 _ => {
648 tracing::warn!(
649 "force_spawn: no config found for language '{}', available configs: {:?}",
650 language,
651 self.config.keys().collect::<Vec<_>>()
652 );
653 return None;
654 }
655 };
656
657 let runtime = match self.runtime.as_ref() {
659 Some(r) => r.clone(),
660 None => {
661 tracing::error!("force_spawn: no tokio runtime available for {}", language);
662 return None;
663 }
664 };
665 let async_bridge = match self.async_bridge.as_ref() {
666 Some(b) => b.clone(),
667 None => {
668 tracing::error!("force_spawn: no async bridge available for {}", language);
669 return None;
670 }
671 };
672
673 let mut spawned_handles = Vec::new();
674
675 for config in &configs {
676 if !config.enabled && !self.allowed_languages.contains(language) {
679 continue;
680 }
681
682 if config.command.is_empty() {
683 tracing::warn!(
684 "force_spawn: LSP command is empty for {} server '{}'",
685 language,
686 config.display_name()
687 );
688 continue;
689 }
690
691 let server_name = config.display_name();
692 tracing::info!(
693 "Spawning LSP server '{}' for language: {}",
694 server_name,
695 language
696 );
697
698 match LspHandle::spawn(
699 &runtime,
700 &config.command,
701 &config.args,
702 config.env.clone(),
703 language.to_string(),
704 server_name.clone(),
705 &async_bridge,
706 config.process_limits.clone(),
707 config.language_id_overrides.clone(),
708 ) {
709 Ok(handle) => {
710 let effective_root = self.resolve_root_uri(language, file_path);
711 if let Err(e) =
712 handle.initialize(effective_root, config.initialization_options.clone())
713 {
714 tracing::error!(
715 "Failed to send initialize command for {} ({}): {}",
716 language,
717 server_name,
718 e
719 );
720 continue;
721 }
722
723 tracing::info!(
724 "LSP initialization started for {} ({}), will be ready asynchronously",
725 language,
726 server_name
727 );
728
729 spawned_handles.push(ServerHandle {
730 name: server_name,
731 handle,
732 feature_filter: config.feature_filter(),
733 capabilities: ServerCapabilitySummary::default(),
734 });
735 }
736 Err(e) => {
737 tracing::error!(
738 "Failed to spawn LSP handle for {} ({}): {}",
739 language,
740 server_name,
741 e
742 );
743 }
744 }
745 }
746
747 if spawned_handles.is_empty() {
748 return None;
749 }
750
751 self.handles.insert(language.to_string(), spawned_handles);
752 self.handles
753 .get_mut(language)
754 .and_then(|v| v.first_mut())
755 .map(|sh| &mut sh.handle)
756 }
757
758 #[allow(clippy::let_underscore_must_use)] pub fn handle_server_crash(&mut self, language: &str) -> String {
763 if let Some(handles) = self.handles.remove(language) {
765 for sh in handles {
766 let _ = sh.handle.shutdown(); }
768 }
769
770 if self.disabled_languages.contains(language) {
773 return format!(
774 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
775 language
776 );
777 }
778
779 if self.restart_cooldown.contains(language) {
781 return format!(
782 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
783 language
784 );
785 }
786
787 let now = Instant::now();
789 let window = Duration::from_secs(RESTART_WINDOW_SECS);
790 let attempts = self
791 .restart_attempts
792 .entry(language.to_string())
793 .or_default();
794 attempts.retain(|t| now.duration_since(*t) < window);
795
796 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
798 self.restart_cooldown.insert(language.to_string());
799 tracing::warn!(
800 "LSP server for {} has crashed {} times in {} minutes, entering cooldown",
801 language,
802 MAX_RESTARTS_IN_WINDOW,
803 RESTART_WINDOW_SECS / 60
804 );
805 return format!(
806 "LSP server for {} has crashed too many times ({} in {} min). Use 'Restart LSP Server' command to manually restart.",
807 language,
808 MAX_RESTARTS_IN_WINDOW,
809 RESTART_WINDOW_SECS / 60
810 );
811 }
812
813 let attempt_number = attempts.len();
815 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
817
818 self.pending_restarts
820 .insert(language.to_string(), restart_time);
821
822 tracing::info!(
823 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
824 language,
825 attempt_number + 1,
826 MAX_RESTARTS_IN_WINDOW,
827 delay_ms
828 );
829
830 format!(
831 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
832 language,
833 attempt_number + 1,
834 MAX_RESTARTS_IN_WINDOW,
835 delay_ms / 1000
836 )
837 }
838
839 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
843 let now = Instant::now();
844 let mut results = Vec::new();
845
846 let due_restarts: Vec<String> = self
848 .pending_restarts
849 .iter()
850 .filter(|(_, time)| **time <= now)
851 .map(|(lang, _)| lang.clone())
852 .collect();
853
854 for language in due_restarts {
855 self.pending_restarts.remove(&language);
856
857 self.restart_attempts
859 .entry(language.clone())
860 .or_default()
861 .push(now);
862
863 if self.force_spawn(&language, None).is_some() {
865 let message = format!("LSP server for {} restarted successfully", language);
866 tracing::info!("{}", message);
867 results.push((language, true, message));
868 } else {
869 let message = format!("Failed to restart LSP server for {}", language);
870 tracing::error!("{}", message);
871 results.push((language, false, message));
872 }
873 }
874
875 results
876 }
877
878 pub fn is_in_cooldown(&self, language: &str) -> bool {
880 self.restart_cooldown.contains(language)
881 }
882
883 pub fn has_pending_restart(&self, language: &str) -> bool {
885 self.pending_restarts.contains_key(language)
886 }
887
888 pub fn clear_cooldown(&mut self, language: &str) {
890 self.restart_cooldown.remove(language);
891 self.restart_attempts.remove(language);
892 self.pending_restarts.remove(language);
893 tracing::info!("Cleared restart cooldown for {}", language);
894 }
895
896 #[allow(clippy::let_underscore_must_use)] pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
904 self.clear_cooldown(language);
906
907 self.disabled_languages.remove(language);
909
910 self.allowed_languages.insert(language.to_string());
912
913 if let Some(handles) = self.handles.remove(language) {
915 for sh in handles {
916 let _ = sh.handle.shutdown();
917 }
918 }
919
920 if self.force_spawn(language, file_path).is_some() {
922 let message = format!("LSP server for {} started", language);
923 tracing::info!("{}", message);
924 (true, message)
925 } else {
926 let message = format!("Failed to start LSP server for {}", language);
927 tracing::error!("{}", message);
928 (false, message)
929 }
930 }
931
932 #[allow(clippy::let_underscore_must_use)]
937 pub fn manual_restart_server(
938 &mut self,
939 language: &str,
940 server_name: &str,
941 file_path: Option<&Path>,
942 ) -> (bool, String) {
943 self.clear_cooldown(language);
944 self.disabled_languages.remove(language);
945 self.allowed_languages.insert(language.to_string());
946
947 if let Some(handles) = self.handles.get_mut(language) {
949 if let Some(idx) = handles.iter().position(|sh| sh.name == server_name) {
950 let sh = handles.remove(idx);
951 let _ = sh.handle.shutdown();
952 }
953 }
954
955 let config = self
957 .config
958 .get(language)
959 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
960 .cloned();
961
962 let Some(config) = config else {
963 let message = format!(
964 "No config found for server '{}' ({})",
965 server_name, language
966 );
967 tracing::error!("{}", message);
968 return (false, message);
969 };
970
971 if config.command.is_empty() {
972 let message = format!(
973 "LSP command is empty for {} server '{}'",
974 language, server_name
975 );
976 tracing::error!("{}", message);
977 return (false, message);
978 }
979
980 let runtime = match self.runtime.as_ref() {
981 Some(r) => r.clone(),
982 None => return (false, "No tokio runtime available".to_string()),
983 };
984 let async_bridge = match self.async_bridge.as_ref() {
985 Some(b) => b.clone(),
986 None => return (false, "No async bridge available".to_string()),
987 };
988
989 match LspHandle::spawn(
990 &runtime,
991 &config.command,
992 &config.args,
993 config.env.clone(),
994 language.to_string(),
995 server_name.to_string(),
996 &async_bridge,
997 config.process_limits.clone(),
998 config.language_id_overrides.clone(),
999 ) {
1000 Ok(handle) => {
1001 let effective_root = self.resolve_root_uri(language, file_path);
1002 if let Err(e) =
1003 handle.initialize(effective_root, config.initialization_options.clone())
1004 {
1005 let message = format!(
1006 "Failed to initialize LSP server '{}' for {}: {}",
1007 server_name, language, e
1008 );
1009 tracing::error!("{}", message);
1010 return (false, message);
1011 }
1012
1013 let sh = ServerHandle {
1014 name: server_name.to_string(),
1015 handle,
1016 feature_filter: config.feature_filter(),
1017 capabilities: ServerCapabilitySummary::default(),
1018 };
1019
1020 self.handles
1021 .entry(language.to_string())
1022 .or_default()
1023 .push(sh);
1024
1025 let message = format!("LSP server '{}' for {} started", server_name, language);
1026 tracing::info!("{}", message);
1027 (true, message)
1028 }
1029 Err(e) => {
1030 let message = format!(
1031 "Failed to start LSP server '{}' for {}: {}",
1032 server_name, language, e
1033 );
1034 tracing::error!("{}", message);
1035 (false, message)
1036 }
1037 }
1038 }
1039
1040 pub fn restart_attempt_count(&self, language: &str) -> usize {
1042 let now = Instant::now();
1043 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1044 self.restart_attempts
1045 .get(language)
1046 .map(|attempts| {
1047 attempts
1048 .iter()
1049 .filter(|t| now.duration_since(**t) < window)
1050 .count()
1051 })
1052 .unwrap_or(0)
1053 }
1054
1055 pub fn running_servers(&self) -> Vec<String> {
1057 self.handles.keys().cloned().collect()
1058 }
1059
1060 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1062 self.handles
1063 .get(language)
1064 .map(|handles| handles.iter().map(|sh| sh.name.clone()).collect())
1065 .unwrap_or_default()
1066 }
1067
1068 pub fn is_server_ready(&self, language: &str) -> bool {
1070 self.handles
1071 .get(language)
1072 .map(|handles| {
1073 handles
1074 .iter()
1075 .any(|sh| sh.handle.state().can_send_requests())
1076 })
1077 .unwrap_or(false)
1078 }
1079
1080 #[allow(clippy::let_underscore_must_use)]
1085 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1086 let Some(handles) = self.handles.get_mut(language) else {
1087 tracing::warn!("No running LSP servers found for {}", language);
1088 return false;
1089 };
1090
1091 let pos = handles.iter().position(|sh| sh.name == server_name);
1092 let Some(idx) = pos else {
1093 tracing::warn!(
1094 "No running LSP server named '{}' found for {}",
1095 server_name,
1096 language
1097 );
1098 return false;
1099 };
1100
1101 let sh = handles.remove(idx);
1102 tracing::info!(
1103 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1104 sh.name,
1105 language
1106 );
1107 let _ = sh.handle.shutdown();
1108
1109 if handles.is_empty() {
1110 self.handles.remove(language);
1112 self.disabled_languages.insert(language.to_string());
1113 self.pending_restarts.remove(language);
1114 self.restart_cooldown.remove(language);
1115 self.allowed_languages.remove(language);
1116 }
1117
1118 true
1119 }
1120
1121 #[allow(clippy::let_underscore_must_use)]
1126 pub fn shutdown_server(&mut self, language: &str) -> bool {
1127 if let Some(handles) = self.handles.remove(language) {
1128 for sh in &handles {
1129 tracing::info!(
1130 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1131 sh.name,
1132 language
1133 );
1134 let _ = sh.handle.shutdown();
1135 }
1136 self.disabled_languages.insert(language.to_string());
1137 self.pending_restarts.remove(language);
1138 self.restart_cooldown.remove(language);
1139 self.allowed_languages.remove(language);
1140 !handles.is_empty()
1141 } else {
1142 tracing::warn!("No running LSP server found for {}", language);
1143 false
1144 }
1145 }
1146
1147 #[allow(clippy::let_underscore_must_use)]
1149 pub fn shutdown_all(&mut self) {
1150 for (language, handles) in self.handles.iter() {
1151 for sh in handles {
1152 tracing::info!("Shutting down LSP server '{}' for {}", sh.name, language);
1153 let _ = sh.handle.shutdown();
1154 }
1155 }
1156 self.handles.clear();
1157 }
1158}
1159
1160impl Drop for LspManager {
1161 fn drop(&mut self) {
1162 self.shutdown_all();
1163 }
1164}
1165
1166pub fn detect_language(
1173 path: &std::path::Path,
1174 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1175) -> Option<String> {
1176 use crate::primitives::glob_match::{
1177 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1178 };
1179
1180 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1181 for (language_name, lang_config) in languages {
1183 if lang_config
1184 .filenames
1185 .iter()
1186 .any(|f| !is_glob_pattern(f) && f == filename)
1187 {
1188 return Some(language_name.clone());
1189 }
1190 }
1191
1192 let path_str = path.to_str().unwrap_or("");
1196 for (language_name, lang_config) in languages {
1197 if lang_config.filenames.iter().any(|f| {
1198 if !is_glob_pattern(f) {
1199 return false;
1200 }
1201 if is_path_pattern(f) {
1202 path_glob_matches(f, path_str)
1203 } else {
1204 filename_glob_matches(f, filename)
1205 }
1206 }) {
1207 return Some(language_name.clone());
1208 }
1209 }
1210 }
1211
1212 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1214 for (language_name, lang_config) in languages {
1215 if lang_config.extensions.iter().any(|ext| ext == extension) {
1216 return Some(language_name.clone());
1217 }
1218 }
1219 }
1220
1221 None
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::*;
1227 use std::path::Path;
1228
1229 #[test]
1230 fn test_lsp_manager_new() {
1231 let root_uri: Option<Uri> = "file:///test".parse().ok();
1232 let manager = LspManager::new(root_uri.clone());
1233
1234 assert_eq!(manager.handles.len(), 0);
1236 assert_eq!(manager.config.len(), 0);
1237 assert!(manager.root_uri.is_some());
1238 assert!(manager.runtime.is_none());
1239 assert!(manager.async_bridge.is_none());
1240 }
1241
1242 #[test]
1243 fn test_lsp_manager_set_language_config() {
1244 let mut manager = LspManager::new(None);
1245
1246 let config = LspServerConfig {
1247 enabled: true,
1248 command: "rust-analyzer".to_string(),
1249 args: vec![],
1250 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1251 auto_start: false,
1252 initialization_options: None,
1253 env: Default::default(),
1254 language_id_overrides: Default::default(),
1255 name: None,
1256 only_features: None,
1257 except_features: None,
1258 root_markers: Default::default(),
1259 };
1260
1261 manager.set_language_config("rust".to_string(), config);
1262
1263 assert_eq!(manager.config.len(), 1);
1264 assert!(manager.config.contains_key("rust"));
1265 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1266 }
1267
1268 #[test]
1269 fn test_lsp_manager_force_spawn_no_runtime() {
1270 let mut manager = LspManager::new(None);
1271
1272 manager.set_language_config(
1274 "rust".to_string(),
1275 LspServerConfig {
1276 enabled: true,
1277 command: "rust-analyzer".to_string(),
1278 args: vec![],
1279 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1280 auto_start: false,
1281 initialization_options: None,
1282 env: Default::default(),
1283 language_id_overrides: Default::default(),
1284 name: None,
1285 only_features: None,
1286 except_features: None,
1287 root_markers: Default::default(),
1288 },
1289 );
1290
1291 let result = manager.force_spawn("rust", None);
1293 assert!(result.is_none());
1294 }
1295
1296 #[test]
1297 fn test_lsp_manager_force_spawn_no_config() {
1298 let rt = tokio::runtime::Runtime::new().unwrap();
1299 let mut manager = LspManager::new(None);
1300 let async_bridge = AsyncBridge::new();
1301
1302 manager.set_runtime(rt.handle().clone(), async_bridge);
1303
1304 let result = manager.force_spawn("rust", None);
1306 assert!(result.is_none());
1307 }
1308
1309 #[test]
1310 fn test_lsp_manager_force_spawn_disabled_language() {
1311 let rt = tokio::runtime::Runtime::new().unwrap();
1312 let mut manager = LspManager::new(None);
1313 let async_bridge = AsyncBridge::new();
1314
1315 manager.set_runtime(rt.handle().clone(), async_bridge);
1316
1317 manager.set_language_config(
1319 "rust".to_string(),
1320 LspServerConfig {
1321 enabled: false,
1322 command: String::new(), args: vec![],
1324 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1325 auto_start: false,
1326 initialization_options: None,
1327 env: Default::default(),
1328 language_id_overrides: Default::default(),
1329 name: None,
1330 only_features: None,
1331 except_features: None,
1332 root_markers: Default::default(),
1333 },
1334 );
1335
1336 let result = manager.force_spawn("rust", None);
1338 assert!(result.is_none());
1339 }
1340
1341 #[test]
1342 fn test_lsp_manager_shutdown_all() {
1343 let mut manager = LspManager::new(None);
1344
1345 manager.shutdown_all();
1347 assert_eq!(manager.handles.len(), 0);
1348 }
1349
1350 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
1351 let mut languages = std::collections::HashMap::new();
1352 languages.insert(
1353 "rust".to_string(),
1354 crate::config::LanguageConfig {
1355 extensions: vec!["rs".to_string()],
1356 filenames: vec![],
1357 grammar: "rust".to_string(),
1358 comment_prefix: Some("//".to_string()),
1359 auto_indent: true,
1360 auto_close: None,
1361 auto_surround: None,
1362 highlighter: crate::config::HighlighterPreference::Auto,
1363 textmate_grammar: None,
1364 show_whitespace_tabs: false,
1365 line_wrap: None,
1366 wrap_column: None,
1367 page_view: None,
1368 page_width: None,
1369 use_tabs: None,
1370 tab_size: None,
1371 formatter: None,
1372 format_on_save: false,
1373 on_save: vec![],
1374 word_characters: None,
1375 },
1376 );
1377 languages.insert(
1378 "javascript".to_string(),
1379 crate::config::LanguageConfig {
1380 extensions: vec!["js".to_string(), "jsx".to_string()],
1381 filenames: vec![],
1382 grammar: "javascript".to_string(),
1383 comment_prefix: Some("//".to_string()),
1384 auto_indent: true,
1385 auto_close: None,
1386 auto_surround: None,
1387 highlighter: crate::config::HighlighterPreference::Auto,
1388 textmate_grammar: None,
1389 show_whitespace_tabs: false,
1390 line_wrap: None,
1391 wrap_column: None,
1392 page_view: None,
1393 page_width: None,
1394 use_tabs: None,
1395 tab_size: None,
1396 formatter: None,
1397 format_on_save: false,
1398 on_save: vec![],
1399 word_characters: None,
1400 },
1401 );
1402 languages.insert(
1403 "csharp".to_string(),
1404 crate::config::LanguageConfig {
1405 extensions: vec!["cs".to_string()],
1406 filenames: vec![],
1407 grammar: "c_sharp".to_string(),
1408 comment_prefix: Some("//".to_string()),
1409 auto_indent: true,
1410 auto_close: None,
1411 auto_surround: None,
1412 highlighter: crate::config::HighlighterPreference::Auto,
1413 textmate_grammar: None,
1414 show_whitespace_tabs: false,
1415 line_wrap: None,
1416 wrap_column: None,
1417 page_view: None,
1418 page_width: None,
1419 use_tabs: None,
1420 tab_size: None,
1421 formatter: None,
1422 format_on_save: false,
1423 on_save: vec![],
1424 word_characters: None,
1425 },
1426 );
1427 languages
1428 }
1429
1430 #[test]
1431 fn test_detect_language_from_config() {
1432 let languages = test_languages();
1433
1434 assert_eq!(
1436 detect_language(Path::new("main.rs"), &languages),
1437 Some("rust".to_string())
1438 );
1439 assert_eq!(
1440 detect_language(Path::new("index.js"), &languages),
1441 Some("javascript".to_string())
1442 );
1443 assert_eq!(
1444 detect_language(Path::new("App.jsx"), &languages),
1445 Some("javascript".to_string())
1446 );
1447 assert_eq!(
1448 detect_language(Path::new("Program.cs"), &languages),
1449 Some("csharp".to_string())
1450 );
1451
1452 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
1454 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
1455 assert_eq!(detect_language(Path::new("file"), &languages), None);
1456 }
1457
1458 #[test]
1459 fn test_detect_language_no_extension() {
1460 let languages = test_languages();
1461 assert_eq!(detect_language(Path::new("README"), &languages), None);
1462 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
1463 }
1464
1465 #[test]
1466 fn test_detect_language_path_glob() {
1467 let mut languages = test_languages();
1468 languages.insert(
1469 "shell".to_string(),
1470 crate::config::LanguageConfig {
1471 extensions: vec!["sh".to_string()],
1472 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
1473 grammar: "bash".to_string(),
1474 comment_prefix: Some("#".to_string()),
1475 auto_indent: true,
1476 auto_close: None,
1477 auto_surround: None,
1478 highlighter: crate::config::HighlighterPreference::Auto,
1479 textmate_grammar: None,
1480 show_whitespace_tabs: false,
1481 line_wrap: None,
1482 wrap_column: None,
1483 page_view: None,
1484 page_width: None,
1485 use_tabs: None,
1486 tab_size: None,
1487 formatter: None,
1488 format_on_save: false,
1489 on_save: vec![],
1490 word_characters: None,
1491 },
1492 );
1493
1494 assert_eq!(
1496 detect_language(Path::new("/etc/rc.conf"), &languages),
1497 Some("shell".to_string())
1498 );
1499 assert_eq!(
1500 detect_language(Path::new("/etc/init/rc.local"), &languages),
1501 Some("shell".to_string())
1502 );
1503 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
1505
1506 assert_eq!(
1508 detect_language(Path::new("lfrc"), &languages),
1509 Some("shell".to_string())
1510 );
1511 }
1512
1513 #[test]
1514 fn test_detect_workspace_root_finds_marker_in_parent() {
1515 let tmp = tempfile::tempdir().unwrap();
1516 let project = tmp.path().join("myproject");
1517 let src = project.join("src");
1518 std::fs::create_dir_all(&src).unwrap();
1519 std::fs::write(project.join("Cargo.toml"), "").unwrap();
1520 let file = src.join("main.rs");
1521 std::fs::write(&file, "").unwrap();
1522
1523 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
1524 assert_eq!(root, project);
1525 }
1526
1527 #[test]
1528 fn test_detect_workspace_root_finds_marker_two_levels_up() {
1529 let tmp = tempfile::tempdir().unwrap();
1530 let project = tmp.path().join("myproject");
1531 let deep = project.join("src").join("nested");
1532 std::fs::create_dir_all(&deep).unwrap();
1533 std::fs::write(project.join("Cargo.toml"), "").unwrap();
1534 let file = deep.join("lib.rs");
1535 std::fs::write(&file, "").unwrap();
1536
1537 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
1538 assert_eq!(root, project);
1539 }
1540
1541 #[test]
1542 fn test_detect_workspace_root_no_marker_returns_parent() {
1543 let tmp = tempfile::tempdir().unwrap();
1544 let dir = tmp.path().join("somedir");
1545 std::fs::create_dir_all(&dir).unwrap();
1546 let file = dir.join("file.txt");
1547 std::fs::write(&file, "").unwrap();
1548
1549 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
1550 assert_eq!(root, dir);
1551 }
1552
1553 #[test]
1554 fn test_detect_workspace_root_empty_markers_returns_parent() {
1555 let tmp = tempfile::tempdir().unwrap();
1556 let dir = tmp.path().join("somedir");
1557 std::fs::create_dir_all(&dir).unwrap();
1558 let file = dir.join("file.txt");
1559 std::fs::write(&file, "").unwrap();
1560
1561 let root = detect_workspace_root(&file, &[]);
1562 assert_eq!(root, dir);
1563 }
1564
1565 #[test]
1566 fn test_detect_workspace_root_directory_marker() {
1567 let tmp = tempfile::tempdir().unwrap();
1568 let project = tmp.path().join("myproject");
1569 let src = project.join("src");
1570 std::fs::create_dir_all(&src).unwrap();
1571 std::fs::create_dir_all(project.join(".git")).unwrap();
1572 let file = src.join("main.rs");
1573 std::fs::write(&file, "").unwrap();
1574
1575 let root = detect_workspace_root(&file, &[".git".to_string()]);
1576 assert_eq!(root, project);
1577 }
1578
1579 #[test]
1580 fn test_path_to_uri_basic() {
1581 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
1582 assert_eq!(uri.as_str(), "file:///tmp/test");
1583 }
1584
1585 #[test]
1586 fn test_path_to_uri_with_spaces() {
1587 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
1588 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
1589 }
1590}