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 workspace_trust: Option<std::sync::Arc<crate::services::workspace_trust::WorkspaceTrust>>,
316
317 path_translation: Option<crate::services::authority::PathTranslation>,
322
323 restart_attempts: HashMap<String, Vec<Instant>>,
325
326 restart_cooldown: HashSet<String>,
328
329 pending_restarts: HashMap<String, Instant>,
331
332 allowed_languages: HashSet<String>,
335
336 disabled_languages: HashSet<String>,
339}
340
341impl LspManager {
342 pub fn window_id(&self) -> fresh_core::WindowId {
344 self.window_id
345 }
346
347 pub fn new(window_id: fresh_core::WindowId, root_uri: Option<Uri>) -> Self {
349 Self {
350 window_id,
351 handles: Vec::new(),
352 config: HashMap::new(),
353 universal_configs: Vec::new(),
354 root_uri,
355 per_language_root_uris: HashMap::new(),
356 runtime: None,
357 async_bridge: None,
358 long_running_spawner: None,
359 workspace_trust: None,
360 path_translation: None,
361 restart_attempts: HashMap::new(),
362 restart_cooldown: HashSet::new(),
363 pending_restarts: HashMap::new(),
364 allowed_languages: HashSet::new(),
365 disabled_languages: HashSet::new(),
366 }
367 }
368
369 pub fn set_long_running_spawner(
378 &mut self,
379 spawner: std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>,
380 ) {
381 self.long_running_spawner = Some(spawner);
382 }
383
384 pub fn set_workspace_trust(
388 &mut self,
389 trust: std::sync::Arc<crate::services::workspace_trust::WorkspaceTrust>,
390 ) {
391 self.workspace_trust = Some(trust);
392 }
393
394 fn lsp_autostart_allowed(&self) -> bool {
398 use crate::services::workspace_trust::TrustLevel;
399 self.workspace_trust
400 .as_ref()
401 .map(|t| t.level() == TrustLevel::Trusted)
402 .unwrap_or(true)
403 }
404
405 pub fn set_path_translation(
410 &mut self,
411 translation: Option<crate::services::authority::PathTranslation>,
412 ) {
413 self.path_translation = translation;
414 }
415
416 pub fn command_exists_via_authority(&self, command: &str) -> bool {
430 if command.is_empty() {
431 return false;
432 }
433 let (Some(runtime), Some(spawner)) =
434 (self.runtime.as_ref(), self.long_running_spawner.as_ref())
435 else {
436 return crate::services::lsp::command_exists(command);
437 };
438 runtime.block_on(spawner.command_exists(command))
439 }
440
441 pub fn is_language_allowed(&self, language: &str) -> bool {
443 self.allowed_languages.contains(language)
444 }
445
446 pub fn allow_language(&mut self, language: &str) {
448 self.allowed_languages.insert(language.to_string());
449 tracing::info!("LSP language '{}' manually enabled", language);
450 }
451
452 pub fn allowed_languages(&self) -> &HashSet<String> {
454 &self.allowed_languages
455 }
456
457 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
459 self.config.get(language).map(|v| v.as_slice())
460 }
461
462 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
464 self.config.get(language).and_then(|v| v.first())
465 }
466
467 pub fn set_server_capabilities(
469 &mut self,
470 _language: &str,
471 server_name: &str,
472 mut capabilities: ServerCapabilitySummary,
473 ) {
474 capabilities.initialized = true;
475
476 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
477 sh.capabilities = capabilities;
478 }
479 }
480
481 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
483 self.get_handles(language).into_iter().find_map(|sh| {
484 if sh.feature_filter.allows(LspFeature::SemanticTokens)
485 && sh.has_capability(LspFeature::SemanticTokens)
486 {
487 sh.capabilities.semantic_tokens_legend.as_ref()
488 } else {
489 None
490 }
491 })
492 }
493
494 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
496 self.get_handles(language).iter().any(|sh| {
497 sh.feature_filter.allows(LspFeature::SemanticTokens)
498 && sh.capabilities.semantic_tokens_full
499 })
500 }
501
502 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
504 self.get_handles(language).iter().any(|sh| {
505 sh.feature_filter.allows(LspFeature::SemanticTokens)
506 && sh.capabilities.semantic_tokens_full_delta
507 })
508 }
509
510 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
512 self.get_handles(language).iter().any(|sh| {
513 sh.feature_filter.allows(LspFeature::SemanticTokens)
514 && sh.capabilities.semantic_tokens_range
515 })
516 }
517
518 pub fn folding_ranges_supported(&self, language: &str) -> bool {
520 self.get_handles(language).iter().any(|sh| {
521 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
522 })
523 }
524
525 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
527 let ch_str = ch.to_string();
528 self.get_handles(language).iter().any(|sh| {
529 sh.feature_filter.allows(LspFeature::Completion)
530 && sh
531 .capabilities
532 .completion_trigger_characters
533 .contains(&ch_str)
534 })
535 }
536
537 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
552 if self
554 .handles
555 .iter()
556 .any(|sh| sh.handle.scope().accepts(language))
557 {
558 self.ensure_universal_servers_running(file_path);
559 return LspSpawnResult::Spawned;
560 }
561
562 if self.runtime.is_none() || self.async_bridge.is_none() {
564 return LspSpawnResult::Failed;
565 }
566
567 if !self.lsp_autostart_allowed() && !self.allowed_languages.contains(language) {
574 tracing::info!(
575 "LSP for '{}' not auto-started: workspace is not trusted \
576 (trust the folder to enable language servers)",
577 language
578 );
579 return LspSpawnResult::NotAutoStart;
580 }
581
582 self.ensure_universal_servers_running(file_path);
584
585 let configs = match self.config.get(language) {
587 Some(configs) if !configs.is_empty() => configs,
588 _ => {
589 if self
591 .handles
592 .iter()
593 .any(|sh| sh.handle.scope().is_universal())
594 {
595 return LspSpawnResult::Spawned;
596 }
597 return LspSpawnResult::NotConfigured;
598 }
599 };
600
601 if !configs.iter().any(|c| c.enabled) {
603 if self
604 .handles
605 .iter()
606 .any(|sh| sh.handle.scope().is_universal())
607 {
608 return LspSpawnResult::Spawned;
609 }
610 return LspSpawnResult::Disabled;
611 }
612
613 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
615 if !any_auto_start && !self.allowed_languages.contains(language) {
616 if self
617 .handles
618 .iter()
619 .any(|sh| sh.handle.scope().is_universal())
620 {
621 return LspSpawnResult::Spawned;
622 }
623 return LspSpawnResult::NotAutoStart;
624 }
625
626 let spawned = self.force_spawn(language, file_path).is_some();
628
629 if spawned
630 || self
631 .handles
632 .iter()
633 .any(|sh| sh.handle.scope().is_universal())
634 {
635 LspSpawnResult::Spawned
636 } else {
637 LspSpawnResult::Failed
638 }
639 }
640
641 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
645 self.runtime = Some(runtime);
646 self.async_bridge = Some(async_bridge);
647 }
648
649 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
651 self.config.insert(language, vec![config]);
652 }
653
654 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
656 self.config.insert(language, configs);
657 }
658
659 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
661 self.config.entry(language).or_default().extend(configs);
662 }
663
664 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
669 self.universal_configs = configs;
670 }
671
672 pub fn configured_languages(&self) -> Vec<String> {
674 self.config.keys().cloned().collect()
675 }
676
677 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
682 self.root_uri = root_uri;
683 }
684
685 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
691 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
692 self.per_language_root_uris
693 .insert(language.to_string(), uri.clone());
694
695 if self
697 .handles
698 .iter()
699 .any(|sh| sh.handle.scope().accepts(language))
700 {
701 tracing::info!(
702 "Restarting {} LSP server with new root: {}",
703 language,
704 uri.as_str()
705 );
706 self.shutdown_server(language);
707 return true;
709 }
710 false
711 }
712
713 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
720 if let Some(uri) = self.per_language_root_uris.get(language) {
722 return Some(uri.clone());
723 }
724
725 if let Some(path) = file_path {
730 let markers = self
731 .config
732 .get(language)
733 .and_then(|configs| configs.first())
734 .map(|c| c.root_markers.as_slice())
735 .unwrap_or(&[]);
736 let root = detect_workspace_root(path, markers);
737 let mapped = self
738 .path_translation
739 .as_ref()
740 .and_then(|t| t.host_to_remote(&root))
741 .unwrap_or(root);
742 if let Some(uri) = path_to_uri(&mapped) {
743 return Some(uri);
744 }
745 }
746
747 self.root_uri.clone()
749 }
750
751 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
755 self.resolve_root_uri(language, None)
756 }
757
758 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
763 self.shutdown_all();
765
766 self.root_uri = new_root_uri;
768
769 self.restart_attempts.clear();
771 self.restart_cooldown.clear();
772 self.pending_restarts.clear();
773
774 tracing::info!(
778 "LSP manager reset for new project: {:?}",
779 self.root_uri.as_ref().map(|u| u.as_str())
780 );
781 }
782
783 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
786 self.handles
787 .iter()
788 .find(|sh| sh.handle.scope().accepts(language))
789 .map(|sh| &sh.handle)
790 }
791
792 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
795 self.handles
796 .iter_mut()
797 .find(|sh| sh.handle.scope().accepts(language))
798 .map(|sh| &mut sh.handle)
799 }
800
801 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
803 self.handles
804 .iter()
805 .filter(|sh| sh.handle.scope().accepts(language))
806 .collect()
807 }
808
809 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
811 self.handles
812 .iter_mut()
813 .filter(|sh| sh.handle.scope().accepts(language))
814 .collect()
815 }
816
817 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
821 self.handles
822 .iter()
823 .find(|sh| sh.name == server_name)
824 .map(|sh| sh.handle.scope())
825 }
826
827 pub fn has_handles(&self, language: &str) -> bool {
829 self.handles
830 .iter()
831 .any(|sh| sh.handle.scope().accepts(language))
832 }
833
834 pub fn handle_count(&self, language: &str) -> usize {
836 self.handles
837 .iter()
838 .filter(|sh| sh.handle.scope().accepts(language))
839 .count()
840 }
841
842 pub fn has_server_named(&self, server_name: &str) -> bool {
844 self.handles.iter().any(|sh| sh.name == server_name)
845 }
846
847 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
853 self.handles
854 .iter()
855 .filter(|sh| sh.handle.scope().accepts(language))
856 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
857 }
858
859 pub fn handle_for_feature_mut(
863 &mut self,
864 language: &str,
865 feature: LspFeature,
866 ) -> Option<&mut ServerHandle> {
867 self.handles
868 .iter_mut()
869 .filter(|sh| sh.handle.scope().accepts(language))
870 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
871 }
872
873 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
877 self.handles
878 .iter()
879 .filter(|sh| sh.handle.scope().accepts(language))
880 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
881 .collect()
882 }
883
884 pub fn handles_for_feature_mut(
888 &mut self,
889 language: &str,
890 feature: LspFeature,
891 ) -> Vec<&mut ServerHandle> {
892 self.handles
893 .iter_mut()
894 .filter(|sh| sh.handle.scope().accepts(language))
895 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
896 .collect()
897 }
898
899 fn spawn_decision(&mut self, language: &str) -> SpawnDecision {
907 if self
908 .handles
909 .iter()
910 .any(|sh| sh.handle.scope().accepts(language))
911 {
912 return SpawnDecision::Existing;
913 }
914 if self.restart_cooldown.contains(language) {
915 return SpawnDecision::CooledDown;
916 }
917 if self.pending_restarts.contains_key(language) {
918 return SpawnDecision::PendingBackoff;
919 }
920
921 let now = Instant::now();
922 let window = Duration::from_secs(RESTART_WINDOW_SECS);
923 let attempts = self
924 .restart_attempts
925 .entry(language.to_string())
926 .or_default();
927 attempts.retain(|t| now.duration_since(*t) < window);
928
929 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
930 self.restart_cooldown.insert(language.to_string());
931 tracing::warn!(
932 "LSP server for {} has spawned {} times in {} minutes, entering cooldown",
933 language,
934 MAX_RESTARTS_IN_WINDOW,
935 RESTART_WINDOW_SECS / 60
936 );
937 return SpawnDecision::CooledDown;
938 }
939
940 attempts.push(now);
941 SpawnDecision::Allow
942 }
943
944 pub fn force_spawn(
963 &mut self,
964 language: &str,
965 file_path: Option<&Path>,
966 ) -> Option<&mut LspHandle> {
967 tracing::debug!("force_spawn called for language: {}", language);
968
969 if self
971 .handles
972 .iter()
973 .any(|sh| sh.handle.scope().accepts(language))
974 {
975 tracing::debug!("force_spawn: returning existing handle for {}", language);
976 return self
977 .handles
978 .iter_mut()
979 .find(|sh| sh.handle.scope().accepts(language))
980 .map(|sh| &mut sh.handle);
981 }
982
983 if self.disabled_languages.contains(language) {
985 tracing::debug!(
986 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
987 language
988 );
989 return None;
990 }
991
992 let configs = match self.config.get(language) {
994 Some(configs) if !configs.is_empty() => configs.clone(),
995 _ => {
996 tracing::warn!(
997 "force_spawn: no config found for language '{}', available configs: {:?}",
998 language,
999 self.config.keys().collect::<Vec<_>>()
1000 );
1001 return None;
1002 }
1003 };
1004
1005 match self.spawn_decision(language) {
1011 SpawnDecision::Existing => {
1012 return self
1015 .handles
1016 .iter_mut()
1017 .find(|sh| sh.handle.scope().accepts(language))
1018 .map(|sh| &mut sh.handle);
1019 }
1020 SpawnDecision::CooledDown => {
1021 tracing::debug!(
1022 "force_spawn: {} is in cooldown, refusing spawn (use Restart LSP command)",
1023 language
1024 );
1025 return None;
1026 }
1027 SpawnDecision::PendingBackoff => {
1028 tracing::debug!(
1029 "force_spawn: {} has a pending restart scheduled, not double-spawning",
1030 language
1031 );
1032 return None;
1033 }
1034 SpawnDecision::Allow => {}
1035 }
1036
1037 let runtime = match self.runtime.as_ref() {
1042 Some(r) => r.clone(),
1043 None => {
1044 tracing::error!("force_spawn: no tokio runtime available for {}", language);
1045 return None;
1046 }
1047 };
1048 let async_bridge = match self.async_bridge.as_ref() {
1049 Some(b) => b.clone(),
1050 None => {
1051 tracing::error!("force_spawn: no async bridge available for {}", language);
1052 return None;
1053 }
1054 };
1055 let long_running_spawner = match self.long_running_spawner.as_ref() {
1063 Some(s) => s.clone(),
1064 None => {
1065 tracing::warn!(
1066 "force_spawn: long-running spawner not wired for {} — \
1067 falling back to host-local spawn (normal for tests \
1068 that skip set_boot_authority)",
1069 language
1070 );
1071 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1072 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1073 std::sync::Arc::new(
1074 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1075 ),
1076 ))
1077 }
1078 };
1079
1080 let mut spawned_handles = Vec::new();
1081 let manually_allowed = self.allowed_languages.contains(language);
1082
1083 for config in &configs {
1084 if manually_allowed {
1085 } else {
1089 if !config.enabled || !config.auto_start {
1095 continue;
1096 }
1097 }
1098
1099 if config.command.is_empty() {
1100 tracing::warn!(
1101 "force_spawn: LSP command is empty for {} server '{}'",
1102 language,
1103 config.display_name()
1104 );
1105 continue;
1106 }
1107
1108 let server_name = config.display_name();
1109 tracing::info!(
1110 "Spawning LSP server '{}' for language: {}",
1111 server_name,
1112 language
1113 );
1114
1115 match LspHandle::spawn(
1116 &runtime,
1117 &config.command,
1118 &config.args,
1119 config.env.clone(),
1120 LanguageScope::single(language),
1121 server_name.clone(),
1122 &async_bridge,
1123 config.process_limits.clone(),
1124 config.language_id_overrides.clone(),
1125 long_running_spawner.clone(),
1126 ) {
1127 Ok(handle) => {
1128 let effective_root = self.resolve_root_uri(language, file_path);
1129 if let Err(e) =
1130 handle.initialize(effective_root, config.initialization_options.clone())
1131 {
1132 tracing::error!(
1133 "Failed to send initialize command for {} ({}): {}",
1134 language,
1135 server_name,
1136 e
1137 );
1138 continue;
1139 }
1140
1141 tracing::info!(
1142 "LSP initialization started for {} ({}), will be ready asynchronously",
1143 language,
1144 server_name
1145 );
1146
1147 spawned_handles.push(ServerHandle {
1148 name: server_name,
1149 handle,
1150 feature_filter: config.feature_filter(),
1151 capabilities: ServerCapabilitySummary::default(),
1152 });
1153 }
1154 Err(e) => {
1155 tracing::error!(
1156 "Failed to spawn LSP handle for {} ({}): {}",
1157 language,
1158 server_name,
1159 e
1160 );
1161 }
1162 }
1163 }
1164
1165 if spawned_handles.is_empty() {
1166 return None;
1167 }
1168
1169 self.handles.extend(spawned_handles);
1170 self.handles
1171 .iter_mut()
1172 .rev()
1173 .find(|sh| sh.handle.scope().accepts(language))
1174 .map(|sh| &mut sh.handle)
1175 }
1176
1177 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
1183 if self
1184 .handles
1185 .iter()
1186 .any(|sh| sh.handle.scope().is_universal())
1187 || self.universal_configs.is_empty()
1188 {
1189 return;
1190 }
1191
1192 let runtime = match self.runtime.as_ref() {
1193 Some(r) => r.clone(),
1194 None => return,
1195 };
1196 let async_bridge = match self.async_bridge.as_ref() {
1197 Some(b) => b.clone(),
1198 None => return,
1199 };
1200 let long_running_spawner =
1201 self.long_running_spawner
1202 .as_ref()
1203 .cloned()
1204 .unwrap_or_else(|| {
1205 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1206 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1207 std::sync::Arc::new(
1208 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1209 ),
1210 ))
1211 });
1212
1213 let mut spawned = Vec::new();
1214 for config in &self.universal_configs {
1215 if !config.enabled || !config.auto_start {
1216 continue;
1217 }
1218 if config.command.is_empty() {
1219 continue;
1220 }
1221
1222 let server_name = config.display_name();
1223 tracing::info!("Spawning universal LSP server '{}'", server_name);
1224
1225 match LspHandle::spawn(
1226 &runtime,
1227 &config.command,
1228 &config.args,
1229 config.env.clone(),
1230 LanguageScope::all(),
1231 server_name.clone(),
1232 &async_bridge,
1233 config.process_limits.clone(),
1234 config.language_id_overrides.clone(),
1235 long_running_spawner.clone(),
1236 ) {
1237 Ok(handle) => {
1238 let effective_root = file_path
1239 .and_then(|p| {
1240 let root = detect_workspace_root(p, &config.root_markers);
1241 path_to_uri(&root)
1242 })
1243 .or_else(|| self.root_uri.clone());
1244 if let Err(e) =
1245 handle.initialize(effective_root, config.initialization_options.clone())
1246 {
1247 tracing::error!(
1248 "Failed to initialize universal LSP server '{}': {}",
1249 server_name,
1250 e
1251 );
1252 continue;
1253 }
1254 tracing::info!(
1255 "Universal LSP server '{}' initialization started",
1256 server_name
1257 );
1258 spawned.push(ServerHandle {
1259 name: server_name,
1260 handle,
1261 feature_filter: config.feature_filter(),
1262 capabilities: ServerCapabilitySummary::default(),
1263 });
1264 }
1265 Err(e) => {
1266 tracing::error!(
1267 "Failed to spawn universal LSP server '{}': {}",
1268 server_name,
1269 e
1270 );
1271 }
1272 }
1273 }
1274
1275 self.handles.extend(spawned);
1276 }
1277
1278 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1282 if self
1284 .handles
1285 .iter()
1286 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1287 {
1288 let universals: Vec<ServerHandle> = {
1290 let mut drained = Vec::new();
1291 let mut i = 0;
1292 while i < self.handles.len() {
1293 if self.handles[i].handle.scope().is_universal() {
1294 drained.push(self.handles.remove(i));
1295 } else {
1296 i += 1;
1297 }
1298 }
1299 drained
1300 };
1301 for sh in universals {
1302 fire_and_forget(sh.handle.shutdown());
1303 }
1304 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1306 }
1307
1308 {
1310 let mut i = 0;
1311 while i < self.handles.len() {
1312 if !self.handles[i].handle.scope().is_universal()
1313 && self.handles[i].handle.scope().accepts(language)
1314 {
1315 let sh = self.handles.remove(i);
1316 fire_and_forget(sh.handle.shutdown());
1317 } else {
1318 i += 1;
1319 }
1320 }
1321 }
1322
1323 if self.disabled_languages.contains(language) {
1326 return format!(
1327 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1328 language
1329 );
1330 }
1331
1332 if self.restart_cooldown.contains(language) {
1334 return format!(
1335 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1336 language
1337 );
1338 }
1339
1340 let now = Instant::now();
1345 let attempt_number = self
1346 .restart_attempts
1347 .get(language)
1348 .map(|v| v.len())
1349 .unwrap_or(0);
1350
1351 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1353
1354 self.pending_restarts
1355 .insert(language.to_string(), restart_time);
1356
1357 tracing::info!(
1358 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1359 language,
1360 attempt_number + 1,
1361 MAX_RESTARTS_IN_WINDOW,
1362 delay_ms
1363 );
1364
1365 format!(
1366 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1367 language,
1368 attempt_number + 1,
1369 MAX_RESTARTS_IN_WINDOW,
1370 delay_ms / 1000
1371 )
1372 }
1373
1374 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1378 let now = Instant::now();
1379 let mut results = Vec::new();
1380
1381 let due_restarts: Vec<String> = self
1383 .pending_restarts
1384 .iter()
1385 .filter(|(_, time)| **time <= now)
1386 .map(|(lang, _)| lang.clone())
1387 .collect();
1388
1389 for language in due_restarts {
1390 self.pending_restarts.remove(&language);
1391
1392 if self.force_spawn(&language, None).is_some() {
1396 let message = format!("LSP server for {} restarted successfully", language);
1397 tracing::info!("{}", message);
1398 results.push((language, true, message));
1399 } else {
1400 let message = format!("Failed to restart LSP server for {}", language);
1401 tracing::error!("{}", message);
1402 results.push((language, false, message));
1403 }
1404 }
1405
1406 results
1407 }
1408
1409 pub fn is_in_cooldown(&self, language: &str) -> bool {
1411 self.restart_cooldown.contains(language)
1412 }
1413
1414 pub fn has_pending_restart(&self, language: &str) -> bool {
1416 self.pending_restarts.contains_key(language)
1417 }
1418
1419 pub fn clear_cooldown(&mut self, language: &str) {
1421 self.restart_cooldown.remove(language);
1422 self.restart_attempts.remove(language);
1423 self.pending_restarts.remove(language);
1424 tracing::info!("Cleared restart cooldown for {}", language);
1425 }
1426
1427 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1434 self.clear_cooldown(language);
1436
1437 self.disabled_languages.remove(language);
1439
1440 self.allowed_languages.insert(language.to_string());
1442
1443 {
1445 let mut i = 0;
1446 while i < self.handles.len() {
1447 if !self.handles[i].handle.scope().is_universal()
1448 && self.handles[i].handle.scope().accepts(language)
1449 {
1450 let sh = self.handles.remove(i);
1451 fire_and_forget(sh.handle.shutdown());
1452 } else {
1453 i += 1;
1454 }
1455 }
1456 }
1457
1458 if self.force_spawn(language, file_path).is_some() {
1460 let message = format!("LSP server for {} started", language);
1461 tracing::info!("{}", message);
1462 (true, message)
1463 } else {
1464 let message = format!("Failed to start LSP server for {}", language);
1465 tracing::error!("{}", message);
1466 (false, message)
1467 }
1468 }
1469
1470 pub fn manual_restart_server(
1475 &mut self,
1476 language: &str,
1477 server_name: &str,
1478 file_path: Option<&Path>,
1479 ) -> (bool, String) {
1480 self.clear_cooldown(language);
1481 self.disabled_languages.remove(language);
1482 self.allowed_languages.insert(language.to_string());
1483
1484 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1486 let sh = self.handles.remove(idx);
1487 fire_and_forget(sh.handle.shutdown());
1488 }
1489
1490 let is_universal = self
1492 .universal_configs
1493 .iter()
1494 .any(|c| c.display_name() == server_name);
1495 let config = if is_universal {
1496 self.universal_configs
1497 .iter()
1498 .find(|c| c.display_name() == server_name)
1499 .cloned()
1500 } else {
1501 self.config
1502 .get(language)
1503 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1504 .cloned()
1505 };
1506
1507 let Some(config) = config else {
1508 let message = format!(
1509 "No config found for server '{}' ({})",
1510 server_name, language
1511 );
1512 tracing::error!("{}", message);
1513 return (false, message);
1514 };
1515
1516 if config.command.is_empty() {
1517 let message = format!(
1518 "LSP command is empty for {} server '{}'",
1519 language, server_name
1520 );
1521 tracing::error!("{}", message);
1522 return (false, message);
1523 }
1524
1525 let runtime = match self.runtime.as_ref() {
1526 Some(r) => r.clone(),
1527 None => return (false, "No tokio runtime available".to_string()),
1528 };
1529 let async_bridge = match self.async_bridge.as_ref() {
1530 Some(b) => b.clone(),
1531 None => return (false, "No async bridge available".to_string()),
1532 };
1533 let long_running_spawner =
1534 self.long_running_spawner
1535 .as_ref()
1536 .cloned()
1537 .unwrap_or_else(|| {
1538 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1539 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1540 std::sync::Arc::new(
1541 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1542 ),
1543 ))
1544 });
1545
1546 let scope = if is_universal {
1547 LanguageScope::all()
1548 } else {
1549 LanguageScope::single(language)
1550 };
1551
1552 match LspHandle::spawn(
1553 &runtime,
1554 &config.command,
1555 &config.args,
1556 config.env.clone(),
1557 scope,
1558 server_name.to_string(),
1559 &async_bridge,
1560 config.process_limits.clone(),
1561 config.language_id_overrides.clone(),
1562 long_running_spawner,
1563 ) {
1564 Ok(handle) => {
1565 let effective_root = if is_universal {
1566 file_path
1567 .and_then(|p| {
1568 let root = detect_workspace_root(p, &config.root_markers);
1569 path_to_uri(&root)
1570 })
1571 .or_else(|| self.root_uri.clone())
1572 } else {
1573 self.resolve_root_uri(language, file_path)
1574 };
1575 if let Err(e) =
1576 handle.initialize(effective_root, config.initialization_options.clone())
1577 {
1578 let message = format!(
1579 "Failed to initialize LSP server '{}' for {}: {}",
1580 server_name, language, e
1581 );
1582 tracing::error!("{}", message);
1583 return (false, message);
1584 }
1585
1586 let sh = ServerHandle {
1587 name: server_name.to_string(),
1588 handle,
1589 feature_filter: config.feature_filter(),
1590 capabilities: ServerCapabilitySummary::default(),
1591 };
1592
1593 self.handles.push(sh);
1594
1595 let message = format!("LSP server '{}' for {} started", server_name, language);
1596 tracing::info!("{}", message);
1597 (true, message)
1598 }
1599 Err(e) => {
1600 let message = format!(
1601 "Failed to start LSP server '{}' for {}: {}",
1602 server_name, language, e
1603 );
1604 tracing::error!("{}", message);
1605 (false, message)
1606 }
1607 }
1608 }
1609
1610 pub fn restart_attempt_count(&self, language: &str) -> usize {
1612 let now = Instant::now();
1613 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1614 self.restart_attempts
1615 .get(language)
1616 .map(|attempts| {
1617 attempts
1618 .iter()
1619 .filter(|t| now.duration_since(**t) < window)
1620 .count()
1621 })
1622 .unwrap_or(0)
1623 }
1624
1625 pub fn running_servers(&self) -> Vec<String> {
1627 let mut labels: Vec<String> = self
1628 .handles
1629 .iter()
1630 .map(|sh| sh.handle.scope().label().to_string())
1631 .collect();
1632 labels.sort();
1633 labels.dedup();
1634 labels
1635 }
1636
1637 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1639 self.handles
1640 .iter()
1641 .filter(|sh| sh.handle.scope().accepts(language))
1642 .map(|sh| sh.name.clone())
1643 .collect()
1644 }
1645
1646 pub fn is_server_ready(&self, language: &str) -> bool {
1648 self.handles
1649 .iter()
1650 .filter(|sh| sh.handle.scope().accepts(language))
1651 .any(|sh| sh.handle.state().can_send_requests())
1652 }
1653
1654 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1659 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1660 tracing::warn!(
1661 "No running LSP server named '{}' found for {}",
1662 server_name,
1663 language
1664 );
1665 return false;
1666 };
1667
1668 let sh = self.handles.remove(idx);
1669 tracing::info!(
1670 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1671 sh.name,
1672 language
1673 );
1674 fire_and_forget(sh.handle.shutdown());
1675
1676 let has_remaining = self
1678 .handles
1679 .iter()
1680 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1681 if !has_remaining {
1682 self.disabled_languages.insert(language.to_string());
1683 self.pending_restarts.remove(language);
1684 self.restart_cooldown.remove(language);
1685 self.allowed_languages.remove(language);
1686 }
1687
1688 true
1689 }
1690
1691 pub fn shutdown_server(&mut self, language: &str) -> bool {
1696 let mut found = false;
1697 let mut i = 0;
1698 while i < self.handles.len() {
1699 if !self.handles[i].handle.scope().is_universal()
1700 && self.handles[i].handle.scope().accepts(language)
1701 {
1702 let sh = self.handles.remove(i);
1703 tracing::info!(
1704 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1705 sh.name,
1706 language
1707 );
1708 fire_and_forget(sh.handle.shutdown());
1709 found = true;
1710 } else {
1711 i += 1;
1712 }
1713 }
1714
1715 if found {
1716 self.disabled_languages.insert(language.to_string());
1717 self.pending_restarts.remove(language);
1718 self.restart_cooldown.remove(language);
1719 self.allowed_languages.remove(language);
1720 } else {
1721 tracing::warn!("No running LSP server found for {}", language);
1722 }
1723
1724 found
1725 }
1726
1727 pub fn shutdown_all(&mut self) {
1729 for sh in &self.handles {
1730 tracing::info!(
1731 "Shutting down LSP server '{}' ({})",
1732 sh.name,
1733 sh.handle.scope().label()
1734 );
1735 fire_and_forget(sh.handle.shutdown());
1736 }
1737 self.handles.clear();
1738 }
1739}
1740
1741impl Drop for LspManager {
1742 fn drop(&mut self) {
1743 self.shutdown_all();
1744 }
1745}
1746
1747pub fn detect_language(
1759 path: &std::path::Path,
1760 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1761) -> Option<String> {
1762 let detected = detect_language_by_config(path, languages);
1763
1764 if detected.as_deref() == Some("c")
1770 && path.extension().and_then(|e| e.to_str()) == Some("h")
1771 && languages.contains_key("cpp")
1772 && header_in_cpp_tree(path)
1773 {
1774 return Some("cpp".to_string());
1775 }
1776
1777 detected
1778}
1779
1780fn detect_language_by_config(
1782 path: &std::path::Path,
1783 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1784) -> Option<String> {
1785 use crate::primitives::glob_match::{
1786 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1787 };
1788
1789 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1790 for (language_name, lang_config) in languages {
1792 if lang_config
1793 .filenames
1794 .iter()
1795 .any(|f| !is_glob_pattern(f) && f == filename)
1796 {
1797 return Some(language_name.clone());
1798 }
1799 }
1800
1801 let path_str = path.to_str().unwrap_or("");
1805 for (language_name, lang_config) in languages {
1806 if lang_config.filenames.iter().any(|f| {
1807 if !is_glob_pattern(f) {
1808 return false;
1809 }
1810 if is_path_pattern(f) {
1811 path_glob_matches(f, path_str)
1812 } else {
1813 filename_glob_matches(f, filename)
1814 }
1815 }) {
1816 return Some(language_name.clone());
1817 }
1818 }
1819 }
1820
1821 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1823 for (language_name, lang_config) in languages {
1824 if lang_config.extensions.iter().any(|ext| ext == extension) {
1825 return Some(language_name.clone());
1826 }
1827 }
1828 }
1829
1830 None
1831}
1832
1833fn header_in_cpp_tree(path: &std::path::Path) -> bool {
1863 let Some(start_dir) = path.parent() else {
1864 return false;
1865 };
1866
1867 if let Ok(entries) = std::fs::read_dir(start_dir) {
1869 for entry in entries.flatten() {
1870 let p = entry.path();
1871 let Some(ext) = p.extension().and_then(|e| e.to_str()) else {
1872 continue;
1873 };
1874 if matches!(
1875 ext,
1876 "cc" | "cpp" | "cxx" | "C" | "c++" | "hpp" | "hh" | "hxx"
1877 ) {
1878 return true;
1879 }
1880 }
1881 }
1882
1883 let mut current = Some(start_dir);
1887 let mut depth = 0u32;
1888 while let Some(dir) = current {
1889 let cc = dir.join("compile_commands.json");
1890 if cc.is_file() && compile_commands_has_cpp_marker(&cc) {
1891 return true;
1892 }
1893 if depth >= 10 {
1894 break;
1895 }
1896 depth += 1;
1897 current = dir.parent();
1898 }
1899
1900 false
1901}
1902
1903fn compile_commands_has_cpp_marker(path: &std::path::Path) -> bool {
1911 use std::io::Read;
1912 const MAX_READ: u64 = 1_048_576;
1913
1914 let Ok(file) = std::fs::File::open(path) else {
1915 return false;
1916 };
1917 let mut buf = Vec::with_capacity(64 * 1024);
1918 if file.take(MAX_READ).read_to_end(&mut buf).is_err() {
1919 return false;
1920 }
1921 let Ok(text) = std::str::from_utf8(&buf) else {
1922 return false;
1923 };
1924
1925 if text.contains("c++") {
1929 return true;
1930 }
1931 text.contains(".cpp") || text.contains(".cxx") || text.contains(".cc\"")
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938 use super::*;
1939 use std::path::Path;
1940
1941 #[test]
1942 fn test_lsp_manager_new() {
1943 let root_uri: Option<Uri> = "file:///test".parse().ok();
1944 let manager = LspManager::new(fresh_core::WindowId(1), root_uri.clone());
1945
1946 assert_eq!(manager.handles.len(), 0);
1948 assert_eq!(manager.config.len(), 0);
1949 assert!(manager.root_uri.is_some());
1950 assert!(manager.runtime.is_none());
1951 assert!(manager.async_bridge.is_none());
1952 }
1953
1954 #[test]
1955 fn test_lsp_manager_set_language_config() {
1956 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
1957
1958 let config = LspServerConfig {
1959 enabled: true,
1960 command: "rust-analyzer".to_string(),
1961 args: vec![],
1962 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1963 auto_start: false,
1964 initialization_options: None,
1965 env: Default::default(),
1966 language_id_overrides: Default::default(),
1967 name: None,
1968 only_features: None,
1969 except_features: None,
1970 root_markers: Default::default(),
1971 };
1972
1973 manager.set_language_config("rust".to_string(), config);
1974
1975 assert_eq!(manager.config.len(), 1);
1976 assert!(manager.config.contains_key("rust"));
1977 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1978 }
1979
1980 #[test]
1981 fn test_lsp_manager_force_spawn_no_runtime() {
1982 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
1983
1984 manager.set_language_config(
1986 "rust".to_string(),
1987 LspServerConfig {
1988 enabled: true,
1989 command: "rust-analyzer".to_string(),
1990 args: vec![],
1991 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1992 auto_start: false,
1993 initialization_options: None,
1994 env: Default::default(),
1995 language_id_overrides: Default::default(),
1996 name: None,
1997 only_features: None,
1998 except_features: None,
1999 root_markers: Default::default(),
2000 },
2001 );
2002
2003 let result = manager.force_spawn("rust", None);
2005 assert!(result.is_none());
2006 }
2007
2008 #[test]
2009 fn test_lsp_manager_force_spawn_no_config() {
2010 let rt = tokio::runtime::Runtime::new().unwrap();
2011 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2012 let async_bridge = AsyncBridge::new();
2013
2014 manager.set_runtime(rt.handle().clone(), async_bridge);
2015
2016 let result = manager.force_spawn("rust", None);
2018 assert!(result.is_none());
2019 }
2020
2021 #[test]
2022 fn test_lsp_manager_force_spawn_disabled_language() {
2023 let rt = tokio::runtime::Runtime::new().unwrap();
2024 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2025 let async_bridge = AsyncBridge::new();
2026
2027 manager.set_runtime(rt.handle().clone(), async_bridge);
2028
2029 manager.set_language_config(
2031 "rust".to_string(),
2032 LspServerConfig {
2033 enabled: false,
2034 command: String::new(), args: vec![],
2036 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2037 auto_start: false,
2038 initialization_options: None,
2039 env: Default::default(),
2040 language_id_overrides: Default::default(),
2041 name: None,
2042 only_features: None,
2043 except_features: None,
2044 root_markers: Default::default(),
2045 },
2046 );
2047
2048 let result = manager.force_spawn("rust", None);
2050 assert!(result.is_none());
2051 }
2052
2053 #[test]
2059 fn test_lsp_manager_try_spawn_returns_disabled_when_all_configs_disabled() {
2060 let rt = tokio::runtime::Runtime::new().unwrap();
2061 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2062 let async_bridge = AsyncBridge::new();
2063 manager.set_runtime(rt.handle().clone(), async_bridge);
2064
2065 manager.set_language_config(
2066 "rust".to_string(),
2067 LspServerConfig {
2068 enabled: false,
2069 command: String::new(),
2070 args: vec![],
2071 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2072 auto_start: false,
2073 initialization_options: None,
2074 env: Default::default(),
2075 language_id_overrides: Default::default(),
2076 name: None,
2077 only_features: None,
2078 except_features: None,
2079 root_markers: Default::default(),
2080 },
2081 );
2082
2083 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2084 }
2085
2086 #[test]
2087 fn test_lsp_manager_shutdown_all() {
2088 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2089
2090 manager.shutdown_all();
2092 assert_eq!(manager.handles.len(), 0);
2093 }
2094
2095 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2096 let mut languages = std::collections::HashMap::new();
2097 languages.insert(
2098 "rust".to_string(),
2099 crate::config::LanguageConfig {
2100 extensions: vec!["rs".to_string()],
2101 filenames: vec![],
2102 grammar: "rust".to_string(),
2103 comment_prefix: Some("//".to_string()),
2104 auto_indent: true,
2105 auto_close: None,
2106 auto_surround: None,
2107 textmate_grammar: None,
2108 show_whitespace_tabs: false,
2109 line_wrap: None,
2110 wrap_column: None,
2111 page_view: None,
2112 page_width: None,
2113 use_tabs: None,
2114 tab_size: None,
2115 formatter: None,
2116 format_on_save: false,
2117 on_save: vec![],
2118 word_characters: None,
2119 },
2120 );
2121 languages.insert(
2122 "javascript".to_string(),
2123 crate::config::LanguageConfig {
2124 extensions: vec!["js".to_string(), "jsx".to_string()],
2125 filenames: vec![],
2126 grammar: "javascript".to_string(),
2127 comment_prefix: Some("//".to_string()),
2128 auto_indent: true,
2129 auto_close: None,
2130 auto_surround: None,
2131 textmate_grammar: None,
2132 show_whitespace_tabs: false,
2133 line_wrap: None,
2134 wrap_column: None,
2135 page_view: None,
2136 page_width: None,
2137 use_tabs: None,
2138 tab_size: None,
2139 formatter: None,
2140 format_on_save: false,
2141 on_save: vec![],
2142 word_characters: None,
2143 },
2144 );
2145 languages.insert(
2146 "csharp".to_string(),
2147 crate::config::LanguageConfig {
2148 extensions: vec!["cs".to_string()],
2149 filenames: vec![],
2150 grammar: "c_sharp".to_string(),
2151 comment_prefix: Some("//".to_string()),
2152 auto_indent: true,
2153 auto_close: None,
2154 auto_surround: None,
2155 textmate_grammar: None,
2156 show_whitespace_tabs: false,
2157 line_wrap: None,
2158 wrap_column: None,
2159 page_view: None,
2160 page_width: None,
2161 use_tabs: None,
2162 tab_size: None,
2163 formatter: None,
2164 format_on_save: false,
2165 on_save: vec![],
2166 word_characters: None,
2167 },
2168 );
2169 languages
2170 }
2171
2172 #[test]
2173 fn test_detect_language_from_config() {
2174 let languages = test_languages();
2175
2176 assert_eq!(
2178 detect_language(Path::new("main.rs"), &languages),
2179 Some("rust".to_string())
2180 );
2181 assert_eq!(
2182 detect_language(Path::new("index.js"), &languages),
2183 Some("javascript".to_string())
2184 );
2185 assert_eq!(
2186 detect_language(Path::new("App.jsx"), &languages),
2187 Some("javascript".to_string())
2188 );
2189 assert_eq!(
2190 detect_language(Path::new("Program.cs"), &languages),
2191 Some("csharp".to_string())
2192 );
2193
2194 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
2196 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
2197 assert_eq!(detect_language(Path::new("file"), &languages), None);
2198 }
2199
2200 #[test]
2201 fn test_detect_language_no_extension() {
2202 let languages = test_languages();
2203 assert_eq!(detect_language(Path::new("README"), &languages), None);
2204 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
2205 }
2206
2207 #[test]
2208 fn test_detect_language_path_glob() {
2209 let mut languages = test_languages();
2210 languages.insert(
2211 "shell".to_string(),
2212 crate::config::LanguageConfig {
2213 extensions: vec!["sh".to_string()],
2214 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
2215 grammar: "bash".to_string(),
2216 comment_prefix: Some("#".to_string()),
2217 auto_indent: true,
2218 auto_close: None,
2219 auto_surround: None,
2220 textmate_grammar: None,
2221 show_whitespace_tabs: false,
2222 line_wrap: None,
2223 wrap_column: None,
2224 page_view: None,
2225 page_width: None,
2226 use_tabs: None,
2227 tab_size: None,
2228 formatter: None,
2229 format_on_save: false,
2230 on_save: vec![],
2231 word_characters: None,
2232 },
2233 );
2234
2235 assert_eq!(
2237 detect_language(Path::new("/etc/rc.conf"), &languages),
2238 Some("shell".to_string())
2239 );
2240 assert_eq!(
2241 detect_language(Path::new("/etc/init/rc.local"), &languages),
2242 Some("shell".to_string())
2243 );
2244 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
2246
2247 assert_eq!(
2249 detect_language(Path::new("lfrc"), &languages),
2250 Some("shell".to_string())
2251 );
2252 }
2253
2254 #[test]
2255 fn test_detect_workspace_root_finds_marker_in_parent() {
2256 let tmp = tempfile::tempdir().unwrap();
2257 let project = tmp.path().join("myproject");
2258 let src = project.join("src");
2259 std::fs::create_dir_all(&src).unwrap();
2260 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2261 let file = src.join("main.rs");
2262 std::fs::write(&file, "").unwrap();
2263
2264 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
2265 assert_eq!(root, project);
2266 }
2267
2268 #[test]
2269 fn test_detect_workspace_root_finds_marker_two_levels_up() {
2270 let tmp = tempfile::tempdir().unwrap();
2271 let project = tmp.path().join("myproject");
2272 let deep = project.join("src").join("nested");
2273 std::fs::create_dir_all(&deep).unwrap();
2274 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2275 let file = deep.join("lib.rs");
2276 std::fs::write(&file, "").unwrap();
2277
2278 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
2279 assert_eq!(root, project);
2280 }
2281
2282 #[test]
2283 fn test_detect_workspace_root_no_marker_returns_parent() {
2284 let tmp = tempfile::tempdir().unwrap();
2285 let dir = tmp.path().join("somedir");
2286 std::fs::create_dir_all(&dir).unwrap();
2287 let file = dir.join("file.txt");
2288 std::fs::write(&file, "").unwrap();
2289
2290 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
2291 assert_eq!(root, dir);
2292 }
2293
2294 #[test]
2295 fn test_detect_workspace_root_empty_markers_returns_parent() {
2296 let tmp = tempfile::tempdir().unwrap();
2297 let dir = tmp.path().join("somedir");
2298 std::fs::create_dir_all(&dir).unwrap();
2299 let file = dir.join("file.txt");
2300 std::fs::write(&file, "").unwrap();
2301
2302 let root = detect_workspace_root(&file, &[]);
2303 assert_eq!(root, dir);
2304 }
2305
2306 #[test]
2307 fn test_detect_workspace_root_directory_marker() {
2308 let tmp = tempfile::tempdir().unwrap();
2309 let project = tmp.path().join("myproject");
2310 let src = project.join("src");
2311 std::fs::create_dir_all(&src).unwrap();
2312 std::fs::create_dir_all(project.join(".git")).unwrap();
2313 let file = src.join("main.rs");
2314 std::fs::write(&file, "").unwrap();
2315
2316 let root = detect_workspace_root(&file, &[".git".to_string()]);
2317 assert_eq!(root, project);
2318 }
2319
2320 fn c_cpp_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2325 use crate::config::LanguageConfig;
2326 let mut languages = std::collections::HashMap::new();
2327 let base = LanguageConfig {
2328 extensions: vec![],
2329 filenames: vec![],
2330 grammar: String::new(),
2331 comment_prefix: Some("//".to_string()),
2332 auto_indent: true,
2333 auto_close: None,
2334 auto_surround: None,
2335 textmate_grammar: None,
2336 show_whitespace_tabs: false,
2337 line_wrap: None,
2338 wrap_column: None,
2339 page_view: None,
2340 page_width: None,
2341 use_tabs: None,
2342 tab_size: None,
2343 formatter: None,
2344 format_on_save: false,
2345 on_save: vec![],
2346 word_characters: None,
2347 };
2348 languages.insert(
2349 "c".to_string(),
2350 LanguageConfig {
2351 extensions: vec!["c".to_string(), "h".to_string()],
2352 grammar: "c".to_string(),
2353 ..base.clone()
2354 },
2355 );
2356 languages.insert(
2357 "cpp".to_string(),
2358 LanguageConfig {
2359 extensions: vec![
2360 "cpp".to_string(),
2361 "cc".to_string(),
2362 "cxx".to_string(),
2363 "hpp".to_string(),
2364 "hh".to_string(),
2365 "hxx".to_string(),
2366 ],
2367 grammar: "cpp".to_string(),
2368 ..base
2369 },
2370 );
2371 languages
2372 }
2373
2374 #[test]
2375 fn test_detect_language_h_stays_c_without_cpp_signals() {
2376 let languages = c_cpp_languages();
2380 assert_eq!(
2381 detect_language(Path::new("foo.h"), &languages),
2382 Some("c".to_string())
2383 );
2384 }
2385
2386 #[test]
2387 fn test_detect_language_h_promotes_to_cpp_with_sibling_cpp_source() {
2388 let tmp = tempfile::tempdir().unwrap();
2389 let project = tmp.path().join("proj");
2390 std::fs::create_dir_all(&project).unwrap();
2391 let header = project.join("widget.h");
2392 std::fs::write(&header, "").unwrap();
2393 std::fs::write(project.join("widget.cpp"), "").unwrap();
2395
2396 let languages = c_cpp_languages();
2397 assert_eq!(
2398 detect_language(&header, &languages),
2399 Some("cpp".to_string())
2400 );
2401 }
2402
2403 #[test]
2404 fn test_detect_language_h_promotes_to_cpp_with_sibling_hpp() {
2405 let tmp = tempfile::tempdir().unwrap();
2406 let project = tmp.path().join("proj");
2407 std::fs::create_dir_all(&project).unwrap();
2408 let header = project.join("a.h");
2409 std::fs::write(&header, "").unwrap();
2410 std::fs::write(project.join("b.hpp"), "").unwrap();
2412
2413 let languages = c_cpp_languages();
2414 assert_eq!(
2415 detect_language(&header, &languages),
2416 Some("cpp".to_string())
2417 );
2418 }
2419
2420 #[test]
2421 fn test_detect_language_h_promotes_to_cpp_with_ancestor_compile_commands() {
2422 let tmp = tempfile::tempdir().unwrap();
2423 let project = tmp.path().join("proj");
2424 let include = project.join("include").join("fmt");
2425 std::fs::create_dir_all(&include).unwrap();
2426 std::fs::write(
2430 project.join("compile_commands.json"),
2431 r#"[{"directory":"/proj","command":"/usr/bin/clang++ -std=c++17 -c src/format.cc","file":"src/format.cc"}]"#,
2432 ).unwrap();
2433 let header = include.join("format.h");
2434 std::fs::write(&header, "").unwrap();
2435
2436 let languages = c_cpp_languages();
2437 assert_eq!(
2438 detect_language(&header, &languages),
2439 Some("cpp".to_string())
2440 );
2441 }
2442
2443 #[test]
2444 fn test_detect_language_h_stays_c_with_pure_c_compile_commands() {
2445 let tmp = tempfile::tempdir().unwrap();
2448 let project = tmp.path().join("cproj");
2449 let include = project.join("include");
2450 std::fs::create_dir_all(&include).unwrap();
2451 std::fs::write(
2452 project.join("compile_commands.json"),
2453 r#"[{"directory":"/cproj","command":"/usr/bin/gcc -std=c11 -c src/lib.c","file":"src/lib.c"}]"#,
2454 )
2455 .unwrap();
2456 let header = include.join("lib.h");
2457 std::fs::write(&header, "").unwrap();
2458
2459 let languages = c_cpp_languages();
2460 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2461 }
2462
2463 #[test]
2464 fn test_detect_language_h_stays_c_in_pure_c_tree() {
2465 let tmp = tempfile::tempdir().unwrap();
2466 let project = tmp.path().join("cproj");
2467 std::fs::create_dir_all(&project).unwrap();
2468 let header = project.join("lib.h");
2469 std::fs::write(&header, "").unwrap();
2470 std::fs::write(project.join("lib.c"), "").unwrap();
2472
2473 let languages = c_cpp_languages();
2474 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2475 }
2476
2477 #[test]
2478 fn test_detect_language_h_stays_c_with_empty_compile_commands() {
2479 let tmp = tempfile::tempdir().unwrap();
2482 let project = tmp.path().join("proj");
2483 std::fs::create_dir_all(&project).unwrap();
2484 std::fs::write(project.join("compile_commands.json"), "[]").unwrap();
2485 let header = project.join("foo.h");
2486 std::fs::write(&header, "").unwrap();
2487
2488 let languages = c_cpp_languages();
2489 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2490 }
2491
2492 #[test]
2493 fn test_detect_language_h_promotes_on_cpp_std_flag_alone() {
2494 let tmp = tempfile::tempdir().unwrap();
2496 let project = tmp.path().join("proj");
2497 let include = project.join("include");
2498 std::fs::create_dir_all(&include).unwrap();
2499 std::fs::write(
2500 project.join("compile_commands.json"),
2501 r#"[{"directory":"/proj","command":"/usr/bin/clang -std=c++20 -c src/x.C","file":"src/x.C"}]"#,
2505 )
2506 .unwrap();
2507 let header = include.join("x.h");
2508 std::fs::write(&header, "").unwrap();
2509
2510 let languages = c_cpp_languages();
2511 assert_eq!(
2512 detect_language(&header, &languages),
2513 Some("cpp".to_string())
2514 );
2515 }
2516
2517 #[test]
2518 fn test_detect_language_c_source_never_promoted() {
2519 let tmp = tempfile::tempdir().unwrap();
2521 let project = tmp.path().join("proj");
2522 std::fs::create_dir_all(&project).unwrap();
2523 let source = project.join("legacy.c");
2524 std::fs::write(&source, "").unwrap();
2525 std::fs::write(project.join("main.cpp"), "").unwrap();
2526
2527 let languages = c_cpp_languages();
2528 assert_eq!(detect_language(&source, &languages), Some("c".to_string()));
2529 }
2530
2531 #[test]
2532 fn test_detect_language_h_no_promotion_without_cpp_config() {
2533 let tmp = tempfile::tempdir().unwrap();
2536 let project = tmp.path().join("proj");
2537 std::fs::create_dir_all(&project).unwrap();
2538 let header = project.join("widget.h");
2539 std::fs::write(&header, "").unwrap();
2540 std::fs::write(project.join("widget.cpp"), "").unwrap();
2541
2542 let mut languages = c_cpp_languages();
2543 languages.remove("cpp");
2544 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2545 }
2546
2547 #[test]
2548 fn test_path_to_uri_basic() {
2549 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
2550 assert_eq!(uri.as_str(), "file:///tmp/test");
2551 }
2552
2553 #[test]
2554 fn test_path_to_uri_with_spaces() {
2555 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
2556 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
2557 }
2558}