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;
14use std::collections::HashSet;
15use std::path::Path;
16use std::time::{Duration, Instant};
17
18fn fire_and_forget<E: std::fmt::Debug>(result: Result<(), E>) {
23 if let Err(e) = result {
24 tracing::trace!(error = ?e, "fire-and-forget operation failed");
25 }
26}
27
28#[derive(Debug, Clone)]
33pub struct LanguageScope(Vec<String>);
34
35impl LanguageScope {
36 pub fn all() -> Self {
38 Self(Vec::new())
39 }
40
41 pub fn single(language: impl Into<String>) -> Self {
43 Self(vec![language.into()])
44 }
45
46 pub fn accepts(&self, language: &str) -> bool {
48 self.0.is_empty() || self.0.iter().any(|l| l == language)
49 }
50
51 pub fn is_universal(&self) -> bool {
53 self.0.is_empty()
54 }
55
56 pub fn languages(&self) -> &[String] {
58 &self.0
59 }
60
61 pub fn label(&self) -> &str {
63 self.0.first().map(|s| s.as_str()).unwrap_or("universal")
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum LspSpawnResult {
70 Spawned,
72 NotAutoStart,
75 NotConfigured,
77 Disabled,
81 Failed,
83}
84
85const MAX_RESTARTS_IN_WINDOW: usize = 5;
87const RESTART_WINDOW_SECS: u64 = 180; const RESTART_BACKOFF_BASE_MS: u64 = 1000; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
100enum SpawnDecision {
101 Existing,
103 Allow,
105 PendingBackoff,
108 CooledDown,
111}
112
113fn path_to_uri(path: &Path) -> Option<Uri> {
115 let abs = if path.is_absolute() {
116 path.to_path_buf()
117 } else {
118 std::env::current_dir().ok()?.join(path)
119 };
120 let encoded: String = abs
122 .components()
123 .filter_map(|c| match c {
124 std::path::Component::RootDir => None, std::path::Component::Normal(s) => {
126 let s = s.to_str()?;
127 let mut out = String::with_capacity(s.len() + 1);
128 out.push('/');
129 for b in s.bytes() {
130 if b.is_ascii_alphanumeric()
131 || matches!(
132 b,
133 b'-' | b'.'
134 | b'_'
135 | b'~'
136 | b'@'
137 | b'!'
138 | b'$'
139 | b'&'
140 | b'\''
141 | b'('
142 | b')'
143 | b'+'
144 | b','
145 | b';'
146 | b'='
147 )
148 {
149 out.push(b as char);
150 } else {
151 out.push_str(&format!("%{:02X}", b));
152 }
153 }
154 Some(out)
155 }
156 _ => None,
157 })
158 .collect();
159 format!("file://{}", encoded).parse().ok()
160}
161
162pub fn detect_workspace_root(file_path: &Path, root_markers: &[String]) -> std::path::PathBuf {
167 let file_dir = file_path.parent().unwrap_or(file_path).to_path_buf();
168
169 if root_markers.is_empty() {
170 return file_dir;
171 }
172
173 let mut dir = Some(file_dir.as_path());
174 while let Some(d) = dir {
175 for marker in root_markers {
176 if d.join(marker).exists() {
177 return d.to_path_buf();
178 }
179 }
180 dir = d.parent();
181 }
182
183 file_dir
184}
185
186#[derive(Debug, Clone, Default)]
193pub struct ServerCapabilitySummary {
194 pub initialized: bool,
197 pub hover: bool,
198 pub completion: bool,
199 pub completion_resolve: bool,
200 pub completion_trigger_characters: Vec<String>,
201 pub definition: bool,
202 pub references: bool,
203 pub document_formatting: bool,
204 pub document_range_formatting: bool,
205 pub rename: bool,
206 pub signature_help: bool,
207 pub inlay_hints: bool,
208 pub folding_ranges: bool,
209 pub semantic_tokens_full: bool,
210 pub semantic_tokens_full_delta: bool,
211 pub semantic_tokens_range: bool,
212 pub semantic_tokens_legend: Option<SemanticTokensLegend>,
213 pub document_highlight: bool,
214 pub code_action: bool,
215 pub code_action_resolve: bool,
216 pub document_symbols: bool,
217 pub workspace_symbols: bool,
218 pub diagnostics: bool,
219}
220
221pub struct ServerHandle {
225 pub name: String,
227 pub handle: LspHandle,
229 pub feature_filter: FeatureFilter,
231 pub capabilities: ServerCapabilitySummary,
233}
234
235impl ServerHandle {
236 pub fn has_capability(&self, feature: LspFeature) -> bool {
245 if !self.capabilities.initialized {
246 return false;
247 }
248 match feature {
249 LspFeature::Hover => self.capabilities.hover,
250 LspFeature::Completion => self.capabilities.completion,
251 LspFeature::Definition => self.capabilities.definition,
252 LspFeature::References => self.capabilities.references,
253 LspFeature::Format => {
254 self.capabilities.document_formatting || self.capabilities.document_range_formatting
255 }
256 LspFeature::Rename => self.capabilities.rename,
257 LspFeature::SignatureHelp => self.capabilities.signature_help,
258 LspFeature::InlayHints => self.capabilities.inlay_hints,
259 LspFeature::FoldingRange => self.capabilities.folding_ranges,
260 LspFeature::SemanticTokens => {
261 self.capabilities.semantic_tokens_full || self.capabilities.semantic_tokens_range
262 }
263 LspFeature::DocumentHighlight => self.capabilities.document_highlight,
264 LspFeature::CodeAction => self.capabilities.code_action,
265 LspFeature::DocumentSymbols => self.capabilities.document_symbols,
266 LspFeature::WorkspaceSymbols => self.capabilities.workspace_symbols,
267 LspFeature::Diagnostics => self.capabilities.diagnostics,
268 }
269 }
270}
271
272pub struct LspManager {
274 window_id: fresh_core::WindowId,
280
281 handles: Vec<ServerHandle>,
284
285 config: HashMap<String, Vec<LspServerConfig>>,
287
288 universal_configs: Vec<LspServerConfig>,
290
291 root_uri: Option<Uri>,
293
294 per_language_root_uris: HashMap<String, Uri>,
296
297 runtime: Option<tokio::runtime::Handle>,
299
300 async_bridge: Option<AsyncBridge>,
302
303 long_running_spawner: Option<std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>>,
309
310 path_translation: Option<crate::services::authority::PathTranslation>,
315
316 restart_attempts: HashMap<String, Vec<Instant>>,
318
319 restart_cooldown: HashSet<String>,
321
322 pending_restarts: HashMap<String, Instant>,
324
325 allowed_languages: HashSet<String>,
328
329 disabled_languages: HashSet<String>,
332}
333
334impl LspManager {
335 pub fn window_id(&self) -> fresh_core::WindowId {
337 self.window_id
338 }
339
340 pub fn new(window_id: fresh_core::WindowId, root_uri: Option<Uri>) -> Self {
342 Self {
343 window_id,
344 handles: Vec::new(),
345 config: HashMap::new(),
346 universal_configs: Vec::new(),
347 root_uri,
348 per_language_root_uris: HashMap::new(),
349 runtime: None,
350 async_bridge: None,
351 long_running_spawner: None,
352 path_translation: None,
353 restart_attempts: HashMap::new(),
354 restart_cooldown: HashSet::new(),
355 pending_restarts: HashMap::new(),
356 allowed_languages: HashSet::new(),
357 disabled_languages: HashSet::new(),
358 }
359 }
360
361 pub fn set_long_running_spawner(
370 &mut self,
371 spawner: std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>,
372 ) {
373 self.long_running_spawner = Some(spawner);
374 }
375
376 pub fn set_path_translation(
381 &mut self,
382 translation: Option<crate::services::authority::PathTranslation>,
383 ) {
384 self.path_translation = translation;
385 }
386
387 pub fn command_exists_via_authority(&self, command: &str) -> bool {
401 if command.is_empty() {
402 return false;
403 }
404 let (Some(runtime), Some(spawner)) =
405 (self.runtime.as_ref(), self.long_running_spawner.as_ref())
406 else {
407 return crate::services::lsp::command_exists(command);
408 };
409 runtime.block_on(spawner.command_exists(command))
410 }
411
412 pub fn is_language_allowed(&self, language: &str) -> bool {
414 self.allowed_languages.contains(language)
415 }
416
417 pub fn allow_language(&mut self, language: &str) {
419 self.allowed_languages.insert(language.to_string());
420 tracing::info!("LSP language '{}' manually enabled", language);
421 }
422
423 pub fn allowed_languages(&self) -> &HashSet<String> {
425 &self.allowed_languages
426 }
427
428 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
430 self.config.get(language).map(|v| v.as_slice())
431 }
432
433 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
435 self.config.get(language).and_then(|v| v.first())
436 }
437
438 pub fn set_server_capabilities(
440 &mut self,
441 _language: &str,
442 server_name: &str,
443 mut capabilities: ServerCapabilitySummary,
444 ) {
445 capabilities.initialized = true;
446
447 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
448 sh.capabilities = capabilities;
449 }
450 }
451
452 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
454 self.get_handles(language).into_iter().find_map(|sh| {
455 if sh.feature_filter.allows(LspFeature::SemanticTokens)
456 && sh.has_capability(LspFeature::SemanticTokens)
457 {
458 sh.capabilities.semantic_tokens_legend.as_ref()
459 } else {
460 None
461 }
462 })
463 }
464
465 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
467 self.get_handles(language).iter().any(|sh| {
468 sh.feature_filter.allows(LspFeature::SemanticTokens)
469 && sh.capabilities.semantic_tokens_full
470 })
471 }
472
473 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
475 self.get_handles(language).iter().any(|sh| {
476 sh.feature_filter.allows(LspFeature::SemanticTokens)
477 && sh.capabilities.semantic_tokens_full_delta
478 })
479 }
480
481 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
483 self.get_handles(language).iter().any(|sh| {
484 sh.feature_filter.allows(LspFeature::SemanticTokens)
485 && sh.capabilities.semantic_tokens_range
486 })
487 }
488
489 pub fn folding_ranges_supported(&self, language: &str) -> bool {
491 self.get_handles(language).iter().any(|sh| {
492 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
493 })
494 }
495
496 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
498 let ch_str = ch.to_string();
499 self.get_handles(language).iter().any(|sh| {
500 sh.feature_filter.allows(LspFeature::Completion)
501 && sh
502 .capabilities
503 .completion_trigger_characters
504 .contains(&ch_str)
505 })
506 }
507
508 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
523 if self
525 .handles
526 .iter()
527 .any(|sh| sh.handle.scope().accepts(language))
528 {
529 self.ensure_universal_servers_running(file_path);
530 return LspSpawnResult::Spawned;
531 }
532
533 if self.runtime.is_none() || self.async_bridge.is_none() {
535 return LspSpawnResult::Failed;
536 }
537
538 self.ensure_universal_servers_running(file_path);
540
541 let configs = match self.config.get(language) {
543 Some(configs) if !configs.is_empty() => configs,
544 _ => {
545 if self
547 .handles
548 .iter()
549 .any(|sh| sh.handle.scope().is_universal())
550 {
551 return LspSpawnResult::Spawned;
552 }
553 return LspSpawnResult::NotConfigured;
554 }
555 };
556
557 if !configs.iter().any(|c| c.enabled) {
559 if self
560 .handles
561 .iter()
562 .any(|sh| sh.handle.scope().is_universal())
563 {
564 return LspSpawnResult::Spawned;
565 }
566 return LspSpawnResult::Disabled;
567 }
568
569 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
571 if !any_auto_start && !self.allowed_languages.contains(language) {
572 if self
573 .handles
574 .iter()
575 .any(|sh| sh.handle.scope().is_universal())
576 {
577 return LspSpawnResult::Spawned;
578 }
579 return LspSpawnResult::NotAutoStart;
580 }
581
582 let spawned = self.force_spawn(language, file_path).is_some();
584
585 if spawned
586 || self
587 .handles
588 .iter()
589 .any(|sh| sh.handle.scope().is_universal())
590 {
591 LspSpawnResult::Spawned
592 } else {
593 LspSpawnResult::Failed
594 }
595 }
596
597 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
601 self.runtime = Some(runtime);
602 self.async_bridge = Some(async_bridge);
603 }
604
605 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
607 self.config.insert(language, vec![config]);
608 }
609
610 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
612 self.config.insert(language, configs);
613 }
614
615 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
617 self.config.entry(language).or_default().extend(configs);
618 }
619
620 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
625 self.universal_configs = configs;
626 }
627
628 pub fn configured_languages(&self) -> Vec<String> {
630 self.config.keys().cloned().collect()
631 }
632
633 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
638 self.root_uri = root_uri;
639 }
640
641 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
647 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
648 self.per_language_root_uris
649 .insert(language.to_string(), uri.clone());
650
651 if self
653 .handles
654 .iter()
655 .any(|sh| sh.handle.scope().accepts(language))
656 {
657 tracing::info!(
658 "Restarting {} LSP server with new root: {}",
659 language,
660 uri.as_str()
661 );
662 self.shutdown_server(language);
663 return true;
665 }
666 false
667 }
668
669 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
676 if let Some(uri) = self.per_language_root_uris.get(language) {
678 return Some(uri.clone());
679 }
680
681 if let Some(path) = file_path {
686 let markers = self
687 .config
688 .get(language)
689 .and_then(|configs| configs.first())
690 .map(|c| c.root_markers.as_slice())
691 .unwrap_or(&[]);
692 let root = detect_workspace_root(path, markers);
693 let mapped = self
694 .path_translation
695 .as_ref()
696 .and_then(|t| t.host_to_remote(&root))
697 .unwrap_or(root);
698 if let Some(uri) = path_to_uri(&mapped) {
699 return Some(uri);
700 }
701 }
702
703 self.root_uri.clone()
705 }
706
707 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
711 self.resolve_root_uri(language, None)
712 }
713
714 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
719 self.shutdown_all();
721
722 self.root_uri = new_root_uri;
724
725 self.restart_attempts.clear();
727 self.restart_cooldown.clear();
728 self.pending_restarts.clear();
729
730 tracing::info!(
734 "LSP manager reset for new project: {:?}",
735 self.root_uri.as_ref().map(|u| u.as_str())
736 );
737 }
738
739 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
742 self.handles
743 .iter()
744 .find(|sh| sh.handle.scope().accepts(language))
745 .map(|sh| &sh.handle)
746 }
747
748 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
751 self.handles
752 .iter_mut()
753 .find(|sh| sh.handle.scope().accepts(language))
754 .map(|sh| &mut sh.handle)
755 }
756
757 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
759 self.handles
760 .iter()
761 .filter(|sh| sh.handle.scope().accepts(language))
762 .collect()
763 }
764
765 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
767 self.handles
768 .iter_mut()
769 .filter(|sh| sh.handle.scope().accepts(language))
770 .collect()
771 }
772
773 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
777 self.handles
778 .iter()
779 .find(|sh| sh.name == server_name)
780 .map(|sh| sh.handle.scope())
781 }
782
783 pub fn has_handles(&self, language: &str) -> bool {
785 self.handles
786 .iter()
787 .any(|sh| sh.handle.scope().accepts(language))
788 }
789
790 pub fn handle_count(&self, language: &str) -> usize {
792 self.handles
793 .iter()
794 .filter(|sh| sh.handle.scope().accepts(language))
795 .count()
796 }
797
798 pub fn has_server_named(&self, server_name: &str) -> bool {
800 self.handles.iter().any(|sh| sh.name == server_name)
801 }
802
803 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
809 self.handles
810 .iter()
811 .filter(|sh| sh.handle.scope().accepts(language))
812 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
813 }
814
815 pub fn handle_for_feature_mut(
819 &mut self,
820 language: &str,
821 feature: LspFeature,
822 ) -> Option<&mut ServerHandle> {
823 self.handles
824 .iter_mut()
825 .filter(|sh| sh.handle.scope().accepts(language))
826 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
827 }
828
829 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
833 self.handles
834 .iter()
835 .filter(|sh| sh.handle.scope().accepts(language))
836 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
837 .collect()
838 }
839
840 pub fn handles_for_feature_mut(
844 &mut self,
845 language: &str,
846 feature: LspFeature,
847 ) -> Vec<&mut ServerHandle> {
848 self.handles
849 .iter_mut()
850 .filter(|sh| sh.handle.scope().accepts(language))
851 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
852 .collect()
853 }
854
855 fn spawn_decision(&mut self, language: &str) -> SpawnDecision {
863 if self
864 .handles
865 .iter()
866 .any(|sh| sh.handle.scope().accepts(language))
867 {
868 return SpawnDecision::Existing;
869 }
870 if self.restart_cooldown.contains(language) {
871 return SpawnDecision::CooledDown;
872 }
873 if self.pending_restarts.contains_key(language) {
874 return SpawnDecision::PendingBackoff;
875 }
876
877 let now = Instant::now();
878 let window = Duration::from_secs(RESTART_WINDOW_SECS);
879 let attempts = self
880 .restart_attempts
881 .entry(language.to_string())
882 .or_default();
883 attempts.retain(|t| now.duration_since(*t) < window);
884
885 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
886 self.restart_cooldown.insert(language.to_string());
887 tracing::warn!(
888 "LSP server for {} has spawned {} times in {} minutes, entering cooldown",
889 language,
890 MAX_RESTARTS_IN_WINDOW,
891 RESTART_WINDOW_SECS / 60
892 );
893 return SpawnDecision::CooledDown;
894 }
895
896 attempts.push(now);
897 SpawnDecision::Allow
898 }
899
900 pub fn force_spawn(
919 &mut self,
920 language: &str,
921 file_path: Option<&Path>,
922 ) -> Option<&mut LspHandle> {
923 tracing::debug!("force_spawn called for language: {}", language);
924
925 if self
927 .handles
928 .iter()
929 .any(|sh| sh.handle.scope().accepts(language))
930 {
931 tracing::debug!("force_spawn: returning existing handle for {}", language);
932 return self
933 .handles
934 .iter_mut()
935 .find(|sh| sh.handle.scope().accepts(language))
936 .map(|sh| &mut sh.handle);
937 }
938
939 if self.disabled_languages.contains(language) {
941 tracing::debug!(
942 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
943 language
944 );
945 return None;
946 }
947
948 let configs = match self.config.get(language) {
950 Some(configs) if !configs.is_empty() => configs.clone(),
951 _ => {
952 tracing::warn!(
953 "force_spawn: no config found for language '{}', available configs: {:?}",
954 language,
955 self.config.keys().collect::<Vec<_>>()
956 );
957 return None;
958 }
959 };
960
961 match self.spawn_decision(language) {
967 SpawnDecision::Existing => {
968 return self
971 .handles
972 .iter_mut()
973 .find(|sh| sh.handle.scope().accepts(language))
974 .map(|sh| &mut sh.handle);
975 }
976 SpawnDecision::CooledDown => {
977 tracing::debug!(
978 "force_spawn: {} is in cooldown, refusing spawn (use Restart LSP command)",
979 language
980 );
981 return None;
982 }
983 SpawnDecision::PendingBackoff => {
984 tracing::debug!(
985 "force_spawn: {} has a pending restart scheduled, not double-spawning",
986 language
987 );
988 return None;
989 }
990 SpawnDecision::Allow => {}
991 }
992
993 let runtime = match self.runtime.as_ref() {
998 Some(r) => r.clone(),
999 None => {
1000 tracing::error!("force_spawn: no tokio runtime available for {}", language);
1001 return None;
1002 }
1003 };
1004 let async_bridge = match self.async_bridge.as_ref() {
1005 Some(b) => b.clone(),
1006 None => {
1007 tracing::error!("force_spawn: no async bridge available for {}", language);
1008 return None;
1009 }
1010 };
1011 let long_running_spawner = match self.long_running_spawner.as_ref() {
1019 Some(s) => s.clone(),
1020 None => {
1021 tracing::warn!(
1022 "force_spawn: long-running spawner not wired for {} — \
1023 falling back to host-local spawn (normal for tests \
1024 that skip set_boot_authority)",
1025 language
1026 );
1027 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1028 }
1029 };
1030
1031 let mut spawned_handles = Vec::new();
1032 let manually_allowed = self.allowed_languages.contains(language);
1033
1034 for config in &configs {
1035 if manually_allowed {
1036 } else {
1040 if !config.enabled || !config.auto_start {
1046 continue;
1047 }
1048 }
1049
1050 if config.command.is_empty() {
1051 tracing::warn!(
1052 "force_spawn: LSP command is empty for {} server '{}'",
1053 language,
1054 config.display_name()
1055 );
1056 continue;
1057 }
1058
1059 let server_name = config.display_name();
1060 tracing::info!(
1061 "Spawning LSP server '{}' for language: {}",
1062 server_name,
1063 language
1064 );
1065
1066 match LspHandle::spawn(
1067 &runtime,
1068 &config.command,
1069 &config.args,
1070 config.env.clone(),
1071 LanguageScope::single(language),
1072 server_name.clone(),
1073 &async_bridge,
1074 config.process_limits.clone(),
1075 config.language_id_overrides.clone(),
1076 long_running_spawner.clone(),
1077 ) {
1078 Ok(handle) => {
1079 let effective_root = self.resolve_root_uri(language, file_path);
1080 if let Err(e) =
1081 handle.initialize(effective_root, config.initialization_options.clone())
1082 {
1083 tracing::error!(
1084 "Failed to send initialize command for {} ({}): {}",
1085 language,
1086 server_name,
1087 e
1088 );
1089 continue;
1090 }
1091
1092 tracing::info!(
1093 "LSP initialization started for {} ({}), will be ready asynchronously",
1094 language,
1095 server_name
1096 );
1097
1098 spawned_handles.push(ServerHandle {
1099 name: server_name,
1100 handle,
1101 feature_filter: config.feature_filter(),
1102 capabilities: ServerCapabilitySummary::default(),
1103 });
1104 }
1105 Err(e) => {
1106 tracing::error!(
1107 "Failed to spawn LSP handle for {} ({}): {}",
1108 language,
1109 server_name,
1110 e
1111 );
1112 }
1113 }
1114 }
1115
1116 if spawned_handles.is_empty() {
1117 return None;
1118 }
1119
1120 self.handles.extend(spawned_handles);
1121 self.handles
1122 .iter_mut()
1123 .rev()
1124 .find(|sh| sh.handle.scope().accepts(language))
1125 .map(|sh| &mut sh.handle)
1126 }
1127
1128 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
1134 if self
1135 .handles
1136 .iter()
1137 .any(|sh| sh.handle.scope().is_universal())
1138 || self.universal_configs.is_empty()
1139 {
1140 return;
1141 }
1142
1143 let runtime = match self.runtime.as_ref() {
1144 Some(r) => r.clone(),
1145 None => return,
1146 };
1147 let async_bridge = match self.async_bridge.as_ref() {
1148 Some(b) => b.clone(),
1149 None => return,
1150 };
1151 let long_running_spawner =
1152 self.long_running_spawner
1153 .as_ref()
1154 .cloned()
1155 .unwrap_or_else(|| {
1156 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1157 });
1158
1159 let mut spawned = Vec::new();
1160 for config in &self.universal_configs {
1161 if !config.enabled || !config.auto_start {
1162 continue;
1163 }
1164 if config.command.is_empty() {
1165 continue;
1166 }
1167
1168 let server_name = config.display_name();
1169 tracing::info!("Spawning universal LSP server '{}'", server_name);
1170
1171 match LspHandle::spawn(
1172 &runtime,
1173 &config.command,
1174 &config.args,
1175 config.env.clone(),
1176 LanguageScope::all(),
1177 server_name.clone(),
1178 &async_bridge,
1179 config.process_limits.clone(),
1180 config.language_id_overrides.clone(),
1181 long_running_spawner.clone(),
1182 ) {
1183 Ok(handle) => {
1184 let effective_root = file_path
1185 .and_then(|p| {
1186 let root = detect_workspace_root(p, &config.root_markers);
1187 path_to_uri(&root)
1188 })
1189 .or_else(|| self.root_uri.clone());
1190 if let Err(e) =
1191 handle.initialize(effective_root, config.initialization_options.clone())
1192 {
1193 tracing::error!(
1194 "Failed to initialize universal LSP server '{}': {}",
1195 server_name,
1196 e
1197 );
1198 continue;
1199 }
1200 tracing::info!(
1201 "Universal LSP server '{}' initialization started",
1202 server_name
1203 );
1204 spawned.push(ServerHandle {
1205 name: server_name,
1206 handle,
1207 feature_filter: config.feature_filter(),
1208 capabilities: ServerCapabilitySummary::default(),
1209 });
1210 }
1211 Err(e) => {
1212 tracing::error!(
1213 "Failed to spawn universal LSP server '{}': {}",
1214 server_name,
1215 e
1216 );
1217 }
1218 }
1219 }
1220
1221 self.handles.extend(spawned);
1222 }
1223
1224 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1228 if self
1230 .handles
1231 .iter()
1232 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1233 {
1234 let universals: Vec<ServerHandle> = {
1236 let mut drained = Vec::new();
1237 let mut i = 0;
1238 while i < self.handles.len() {
1239 if self.handles[i].handle.scope().is_universal() {
1240 drained.push(self.handles.remove(i));
1241 } else {
1242 i += 1;
1243 }
1244 }
1245 drained
1246 };
1247 for sh in universals {
1248 fire_and_forget(sh.handle.shutdown());
1249 }
1250 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1252 }
1253
1254 {
1256 let mut i = 0;
1257 while i < self.handles.len() {
1258 if !self.handles[i].handle.scope().is_universal()
1259 && self.handles[i].handle.scope().accepts(language)
1260 {
1261 let sh = self.handles.remove(i);
1262 fire_and_forget(sh.handle.shutdown());
1263 } else {
1264 i += 1;
1265 }
1266 }
1267 }
1268
1269 if self.disabled_languages.contains(language) {
1272 return format!(
1273 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1274 language
1275 );
1276 }
1277
1278 if self.restart_cooldown.contains(language) {
1280 return format!(
1281 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1282 language
1283 );
1284 }
1285
1286 let now = Instant::now();
1291 let attempt_number = self
1292 .restart_attempts
1293 .get(language)
1294 .map(|v| v.len())
1295 .unwrap_or(0);
1296
1297 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1299
1300 self.pending_restarts
1301 .insert(language.to_string(), restart_time);
1302
1303 tracing::info!(
1304 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1305 language,
1306 attempt_number + 1,
1307 MAX_RESTARTS_IN_WINDOW,
1308 delay_ms
1309 );
1310
1311 format!(
1312 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1313 language,
1314 attempt_number + 1,
1315 MAX_RESTARTS_IN_WINDOW,
1316 delay_ms / 1000
1317 )
1318 }
1319
1320 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1324 let now = Instant::now();
1325 let mut results = Vec::new();
1326
1327 let due_restarts: Vec<String> = self
1329 .pending_restarts
1330 .iter()
1331 .filter(|(_, time)| **time <= now)
1332 .map(|(lang, _)| lang.clone())
1333 .collect();
1334
1335 for language in due_restarts {
1336 self.pending_restarts.remove(&language);
1337
1338 if self.force_spawn(&language, None).is_some() {
1342 let message = format!("LSP server for {} restarted successfully", language);
1343 tracing::info!("{}", message);
1344 results.push((language, true, message));
1345 } else {
1346 let message = format!("Failed to restart LSP server for {}", language);
1347 tracing::error!("{}", message);
1348 results.push((language, false, message));
1349 }
1350 }
1351
1352 results
1353 }
1354
1355 pub fn is_in_cooldown(&self, language: &str) -> bool {
1357 self.restart_cooldown.contains(language)
1358 }
1359
1360 pub fn has_pending_restart(&self, language: &str) -> bool {
1362 self.pending_restarts.contains_key(language)
1363 }
1364
1365 pub fn clear_cooldown(&mut self, language: &str) {
1367 self.restart_cooldown.remove(language);
1368 self.restart_attempts.remove(language);
1369 self.pending_restarts.remove(language);
1370 tracing::info!("Cleared restart cooldown for {}", language);
1371 }
1372
1373 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1380 self.clear_cooldown(language);
1382
1383 self.disabled_languages.remove(language);
1385
1386 self.allowed_languages.insert(language.to_string());
1388
1389 {
1391 let mut i = 0;
1392 while i < self.handles.len() {
1393 if !self.handles[i].handle.scope().is_universal()
1394 && self.handles[i].handle.scope().accepts(language)
1395 {
1396 let sh = self.handles.remove(i);
1397 fire_and_forget(sh.handle.shutdown());
1398 } else {
1399 i += 1;
1400 }
1401 }
1402 }
1403
1404 if self.force_spawn(language, file_path).is_some() {
1406 let message = format!("LSP server for {} started", language);
1407 tracing::info!("{}", message);
1408 (true, message)
1409 } else {
1410 let message = format!("Failed to start LSP server for {}", language);
1411 tracing::error!("{}", message);
1412 (false, message)
1413 }
1414 }
1415
1416 pub fn manual_restart_server(
1421 &mut self,
1422 language: &str,
1423 server_name: &str,
1424 file_path: Option<&Path>,
1425 ) -> (bool, String) {
1426 self.clear_cooldown(language);
1427 self.disabled_languages.remove(language);
1428 self.allowed_languages.insert(language.to_string());
1429
1430 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1432 let sh = self.handles.remove(idx);
1433 fire_and_forget(sh.handle.shutdown());
1434 }
1435
1436 let is_universal = self
1438 .universal_configs
1439 .iter()
1440 .any(|c| c.display_name() == server_name);
1441 let config = if is_universal {
1442 self.universal_configs
1443 .iter()
1444 .find(|c| c.display_name() == server_name)
1445 .cloned()
1446 } else {
1447 self.config
1448 .get(language)
1449 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1450 .cloned()
1451 };
1452
1453 let Some(config) = config else {
1454 let message = format!(
1455 "No config found for server '{}' ({})",
1456 server_name, language
1457 );
1458 tracing::error!("{}", message);
1459 return (false, message);
1460 };
1461
1462 if config.command.is_empty() {
1463 let message = format!(
1464 "LSP command is empty for {} server '{}'",
1465 language, server_name
1466 );
1467 tracing::error!("{}", message);
1468 return (false, message);
1469 }
1470
1471 let runtime = match self.runtime.as_ref() {
1472 Some(r) => r.clone(),
1473 None => return (false, "No tokio runtime available".to_string()),
1474 };
1475 let async_bridge = match self.async_bridge.as_ref() {
1476 Some(b) => b.clone(),
1477 None => return (false, "No async bridge available".to_string()),
1478 };
1479 let long_running_spawner =
1480 self.long_running_spawner
1481 .as_ref()
1482 .cloned()
1483 .unwrap_or_else(|| {
1484 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1485 });
1486
1487 let scope = if is_universal {
1488 LanguageScope::all()
1489 } else {
1490 LanguageScope::single(language)
1491 };
1492
1493 match LspHandle::spawn(
1494 &runtime,
1495 &config.command,
1496 &config.args,
1497 config.env.clone(),
1498 scope,
1499 server_name.to_string(),
1500 &async_bridge,
1501 config.process_limits.clone(),
1502 config.language_id_overrides.clone(),
1503 long_running_spawner,
1504 ) {
1505 Ok(handle) => {
1506 let effective_root = if is_universal {
1507 file_path
1508 .and_then(|p| {
1509 let root = detect_workspace_root(p, &config.root_markers);
1510 path_to_uri(&root)
1511 })
1512 .or_else(|| self.root_uri.clone())
1513 } else {
1514 self.resolve_root_uri(language, file_path)
1515 };
1516 if let Err(e) =
1517 handle.initialize(effective_root, config.initialization_options.clone())
1518 {
1519 let message = format!(
1520 "Failed to initialize LSP server '{}' for {}: {}",
1521 server_name, language, e
1522 );
1523 tracing::error!("{}", message);
1524 return (false, message);
1525 }
1526
1527 let sh = ServerHandle {
1528 name: server_name.to_string(),
1529 handle,
1530 feature_filter: config.feature_filter(),
1531 capabilities: ServerCapabilitySummary::default(),
1532 };
1533
1534 self.handles.push(sh);
1535
1536 let message = format!("LSP server '{}' for {} started", server_name, language);
1537 tracing::info!("{}", message);
1538 (true, message)
1539 }
1540 Err(e) => {
1541 let message = format!(
1542 "Failed to start LSP server '{}' for {}: {}",
1543 server_name, language, e
1544 );
1545 tracing::error!("{}", message);
1546 (false, message)
1547 }
1548 }
1549 }
1550
1551 pub fn restart_attempt_count(&self, language: &str) -> usize {
1553 let now = Instant::now();
1554 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1555 self.restart_attempts
1556 .get(language)
1557 .map(|attempts| {
1558 attempts
1559 .iter()
1560 .filter(|t| now.duration_since(**t) < window)
1561 .count()
1562 })
1563 .unwrap_or(0)
1564 }
1565
1566 pub fn running_servers(&self) -> Vec<String> {
1568 let mut labels: Vec<String> = self
1569 .handles
1570 .iter()
1571 .map(|sh| sh.handle.scope().label().to_string())
1572 .collect();
1573 labels.sort();
1574 labels.dedup();
1575 labels
1576 }
1577
1578 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1580 self.handles
1581 .iter()
1582 .filter(|sh| sh.handle.scope().accepts(language))
1583 .map(|sh| sh.name.clone())
1584 .collect()
1585 }
1586
1587 pub fn is_server_ready(&self, language: &str) -> bool {
1589 self.handles
1590 .iter()
1591 .filter(|sh| sh.handle.scope().accepts(language))
1592 .any(|sh| sh.handle.state().can_send_requests())
1593 }
1594
1595 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1600 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1601 tracing::warn!(
1602 "No running LSP server named '{}' found for {}",
1603 server_name,
1604 language
1605 );
1606 return false;
1607 };
1608
1609 let sh = self.handles.remove(idx);
1610 tracing::info!(
1611 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1612 sh.name,
1613 language
1614 );
1615 fire_and_forget(sh.handle.shutdown());
1616
1617 let has_remaining = self
1619 .handles
1620 .iter()
1621 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1622 if !has_remaining {
1623 self.disabled_languages.insert(language.to_string());
1624 self.pending_restarts.remove(language);
1625 self.restart_cooldown.remove(language);
1626 self.allowed_languages.remove(language);
1627 }
1628
1629 true
1630 }
1631
1632 pub fn shutdown_server(&mut self, language: &str) -> bool {
1637 let mut found = false;
1638 let mut i = 0;
1639 while i < self.handles.len() {
1640 if !self.handles[i].handle.scope().is_universal()
1641 && self.handles[i].handle.scope().accepts(language)
1642 {
1643 let sh = self.handles.remove(i);
1644 tracing::info!(
1645 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1646 sh.name,
1647 language
1648 );
1649 fire_and_forget(sh.handle.shutdown());
1650 found = true;
1651 } else {
1652 i += 1;
1653 }
1654 }
1655
1656 if found {
1657 self.disabled_languages.insert(language.to_string());
1658 self.pending_restarts.remove(language);
1659 self.restart_cooldown.remove(language);
1660 self.allowed_languages.remove(language);
1661 } else {
1662 tracing::warn!("No running LSP server found for {}", language);
1663 }
1664
1665 found
1666 }
1667
1668 pub fn shutdown_all(&mut self) {
1670 for sh in &self.handles {
1671 tracing::info!(
1672 "Shutting down LSP server '{}' ({})",
1673 sh.name,
1674 sh.handle.scope().label()
1675 );
1676 fire_and_forget(sh.handle.shutdown());
1677 }
1678 self.handles.clear();
1679 }
1680}
1681
1682impl Drop for LspManager {
1683 fn drop(&mut self) {
1684 self.shutdown_all();
1685 }
1686}
1687
1688pub fn detect_language(
1700 path: &std::path::Path,
1701 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1702) -> Option<String> {
1703 let detected = detect_language_by_config(path, languages);
1704
1705 if detected.as_deref() == Some("c")
1711 && path.extension().and_then(|e| e.to_str()) == Some("h")
1712 && languages.contains_key("cpp")
1713 && header_in_cpp_tree(path)
1714 {
1715 return Some("cpp".to_string());
1716 }
1717
1718 detected
1719}
1720
1721fn detect_language_by_config(
1723 path: &std::path::Path,
1724 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1725) -> Option<String> {
1726 use crate::primitives::glob_match::{
1727 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1728 };
1729
1730 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1731 for (language_name, lang_config) in languages {
1733 if lang_config
1734 .filenames
1735 .iter()
1736 .any(|f| !is_glob_pattern(f) && f == filename)
1737 {
1738 return Some(language_name.clone());
1739 }
1740 }
1741
1742 let path_str = path.to_str().unwrap_or("");
1746 for (language_name, lang_config) in languages {
1747 if lang_config.filenames.iter().any(|f| {
1748 if !is_glob_pattern(f) {
1749 return false;
1750 }
1751 if is_path_pattern(f) {
1752 path_glob_matches(f, path_str)
1753 } else {
1754 filename_glob_matches(f, filename)
1755 }
1756 }) {
1757 return Some(language_name.clone());
1758 }
1759 }
1760 }
1761
1762 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1764 for (language_name, lang_config) in languages {
1765 if lang_config.extensions.iter().any(|ext| ext == extension) {
1766 return Some(language_name.clone());
1767 }
1768 }
1769 }
1770
1771 None
1772}
1773
1774fn header_in_cpp_tree(path: &std::path::Path) -> bool {
1804 let Some(start_dir) = path.parent() else {
1805 return false;
1806 };
1807
1808 if let Ok(entries) = std::fs::read_dir(start_dir) {
1810 for entry in entries.flatten() {
1811 let p = entry.path();
1812 let Some(ext) = p.extension().and_then(|e| e.to_str()) else {
1813 continue;
1814 };
1815 if matches!(
1816 ext,
1817 "cc" | "cpp" | "cxx" | "C" | "c++" | "hpp" | "hh" | "hxx"
1818 ) {
1819 return true;
1820 }
1821 }
1822 }
1823
1824 let mut current = Some(start_dir);
1828 let mut depth = 0u32;
1829 while let Some(dir) = current {
1830 let cc = dir.join("compile_commands.json");
1831 if cc.is_file() && compile_commands_has_cpp_marker(&cc) {
1832 return true;
1833 }
1834 if depth >= 10 {
1835 break;
1836 }
1837 depth += 1;
1838 current = dir.parent();
1839 }
1840
1841 false
1842}
1843
1844fn compile_commands_has_cpp_marker(path: &std::path::Path) -> bool {
1852 use std::io::Read;
1853 const MAX_READ: u64 = 1_048_576;
1854
1855 let Ok(file) = std::fs::File::open(path) else {
1856 return false;
1857 };
1858 let mut buf = Vec::with_capacity(64 * 1024);
1859 if file.take(MAX_READ).read_to_end(&mut buf).is_err() {
1860 return false;
1861 }
1862 let Ok(text) = std::str::from_utf8(&buf) else {
1863 return false;
1864 };
1865
1866 if text.contains("c++") {
1870 return true;
1871 }
1872 text.contains(".cpp") || text.contains(".cxx") || text.contains(".cc\"")
1875}
1876
1877#[cfg(test)]
1878mod tests {
1879 use super::*;
1880 use std::path::Path;
1881
1882 #[test]
1883 fn test_lsp_manager_new() {
1884 let root_uri: Option<Uri> = "file:///test".parse().ok();
1885 let manager = LspManager::new(fresh_core::WindowId(1), root_uri.clone());
1886
1887 assert_eq!(manager.handles.len(), 0);
1889 assert_eq!(manager.config.len(), 0);
1890 assert!(manager.root_uri.is_some());
1891 assert!(manager.runtime.is_none());
1892 assert!(manager.async_bridge.is_none());
1893 }
1894
1895 #[test]
1896 fn test_lsp_manager_set_language_config() {
1897 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
1898
1899 let config = LspServerConfig {
1900 enabled: true,
1901 command: "rust-analyzer".to_string(),
1902 args: vec![],
1903 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1904 auto_start: false,
1905 initialization_options: None,
1906 env: Default::default(),
1907 language_id_overrides: Default::default(),
1908 name: None,
1909 only_features: None,
1910 except_features: None,
1911 root_markers: Default::default(),
1912 };
1913
1914 manager.set_language_config("rust".to_string(), config);
1915
1916 assert_eq!(manager.config.len(), 1);
1917 assert!(manager.config.contains_key("rust"));
1918 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1919 }
1920
1921 #[test]
1922 fn test_lsp_manager_force_spawn_no_runtime() {
1923 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
1924
1925 manager.set_language_config(
1927 "rust".to_string(),
1928 LspServerConfig {
1929 enabled: true,
1930 command: "rust-analyzer".to_string(),
1931 args: vec![],
1932 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1933 auto_start: false,
1934 initialization_options: None,
1935 env: Default::default(),
1936 language_id_overrides: Default::default(),
1937 name: None,
1938 only_features: None,
1939 except_features: None,
1940 root_markers: Default::default(),
1941 },
1942 );
1943
1944 let result = manager.force_spawn("rust", None);
1946 assert!(result.is_none());
1947 }
1948
1949 #[test]
1950 fn test_lsp_manager_force_spawn_no_config() {
1951 let rt = tokio::runtime::Runtime::new().unwrap();
1952 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
1953 let async_bridge = AsyncBridge::new();
1954
1955 manager.set_runtime(rt.handle().clone(), async_bridge);
1956
1957 let result = manager.force_spawn("rust", None);
1959 assert!(result.is_none());
1960 }
1961
1962 #[test]
1963 fn test_lsp_manager_force_spawn_disabled_language() {
1964 let rt = tokio::runtime::Runtime::new().unwrap();
1965 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
1966 let async_bridge = AsyncBridge::new();
1967
1968 manager.set_runtime(rt.handle().clone(), async_bridge);
1969
1970 manager.set_language_config(
1972 "rust".to_string(),
1973 LspServerConfig {
1974 enabled: false,
1975 command: String::new(), args: vec![],
1977 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1978 auto_start: false,
1979 initialization_options: None,
1980 env: Default::default(),
1981 language_id_overrides: Default::default(),
1982 name: None,
1983 only_features: None,
1984 except_features: None,
1985 root_markers: Default::default(),
1986 },
1987 );
1988
1989 let result = manager.force_spawn("rust", None);
1991 assert!(result.is_none());
1992 }
1993
1994 #[test]
2000 fn test_lsp_manager_try_spawn_returns_disabled_when_all_configs_disabled() {
2001 let rt = tokio::runtime::Runtime::new().unwrap();
2002 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2003 let async_bridge = AsyncBridge::new();
2004 manager.set_runtime(rt.handle().clone(), async_bridge);
2005
2006 manager.set_language_config(
2007 "rust".to_string(),
2008 LspServerConfig {
2009 enabled: false,
2010 command: String::new(),
2011 args: vec![],
2012 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2013 auto_start: false,
2014 initialization_options: None,
2015 env: Default::default(),
2016 language_id_overrides: Default::default(),
2017 name: None,
2018 only_features: None,
2019 except_features: None,
2020 root_markers: Default::default(),
2021 },
2022 );
2023
2024 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2025 }
2026
2027 #[test]
2028 fn test_lsp_manager_shutdown_all() {
2029 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2030
2031 manager.shutdown_all();
2033 assert_eq!(manager.handles.len(), 0);
2034 }
2035
2036 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2037 let mut languages = std::collections::HashMap::new();
2038 languages.insert(
2039 "rust".to_string(),
2040 crate::config::LanguageConfig {
2041 extensions: vec!["rs".to_string()],
2042 filenames: vec![],
2043 grammar: "rust".to_string(),
2044 comment_prefix: Some("//".to_string()),
2045 auto_indent: true,
2046 auto_close: None,
2047 auto_surround: None,
2048 textmate_grammar: None,
2049 show_whitespace_tabs: false,
2050 line_wrap: None,
2051 wrap_column: None,
2052 page_view: None,
2053 page_width: None,
2054 use_tabs: None,
2055 tab_size: None,
2056 formatter: None,
2057 format_on_save: false,
2058 on_save: vec![],
2059 word_characters: None,
2060 },
2061 );
2062 languages.insert(
2063 "javascript".to_string(),
2064 crate::config::LanguageConfig {
2065 extensions: vec!["js".to_string(), "jsx".to_string()],
2066 filenames: vec![],
2067 grammar: "javascript".to_string(),
2068 comment_prefix: Some("//".to_string()),
2069 auto_indent: true,
2070 auto_close: None,
2071 auto_surround: None,
2072 textmate_grammar: None,
2073 show_whitespace_tabs: false,
2074 line_wrap: None,
2075 wrap_column: None,
2076 page_view: None,
2077 page_width: None,
2078 use_tabs: None,
2079 tab_size: None,
2080 formatter: None,
2081 format_on_save: false,
2082 on_save: vec![],
2083 word_characters: None,
2084 },
2085 );
2086 languages.insert(
2087 "csharp".to_string(),
2088 crate::config::LanguageConfig {
2089 extensions: vec!["cs".to_string()],
2090 filenames: vec![],
2091 grammar: "c_sharp".to_string(),
2092 comment_prefix: Some("//".to_string()),
2093 auto_indent: true,
2094 auto_close: None,
2095 auto_surround: None,
2096 textmate_grammar: None,
2097 show_whitespace_tabs: false,
2098 line_wrap: None,
2099 wrap_column: None,
2100 page_view: None,
2101 page_width: None,
2102 use_tabs: None,
2103 tab_size: None,
2104 formatter: None,
2105 format_on_save: false,
2106 on_save: vec![],
2107 word_characters: None,
2108 },
2109 );
2110 languages
2111 }
2112
2113 #[test]
2114 fn test_detect_language_from_config() {
2115 let languages = test_languages();
2116
2117 assert_eq!(
2119 detect_language(Path::new("main.rs"), &languages),
2120 Some("rust".to_string())
2121 );
2122 assert_eq!(
2123 detect_language(Path::new("index.js"), &languages),
2124 Some("javascript".to_string())
2125 );
2126 assert_eq!(
2127 detect_language(Path::new("App.jsx"), &languages),
2128 Some("javascript".to_string())
2129 );
2130 assert_eq!(
2131 detect_language(Path::new("Program.cs"), &languages),
2132 Some("csharp".to_string())
2133 );
2134
2135 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
2137 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
2138 assert_eq!(detect_language(Path::new("file"), &languages), None);
2139 }
2140
2141 #[test]
2142 fn test_detect_language_no_extension() {
2143 let languages = test_languages();
2144 assert_eq!(detect_language(Path::new("README"), &languages), None);
2145 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
2146 }
2147
2148 #[test]
2149 fn test_detect_language_path_glob() {
2150 let mut languages = test_languages();
2151 languages.insert(
2152 "shell".to_string(),
2153 crate::config::LanguageConfig {
2154 extensions: vec!["sh".to_string()],
2155 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
2156 grammar: "bash".to_string(),
2157 comment_prefix: Some("#".to_string()),
2158 auto_indent: true,
2159 auto_close: None,
2160 auto_surround: None,
2161 textmate_grammar: None,
2162 show_whitespace_tabs: false,
2163 line_wrap: None,
2164 wrap_column: None,
2165 page_view: None,
2166 page_width: None,
2167 use_tabs: None,
2168 tab_size: None,
2169 formatter: None,
2170 format_on_save: false,
2171 on_save: vec![],
2172 word_characters: None,
2173 },
2174 );
2175
2176 assert_eq!(
2178 detect_language(Path::new("/etc/rc.conf"), &languages),
2179 Some("shell".to_string())
2180 );
2181 assert_eq!(
2182 detect_language(Path::new("/etc/init/rc.local"), &languages),
2183 Some("shell".to_string())
2184 );
2185 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
2187
2188 assert_eq!(
2190 detect_language(Path::new("lfrc"), &languages),
2191 Some("shell".to_string())
2192 );
2193 }
2194
2195 #[test]
2196 fn test_detect_workspace_root_finds_marker_in_parent() {
2197 let tmp = tempfile::tempdir().unwrap();
2198 let project = tmp.path().join("myproject");
2199 let src = project.join("src");
2200 std::fs::create_dir_all(&src).unwrap();
2201 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2202 let file = src.join("main.rs");
2203 std::fs::write(&file, "").unwrap();
2204
2205 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
2206 assert_eq!(root, project);
2207 }
2208
2209 #[test]
2210 fn test_detect_workspace_root_finds_marker_two_levels_up() {
2211 let tmp = tempfile::tempdir().unwrap();
2212 let project = tmp.path().join("myproject");
2213 let deep = project.join("src").join("nested");
2214 std::fs::create_dir_all(&deep).unwrap();
2215 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2216 let file = deep.join("lib.rs");
2217 std::fs::write(&file, "").unwrap();
2218
2219 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
2220 assert_eq!(root, project);
2221 }
2222
2223 #[test]
2224 fn test_detect_workspace_root_no_marker_returns_parent() {
2225 let tmp = tempfile::tempdir().unwrap();
2226 let dir = tmp.path().join("somedir");
2227 std::fs::create_dir_all(&dir).unwrap();
2228 let file = dir.join("file.txt");
2229 std::fs::write(&file, "").unwrap();
2230
2231 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
2232 assert_eq!(root, dir);
2233 }
2234
2235 #[test]
2236 fn test_detect_workspace_root_empty_markers_returns_parent() {
2237 let tmp = tempfile::tempdir().unwrap();
2238 let dir = tmp.path().join("somedir");
2239 std::fs::create_dir_all(&dir).unwrap();
2240 let file = dir.join("file.txt");
2241 std::fs::write(&file, "").unwrap();
2242
2243 let root = detect_workspace_root(&file, &[]);
2244 assert_eq!(root, dir);
2245 }
2246
2247 #[test]
2248 fn test_detect_workspace_root_directory_marker() {
2249 let tmp = tempfile::tempdir().unwrap();
2250 let project = tmp.path().join("myproject");
2251 let src = project.join("src");
2252 std::fs::create_dir_all(&src).unwrap();
2253 std::fs::create_dir_all(project.join(".git")).unwrap();
2254 let file = src.join("main.rs");
2255 std::fs::write(&file, "").unwrap();
2256
2257 let root = detect_workspace_root(&file, &[".git".to_string()]);
2258 assert_eq!(root, project);
2259 }
2260
2261 fn c_cpp_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2266 use crate::config::LanguageConfig;
2267 let mut languages = std::collections::HashMap::new();
2268 let base = LanguageConfig {
2269 extensions: vec![],
2270 filenames: vec![],
2271 grammar: String::new(),
2272 comment_prefix: Some("//".to_string()),
2273 auto_indent: true,
2274 auto_close: None,
2275 auto_surround: None,
2276 textmate_grammar: None,
2277 show_whitespace_tabs: false,
2278 line_wrap: None,
2279 wrap_column: None,
2280 page_view: None,
2281 page_width: None,
2282 use_tabs: None,
2283 tab_size: None,
2284 formatter: None,
2285 format_on_save: false,
2286 on_save: vec![],
2287 word_characters: None,
2288 };
2289 languages.insert(
2290 "c".to_string(),
2291 LanguageConfig {
2292 extensions: vec!["c".to_string(), "h".to_string()],
2293 grammar: "c".to_string(),
2294 ..base.clone()
2295 },
2296 );
2297 languages.insert(
2298 "cpp".to_string(),
2299 LanguageConfig {
2300 extensions: vec![
2301 "cpp".to_string(),
2302 "cc".to_string(),
2303 "cxx".to_string(),
2304 "hpp".to_string(),
2305 "hh".to_string(),
2306 "hxx".to_string(),
2307 ],
2308 grammar: "cpp".to_string(),
2309 ..base
2310 },
2311 );
2312 languages
2313 }
2314
2315 #[test]
2316 fn test_detect_language_h_stays_c_without_cpp_signals() {
2317 let languages = c_cpp_languages();
2321 assert_eq!(
2322 detect_language(Path::new("foo.h"), &languages),
2323 Some("c".to_string())
2324 );
2325 }
2326
2327 #[test]
2328 fn test_detect_language_h_promotes_to_cpp_with_sibling_cpp_source() {
2329 let tmp = tempfile::tempdir().unwrap();
2330 let project = tmp.path().join("proj");
2331 std::fs::create_dir_all(&project).unwrap();
2332 let header = project.join("widget.h");
2333 std::fs::write(&header, "").unwrap();
2334 std::fs::write(project.join("widget.cpp"), "").unwrap();
2336
2337 let languages = c_cpp_languages();
2338 assert_eq!(
2339 detect_language(&header, &languages),
2340 Some("cpp".to_string())
2341 );
2342 }
2343
2344 #[test]
2345 fn test_detect_language_h_promotes_to_cpp_with_sibling_hpp() {
2346 let tmp = tempfile::tempdir().unwrap();
2347 let project = tmp.path().join("proj");
2348 std::fs::create_dir_all(&project).unwrap();
2349 let header = project.join("a.h");
2350 std::fs::write(&header, "").unwrap();
2351 std::fs::write(project.join("b.hpp"), "").unwrap();
2353
2354 let languages = c_cpp_languages();
2355 assert_eq!(
2356 detect_language(&header, &languages),
2357 Some("cpp".to_string())
2358 );
2359 }
2360
2361 #[test]
2362 fn test_detect_language_h_promotes_to_cpp_with_ancestor_compile_commands() {
2363 let tmp = tempfile::tempdir().unwrap();
2364 let project = tmp.path().join("proj");
2365 let include = project.join("include").join("fmt");
2366 std::fs::create_dir_all(&include).unwrap();
2367 std::fs::write(
2371 project.join("compile_commands.json"),
2372 r#"[{"directory":"/proj","command":"/usr/bin/clang++ -std=c++17 -c src/format.cc","file":"src/format.cc"}]"#,
2373 ).unwrap();
2374 let header = include.join("format.h");
2375 std::fs::write(&header, "").unwrap();
2376
2377 let languages = c_cpp_languages();
2378 assert_eq!(
2379 detect_language(&header, &languages),
2380 Some("cpp".to_string())
2381 );
2382 }
2383
2384 #[test]
2385 fn test_detect_language_h_stays_c_with_pure_c_compile_commands() {
2386 let tmp = tempfile::tempdir().unwrap();
2389 let project = tmp.path().join("cproj");
2390 let include = project.join("include");
2391 std::fs::create_dir_all(&include).unwrap();
2392 std::fs::write(
2393 project.join("compile_commands.json"),
2394 r#"[{"directory":"/cproj","command":"/usr/bin/gcc -std=c11 -c src/lib.c","file":"src/lib.c"}]"#,
2395 )
2396 .unwrap();
2397 let header = include.join("lib.h");
2398 std::fs::write(&header, "").unwrap();
2399
2400 let languages = c_cpp_languages();
2401 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2402 }
2403
2404 #[test]
2405 fn test_detect_language_h_stays_c_in_pure_c_tree() {
2406 let tmp = tempfile::tempdir().unwrap();
2407 let project = tmp.path().join("cproj");
2408 std::fs::create_dir_all(&project).unwrap();
2409 let header = project.join("lib.h");
2410 std::fs::write(&header, "").unwrap();
2411 std::fs::write(project.join("lib.c"), "").unwrap();
2413
2414 let languages = c_cpp_languages();
2415 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2416 }
2417
2418 #[test]
2419 fn test_detect_language_h_stays_c_with_empty_compile_commands() {
2420 let tmp = tempfile::tempdir().unwrap();
2423 let project = tmp.path().join("proj");
2424 std::fs::create_dir_all(&project).unwrap();
2425 std::fs::write(project.join("compile_commands.json"), "[]").unwrap();
2426 let header = project.join("foo.h");
2427 std::fs::write(&header, "").unwrap();
2428
2429 let languages = c_cpp_languages();
2430 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2431 }
2432
2433 #[test]
2434 fn test_detect_language_h_promotes_on_cpp_std_flag_alone() {
2435 let tmp = tempfile::tempdir().unwrap();
2437 let project = tmp.path().join("proj");
2438 let include = project.join("include");
2439 std::fs::create_dir_all(&include).unwrap();
2440 std::fs::write(
2441 project.join("compile_commands.json"),
2442 r#"[{"directory":"/proj","command":"/usr/bin/clang -std=c++20 -c src/x.C","file":"src/x.C"}]"#,
2446 )
2447 .unwrap();
2448 let header = include.join("x.h");
2449 std::fs::write(&header, "").unwrap();
2450
2451 let languages = c_cpp_languages();
2452 assert_eq!(
2453 detect_language(&header, &languages),
2454 Some("cpp".to_string())
2455 );
2456 }
2457
2458 #[test]
2459 fn test_detect_language_c_source_never_promoted() {
2460 let tmp = tempfile::tempdir().unwrap();
2462 let project = tmp.path().join("proj");
2463 std::fs::create_dir_all(&project).unwrap();
2464 let source = project.join("legacy.c");
2465 std::fs::write(&source, "").unwrap();
2466 std::fs::write(project.join("main.cpp"), "").unwrap();
2467
2468 let languages = c_cpp_languages();
2469 assert_eq!(detect_language(&source, &languages), Some("c".to_string()));
2470 }
2471
2472 #[test]
2473 fn test_detect_language_h_no_promotion_without_cpp_config() {
2474 let tmp = tempfile::tempdir().unwrap();
2477 let project = tmp.path().join("proj");
2478 std::fs::create_dir_all(&project).unwrap();
2479 let header = project.join("widget.h");
2480 std::fs::write(&header, "").unwrap();
2481 std::fs::write(project.join("widget.cpp"), "").unwrap();
2482
2483 let mut languages = c_cpp_languages();
2484 languages.remove("cpp");
2485 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2486 }
2487
2488 #[test]
2489 fn test_path_to_uri_basic() {
2490 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
2491 assert_eq!(uri.as_str(), "file:///tmp/test");
2492 }
2493
2494 #[test]
2495 fn test_path_to_uri_with_spaces() {
2496 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
2497 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
2498 }
2499}