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 handles: Vec<ServerHandle>,
277
278 config: HashMap<String, Vec<LspServerConfig>>,
280
281 universal_configs: Vec<LspServerConfig>,
283
284 root_uri: Option<Uri>,
286
287 per_language_root_uris: HashMap<String, Uri>,
289
290 runtime: Option<tokio::runtime::Handle>,
292
293 async_bridge: Option<AsyncBridge>,
295
296 long_running_spawner: Option<std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>>,
302
303 path_translation: Option<crate::services::authority::PathTranslation>,
308
309 restart_attempts: HashMap<String, Vec<Instant>>,
311
312 restart_cooldown: HashSet<String>,
314
315 pending_restarts: HashMap<String, Instant>,
317
318 allowed_languages: HashSet<String>,
321
322 disabled_languages: HashSet<String>,
325}
326
327impl LspManager {
328 pub fn new(root_uri: Option<Uri>) -> Self {
330 Self {
331 handles: Vec::new(),
332 config: HashMap::new(),
333 universal_configs: Vec::new(),
334 root_uri,
335 per_language_root_uris: HashMap::new(),
336 runtime: None,
337 async_bridge: None,
338 long_running_spawner: None,
339 path_translation: None,
340 restart_attempts: HashMap::new(),
341 restart_cooldown: HashSet::new(),
342 pending_restarts: HashMap::new(),
343 allowed_languages: HashSet::new(),
344 disabled_languages: HashSet::new(),
345 }
346 }
347
348 pub fn set_long_running_spawner(
357 &mut self,
358 spawner: std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>,
359 ) {
360 self.long_running_spawner = Some(spawner);
361 }
362
363 pub fn set_path_translation(
368 &mut self,
369 translation: Option<crate::services::authority::PathTranslation>,
370 ) {
371 self.path_translation = translation;
372 }
373
374 pub fn command_exists_via_authority(&self, command: &str) -> bool {
388 if command.is_empty() {
389 return false;
390 }
391 let (Some(runtime), Some(spawner)) =
392 (self.runtime.as_ref(), self.long_running_spawner.as_ref())
393 else {
394 return crate::services::lsp::command_exists(command);
395 };
396 runtime.block_on(spawner.command_exists(command))
397 }
398
399 pub fn is_language_allowed(&self, language: &str) -> bool {
401 self.allowed_languages.contains(language)
402 }
403
404 pub fn allow_language(&mut self, language: &str) {
406 self.allowed_languages.insert(language.to_string());
407 tracing::info!("LSP language '{}' manually enabled", language);
408 }
409
410 pub fn allowed_languages(&self) -> &HashSet<String> {
412 &self.allowed_languages
413 }
414
415 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
417 self.config.get(language).map(|v| v.as_slice())
418 }
419
420 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
422 self.config.get(language).and_then(|v| v.first())
423 }
424
425 pub fn set_server_capabilities(
427 &mut self,
428 _language: &str,
429 server_name: &str,
430 mut capabilities: ServerCapabilitySummary,
431 ) {
432 capabilities.initialized = true;
433
434 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
435 sh.capabilities = capabilities;
436 }
437 }
438
439 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
441 self.get_handles(language).into_iter().find_map(|sh| {
442 if sh.feature_filter.allows(LspFeature::SemanticTokens)
443 && sh.has_capability(LspFeature::SemanticTokens)
444 {
445 sh.capabilities.semantic_tokens_legend.as_ref()
446 } else {
447 None
448 }
449 })
450 }
451
452 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
454 self.get_handles(language).iter().any(|sh| {
455 sh.feature_filter.allows(LspFeature::SemanticTokens)
456 && sh.capabilities.semantic_tokens_full
457 })
458 }
459
460 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
462 self.get_handles(language).iter().any(|sh| {
463 sh.feature_filter.allows(LspFeature::SemanticTokens)
464 && sh.capabilities.semantic_tokens_full_delta
465 })
466 }
467
468 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
470 self.get_handles(language).iter().any(|sh| {
471 sh.feature_filter.allows(LspFeature::SemanticTokens)
472 && sh.capabilities.semantic_tokens_range
473 })
474 }
475
476 pub fn folding_ranges_supported(&self, language: &str) -> bool {
478 self.get_handles(language).iter().any(|sh| {
479 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
480 })
481 }
482
483 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
485 let ch_str = ch.to_string();
486 self.get_handles(language).iter().any(|sh| {
487 sh.feature_filter.allows(LspFeature::Completion)
488 && sh
489 .capabilities
490 .completion_trigger_characters
491 .contains(&ch_str)
492 })
493 }
494
495 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
510 if self
512 .handles
513 .iter()
514 .any(|sh| sh.handle.scope().accepts(language))
515 {
516 self.ensure_universal_servers_running(file_path);
517 return LspSpawnResult::Spawned;
518 }
519
520 if self.runtime.is_none() || self.async_bridge.is_none() {
522 return LspSpawnResult::Failed;
523 }
524
525 self.ensure_universal_servers_running(file_path);
527
528 let configs = match self.config.get(language) {
530 Some(configs) if !configs.is_empty() => configs,
531 _ => {
532 if self
534 .handles
535 .iter()
536 .any(|sh| sh.handle.scope().is_universal())
537 {
538 return LspSpawnResult::Spawned;
539 }
540 return LspSpawnResult::NotConfigured;
541 }
542 };
543
544 if !configs.iter().any(|c| c.enabled) {
546 if self
547 .handles
548 .iter()
549 .any(|sh| sh.handle.scope().is_universal())
550 {
551 return LspSpawnResult::Spawned;
552 }
553 return LspSpawnResult::Disabled;
554 }
555
556 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
558 if !any_auto_start && !self.allowed_languages.contains(language) {
559 if self
560 .handles
561 .iter()
562 .any(|sh| sh.handle.scope().is_universal())
563 {
564 return LspSpawnResult::Spawned;
565 }
566 return LspSpawnResult::NotAutoStart;
567 }
568
569 let spawned = self.force_spawn(language, file_path).is_some();
571
572 if spawned
573 || self
574 .handles
575 .iter()
576 .any(|sh| sh.handle.scope().is_universal())
577 {
578 LspSpawnResult::Spawned
579 } else {
580 LspSpawnResult::Failed
581 }
582 }
583
584 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
588 self.runtime = Some(runtime);
589 self.async_bridge = Some(async_bridge);
590 }
591
592 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
594 self.config.insert(language, vec![config]);
595 }
596
597 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
599 self.config.insert(language, configs);
600 }
601
602 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
604 self.config.entry(language).or_default().extend(configs);
605 }
606
607 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
612 self.universal_configs = configs;
613 }
614
615 pub fn configured_languages(&self) -> Vec<String> {
617 self.config.keys().cloned().collect()
618 }
619
620 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
625 self.root_uri = root_uri;
626 }
627
628 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
634 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
635 self.per_language_root_uris
636 .insert(language.to_string(), uri.clone());
637
638 if self
640 .handles
641 .iter()
642 .any(|sh| sh.handle.scope().accepts(language))
643 {
644 tracing::info!(
645 "Restarting {} LSP server with new root: {}",
646 language,
647 uri.as_str()
648 );
649 self.shutdown_server(language);
650 return true;
652 }
653 false
654 }
655
656 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
663 if let Some(uri) = self.per_language_root_uris.get(language) {
665 return Some(uri.clone());
666 }
667
668 if let Some(path) = file_path {
673 let markers = self
674 .config
675 .get(language)
676 .and_then(|configs| configs.first())
677 .map(|c| c.root_markers.as_slice())
678 .unwrap_or(&[]);
679 let root = detect_workspace_root(path, markers);
680 let mapped = self
681 .path_translation
682 .as_ref()
683 .and_then(|t| t.host_to_remote(&root))
684 .unwrap_or(root);
685 if let Some(uri) = path_to_uri(&mapped) {
686 return Some(uri);
687 }
688 }
689
690 self.root_uri.clone()
692 }
693
694 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
698 self.resolve_root_uri(language, None)
699 }
700
701 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
706 self.shutdown_all();
708
709 self.root_uri = new_root_uri;
711
712 self.restart_attempts.clear();
714 self.restart_cooldown.clear();
715 self.pending_restarts.clear();
716
717 tracing::info!(
721 "LSP manager reset for new project: {:?}",
722 self.root_uri.as_ref().map(|u| u.as_str())
723 );
724 }
725
726 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
729 self.handles
730 .iter()
731 .find(|sh| sh.handle.scope().accepts(language))
732 .map(|sh| &sh.handle)
733 }
734
735 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
738 self.handles
739 .iter_mut()
740 .find(|sh| sh.handle.scope().accepts(language))
741 .map(|sh| &mut sh.handle)
742 }
743
744 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
746 self.handles
747 .iter()
748 .filter(|sh| sh.handle.scope().accepts(language))
749 .collect()
750 }
751
752 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
754 self.handles
755 .iter_mut()
756 .filter(|sh| sh.handle.scope().accepts(language))
757 .collect()
758 }
759
760 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
764 self.handles
765 .iter()
766 .find(|sh| sh.name == server_name)
767 .map(|sh| sh.handle.scope())
768 }
769
770 pub fn has_handles(&self, language: &str) -> bool {
772 self.handles
773 .iter()
774 .any(|sh| sh.handle.scope().accepts(language))
775 }
776
777 pub fn handle_count(&self, language: &str) -> usize {
779 self.handles
780 .iter()
781 .filter(|sh| sh.handle.scope().accepts(language))
782 .count()
783 }
784
785 pub fn has_server_named(&self, server_name: &str) -> bool {
787 self.handles.iter().any(|sh| sh.name == server_name)
788 }
789
790 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
796 self.handles
797 .iter()
798 .filter(|sh| sh.handle.scope().accepts(language))
799 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
800 }
801
802 pub fn handle_for_feature_mut(
806 &mut self,
807 language: &str,
808 feature: LspFeature,
809 ) -> Option<&mut ServerHandle> {
810 self.handles
811 .iter_mut()
812 .filter(|sh| sh.handle.scope().accepts(language))
813 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
814 }
815
816 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
820 self.handles
821 .iter()
822 .filter(|sh| sh.handle.scope().accepts(language))
823 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
824 .collect()
825 }
826
827 pub fn handles_for_feature_mut(
831 &mut self,
832 language: &str,
833 feature: LspFeature,
834 ) -> Vec<&mut ServerHandle> {
835 self.handles
836 .iter_mut()
837 .filter(|sh| sh.handle.scope().accepts(language))
838 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
839 .collect()
840 }
841
842 fn spawn_decision(&mut self, language: &str) -> SpawnDecision {
850 if self
851 .handles
852 .iter()
853 .any(|sh| sh.handle.scope().accepts(language))
854 {
855 return SpawnDecision::Existing;
856 }
857 if self.restart_cooldown.contains(language) {
858 return SpawnDecision::CooledDown;
859 }
860 if self.pending_restarts.contains_key(language) {
861 return SpawnDecision::PendingBackoff;
862 }
863
864 let now = Instant::now();
865 let window = Duration::from_secs(RESTART_WINDOW_SECS);
866 let attempts = self
867 .restart_attempts
868 .entry(language.to_string())
869 .or_default();
870 attempts.retain(|t| now.duration_since(*t) < window);
871
872 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
873 self.restart_cooldown.insert(language.to_string());
874 tracing::warn!(
875 "LSP server for {} has spawned {} times in {} minutes, entering cooldown",
876 language,
877 MAX_RESTARTS_IN_WINDOW,
878 RESTART_WINDOW_SECS / 60
879 );
880 return SpawnDecision::CooledDown;
881 }
882
883 attempts.push(now);
884 SpawnDecision::Allow
885 }
886
887 pub fn force_spawn(
906 &mut self,
907 language: &str,
908 file_path: Option<&Path>,
909 ) -> Option<&mut LspHandle> {
910 tracing::debug!("force_spawn called for language: {}", language);
911
912 if self
914 .handles
915 .iter()
916 .any(|sh| sh.handle.scope().accepts(language))
917 {
918 tracing::debug!("force_spawn: returning existing handle for {}", language);
919 return self
920 .handles
921 .iter_mut()
922 .find(|sh| sh.handle.scope().accepts(language))
923 .map(|sh| &mut sh.handle);
924 }
925
926 if self.disabled_languages.contains(language) {
928 tracing::debug!(
929 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
930 language
931 );
932 return None;
933 }
934
935 let configs = match self.config.get(language) {
937 Some(configs) if !configs.is_empty() => configs.clone(),
938 _ => {
939 tracing::warn!(
940 "force_spawn: no config found for language '{}', available configs: {:?}",
941 language,
942 self.config.keys().collect::<Vec<_>>()
943 );
944 return None;
945 }
946 };
947
948 match self.spawn_decision(language) {
954 SpawnDecision::Existing => {
955 return self
958 .handles
959 .iter_mut()
960 .find(|sh| sh.handle.scope().accepts(language))
961 .map(|sh| &mut sh.handle);
962 }
963 SpawnDecision::CooledDown => {
964 tracing::debug!(
965 "force_spawn: {} is in cooldown, refusing spawn (use Restart LSP command)",
966 language
967 );
968 return None;
969 }
970 SpawnDecision::PendingBackoff => {
971 tracing::debug!(
972 "force_spawn: {} has a pending restart scheduled, not double-spawning",
973 language
974 );
975 return None;
976 }
977 SpawnDecision::Allow => {}
978 }
979
980 let runtime = match self.runtime.as_ref() {
985 Some(r) => r.clone(),
986 None => {
987 tracing::error!("force_spawn: no tokio runtime available for {}", language);
988 return None;
989 }
990 };
991 let async_bridge = match self.async_bridge.as_ref() {
992 Some(b) => b.clone(),
993 None => {
994 tracing::error!("force_spawn: no async bridge available for {}", language);
995 return None;
996 }
997 };
998 let long_running_spawner = match self.long_running_spawner.as_ref() {
1006 Some(s) => s.clone(),
1007 None => {
1008 tracing::warn!(
1009 "force_spawn: long-running spawner not wired for {} — \
1010 falling back to host-local spawn (normal for tests \
1011 that skip set_boot_authority)",
1012 language
1013 );
1014 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1015 }
1016 };
1017
1018 let mut spawned_handles = Vec::new();
1019 let manually_allowed = self.allowed_languages.contains(language);
1020
1021 for config in &configs {
1022 if manually_allowed {
1023 } else {
1027 if !config.enabled || !config.auto_start {
1033 continue;
1034 }
1035 }
1036
1037 if config.command.is_empty() {
1038 tracing::warn!(
1039 "force_spawn: LSP command is empty for {} server '{}'",
1040 language,
1041 config.display_name()
1042 );
1043 continue;
1044 }
1045
1046 let server_name = config.display_name();
1047 tracing::info!(
1048 "Spawning LSP server '{}' for language: {}",
1049 server_name,
1050 language
1051 );
1052
1053 match LspHandle::spawn(
1054 &runtime,
1055 &config.command,
1056 &config.args,
1057 config.env.clone(),
1058 LanguageScope::single(language),
1059 server_name.clone(),
1060 &async_bridge,
1061 config.process_limits.clone(),
1062 config.language_id_overrides.clone(),
1063 long_running_spawner.clone(),
1064 ) {
1065 Ok(handle) => {
1066 let effective_root = self.resolve_root_uri(language, file_path);
1067 if let Err(e) =
1068 handle.initialize(effective_root, config.initialization_options.clone())
1069 {
1070 tracing::error!(
1071 "Failed to send initialize command for {} ({}): {}",
1072 language,
1073 server_name,
1074 e
1075 );
1076 continue;
1077 }
1078
1079 tracing::info!(
1080 "LSP initialization started for {} ({}), will be ready asynchronously",
1081 language,
1082 server_name
1083 );
1084
1085 spawned_handles.push(ServerHandle {
1086 name: server_name,
1087 handle,
1088 feature_filter: config.feature_filter(),
1089 capabilities: ServerCapabilitySummary::default(),
1090 });
1091 }
1092 Err(e) => {
1093 tracing::error!(
1094 "Failed to spawn LSP handle for {} ({}): {}",
1095 language,
1096 server_name,
1097 e
1098 );
1099 }
1100 }
1101 }
1102
1103 if spawned_handles.is_empty() {
1104 return None;
1105 }
1106
1107 self.handles.extend(spawned_handles);
1108 self.handles
1109 .iter_mut()
1110 .rev()
1111 .find(|sh| sh.handle.scope().accepts(language))
1112 .map(|sh| &mut sh.handle)
1113 }
1114
1115 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
1121 if self
1122 .handles
1123 .iter()
1124 .any(|sh| sh.handle.scope().is_universal())
1125 || self.universal_configs.is_empty()
1126 {
1127 return;
1128 }
1129
1130 let runtime = match self.runtime.as_ref() {
1131 Some(r) => r.clone(),
1132 None => return,
1133 };
1134 let async_bridge = match self.async_bridge.as_ref() {
1135 Some(b) => b.clone(),
1136 None => return,
1137 };
1138 let long_running_spawner =
1139 self.long_running_spawner
1140 .as_ref()
1141 .cloned()
1142 .unwrap_or_else(|| {
1143 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1144 });
1145
1146 let mut spawned = Vec::new();
1147 for config in &self.universal_configs {
1148 if !config.enabled || !config.auto_start {
1149 continue;
1150 }
1151 if config.command.is_empty() {
1152 continue;
1153 }
1154
1155 let server_name = config.display_name();
1156 tracing::info!("Spawning universal LSP server '{}'", server_name);
1157
1158 match LspHandle::spawn(
1159 &runtime,
1160 &config.command,
1161 &config.args,
1162 config.env.clone(),
1163 LanguageScope::all(),
1164 server_name.clone(),
1165 &async_bridge,
1166 config.process_limits.clone(),
1167 config.language_id_overrides.clone(),
1168 long_running_spawner.clone(),
1169 ) {
1170 Ok(handle) => {
1171 let effective_root = file_path
1172 .and_then(|p| {
1173 let root = detect_workspace_root(p, &config.root_markers);
1174 path_to_uri(&root)
1175 })
1176 .or_else(|| self.root_uri.clone());
1177 if let Err(e) =
1178 handle.initialize(effective_root, config.initialization_options.clone())
1179 {
1180 tracing::error!(
1181 "Failed to initialize universal LSP server '{}': {}",
1182 server_name,
1183 e
1184 );
1185 continue;
1186 }
1187 tracing::info!(
1188 "Universal LSP server '{}' initialization started",
1189 server_name
1190 );
1191 spawned.push(ServerHandle {
1192 name: server_name,
1193 handle,
1194 feature_filter: config.feature_filter(),
1195 capabilities: ServerCapabilitySummary::default(),
1196 });
1197 }
1198 Err(e) => {
1199 tracing::error!(
1200 "Failed to spawn universal LSP server '{}': {}",
1201 server_name,
1202 e
1203 );
1204 }
1205 }
1206 }
1207
1208 self.handles.extend(spawned);
1209 }
1210
1211 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1215 if self
1217 .handles
1218 .iter()
1219 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1220 {
1221 let universals: Vec<ServerHandle> = {
1223 let mut drained = Vec::new();
1224 let mut i = 0;
1225 while i < self.handles.len() {
1226 if self.handles[i].handle.scope().is_universal() {
1227 drained.push(self.handles.remove(i));
1228 } else {
1229 i += 1;
1230 }
1231 }
1232 drained
1233 };
1234 for sh in universals {
1235 fire_and_forget(sh.handle.shutdown());
1236 }
1237 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1239 }
1240
1241 {
1243 let mut i = 0;
1244 while i < self.handles.len() {
1245 if !self.handles[i].handle.scope().is_universal()
1246 && self.handles[i].handle.scope().accepts(language)
1247 {
1248 let sh = self.handles.remove(i);
1249 fire_and_forget(sh.handle.shutdown());
1250 } else {
1251 i += 1;
1252 }
1253 }
1254 }
1255
1256 if self.disabled_languages.contains(language) {
1259 return format!(
1260 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1261 language
1262 );
1263 }
1264
1265 if self.restart_cooldown.contains(language) {
1267 return format!(
1268 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1269 language
1270 );
1271 }
1272
1273 let now = Instant::now();
1278 let attempt_number = self
1279 .restart_attempts
1280 .get(language)
1281 .map(|v| v.len())
1282 .unwrap_or(0);
1283
1284 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1286
1287 self.pending_restarts
1288 .insert(language.to_string(), restart_time);
1289
1290 tracing::info!(
1291 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1292 language,
1293 attempt_number + 1,
1294 MAX_RESTARTS_IN_WINDOW,
1295 delay_ms
1296 );
1297
1298 format!(
1299 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1300 language,
1301 attempt_number + 1,
1302 MAX_RESTARTS_IN_WINDOW,
1303 delay_ms / 1000
1304 )
1305 }
1306
1307 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1311 let now = Instant::now();
1312 let mut results = Vec::new();
1313
1314 let due_restarts: Vec<String> = self
1316 .pending_restarts
1317 .iter()
1318 .filter(|(_, time)| **time <= now)
1319 .map(|(lang, _)| lang.clone())
1320 .collect();
1321
1322 for language in due_restarts {
1323 self.pending_restarts.remove(&language);
1324
1325 if self.force_spawn(&language, None).is_some() {
1329 let message = format!("LSP server for {} restarted successfully", language);
1330 tracing::info!("{}", message);
1331 results.push((language, true, message));
1332 } else {
1333 let message = format!("Failed to restart LSP server for {}", language);
1334 tracing::error!("{}", message);
1335 results.push((language, false, message));
1336 }
1337 }
1338
1339 results
1340 }
1341
1342 pub fn is_in_cooldown(&self, language: &str) -> bool {
1344 self.restart_cooldown.contains(language)
1345 }
1346
1347 pub fn has_pending_restart(&self, language: &str) -> bool {
1349 self.pending_restarts.contains_key(language)
1350 }
1351
1352 pub fn clear_cooldown(&mut self, language: &str) {
1354 self.restart_cooldown.remove(language);
1355 self.restart_attempts.remove(language);
1356 self.pending_restarts.remove(language);
1357 tracing::info!("Cleared restart cooldown for {}", language);
1358 }
1359
1360 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1367 self.clear_cooldown(language);
1369
1370 self.disabled_languages.remove(language);
1372
1373 self.allowed_languages.insert(language.to_string());
1375
1376 {
1378 let mut i = 0;
1379 while i < self.handles.len() {
1380 if !self.handles[i].handle.scope().is_universal()
1381 && self.handles[i].handle.scope().accepts(language)
1382 {
1383 let sh = self.handles.remove(i);
1384 fire_and_forget(sh.handle.shutdown());
1385 } else {
1386 i += 1;
1387 }
1388 }
1389 }
1390
1391 if self.force_spawn(language, file_path).is_some() {
1393 let message = format!("LSP server for {} started", language);
1394 tracing::info!("{}", message);
1395 (true, message)
1396 } else {
1397 let message = format!("Failed to start LSP server for {}", language);
1398 tracing::error!("{}", message);
1399 (false, message)
1400 }
1401 }
1402
1403 pub fn manual_restart_server(
1408 &mut self,
1409 language: &str,
1410 server_name: &str,
1411 file_path: Option<&Path>,
1412 ) -> (bool, String) {
1413 self.clear_cooldown(language);
1414 self.disabled_languages.remove(language);
1415 self.allowed_languages.insert(language.to_string());
1416
1417 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1419 let sh = self.handles.remove(idx);
1420 fire_and_forget(sh.handle.shutdown());
1421 }
1422
1423 let is_universal = self
1425 .universal_configs
1426 .iter()
1427 .any(|c| c.display_name() == server_name);
1428 let config = if is_universal {
1429 self.universal_configs
1430 .iter()
1431 .find(|c| c.display_name() == server_name)
1432 .cloned()
1433 } else {
1434 self.config
1435 .get(language)
1436 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1437 .cloned()
1438 };
1439
1440 let Some(config) = config else {
1441 let message = format!(
1442 "No config found for server '{}' ({})",
1443 server_name, language
1444 );
1445 tracing::error!("{}", message);
1446 return (false, message);
1447 };
1448
1449 if config.command.is_empty() {
1450 let message = format!(
1451 "LSP command is empty for {} server '{}'",
1452 language, server_name
1453 );
1454 tracing::error!("{}", message);
1455 return (false, message);
1456 }
1457
1458 let runtime = match self.runtime.as_ref() {
1459 Some(r) => r.clone(),
1460 None => return (false, "No tokio runtime available".to_string()),
1461 };
1462 let async_bridge = match self.async_bridge.as_ref() {
1463 Some(b) => b.clone(),
1464 None => return (false, "No async bridge available".to_string()),
1465 };
1466 let long_running_spawner =
1467 self.long_running_spawner
1468 .as_ref()
1469 .cloned()
1470 .unwrap_or_else(|| {
1471 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1472 });
1473
1474 let scope = if is_universal {
1475 LanguageScope::all()
1476 } else {
1477 LanguageScope::single(language)
1478 };
1479
1480 match LspHandle::spawn(
1481 &runtime,
1482 &config.command,
1483 &config.args,
1484 config.env.clone(),
1485 scope,
1486 server_name.to_string(),
1487 &async_bridge,
1488 config.process_limits.clone(),
1489 config.language_id_overrides.clone(),
1490 long_running_spawner,
1491 ) {
1492 Ok(handle) => {
1493 let effective_root = if is_universal {
1494 file_path
1495 .and_then(|p| {
1496 let root = detect_workspace_root(p, &config.root_markers);
1497 path_to_uri(&root)
1498 })
1499 .or_else(|| self.root_uri.clone())
1500 } else {
1501 self.resolve_root_uri(language, file_path)
1502 };
1503 if let Err(e) =
1504 handle.initialize(effective_root, config.initialization_options.clone())
1505 {
1506 let message = format!(
1507 "Failed to initialize LSP server '{}' for {}: {}",
1508 server_name, language, e
1509 );
1510 tracing::error!("{}", message);
1511 return (false, message);
1512 }
1513
1514 let sh = ServerHandle {
1515 name: server_name.to_string(),
1516 handle,
1517 feature_filter: config.feature_filter(),
1518 capabilities: ServerCapabilitySummary::default(),
1519 };
1520
1521 self.handles.push(sh);
1522
1523 let message = format!("LSP server '{}' for {} started", server_name, language);
1524 tracing::info!("{}", message);
1525 (true, message)
1526 }
1527 Err(e) => {
1528 let message = format!(
1529 "Failed to start LSP server '{}' for {}: {}",
1530 server_name, language, e
1531 );
1532 tracing::error!("{}", message);
1533 (false, message)
1534 }
1535 }
1536 }
1537
1538 pub fn restart_attempt_count(&self, language: &str) -> usize {
1540 let now = Instant::now();
1541 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1542 self.restart_attempts
1543 .get(language)
1544 .map(|attempts| {
1545 attempts
1546 .iter()
1547 .filter(|t| now.duration_since(**t) < window)
1548 .count()
1549 })
1550 .unwrap_or(0)
1551 }
1552
1553 pub fn running_servers(&self) -> Vec<String> {
1555 let mut labels: Vec<String> = self
1556 .handles
1557 .iter()
1558 .map(|sh| sh.handle.scope().label().to_string())
1559 .collect();
1560 labels.sort();
1561 labels.dedup();
1562 labels
1563 }
1564
1565 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1567 self.handles
1568 .iter()
1569 .filter(|sh| sh.handle.scope().accepts(language))
1570 .map(|sh| sh.name.clone())
1571 .collect()
1572 }
1573
1574 pub fn is_server_ready(&self, language: &str) -> bool {
1576 self.handles
1577 .iter()
1578 .filter(|sh| sh.handle.scope().accepts(language))
1579 .any(|sh| sh.handle.state().can_send_requests())
1580 }
1581
1582 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1587 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1588 tracing::warn!(
1589 "No running LSP server named '{}' found for {}",
1590 server_name,
1591 language
1592 );
1593 return false;
1594 };
1595
1596 let sh = self.handles.remove(idx);
1597 tracing::info!(
1598 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1599 sh.name,
1600 language
1601 );
1602 fire_and_forget(sh.handle.shutdown());
1603
1604 let has_remaining = self
1606 .handles
1607 .iter()
1608 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1609 if !has_remaining {
1610 self.disabled_languages.insert(language.to_string());
1611 self.pending_restarts.remove(language);
1612 self.restart_cooldown.remove(language);
1613 self.allowed_languages.remove(language);
1614 }
1615
1616 true
1617 }
1618
1619 pub fn shutdown_server(&mut self, language: &str) -> bool {
1624 let mut found = false;
1625 let mut i = 0;
1626 while i < self.handles.len() {
1627 if !self.handles[i].handle.scope().is_universal()
1628 && self.handles[i].handle.scope().accepts(language)
1629 {
1630 let sh = self.handles.remove(i);
1631 tracing::info!(
1632 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1633 sh.name,
1634 language
1635 );
1636 fire_and_forget(sh.handle.shutdown());
1637 found = true;
1638 } else {
1639 i += 1;
1640 }
1641 }
1642
1643 if found {
1644 self.disabled_languages.insert(language.to_string());
1645 self.pending_restarts.remove(language);
1646 self.restart_cooldown.remove(language);
1647 self.allowed_languages.remove(language);
1648 } else {
1649 tracing::warn!("No running LSP server found for {}", language);
1650 }
1651
1652 found
1653 }
1654
1655 pub fn shutdown_all(&mut self) {
1657 for sh in &self.handles {
1658 tracing::info!(
1659 "Shutting down LSP server '{}' ({})",
1660 sh.name,
1661 sh.handle.scope().label()
1662 );
1663 fire_and_forget(sh.handle.shutdown());
1664 }
1665 self.handles.clear();
1666 }
1667}
1668
1669impl Drop for LspManager {
1670 fn drop(&mut self) {
1671 self.shutdown_all();
1672 }
1673}
1674
1675pub fn detect_language(
1687 path: &std::path::Path,
1688 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1689) -> Option<String> {
1690 let detected = detect_language_by_config(path, languages);
1691
1692 if detected.as_deref() == Some("c")
1698 && path.extension().and_then(|e| e.to_str()) == Some("h")
1699 && languages.contains_key("cpp")
1700 && header_in_cpp_tree(path)
1701 {
1702 return Some("cpp".to_string());
1703 }
1704
1705 detected
1706}
1707
1708fn detect_language_by_config(
1710 path: &std::path::Path,
1711 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1712) -> Option<String> {
1713 use crate::primitives::glob_match::{
1714 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1715 };
1716
1717 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1718 for (language_name, lang_config) in languages {
1720 if lang_config
1721 .filenames
1722 .iter()
1723 .any(|f| !is_glob_pattern(f) && f == filename)
1724 {
1725 return Some(language_name.clone());
1726 }
1727 }
1728
1729 let path_str = path.to_str().unwrap_or("");
1733 for (language_name, lang_config) in languages {
1734 if lang_config.filenames.iter().any(|f| {
1735 if !is_glob_pattern(f) {
1736 return false;
1737 }
1738 if is_path_pattern(f) {
1739 path_glob_matches(f, path_str)
1740 } else {
1741 filename_glob_matches(f, filename)
1742 }
1743 }) {
1744 return Some(language_name.clone());
1745 }
1746 }
1747 }
1748
1749 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1751 for (language_name, lang_config) in languages {
1752 if lang_config.extensions.iter().any(|ext| ext == extension) {
1753 return Some(language_name.clone());
1754 }
1755 }
1756 }
1757
1758 None
1759}
1760
1761fn header_in_cpp_tree(path: &std::path::Path) -> bool {
1791 let Some(start_dir) = path.parent() else {
1792 return false;
1793 };
1794
1795 if let Ok(entries) = std::fs::read_dir(start_dir) {
1797 for entry in entries.flatten() {
1798 let p = entry.path();
1799 let Some(ext) = p.extension().and_then(|e| e.to_str()) else {
1800 continue;
1801 };
1802 if matches!(
1803 ext,
1804 "cc" | "cpp" | "cxx" | "C" | "c++" | "hpp" | "hh" | "hxx"
1805 ) {
1806 return true;
1807 }
1808 }
1809 }
1810
1811 let mut current = Some(start_dir);
1815 let mut depth = 0u32;
1816 while let Some(dir) = current {
1817 let cc = dir.join("compile_commands.json");
1818 if cc.is_file() && compile_commands_has_cpp_marker(&cc) {
1819 return true;
1820 }
1821 if depth >= 10 {
1822 break;
1823 }
1824 depth += 1;
1825 current = dir.parent();
1826 }
1827
1828 false
1829}
1830
1831fn compile_commands_has_cpp_marker(path: &std::path::Path) -> bool {
1839 use std::io::Read;
1840 const MAX_READ: u64 = 1_048_576;
1841
1842 let Ok(file) = std::fs::File::open(path) else {
1843 return false;
1844 };
1845 let mut buf = Vec::with_capacity(64 * 1024);
1846 if file.take(MAX_READ).read_to_end(&mut buf).is_err() {
1847 return false;
1848 }
1849 let Ok(text) = std::str::from_utf8(&buf) else {
1850 return false;
1851 };
1852
1853 if text.contains("c++") {
1857 return true;
1858 }
1859 text.contains(".cpp") || text.contains(".cxx") || text.contains(".cc\"")
1862}
1863
1864#[cfg(test)]
1865mod tests {
1866 use super::*;
1867 use std::path::Path;
1868
1869 #[test]
1870 fn test_lsp_manager_new() {
1871 let root_uri: Option<Uri> = "file:///test".parse().ok();
1872 let manager = LspManager::new(root_uri.clone());
1873
1874 assert_eq!(manager.handles.len(), 0);
1876 assert_eq!(manager.config.len(), 0);
1877 assert!(manager.root_uri.is_some());
1878 assert!(manager.runtime.is_none());
1879 assert!(manager.async_bridge.is_none());
1880 }
1881
1882 #[test]
1883 fn test_lsp_manager_set_language_config() {
1884 let mut manager = LspManager::new(None);
1885
1886 let config = LspServerConfig {
1887 enabled: true,
1888 command: "rust-analyzer".to_string(),
1889 args: vec![],
1890 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1891 auto_start: false,
1892 initialization_options: None,
1893 env: Default::default(),
1894 language_id_overrides: Default::default(),
1895 name: None,
1896 only_features: None,
1897 except_features: None,
1898 root_markers: Default::default(),
1899 };
1900
1901 manager.set_language_config("rust".to_string(), config);
1902
1903 assert_eq!(manager.config.len(), 1);
1904 assert!(manager.config.contains_key("rust"));
1905 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1906 }
1907
1908 #[test]
1909 fn test_lsp_manager_force_spawn_no_runtime() {
1910 let mut manager = LspManager::new(None);
1911
1912 manager.set_language_config(
1914 "rust".to_string(),
1915 LspServerConfig {
1916 enabled: true,
1917 command: "rust-analyzer".to_string(),
1918 args: vec![],
1919 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1920 auto_start: false,
1921 initialization_options: None,
1922 env: Default::default(),
1923 language_id_overrides: Default::default(),
1924 name: None,
1925 only_features: None,
1926 except_features: None,
1927 root_markers: Default::default(),
1928 },
1929 );
1930
1931 let result = manager.force_spawn("rust", None);
1933 assert!(result.is_none());
1934 }
1935
1936 #[test]
1937 fn test_lsp_manager_force_spawn_no_config() {
1938 let rt = tokio::runtime::Runtime::new().unwrap();
1939 let mut manager = LspManager::new(None);
1940 let async_bridge = AsyncBridge::new();
1941
1942 manager.set_runtime(rt.handle().clone(), async_bridge);
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_disabled_language() {
1951 let rt = tokio::runtime::Runtime::new().unwrap();
1952 let mut manager = LspManager::new(None);
1953 let async_bridge = AsyncBridge::new();
1954
1955 manager.set_runtime(rt.handle().clone(), async_bridge);
1956
1957 manager.set_language_config(
1959 "rust".to_string(),
1960 LspServerConfig {
1961 enabled: false,
1962 command: String::new(), args: vec![],
1964 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1965 auto_start: false,
1966 initialization_options: None,
1967 env: Default::default(),
1968 language_id_overrides: Default::default(),
1969 name: None,
1970 only_features: None,
1971 except_features: None,
1972 root_markers: Default::default(),
1973 },
1974 );
1975
1976 let result = manager.force_spawn("rust", None);
1978 assert!(result.is_none());
1979 }
1980
1981 #[test]
1987 fn test_lsp_manager_try_spawn_returns_disabled_when_all_configs_disabled() {
1988 let rt = tokio::runtime::Runtime::new().unwrap();
1989 let mut manager = LspManager::new(None);
1990 let async_bridge = AsyncBridge::new();
1991 manager.set_runtime(rt.handle().clone(), async_bridge);
1992
1993 manager.set_language_config(
1994 "rust".to_string(),
1995 LspServerConfig {
1996 enabled: false,
1997 command: String::new(),
1998 args: vec![],
1999 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2000 auto_start: false,
2001 initialization_options: None,
2002 env: Default::default(),
2003 language_id_overrides: Default::default(),
2004 name: None,
2005 only_features: None,
2006 except_features: None,
2007 root_markers: Default::default(),
2008 },
2009 );
2010
2011 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2012 }
2013
2014 #[test]
2015 fn test_lsp_manager_shutdown_all() {
2016 let mut manager = LspManager::new(None);
2017
2018 manager.shutdown_all();
2020 assert_eq!(manager.handles.len(), 0);
2021 }
2022
2023 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2024 let mut languages = std::collections::HashMap::new();
2025 languages.insert(
2026 "rust".to_string(),
2027 crate::config::LanguageConfig {
2028 extensions: vec!["rs".to_string()],
2029 filenames: vec![],
2030 grammar: "rust".to_string(),
2031 comment_prefix: Some("//".to_string()),
2032 auto_indent: true,
2033 auto_close: None,
2034 auto_surround: None,
2035 textmate_grammar: None,
2036 show_whitespace_tabs: false,
2037 line_wrap: None,
2038 wrap_column: None,
2039 page_view: None,
2040 page_width: None,
2041 use_tabs: None,
2042 tab_size: None,
2043 formatter: None,
2044 format_on_save: false,
2045 on_save: vec![],
2046 word_characters: None,
2047 },
2048 );
2049 languages.insert(
2050 "javascript".to_string(),
2051 crate::config::LanguageConfig {
2052 extensions: vec!["js".to_string(), "jsx".to_string()],
2053 filenames: vec![],
2054 grammar: "javascript".to_string(),
2055 comment_prefix: Some("//".to_string()),
2056 auto_indent: true,
2057 auto_close: None,
2058 auto_surround: None,
2059 textmate_grammar: None,
2060 show_whitespace_tabs: false,
2061 line_wrap: None,
2062 wrap_column: None,
2063 page_view: None,
2064 page_width: None,
2065 use_tabs: None,
2066 tab_size: None,
2067 formatter: None,
2068 format_on_save: false,
2069 on_save: vec![],
2070 word_characters: None,
2071 },
2072 );
2073 languages.insert(
2074 "csharp".to_string(),
2075 crate::config::LanguageConfig {
2076 extensions: vec!["cs".to_string()],
2077 filenames: vec![],
2078 grammar: "c_sharp".to_string(),
2079 comment_prefix: Some("//".to_string()),
2080 auto_indent: true,
2081 auto_close: None,
2082 auto_surround: None,
2083 textmate_grammar: None,
2084 show_whitespace_tabs: false,
2085 line_wrap: None,
2086 wrap_column: None,
2087 page_view: None,
2088 page_width: None,
2089 use_tabs: None,
2090 tab_size: None,
2091 formatter: None,
2092 format_on_save: false,
2093 on_save: vec![],
2094 word_characters: None,
2095 },
2096 );
2097 languages
2098 }
2099
2100 #[test]
2101 fn test_detect_language_from_config() {
2102 let languages = test_languages();
2103
2104 assert_eq!(
2106 detect_language(Path::new("main.rs"), &languages),
2107 Some("rust".to_string())
2108 );
2109 assert_eq!(
2110 detect_language(Path::new("index.js"), &languages),
2111 Some("javascript".to_string())
2112 );
2113 assert_eq!(
2114 detect_language(Path::new("App.jsx"), &languages),
2115 Some("javascript".to_string())
2116 );
2117 assert_eq!(
2118 detect_language(Path::new("Program.cs"), &languages),
2119 Some("csharp".to_string())
2120 );
2121
2122 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
2124 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
2125 assert_eq!(detect_language(Path::new("file"), &languages), None);
2126 }
2127
2128 #[test]
2129 fn test_detect_language_no_extension() {
2130 let languages = test_languages();
2131 assert_eq!(detect_language(Path::new("README"), &languages), None);
2132 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
2133 }
2134
2135 #[test]
2136 fn test_detect_language_path_glob() {
2137 let mut languages = test_languages();
2138 languages.insert(
2139 "shell".to_string(),
2140 crate::config::LanguageConfig {
2141 extensions: vec!["sh".to_string()],
2142 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
2143 grammar: "bash".to_string(),
2144 comment_prefix: Some("#".to_string()),
2145 auto_indent: true,
2146 auto_close: None,
2147 auto_surround: None,
2148 textmate_grammar: None,
2149 show_whitespace_tabs: false,
2150 line_wrap: None,
2151 wrap_column: None,
2152 page_view: None,
2153 page_width: None,
2154 use_tabs: None,
2155 tab_size: None,
2156 formatter: None,
2157 format_on_save: false,
2158 on_save: vec![],
2159 word_characters: None,
2160 },
2161 );
2162
2163 assert_eq!(
2165 detect_language(Path::new("/etc/rc.conf"), &languages),
2166 Some("shell".to_string())
2167 );
2168 assert_eq!(
2169 detect_language(Path::new("/etc/init/rc.local"), &languages),
2170 Some("shell".to_string())
2171 );
2172 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
2174
2175 assert_eq!(
2177 detect_language(Path::new("lfrc"), &languages),
2178 Some("shell".to_string())
2179 );
2180 }
2181
2182 #[test]
2183 fn test_detect_workspace_root_finds_marker_in_parent() {
2184 let tmp = tempfile::tempdir().unwrap();
2185 let project = tmp.path().join("myproject");
2186 let src = project.join("src");
2187 std::fs::create_dir_all(&src).unwrap();
2188 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2189 let file = src.join("main.rs");
2190 std::fs::write(&file, "").unwrap();
2191
2192 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
2193 assert_eq!(root, project);
2194 }
2195
2196 #[test]
2197 fn test_detect_workspace_root_finds_marker_two_levels_up() {
2198 let tmp = tempfile::tempdir().unwrap();
2199 let project = tmp.path().join("myproject");
2200 let deep = project.join("src").join("nested");
2201 std::fs::create_dir_all(&deep).unwrap();
2202 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2203 let file = deep.join("lib.rs");
2204 std::fs::write(&file, "").unwrap();
2205
2206 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
2207 assert_eq!(root, project);
2208 }
2209
2210 #[test]
2211 fn test_detect_workspace_root_no_marker_returns_parent() {
2212 let tmp = tempfile::tempdir().unwrap();
2213 let dir = tmp.path().join("somedir");
2214 std::fs::create_dir_all(&dir).unwrap();
2215 let file = dir.join("file.txt");
2216 std::fs::write(&file, "").unwrap();
2217
2218 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
2219 assert_eq!(root, dir);
2220 }
2221
2222 #[test]
2223 fn test_detect_workspace_root_empty_markers_returns_parent() {
2224 let tmp = tempfile::tempdir().unwrap();
2225 let dir = tmp.path().join("somedir");
2226 std::fs::create_dir_all(&dir).unwrap();
2227 let file = dir.join("file.txt");
2228 std::fs::write(&file, "").unwrap();
2229
2230 let root = detect_workspace_root(&file, &[]);
2231 assert_eq!(root, dir);
2232 }
2233
2234 #[test]
2235 fn test_detect_workspace_root_directory_marker() {
2236 let tmp = tempfile::tempdir().unwrap();
2237 let project = tmp.path().join("myproject");
2238 let src = project.join("src");
2239 std::fs::create_dir_all(&src).unwrap();
2240 std::fs::create_dir_all(project.join(".git")).unwrap();
2241 let file = src.join("main.rs");
2242 std::fs::write(&file, "").unwrap();
2243
2244 let root = detect_workspace_root(&file, &[".git".to_string()]);
2245 assert_eq!(root, project);
2246 }
2247
2248 fn c_cpp_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2253 use crate::config::LanguageConfig;
2254 let mut languages = std::collections::HashMap::new();
2255 let base = LanguageConfig {
2256 extensions: vec![],
2257 filenames: vec![],
2258 grammar: String::new(),
2259 comment_prefix: Some("//".to_string()),
2260 auto_indent: true,
2261 auto_close: None,
2262 auto_surround: None,
2263 textmate_grammar: None,
2264 show_whitespace_tabs: false,
2265 line_wrap: None,
2266 wrap_column: None,
2267 page_view: None,
2268 page_width: None,
2269 use_tabs: None,
2270 tab_size: None,
2271 formatter: None,
2272 format_on_save: false,
2273 on_save: vec![],
2274 word_characters: None,
2275 };
2276 languages.insert(
2277 "c".to_string(),
2278 LanguageConfig {
2279 extensions: vec!["c".to_string(), "h".to_string()],
2280 grammar: "c".to_string(),
2281 ..base.clone()
2282 },
2283 );
2284 languages.insert(
2285 "cpp".to_string(),
2286 LanguageConfig {
2287 extensions: vec![
2288 "cpp".to_string(),
2289 "cc".to_string(),
2290 "cxx".to_string(),
2291 "hpp".to_string(),
2292 "hh".to_string(),
2293 "hxx".to_string(),
2294 ],
2295 grammar: "cpp".to_string(),
2296 ..base
2297 },
2298 );
2299 languages
2300 }
2301
2302 #[test]
2303 fn test_detect_language_h_stays_c_without_cpp_signals() {
2304 let languages = c_cpp_languages();
2308 assert_eq!(
2309 detect_language(Path::new("foo.h"), &languages),
2310 Some("c".to_string())
2311 );
2312 }
2313
2314 #[test]
2315 fn test_detect_language_h_promotes_to_cpp_with_sibling_cpp_source() {
2316 let tmp = tempfile::tempdir().unwrap();
2317 let project = tmp.path().join("proj");
2318 std::fs::create_dir_all(&project).unwrap();
2319 let header = project.join("widget.h");
2320 std::fs::write(&header, "").unwrap();
2321 std::fs::write(project.join("widget.cpp"), "").unwrap();
2323
2324 let languages = c_cpp_languages();
2325 assert_eq!(
2326 detect_language(&header, &languages),
2327 Some("cpp".to_string())
2328 );
2329 }
2330
2331 #[test]
2332 fn test_detect_language_h_promotes_to_cpp_with_sibling_hpp() {
2333 let tmp = tempfile::tempdir().unwrap();
2334 let project = tmp.path().join("proj");
2335 std::fs::create_dir_all(&project).unwrap();
2336 let header = project.join("a.h");
2337 std::fs::write(&header, "").unwrap();
2338 std::fs::write(project.join("b.hpp"), "").unwrap();
2340
2341 let languages = c_cpp_languages();
2342 assert_eq!(
2343 detect_language(&header, &languages),
2344 Some("cpp".to_string())
2345 );
2346 }
2347
2348 #[test]
2349 fn test_detect_language_h_promotes_to_cpp_with_ancestor_compile_commands() {
2350 let tmp = tempfile::tempdir().unwrap();
2351 let project = tmp.path().join("proj");
2352 let include = project.join("include").join("fmt");
2353 std::fs::create_dir_all(&include).unwrap();
2354 std::fs::write(
2358 project.join("compile_commands.json"),
2359 r#"[{"directory":"/proj","command":"/usr/bin/clang++ -std=c++17 -c src/format.cc","file":"src/format.cc"}]"#,
2360 ).unwrap();
2361 let header = include.join("format.h");
2362 std::fs::write(&header, "").unwrap();
2363
2364 let languages = c_cpp_languages();
2365 assert_eq!(
2366 detect_language(&header, &languages),
2367 Some("cpp".to_string())
2368 );
2369 }
2370
2371 #[test]
2372 fn test_detect_language_h_stays_c_with_pure_c_compile_commands() {
2373 let tmp = tempfile::tempdir().unwrap();
2376 let project = tmp.path().join("cproj");
2377 let include = project.join("include");
2378 std::fs::create_dir_all(&include).unwrap();
2379 std::fs::write(
2380 project.join("compile_commands.json"),
2381 r#"[{"directory":"/cproj","command":"/usr/bin/gcc -std=c11 -c src/lib.c","file":"src/lib.c"}]"#,
2382 )
2383 .unwrap();
2384 let header = include.join("lib.h");
2385 std::fs::write(&header, "").unwrap();
2386
2387 let languages = c_cpp_languages();
2388 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2389 }
2390
2391 #[test]
2392 fn test_detect_language_h_stays_c_in_pure_c_tree() {
2393 let tmp = tempfile::tempdir().unwrap();
2394 let project = tmp.path().join("cproj");
2395 std::fs::create_dir_all(&project).unwrap();
2396 let header = project.join("lib.h");
2397 std::fs::write(&header, "").unwrap();
2398 std::fs::write(project.join("lib.c"), "").unwrap();
2400
2401 let languages = c_cpp_languages();
2402 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2403 }
2404
2405 #[test]
2406 fn test_detect_language_h_stays_c_with_empty_compile_commands() {
2407 let tmp = tempfile::tempdir().unwrap();
2410 let project = tmp.path().join("proj");
2411 std::fs::create_dir_all(&project).unwrap();
2412 std::fs::write(project.join("compile_commands.json"), "[]").unwrap();
2413 let header = project.join("foo.h");
2414 std::fs::write(&header, "").unwrap();
2415
2416 let languages = c_cpp_languages();
2417 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2418 }
2419
2420 #[test]
2421 fn test_detect_language_h_promotes_on_cpp_std_flag_alone() {
2422 let tmp = tempfile::tempdir().unwrap();
2424 let project = tmp.path().join("proj");
2425 let include = project.join("include");
2426 std::fs::create_dir_all(&include).unwrap();
2427 std::fs::write(
2428 project.join("compile_commands.json"),
2429 r#"[{"directory":"/proj","command":"/usr/bin/clang -std=c++20 -c src/x.C","file":"src/x.C"}]"#,
2433 )
2434 .unwrap();
2435 let header = include.join("x.h");
2436 std::fs::write(&header, "").unwrap();
2437
2438 let languages = c_cpp_languages();
2439 assert_eq!(
2440 detect_language(&header, &languages),
2441 Some("cpp".to_string())
2442 );
2443 }
2444
2445 #[test]
2446 fn test_detect_language_c_source_never_promoted() {
2447 let tmp = tempfile::tempdir().unwrap();
2449 let project = tmp.path().join("proj");
2450 std::fs::create_dir_all(&project).unwrap();
2451 let source = project.join("legacy.c");
2452 std::fs::write(&source, "").unwrap();
2453 std::fs::write(project.join("main.cpp"), "").unwrap();
2454
2455 let languages = c_cpp_languages();
2456 assert_eq!(detect_language(&source, &languages), Some("c".to_string()));
2457 }
2458
2459 #[test]
2460 fn test_detect_language_h_no_promotion_without_cpp_config() {
2461 let tmp = tempfile::tempdir().unwrap();
2464 let project = tmp.path().join("proj");
2465 std::fs::create_dir_all(&project).unwrap();
2466 let header = project.join("widget.h");
2467 std::fs::write(&header, "").unwrap();
2468 std::fs::write(project.join("widget.cpp"), "").unwrap();
2469
2470 let mut languages = c_cpp_languages();
2471 languages.remove("cpp");
2472 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2473 }
2474
2475 #[test]
2476 fn test_path_to_uri_basic() {
2477 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
2478 assert_eq!(uri.as_str(), "file:///tmp/test");
2479 }
2480
2481 #[test]
2482 fn test_path_to_uri_with_spaces() {
2483 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
2484 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
2485 }
2486}