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 restart_attempts: HashMap<String, Vec<Instant>>,
305
306 restart_cooldown: HashSet<String>,
308
309 pending_restarts: HashMap<String, Instant>,
311
312 allowed_languages: HashSet<String>,
315
316 disabled_languages: HashSet<String>,
319}
320
321impl LspManager {
322 pub fn new(root_uri: Option<Uri>) -> Self {
324 Self {
325 handles: Vec::new(),
326 config: HashMap::new(),
327 universal_configs: Vec::new(),
328 root_uri,
329 per_language_root_uris: HashMap::new(),
330 runtime: None,
331 async_bridge: None,
332 long_running_spawner: None,
333 restart_attempts: HashMap::new(),
334 restart_cooldown: HashSet::new(),
335 pending_restarts: HashMap::new(),
336 allowed_languages: HashSet::new(),
337 disabled_languages: HashSet::new(),
338 }
339 }
340
341 pub fn set_long_running_spawner(
350 &mut self,
351 spawner: std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>,
352 ) {
353 self.long_running_spawner = Some(spawner);
354 }
355
356 pub fn command_exists_via_authority(&self, command: &str) -> bool {
370 if command.is_empty() {
371 return false;
372 }
373 let (Some(runtime), Some(spawner)) =
374 (self.runtime.as_ref(), self.long_running_spawner.as_ref())
375 else {
376 return crate::services::lsp::command_exists(command);
377 };
378 runtime.block_on(spawner.command_exists(command))
379 }
380
381 pub fn is_language_allowed(&self, language: &str) -> bool {
383 self.allowed_languages.contains(language)
384 }
385
386 pub fn allow_language(&mut self, language: &str) {
388 self.allowed_languages.insert(language.to_string());
389 tracing::info!("LSP language '{}' manually enabled", language);
390 }
391
392 pub fn allowed_languages(&self) -> &HashSet<String> {
394 &self.allowed_languages
395 }
396
397 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
399 self.config.get(language).map(|v| v.as_slice())
400 }
401
402 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
404 self.config.get(language).and_then(|v| v.first())
405 }
406
407 pub fn set_server_capabilities(
409 &mut self,
410 _language: &str,
411 server_name: &str,
412 mut capabilities: ServerCapabilitySummary,
413 ) {
414 capabilities.initialized = true;
415
416 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
417 sh.capabilities = capabilities;
418 }
419 }
420
421 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
423 self.get_handles(language).into_iter().find_map(|sh| {
424 if sh.feature_filter.allows(LspFeature::SemanticTokens)
425 && sh.has_capability(LspFeature::SemanticTokens)
426 {
427 sh.capabilities.semantic_tokens_legend.as_ref()
428 } else {
429 None
430 }
431 })
432 }
433
434 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
436 self.get_handles(language).iter().any(|sh| {
437 sh.feature_filter.allows(LspFeature::SemanticTokens)
438 && sh.capabilities.semantic_tokens_full
439 })
440 }
441
442 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
444 self.get_handles(language).iter().any(|sh| {
445 sh.feature_filter.allows(LspFeature::SemanticTokens)
446 && sh.capabilities.semantic_tokens_full_delta
447 })
448 }
449
450 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
452 self.get_handles(language).iter().any(|sh| {
453 sh.feature_filter.allows(LspFeature::SemanticTokens)
454 && sh.capabilities.semantic_tokens_range
455 })
456 }
457
458 pub fn folding_ranges_supported(&self, language: &str) -> bool {
460 self.get_handles(language).iter().any(|sh| {
461 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
462 })
463 }
464
465 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
467 let ch_str = ch.to_string();
468 self.get_handles(language).iter().any(|sh| {
469 sh.feature_filter.allows(LspFeature::Completion)
470 && sh
471 .capabilities
472 .completion_trigger_characters
473 .contains(&ch_str)
474 })
475 }
476
477 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
492 if self
494 .handles
495 .iter()
496 .any(|sh| sh.handle.scope().accepts(language))
497 {
498 self.ensure_universal_servers_running(file_path);
499 return LspSpawnResult::Spawned;
500 }
501
502 if self.runtime.is_none() || self.async_bridge.is_none() {
504 return LspSpawnResult::Failed;
505 }
506
507 self.ensure_universal_servers_running(file_path);
509
510 let configs = match self.config.get(language) {
512 Some(configs) if !configs.is_empty() => configs,
513 _ => {
514 if self
516 .handles
517 .iter()
518 .any(|sh| sh.handle.scope().is_universal())
519 {
520 return LspSpawnResult::Spawned;
521 }
522 return LspSpawnResult::NotConfigured;
523 }
524 };
525
526 if !configs.iter().any(|c| c.enabled) {
528 if self
529 .handles
530 .iter()
531 .any(|sh| sh.handle.scope().is_universal())
532 {
533 return LspSpawnResult::Spawned;
534 }
535 return LspSpawnResult::Disabled;
536 }
537
538 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
540 if !any_auto_start && !self.allowed_languages.contains(language) {
541 if self
542 .handles
543 .iter()
544 .any(|sh| sh.handle.scope().is_universal())
545 {
546 return LspSpawnResult::Spawned;
547 }
548 return LspSpawnResult::NotAutoStart;
549 }
550
551 let spawned = self.force_spawn(language, file_path).is_some();
553
554 if spawned
555 || self
556 .handles
557 .iter()
558 .any(|sh| sh.handle.scope().is_universal())
559 {
560 LspSpawnResult::Spawned
561 } else {
562 LspSpawnResult::Failed
563 }
564 }
565
566 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
570 self.runtime = Some(runtime);
571 self.async_bridge = Some(async_bridge);
572 }
573
574 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
576 self.config.insert(language, vec![config]);
577 }
578
579 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
581 self.config.insert(language, configs);
582 }
583
584 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
586 self.config.entry(language).or_default().extend(configs);
587 }
588
589 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
594 self.universal_configs = configs;
595 }
596
597 pub fn configured_languages(&self) -> Vec<String> {
599 self.config.keys().cloned().collect()
600 }
601
602 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
607 self.root_uri = root_uri;
608 }
609
610 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
616 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
617 self.per_language_root_uris
618 .insert(language.to_string(), uri.clone());
619
620 if self
622 .handles
623 .iter()
624 .any(|sh| sh.handle.scope().accepts(language))
625 {
626 tracing::info!(
627 "Restarting {} LSP server with new root: {}",
628 language,
629 uri.as_str()
630 );
631 self.shutdown_server(language);
632 return true;
634 }
635 false
636 }
637
638 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
645 if let Some(uri) = self.per_language_root_uris.get(language) {
647 return Some(uri.clone());
648 }
649
650 if let Some(path) = file_path {
652 let markers = self
653 .config
654 .get(language)
655 .and_then(|configs| configs.first())
656 .map(|c| c.root_markers.as_slice())
657 .unwrap_or(&[]);
658 let root = detect_workspace_root(path, markers);
659 if let Some(uri) = path_to_uri(&root) {
660 return Some(uri);
661 }
662 }
663
664 self.root_uri.clone()
666 }
667
668 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
672 self.resolve_root_uri(language, None)
673 }
674
675 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
680 self.shutdown_all();
682
683 self.root_uri = new_root_uri;
685
686 self.restart_attempts.clear();
688 self.restart_cooldown.clear();
689 self.pending_restarts.clear();
690
691 tracing::info!(
695 "LSP manager reset for new project: {:?}",
696 self.root_uri.as_ref().map(|u| u.as_str())
697 );
698 }
699
700 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
703 self.handles
704 .iter()
705 .find(|sh| sh.handle.scope().accepts(language))
706 .map(|sh| &sh.handle)
707 }
708
709 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
712 self.handles
713 .iter_mut()
714 .find(|sh| sh.handle.scope().accepts(language))
715 .map(|sh| &mut sh.handle)
716 }
717
718 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
720 self.handles
721 .iter()
722 .filter(|sh| sh.handle.scope().accepts(language))
723 .collect()
724 }
725
726 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
728 self.handles
729 .iter_mut()
730 .filter(|sh| sh.handle.scope().accepts(language))
731 .collect()
732 }
733
734 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
738 self.handles
739 .iter()
740 .find(|sh| sh.name == server_name)
741 .map(|sh| sh.handle.scope())
742 }
743
744 pub fn has_handles(&self, language: &str) -> bool {
746 self.handles
747 .iter()
748 .any(|sh| sh.handle.scope().accepts(language))
749 }
750
751 pub fn handle_count(&self, language: &str) -> usize {
753 self.handles
754 .iter()
755 .filter(|sh| sh.handle.scope().accepts(language))
756 .count()
757 }
758
759 pub fn has_server_named(&self, server_name: &str) -> bool {
761 self.handles.iter().any(|sh| sh.name == server_name)
762 }
763
764 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
770 self.handles
771 .iter()
772 .filter(|sh| sh.handle.scope().accepts(language))
773 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
774 }
775
776 pub fn handle_for_feature_mut(
780 &mut self,
781 language: &str,
782 feature: LspFeature,
783 ) -> Option<&mut ServerHandle> {
784 self.handles
785 .iter_mut()
786 .filter(|sh| sh.handle.scope().accepts(language))
787 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
788 }
789
790 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
794 self.handles
795 .iter()
796 .filter(|sh| sh.handle.scope().accepts(language))
797 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
798 .collect()
799 }
800
801 pub fn handles_for_feature_mut(
805 &mut self,
806 language: &str,
807 feature: LspFeature,
808 ) -> Vec<&mut ServerHandle> {
809 self.handles
810 .iter_mut()
811 .filter(|sh| sh.handle.scope().accepts(language))
812 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
813 .collect()
814 }
815
816 fn spawn_decision(&mut self, language: &str) -> SpawnDecision {
824 if self
825 .handles
826 .iter()
827 .any(|sh| sh.handle.scope().accepts(language))
828 {
829 return SpawnDecision::Existing;
830 }
831 if self.restart_cooldown.contains(language) {
832 return SpawnDecision::CooledDown;
833 }
834 if self.pending_restarts.contains_key(language) {
835 return SpawnDecision::PendingBackoff;
836 }
837
838 let now = Instant::now();
839 let window = Duration::from_secs(RESTART_WINDOW_SECS);
840 let attempts = self
841 .restart_attempts
842 .entry(language.to_string())
843 .or_default();
844 attempts.retain(|t| now.duration_since(*t) < window);
845
846 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
847 self.restart_cooldown.insert(language.to_string());
848 tracing::warn!(
849 "LSP server for {} has spawned {} times in {} minutes, entering cooldown",
850 language,
851 MAX_RESTARTS_IN_WINDOW,
852 RESTART_WINDOW_SECS / 60
853 );
854 return SpawnDecision::CooledDown;
855 }
856
857 attempts.push(now);
858 SpawnDecision::Allow
859 }
860
861 pub fn force_spawn(
880 &mut self,
881 language: &str,
882 file_path: Option<&Path>,
883 ) -> Option<&mut LspHandle> {
884 tracing::debug!("force_spawn called for language: {}", language);
885
886 if self
888 .handles
889 .iter()
890 .any(|sh| sh.handle.scope().accepts(language))
891 {
892 tracing::debug!("force_spawn: returning existing handle for {}", language);
893 return self
894 .handles
895 .iter_mut()
896 .find(|sh| sh.handle.scope().accepts(language))
897 .map(|sh| &mut sh.handle);
898 }
899
900 if self.disabled_languages.contains(language) {
902 tracing::debug!(
903 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
904 language
905 );
906 return None;
907 }
908
909 let configs = match self.config.get(language) {
911 Some(configs) if !configs.is_empty() => configs.clone(),
912 _ => {
913 tracing::warn!(
914 "force_spawn: no config found for language '{}', available configs: {:?}",
915 language,
916 self.config.keys().collect::<Vec<_>>()
917 );
918 return None;
919 }
920 };
921
922 match self.spawn_decision(language) {
928 SpawnDecision::Existing => {
929 return self
932 .handles
933 .iter_mut()
934 .find(|sh| sh.handle.scope().accepts(language))
935 .map(|sh| &mut sh.handle);
936 }
937 SpawnDecision::CooledDown => {
938 tracing::debug!(
939 "force_spawn: {} is in cooldown, refusing spawn (use Restart LSP command)",
940 language
941 );
942 return None;
943 }
944 SpawnDecision::PendingBackoff => {
945 tracing::debug!(
946 "force_spawn: {} has a pending restart scheduled, not double-spawning",
947 language
948 );
949 return None;
950 }
951 SpawnDecision::Allow => {}
952 }
953
954 let runtime = match self.runtime.as_ref() {
959 Some(r) => r.clone(),
960 None => {
961 tracing::error!("force_spawn: no tokio runtime available for {}", language);
962 return None;
963 }
964 };
965 let async_bridge = match self.async_bridge.as_ref() {
966 Some(b) => b.clone(),
967 None => {
968 tracing::error!("force_spawn: no async bridge available for {}", language);
969 return None;
970 }
971 };
972 let long_running_spawner = match self.long_running_spawner.as_ref() {
980 Some(s) => s.clone(),
981 None => {
982 tracing::warn!(
983 "force_spawn: long-running spawner not wired for {} — \
984 falling back to host-local spawn (normal for tests \
985 that skip set_boot_authority)",
986 language
987 );
988 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
989 }
990 };
991
992 let mut spawned_handles = Vec::new();
993 let manually_allowed = self.allowed_languages.contains(language);
994
995 for config in &configs {
996 if manually_allowed {
997 } else {
1001 if !config.enabled || !config.auto_start {
1007 continue;
1008 }
1009 }
1010
1011 if config.command.is_empty() {
1012 tracing::warn!(
1013 "force_spawn: LSP command is empty for {} server '{}'",
1014 language,
1015 config.display_name()
1016 );
1017 continue;
1018 }
1019
1020 let server_name = config.display_name();
1021 tracing::info!(
1022 "Spawning LSP server '{}' for language: {}",
1023 server_name,
1024 language
1025 );
1026
1027 match LspHandle::spawn(
1028 &runtime,
1029 &config.command,
1030 &config.args,
1031 config.env.clone(),
1032 LanguageScope::single(language),
1033 server_name.clone(),
1034 &async_bridge,
1035 config.process_limits.clone(),
1036 config.language_id_overrides.clone(),
1037 long_running_spawner.clone(),
1038 ) {
1039 Ok(handle) => {
1040 let effective_root = self.resolve_root_uri(language, file_path);
1041 if let Err(e) =
1042 handle.initialize(effective_root, config.initialization_options.clone())
1043 {
1044 tracing::error!(
1045 "Failed to send initialize command for {} ({}): {}",
1046 language,
1047 server_name,
1048 e
1049 );
1050 continue;
1051 }
1052
1053 tracing::info!(
1054 "LSP initialization started for {} ({}), will be ready asynchronously",
1055 language,
1056 server_name
1057 );
1058
1059 spawned_handles.push(ServerHandle {
1060 name: server_name,
1061 handle,
1062 feature_filter: config.feature_filter(),
1063 capabilities: ServerCapabilitySummary::default(),
1064 });
1065 }
1066 Err(e) => {
1067 tracing::error!(
1068 "Failed to spawn LSP handle for {} ({}): {}",
1069 language,
1070 server_name,
1071 e
1072 );
1073 }
1074 }
1075 }
1076
1077 if spawned_handles.is_empty() {
1078 return None;
1079 }
1080
1081 self.handles.extend(spawned_handles);
1082 self.handles
1083 .iter_mut()
1084 .rev()
1085 .find(|sh| sh.handle.scope().accepts(language))
1086 .map(|sh| &mut sh.handle)
1087 }
1088
1089 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
1095 if self
1096 .handles
1097 .iter()
1098 .any(|sh| sh.handle.scope().is_universal())
1099 || self.universal_configs.is_empty()
1100 {
1101 return;
1102 }
1103
1104 let runtime = match self.runtime.as_ref() {
1105 Some(r) => r.clone(),
1106 None => return,
1107 };
1108 let async_bridge = match self.async_bridge.as_ref() {
1109 Some(b) => b.clone(),
1110 None => return,
1111 };
1112 let long_running_spawner =
1113 self.long_running_spawner
1114 .as_ref()
1115 .cloned()
1116 .unwrap_or_else(|| {
1117 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1118 });
1119
1120 let mut spawned = Vec::new();
1121 for config in &self.universal_configs {
1122 if !config.enabled || !config.auto_start {
1123 continue;
1124 }
1125 if config.command.is_empty() {
1126 continue;
1127 }
1128
1129 let server_name = config.display_name();
1130 tracing::info!("Spawning universal LSP server '{}'", server_name);
1131
1132 match LspHandle::spawn(
1133 &runtime,
1134 &config.command,
1135 &config.args,
1136 config.env.clone(),
1137 LanguageScope::all(),
1138 server_name.clone(),
1139 &async_bridge,
1140 config.process_limits.clone(),
1141 config.language_id_overrides.clone(),
1142 long_running_spawner.clone(),
1143 ) {
1144 Ok(handle) => {
1145 let effective_root = file_path
1146 .map(|p| {
1147 let root = detect_workspace_root(p, &config.root_markers);
1148 path_to_uri(&root)
1149 })
1150 .flatten()
1151 .or_else(|| self.root_uri.clone());
1152 if let Err(e) =
1153 handle.initialize(effective_root, config.initialization_options.clone())
1154 {
1155 tracing::error!(
1156 "Failed to initialize universal LSP server '{}': {}",
1157 server_name,
1158 e
1159 );
1160 continue;
1161 }
1162 tracing::info!(
1163 "Universal LSP server '{}' initialization started",
1164 server_name
1165 );
1166 spawned.push(ServerHandle {
1167 name: server_name,
1168 handle,
1169 feature_filter: config.feature_filter(),
1170 capabilities: ServerCapabilitySummary::default(),
1171 });
1172 }
1173 Err(e) => {
1174 tracing::error!(
1175 "Failed to spawn universal LSP server '{}': {}",
1176 server_name,
1177 e
1178 );
1179 }
1180 }
1181 }
1182
1183 self.handles.extend(spawned);
1184 }
1185
1186 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1190 if self
1192 .handles
1193 .iter()
1194 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1195 {
1196 let universals: Vec<ServerHandle> = {
1198 let mut drained = Vec::new();
1199 let mut i = 0;
1200 while i < self.handles.len() {
1201 if self.handles[i].handle.scope().is_universal() {
1202 drained.push(self.handles.remove(i));
1203 } else {
1204 i += 1;
1205 }
1206 }
1207 drained
1208 };
1209 for sh in universals {
1210 fire_and_forget(sh.handle.shutdown());
1211 }
1212 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1214 }
1215
1216 {
1218 let mut i = 0;
1219 while i < self.handles.len() {
1220 if !self.handles[i].handle.scope().is_universal()
1221 && self.handles[i].handle.scope().accepts(language)
1222 {
1223 let sh = self.handles.remove(i);
1224 fire_and_forget(sh.handle.shutdown());
1225 } else {
1226 i += 1;
1227 }
1228 }
1229 }
1230
1231 if self.disabled_languages.contains(language) {
1234 return format!(
1235 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1236 language
1237 );
1238 }
1239
1240 if self.restart_cooldown.contains(language) {
1242 return format!(
1243 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1244 language
1245 );
1246 }
1247
1248 let now = Instant::now();
1253 let attempt_number = self
1254 .restart_attempts
1255 .get(language)
1256 .map(|v| v.len())
1257 .unwrap_or(0);
1258
1259 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1261
1262 self.pending_restarts
1263 .insert(language.to_string(), restart_time);
1264
1265 tracing::info!(
1266 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1267 language,
1268 attempt_number + 1,
1269 MAX_RESTARTS_IN_WINDOW,
1270 delay_ms
1271 );
1272
1273 format!(
1274 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1275 language,
1276 attempt_number + 1,
1277 MAX_RESTARTS_IN_WINDOW,
1278 delay_ms / 1000
1279 )
1280 }
1281
1282 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1286 let now = Instant::now();
1287 let mut results = Vec::new();
1288
1289 let due_restarts: Vec<String> = self
1291 .pending_restarts
1292 .iter()
1293 .filter(|(_, time)| **time <= now)
1294 .map(|(lang, _)| lang.clone())
1295 .collect();
1296
1297 for language in due_restarts {
1298 self.pending_restarts.remove(&language);
1299
1300 if self.force_spawn(&language, None).is_some() {
1304 let message = format!("LSP server for {} restarted successfully", language);
1305 tracing::info!("{}", message);
1306 results.push((language, true, message));
1307 } else {
1308 let message = format!("Failed to restart LSP server for {}", language);
1309 tracing::error!("{}", message);
1310 results.push((language, false, message));
1311 }
1312 }
1313
1314 results
1315 }
1316
1317 pub fn is_in_cooldown(&self, language: &str) -> bool {
1319 self.restart_cooldown.contains(language)
1320 }
1321
1322 pub fn has_pending_restart(&self, language: &str) -> bool {
1324 self.pending_restarts.contains_key(language)
1325 }
1326
1327 pub fn clear_cooldown(&mut self, language: &str) {
1329 self.restart_cooldown.remove(language);
1330 self.restart_attempts.remove(language);
1331 self.pending_restarts.remove(language);
1332 tracing::info!("Cleared restart cooldown for {}", language);
1333 }
1334
1335 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1342 self.clear_cooldown(language);
1344
1345 self.disabled_languages.remove(language);
1347
1348 self.allowed_languages.insert(language.to_string());
1350
1351 {
1353 let mut i = 0;
1354 while i < self.handles.len() {
1355 if !self.handles[i].handle.scope().is_universal()
1356 && self.handles[i].handle.scope().accepts(language)
1357 {
1358 let sh = self.handles.remove(i);
1359 fire_and_forget(sh.handle.shutdown());
1360 } else {
1361 i += 1;
1362 }
1363 }
1364 }
1365
1366 if self.force_spawn(language, file_path).is_some() {
1368 let message = format!("LSP server for {} started", language);
1369 tracing::info!("{}", message);
1370 (true, message)
1371 } else {
1372 let message = format!("Failed to start LSP server for {}", language);
1373 tracing::error!("{}", message);
1374 (false, message)
1375 }
1376 }
1377
1378 pub fn manual_restart_server(
1383 &mut self,
1384 language: &str,
1385 server_name: &str,
1386 file_path: Option<&Path>,
1387 ) -> (bool, String) {
1388 self.clear_cooldown(language);
1389 self.disabled_languages.remove(language);
1390 self.allowed_languages.insert(language.to_string());
1391
1392 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1394 let sh = self.handles.remove(idx);
1395 fire_and_forget(sh.handle.shutdown());
1396 }
1397
1398 let is_universal = self
1400 .universal_configs
1401 .iter()
1402 .any(|c| c.display_name() == server_name);
1403 let config = if is_universal {
1404 self.universal_configs
1405 .iter()
1406 .find(|c| c.display_name() == server_name)
1407 .cloned()
1408 } else {
1409 self.config
1410 .get(language)
1411 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1412 .cloned()
1413 };
1414
1415 let Some(config) = config else {
1416 let message = format!(
1417 "No config found for server '{}' ({})",
1418 server_name, language
1419 );
1420 tracing::error!("{}", message);
1421 return (false, message);
1422 };
1423
1424 if config.command.is_empty() {
1425 let message = format!(
1426 "LSP command is empty for {} server '{}'",
1427 language, server_name
1428 );
1429 tracing::error!("{}", message);
1430 return (false, message);
1431 }
1432
1433 let runtime = match self.runtime.as_ref() {
1434 Some(r) => r.clone(),
1435 None => return (false, "No tokio runtime available".to_string()),
1436 };
1437 let async_bridge = match self.async_bridge.as_ref() {
1438 Some(b) => b.clone(),
1439 None => return (false, "No async bridge available".to_string()),
1440 };
1441 let long_running_spawner =
1442 self.long_running_spawner
1443 .as_ref()
1444 .cloned()
1445 .unwrap_or_else(|| {
1446 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner)
1447 });
1448
1449 let scope = if is_universal {
1450 LanguageScope::all()
1451 } else {
1452 LanguageScope::single(language)
1453 };
1454
1455 match LspHandle::spawn(
1456 &runtime,
1457 &config.command,
1458 &config.args,
1459 config.env.clone(),
1460 scope,
1461 server_name.to_string(),
1462 &async_bridge,
1463 config.process_limits.clone(),
1464 config.language_id_overrides.clone(),
1465 long_running_spawner,
1466 ) {
1467 Ok(handle) => {
1468 let effective_root = if is_universal {
1469 file_path
1470 .map(|p| {
1471 let root = detect_workspace_root(p, &config.root_markers);
1472 path_to_uri(&root)
1473 })
1474 .flatten()
1475 .or_else(|| self.root_uri.clone())
1476 } else {
1477 self.resolve_root_uri(language, file_path)
1478 };
1479 if let Err(e) =
1480 handle.initialize(effective_root, config.initialization_options.clone())
1481 {
1482 let message = format!(
1483 "Failed to initialize LSP server '{}' for {}: {}",
1484 server_name, language, e
1485 );
1486 tracing::error!("{}", message);
1487 return (false, message);
1488 }
1489
1490 let sh = ServerHandle {
1491 name: server_name.to_string(),
1492 handle,
1493 feature_filter: config.feature_filter(),
1494 capabilities: ServerCapabilitySummary::default(),
1495 };
1496
1497 self.handles.push(sh);
1498
1499 let message = format!("LSP server '{}' for {} started", server_name, language);
1500 tracing::info!("{}", message);
1501 (true, message)
1502 }
1503 Err(e) => {
1504 let message = format!(
1505 "Failed to start LSP server '{}' for {}: {}",
1506 server_name, language, e
1507 );
1508 tracing::error!("{}", message);
1509 (false, message)
1510 }
1511 }
1512 }
1513
1514 pub fn restart_attempt_count(&self, language: &str) -> usize {
1516 let now = Instant::now();
1517 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1518 self.restart_attempts
1519 .get(language)
1520 .map(|attempts| {
1521 attempts
1522 .iter()
1523 .filter(|t| now.duration_since(**t) < window)
1524 .count()
1525 })
1526 .unwrap_or(0)
1527 }
1528
1529 pub fn running_servers(&self) -> Vec<String> {
1531 let mut labels: Vec<String> = self
1532 .handles
1533 .iter()
1534 .map(|sh| sh.handle.scope().label().to_string())
1535 .collect();
1536 labels.sort();
1537 labels.dedup();
1538 labels
1539 }
1540
1541 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1543 self.handles
1544 .iter()
1545 .filter(|sh| sh.handle.scope().accepts(language))
1546 .map(|sh| sh.name.clone())
1547 .collect()
1548 }
1549
1550 pub fn is_server_ready(&self, language: &str) -> bool {
1552 self.handles
1553 .iter()
1554 .filter(|sh| sh.handle.scope().accepts(language))
1555 .any(|sh| sh.handle.state().can_send_requests())
1556 }
1557
1558 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1563 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1564 tracing::warn!(
1565 "No running LSP server named '{}' found for {}",
1566 server_name,
1567 language
1568 );
1569 return false;
1570 };
1571
1572 let sh = self.handles.remove(idx);
1573 tracing::info!(
1574 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1575 sh.name,
1576 language
1577 );
1578 fire_and_forget(sh.handle.shutdown());
1579
1580 let has_remaining = self
1582 .handles
1583 .iter()
1584 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1585 if !has_remaining {
1586 self.disabled_languages.insert(language.to_string());
1587 self.pending_restarts.remove(language);
1588 self.restart_cooldown.remove(language);
1589 self.allowed_languages.remove(language);
1590 }
1591
1592 true
1593 }
1594
1595 pub fn shutdown_server(&mut self, language: &str) -> bool {
1600 let mut found = false;
1601 let mut i = 0;
1602 while i < self.handles.len() {
1603 if !self.handles[i].handle.scope().is_universal()
1604 && self.handles[i].handle.scope().accepts(language)
1605 {
1606 let sh = self.handles.remove(i);
1607 tracing::info!(
1608 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1609 sh.name,
1610 language
1611 );
1612 fire_and_forget(sh.handle.shutdown());
1613 found = true;
1614 } else {
1615 i += 1;
1616 }
1617 }
1618
1619 if found {
1620 self.disabled_languages.insert(language.to_string());
1621 self.pending_restarts.remove(language);
1622 self.restart_cooldown.remove(language);
1623 self.allowed_languages.remove(language);
1624 } else {
1625 tracing::warn!("No running LSP server found for {}", language);
1626 }
1627
1628 found
1629 }
1630
1631 pub fn shutdown_all(&mut self) {
1633 for sh in &self.handles {
1634 tracing::info!(
1635 "Shutting down LSP server '{}' ({})",
1636 sh.name,
1637 sh.handle.scope().label()
1638 );
1639 fire_and_forget(sh.handle.shutdown());
1640 }
1641 self.handles.clear();
1642 }
1643}
1644
1645impl Drop for LspManager {
1646 fn drop(&mut self) {
1647 self.shutdown_all();
1648 }
1649}
1650
1651pub fn detect_language(
1663 path: &std::path::Path,
1664 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1665) -> Option<String> {
1666 let detected = detect_language_by_config(path, languages);
1667
1668 if detected.as_deref() == Some("c")
1674 && path.extension().and_then(|e| e.to_str()) == Some("h")
1675 && languages.contains_key("cpp")
1676 && header_in_cpp_tree(path)
1677 {
1678 return Some("cpp".to_string());
1679 }
1680
1681 detected
1682}
1683
1684fn detect_language_by_config(
1686 path: &std::path::Path,
1687 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1688) -> Option<String> {
1689 use crate::primitives::glob_match::{
1690 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1691 };
1692
1693 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1694 for (language_name, lang_config) in languages {
1696 if lang_config
1697 .filenames
1698 .iter()
1699 .any(|f| !is_glob_pattern(f) && f == filename)
1700 {
1701 return Some(language_name.clone());
1702 }
1703 }
1704
1705 let path_str = path.to_str().unwrap_or("");
1709 for (language_name, lang_config) in languages {
1710 if lang_config.filenames.iter().any(|f| {
1711 if !is_glob_pattern(f) {
1712 return false;
1713 }
1714 if is_path_pattern(f) {
1715 path_glob_matches(f, path_str)
1716 } else {
1717 filename_glob_matches(f, filename)
1718 }
1719 }) {
1720 return Some(language_name.clone());
1721 }
1722 }
1723 }
1724
1725 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1727 for (language_name, lang_config) in languages {
1728 if lang_config.extensions.iter().any(|ext| ext == extension) {
1729 return Some(language_name.clone());
1730 }
1731 }
1732 }
1733
1734 None
1735}
1736
1737fn header_in_cpp_tree(path: &std::path::Path) -> bool {
1767 let Some(start_dir) = path.parent() else {
1768 return false;
1769 };
1770
1771 if let Ok(entries) = std::fs::read_dir(start_dir) {
1773 for entry in entries.flatten() {
1774 let p = entry.path();
1775 let Some(ext) = p.extension().and_then(|e| e.to_str()) else {
1776 continue;
1777 };
1778 if matches!(
1779 ext,
1780 "cc" | "cpp" | "cxx" | "C" | "c++" | "hpp" | "hh" | "hxx"
1781 ) {
1782 return true;
1783 }
1784 }
1785 }
1786
1787 let mut current = Some(start_dir);
1791 let mut depth = 0u32;
1792 while let Some(dir) = current {
1793 let cc = dir.join("compile_commands.json");
1794 if cc.is_file() && compile_commands_has_cpp_marker(&cc) {
1795 return true;
1796 }
1797 if depth >= 10 {
1798 break;
1799 }
1800 depth += 1;
1801 current = dir.parent();
1802 }
1803
1804 false
1805}
1806
1807fn compile_commands_has_cpp_marker(path: &std::path::Path) -> bool {
1815 use std::io::Read;
1816 const MAX_READ: u64 = 1_048_576;
1817
1818 let Ok(file) = std::fs::File::open(path) else {
1819 return false;
1820 };
1821 let mut buf = Vec::with_capacity(64 * 1024);
1822 if file.take(MAX_READ).read_to_end(&mut buf).is_err() {
1823 return false;
1824 }
1825 let Ok(text) = std::str::from_utf8(&buf) else {
1826 return false;
1827 };
1828
1829 if text.contains("c++") {
1833 return true;
1834 }
1835 text.contains(".cpp") || text.contains(".cxx") || text.contains(".cc\"")
1838}
1839
1840#[cfg(test)]
1841mod tests {
1842 use super::*;
1843 use std::path::Path;
1844
1845 #[test]
1846 fn test_lsp_manager_new() {
1847 let root_uri: Option<Uri> = "file:///test".parse().ok();
1848 let manager = LspManager::new(root_uri.clone());
1849
1850 assert_eq!(manager.handles.len(), 0);
1852 assert_eq!(manager.config.len(), 0);
1853 assert!(manager.root_uri.is_some());
1854 assert!(manager.runtime.is_none());
1855 assert!(manager.async_bridge.is_none());
1856 }
1857
1858 #[test]
1859 fn test_lsp_manager_set_language_config() {
1860 let mut manager = LspManager::new(None);
1861
1862 let config = LspServerConfig {
1863 enabled: true,
1864 command: "rust-analyzer".to_string(),
1865 args: vec![],
1866 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1867 auto_start: false,
1868 initialization_options: None,
1869 env: Default::default(),
1870 language_id_overrides: Default::default(),
1871 name: None,
1872 only_features: None,
1873 except_features: None,
1874 root_markers: Default::default(),
1875 };
1876
1877 manager.set_language_config("rust".to_string(), config);
1878
1879 assert_eq!(manager.config.len(), 1);
1880 assert!(manager.config.contains_key("rust"));
1881 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1882 }
1883
1884 #[test]
1885 fn test_lsp_manager_force_spawn_no_runtime() {
1886 let mut manager = LspManager::new(None);
1887
1888 manager.set_language_config(
1890 "rust".to_string(),
1891 LspServerConfig {
1892 enabled: true,
1893 command: "rust-analyzer".to_string(),
1894 args: vec![],
1895 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1896 auto_start: false,
1897 initialization_options: None,
1898 env: Default::default(),
1899 language_id_overrides: Default::default(),
1900 name: None,
1901 only_features: None,
1902 except_features: None,
1903 root_markers: Default::default(),
1904 },
1905 );
1906
1907 let result = manager.force_spawn("rust", None);
1909 assert!(result.is_none());
1910 }
1911
1912 #[test]
1913 fn test_lsp_manager_force_spawn_no_config() {
1914 let rt = tokio::runtime::Runtime::new().unwrap();
1915 let mut manager = LspManager::new(None);
1916 let async_bridge = AsyncBridge::new();
1917
1918 manager.set_runtime(rt.handle().clone(), async_bridge);
1919
1920 let result = manager.force_spawn("rust", None);
1922 assert!(result.is_none());
1923 }
1924
1925 #[test]
1926 fn test_lsp_manager_force_spawn_disabled_language() {
1927 let rt = tokio::runtime::Runtime::new().unwrap();
1928 let mut manager = LspManager::new(None);
1929 let async_bridge = AsyncBridge::new();
1930
1931 manager.set_runtime(rt.handle().clone(), async_bridge);
1932
1933 manager.set_language_config(
1935 "rust".to_string(),
1936 LspServerConfig {
1937 enabled: false,
1938 command: String::new(), args: vec![],
1940 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1941 auto_start: false,
1942 initialization_options: None,
1943 env: Default::default(),
1944 language_id_overrides: Default::default(),
1945 name: None,
1946 only_features: None,
1947 except_features: None,
1948 root_markers: Default::default(),
1949 },
1950 );
1951
1952 let result = manager.force_spawn("rust", None);
1954 assert!(result.is_none());
1955 }
1956
1957 #[test]
1963 fn test_lsp_manager_try_spawn_returns_disabled_when_all_configs_disabled() {
1964 let rt = tokio::runtime::Runtime::new().unwrap();
1965 let mut manager = LspManager::new(None);
1966 let async_bridge = AsyncBridge::new();
1967 manager.set_runtime(rt.handle().clone(), async_bridge);
1968
1969 manager.set_language_config(
1970 "rust".to_string(),
1971 LspServerConfig {
1972 enabled: false,
1973 command: String::new(),
1974 args: vec![],
1975 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1976 auto_start: false,
1977 initialization_options: None,
1978 env: Default::default(),
1979 language_id_overrides: Default::default(),
1980 name: None,
1981 only_features: None,
1982 except_features: None,
1983 root_markers: Default::default(),
1984 },
1985 );
1986
1987 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
1988 }
1989
1990 #[test]
1991 fn test_lsp_manager_shutdown_all() {
1992 let mut manager = LspManager::new(None);
1993
1994 manager.shutdown_all();
1996 assert_eq!(manager.handles.len(), 0);
1997 }
1998
1999 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2000 let mut languages = std::collections::HashMap::new();
2001 languages.insert(
2002 "rust".to_string(),
2003 crate::config::LanguageConfig {
2004 extensions: vec!["rs".to_string()],
2005 filenames: vec![],
2006 grammar: "rust".to_string(),
2007 comment_prefix: Some("//".to_string()),
2008 auto_indent: true,
2009 auto_close: None,
2010 auto_surround: None,
2011 textmate_grammar: None,
2012 show_whitespace_tabs: false,
2013 line_wrap: None,
2014 wrap_column: None,
2015 page_view: None,
2016 page_width: None,
2017 use_tabs: None,
2018 tab_size: None,
2019 formatter: None,
2020 format_on_save: false,
2021 on_save: vec![],
2022 word_characters: None,
2023 },
2024 );
2025 languages.insert(
2026 "javascript".to_string(),
2027 crate::config::LanguageConfig {
2028 extensions: vec!["js".to_string(), "jsx".to_string()],
2029 filenames: vec![],
2030 grammar: "javascript".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 "csharp".to_string(),
2051 crate::config::LanguageConfig {
2052 extensions: vec!["cs".to_string()],
2053 filenames: vec![],
2054 grammar: "c_sharp".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
2074 }
2075
2076 #[test]
2077 fn test_detect_language_from_config() {
2078 let languages = test_languages();
2079
2080 assert_eq!(
2082 detect_language(Path::new("main.rs"), &languages),
2083 Some("rust".to_string())
2084 );
2085 assert_eq!(
2086 detect_language(Path::new("index.js"), &languages),
2087 Some("javascript".to_string())
2088 );
2089 assert_eq!(
2090 detect_language(Path::new("App.jsx"), &languages),
2091 Some("javascript".to_string())
2092 );
2093 assert_eq!(
2094 detect_language(Path::new("Program.cs"), &languages),
2095 Some("csharp".to_string())
2096 );
2097
2098 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
2100 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
2101 assert_eq!(detect_language(Path::new("file"), &languages), None);
2102 }
2103
2104 #[test]
2105 fn test_detect_language_no_extension() {
2106 let languages = test_languages();
2107 assert_eq!(detect_language(Path::new("README"), &languages), None);
2108 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
2109 }
2110
2111 #[test]
2112 fn test_detect_language_path_glob() {
2113 let mut languages = test_languages();
2114 languages.insert(
2115 "shell".to_string(),
2116 crate::config::LanguageConfig {
2117 extensions: vec!["sh".to_string()],
2118 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
2119 grammar: "bash".to_string(),
2120 comment_prefix: Some("#".to_string()),
2121 auto_indent: true,
2122 auto_close: None,
2123 auto_surround: None,
2124 textmate_grammar: None,
2125 show_whitespace_tabs: false,
2126 line_wrap: None,
2127 wrap_column: None,
2128 page_view: None,
2129 page_width: None,
2130 use_tabs: None,
2131 tab_size: None,
2132 formatter: None,
2133 format_on_save: false,
2134 on_save: vec![],
2135 word_characters: None,
2136 },
2137 );
2138
2139 assert_eq!(
2141 detect_language(Path::new("/etc/rc.conf"), &languages),
2142 Some("shell".to_string())
2143 );
2144 assert_eq!(
2145 detect_language(Path::new("/etc/init/rc.local"), &languages),
2146 Some("shell".to_string())
2147 );
2148 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
2150
2151 assert_eq!(
2153 detect_language(Path::new("lfrc"), &languages),
2154 Some("shell".to_string())
2155 );
2156 }
2157
2158 #[test]
2159 fn test_detect_workspace_root_finds_marker_in_parent() {
2160 let tmp = tempfile::tempdir().unwrap();
2161 let project = tmp.path().join("myproject");
2162 let src = project.join("src");
2163 std::fs::create_dir_all(&src).unwrap();
2164 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2165 let file = src.join("main.rs");
2166 std::fs::write(&file, "").unwrap();
2167
2168 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
2169 assert_eq!(root, project);
2170 }
2171
2172 #[test]
2173 fn test_detect_workspace_root_finds_marker_two_levels_up() {
2174 let tmp = tempfile::tempdir().unwrap();
2175 let project = tmp.path().join("myproject");
2176 let deep = project.join("src").join("nested");
2177 std::fs::create_dir_all(&deep).unwrap();
2178 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2179 let file = deep.join("lib.rs");
2180 std::fs::write(&file, "").unwrap();
2181
2182 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
2183 assert_eq!(root, project);
2184 }
2185
2186 #[test]
2187 fn test_detect_workspace_root_no_marker_returns_parent() {
2188 let tmp = tempfile::tempdir().unwrap();
2189 let dir = tmp.path().join("somedir");
2190 std::fs::create_dir_all(&dir).unwrap();
2191 let file = dir.join("file.txt");
2192 std::fs::write(&file, "").unwrap();
2193
2194 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
2195 assert_eq!(root, dir);
2196 }
2197
2198 #[test]
2199 fn test_detect_workspace_root_empty_markers_returns_parent() {
2200 let tmp = tempfile::tempdir().unwrap();
2201 let dir = tmp.path().join("somedir");
2202 std::fs::create_dir_all(&dir).unwrap();
2203 let file = dir.join("file.txt");
2204 std::fs::write(&file, "").unwrap();
2205
2206 let root = detect_workspace_root(&file, &[]);
2207 assert_eq!(root, dir);
2208 }
2209
2210 #[test]
2211 fn test_detect_workspace_root_directory_marker() {
2212 let tmp = tempfile::tempdir().unwrap();
2213 let project = tmp.path().join("myproject");
2214 let src = project.join("src");
2215 std::fs::create_dir_all(&src).unwrap();
2216 std::fs::create_dir_all(project.join(".git")).unwrap();
2217 let file = src.join("main.rs");
2218 std::fs::write(&file, "").unwrap();
2219
2220 let root = detect_workspace_root(&file, &[".git".to_string()]);
2221 assert_eq!(root, project);
2222 }
2223
2224 fn c_cpp_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2229 use crate::config::LanguageConfig;
2230 let mut languages = std::collections::HashMap::new();
2231 let base = LanguageConfig {
2232 extensions: vec![],
2233 filenames: vec![],
2234 grammar: String::new(),
2235 comment_prefix: Some("//".to_string()),
2236 auto_indent: true,
2237 auto_close: None,
2238 auto_surround: None,
2239 textmate_grammar: None,
2240 show_whitespace_tabs: false,
2241 line_wrap: None,
2242 wrap_column: None,
2243 page_view: None,
2244 page_width: None,
2245 use_tabs: None,
2246 tab_size: None,
2247 formatter: None,
2248 format_on_save: false,
2249 on_save: vec![],
2250 word_characters: None,
2251 };
2252 languages.insert(
2253 "c".to_string(),
2254 LanguageConfig {
2255 extensions: vec!["c".to_string(), "h".to_string()],
2256 grammar: "c".to_string(),
2257 ..base.clone()
2258 },
2259 );
2260 languages.insert(
2261 "cpp".to_string(),
2262 LanguageConfig {
2263 extensions: vec![
2264 "cpp".to_string(),
2265 "cc".to_string(),
2266 "cxx".to_string(),
2267 "hpp".to_string(),
2268 "hh".to_string(),
2269 "hxx".to_string(),
2270 ],
2271 grammar: "cpp".to_string(),
2272 ..base
2273 },
2274 );
2275 languages
2276 }
2277
2278 #[test]
2279 fn test_detect_language_h_stays_c_without_cpp_signals() {
2280 let languages = c_cpp_languages();
2284 assert_eq!(
2285 detect_language(Path::new("foo.h"), &languages),
2286 Some("c".to_string())
2287 );
2288 }
2289
2290 #[test]
2291 fn test_detect_language_h_promotes_to_cpp_with_sibling_cpp_source() {
2292 let tmp = tempfile::tempdir().unwrap();
2293 let project = tmp.path().join("proj");
2294 std::fs::create_dir_all(&project).unwrap();
2295 let header = project.join("widget.h");
2296 std::fs::write(&header, "").unwrap();
2297 std::fs::write(project.join("widget.cpp"), "").unwrap();
2299
2300 let languages = c_cpp_languages();
2301 assert_eq!(
2302 detect_language(&header, &languages),
2303 Some("cpp".to_string())
2304 );
2305 }
2306
2307 #[test]
2308 fn test_detect_language_h_promotes_to_cpp_with_sibling_hpp() {
2309 let tmp = tempfile::tempdir().unwrap();
2310 let project = tmp.path().join("proj");
2311 std::fs::create_dir_all(&project).unwrap();
2312 let header = project.join("a.h");
2313 std::fs::write(&header, "").unwrap();
2314 std::fs::write(project.join("b.hpp"), "").unwrap();
2316
2317 let languages = c_cpp_languages();
2318 assert_eq!(
2319 detect_language(&header, &languages),
2320 Some("cpp".to_string())
2321 );
2322 }
2323
2324 #[test]
2325 fn test_detect_language_h_promotes_to_cpp_with_ancestor_compile_commands() {
2326 let tmp = tempfile::tempdir().unwrap();
2327 let project = tmp.path().join("proj");
2328 let include = project.join("include").join("fmt");
2329 std::fs::create_dir_all(&include).unwrap();
2330 std::fs::write(
2334 project.join("compile_commands.json"),
2335 r#"[{"directory":"/proj","command":"/usr/bin/clang++ -std=c++17 -c src/format.cc","file":"src/format.cc"}]"#,
2336 ).unwrap();
2337 let header = include.join("format.h");
2338 std::fs::write(&header, "").unwrap();
2339
2340 let languages = c_cpp_languages();
2341 assert_eq!(
2342 detect_language(&header, &languages),
2343 Some("cpp".to_string())
2344 );
2345 }
2346
2347 #[test]
2348 fn test_detect_language_h_stays_c_with_pure_c_compile_commands() {
2349 let tmp = tempfile::tempdir().unwrap();
2352 let project = tmp.path().join("cproj");
2353 let include = project.join("include");
2354 std::fs::create_dir_all(&include).unwrap();
2355 std::fs::write(
2356 project.join("compile_commands.json"),
2357 r#"[{"directory":"/cproj","command":"/usr/bin/gcc -std=c11 -c src/lib.c","file":"src/lib.c"}]"#,
2358 )
2359 .unwrap();
2360 let header = include.join("lib.h");
2361 std::fs::write(&header, "").unwrap();
2362
2363 let languages = c_cpp_languages();
2364 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2365 }
2366
2367 #[test]
2368 fn test_detect_language_h_stays_c_in_pure_c_tree() {
2369 let tmp = tempfile::tempdir().unwrap();
2370 let project = tmp.path().join("cproj");
2371 std::fs::create_dir_all(&project).unwrap();
2372 let header = project.join("lib.h");
2373 std::fs::write(&header, "").unwrap();
2374 std::fs::write(project.join("lib.c"), "").unwrap();
2376
2377 let languages = c_cpp_languages();
2378 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2379 }
2380
2381 #[test]
2382 fn test_detect_language_h_stays_c_with_empty_compile_commands() {
2383 let tmp = tempfile::tempdir().unwrap();
2386 let project = tmp.path().join("proj");
2387 std::fs::create_dir_all(&project).unwrap();
2388 std::fs::write(project.join("compile_commands.json"), "[]").unwrap();
2389 let header = project.join("foo.h");
2390 std::fs::write(&header, "").unwrap();
2391
2392 let languages = c_cpp_languages();
2393 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2394 }
2395
2396 #[test]
2397 fn test_detect_language_h_promotes_on_cpp_std_flag_alone() {
2398 let tmp = tempfile::tempdir().unwrap();
2400 let project = tmp.path().join("proj");
2401 let include = project.join("include");
2402 std::fs::create_dir_all(&include).unwrap();
2403 std::fs::write(
2404 project.join("compile_commands.json"),
2405 r#"[{"directory":"/proj","command":"/usr/bin/clang -std=c++20 -c src/x.C","file":"src/x.C"}]"#,
2409 )
2410 .unwrap();
2411 let header = include.join("x.h");
2412 std::fs::write(&header, "").unwrap();
2413
2414 let languages = c_cpp_languages();
2415 assert_eq!(
2416 detect_language(&header, &languages),
2417 Some("cpp".to_string())
2418 );
2419 }
2420
2421 #[test]
2422 fn test_detect_language_c_source_never_promoted() {
2423 let tmp = tempfile::tempdir().unwrap();
2425 let project = tmp.path().join("proj");
2426 std::fs::create_dir_all(&project).unwrap();
2427 let source = project.join("legacy.c");
2428 std::fs::write(&source, "").unwrap();
2429 std::fs::write(project.join("main.cpp"), "").unwrap();
2430
2431 let languages = c_cpp_languages();
2432 assert_eq!(detect_language(&source, &languages), Some("c".to_string()));
2433 }
2434
2435 #[test]
2436 fn test_detect_language_h_no_promotion_without_cpp_config() {
2437 let tmp = tempfile::tempdir().unwrap();
2440 let project = tmp.path().join("proj");
2441 std::fs::create_dir_all(&project).unwrap();
2442 let header = project.join("widget.h");
2443 std::fs::write(&header, "").unwrap();
2444 std::fs::write(project.join("widget.cpp"), "").unwrap();
2445
2446 let mut languages = c_cpp_languages();
2447 languages.remove("cpp");
2448 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2449 }
2450
2451 #[test]
2452 fn test_path_to_uri_basic() {
2453 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
2454 assert_eq!(uri.as_str(), "file:///tmp/test");
2455 }
2456
2457 #[test]
2458 fn test_path_to_uri_with_spaces() {
2459 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
2460 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
2461 }
2462}