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 implementation: bool,
203 pub references: bool,
204 pub document_formatting: bool,
205 pub document_range_formatting: bool,
206 pub rename: bool,
207 pub signature_help: bool,
208 pub inlay_hints: bool,
209 pub folding_ranges: bool,
210 pub semantic_tokens_full: bool,
211 pub semantic_tokens_full_delta: bool,
212 pub semantic_tokens_range: bool,
213 pub semantic_tokens_legend: Option<SemanticTokensLegend>,
214 pub document_highlight: bool,
215 pub code_action: bool,
216 pub code_action_resolve: bool,
217 pub document_symbols: bool,
218 pub workspace_symbols: bool,
219 pub diagnostics: bool,
220}
221
222impl ServerCapabilitySummary {
223 pub fn apply_dynamic_registration(
238 &mut self,
239 method: &str,
240 register_options: Option<&serde_json::Value>,
241 register: bool,
242 ) -> bool {
243 use lsp_types::SemanticTokensFullOptions;
244
245 match method {
246 "textDocument/hover" => self.hover = register,
247 "textDocument/completion" => {
248 self.completion = register;
249 if register {
250 if let Some(opts) = register_options {
251 if let Some(chars) =
252 opts.get("triggerCharacters").and_then(|v| v.as_array())
253 {
254 self.completion_trigger_characters = chars
255 .iter()
256 .filter_map(|v| v.as_str().map(str::to_string))
257 .collect();
258 }
259 if let Some(resolve) = opts
260 .get("resolveProvider")
261 .and_then(serde_json::Value::as_bool)
262 {
263 self.completion_resolve = resolve;
264 }
265 }
266 } else {
267 self.completion_trigger_characters.clear();
268 self.completion_resolve = false;
269 }
270 }
271 "textDocument/definition" => self.definition = register,
272 "textDocument/implementation" => self.implementation = register,
273 "textDocument/references" => self.references = register,
274 "textDocument/formatting" => self.document_formatting = register,
275 "textDocument/rangeFormatting" => self.document_range_formatting = register,
276 "textDocument/rename" => self.rename = register,
277 "textDocument/signatureHelp" => self.signature_help = register,
278 "textDocument/inlayHint" => self.inlay_hints = register,
279 "textDocument/foldingRange" => self.folding_ranges = register,
280 "textDocument/documentHighlight" => self.document_highlight = register,
281 "textDocument/codeAction" => {
282 self.code_action = register;
283 if register {
284 if let Some(resolve) = register_options
285 .and_then(|opts| opts.get("resolveProvider"))
286 .and_then(serde_json::Value::as_bool)
287 {
288 self.code_action_resolve = resolve;
289 }
290 } else {
291 self.code_action_resolve = false;
292 }
293 }
294 "textDocument/documentSymbol" => self.document_symbols = register,
295 "workspace/symbol" => self.workspace_symbols = register,
296 "textDocument/diagnostic" => self.diagnostics = register,
297 "textDocument/semanticTokens" => {
298 if register {
299 match register_options.and_then(|opts| {
305 serde_json::from_value::<lsp_types::SemanticTokensOptions>(opts.clone())
306 .ok()
307 }) {
308 Some(opts) => {
309 self.semantic_tokens_legend = Some(opts.legend);
310 match opts.full {
311 Some(SemanticTokensFullOptions::Bool(v)) => {
312 self.semantic_tokens_full = v;
313 self.semantic_tokens_full_delta = false;
314 }
315 Some(SemanticTokensFullOptions::Delta { delta }) => {
316 self.semantic_tokens_full = true;
317 self.semantic_tokens_full_delta = delta.unwrap_or(false);
318 }
319 None => {
320 self.semantic_tokens_full = false;
321 self.semantic_tokens_full_delta = false;
322 }
323 }
324 self.semantic_tokens_range = opts.range.unwrap_or(false);
325 }
326 None => self.semantic_tokens_full = true,
330 }
331 } else {
332 self.semantic_tokens_full = false;
333 self.semantic_tokens_full_delta = false;
334 self.semantic_tokens_range = false;
335 self.semantic_tokens_legend = None;
336 }
337 }
338 _ => return false,
339 }
340 true
341 }
342}
343
344pub struct ServerHandle {
348 pub name: String,
350 pub handle: LspHandle,
352 pub feature_filter: FeatureFilter,
354 pub capabilities: ServerCapabilitySummary,
356}
357
358impl ServerHandle {
359 pub fn has_capability(&self, feature: LspFeature) -> bool {
368 if !self.capabilities.initialized {
369 return false;
370 }
371 match feature {
372 LspFeature::Hover => self.capabilities.hover,
373 LspFeature::Completion => self.capabilities.completion,
374 LspFeature::Definition => self.capabilities.definition,
375 LspFeature::Implementation => self.capabilities.implementation,
376 LspFeature::References => self.capabilities.references,
377 LspFeature::Format => {
378 self.capabilities.document_formatting || self.capabilities.document_range_formatting
379 }
380 LspFeature::Rename => self.capabilities.rename,
381 LspFeature::SignatureHelp => self.capabilities.signature_help,
382 LspFeature::InlayHints => self.capabilities.inlay_hints,
383 LspFeature::FoldingRange => self.capabilities.folding_ranges,
384 LspFeature::SemanticTokens => {
385 self.capabilities.semantic_tokens_full || self.capabilities.semantic_tokens_range
386 }
387 LspFeature::DocumentHighlight => self.capabilities.document_highlight,
388 LspFeature::CodeAction => self.capabilities.code_action,
389 LspFeature::DocumentSymbols => self.capabilities.document_symbols,
390 LspFeature::WorkspaceSymbols => self.capabilities.workspace_symbols,
391 LspFeature::Diagnostics => self.capabilities.diagnostics,
392 }
393 }
394}
395
396pub struct LspManager {
398 window_id: fresh_core::WindowId,
404
405 handles: Vec<ServerHandle>,
408
409 config: HashMap<String, Vec<LspServerConfig>>,
411
412 universal_configs: Vec<LspServerConfig>,
414
415 root_uri: Option<Uri>,
417
418 per_language_root_uris: HashMap<String, Uri>,
420
421 runtime: Option<tokio::runtime::Handle>,
423
424 async_bridge: Option<AsyncBridge>,
426
427 long_running_spawner: Option<std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>>,
433
434 workspace_trust: Option<std::sync::Arc<crate::services::workspace_trust::WorkspaceTrust>>,
440
441 path_translation: Option<crate::services::authority::PathTranslation>,
446
447 restart_attempts: HashMap<String, Vec<Instant>>,
449
450 restart_cooldown: HashSet<String>,
452
453 pending_restarts: HashMap<String, Instant>,
455
456 allowed_languages: HashSet<String>,
459
460 disabled_languages: HashSet<String>,
463
464 globally_enabled: bool,
470}
471
472impl LspManager {
473 pub fn window_id(&self) -> fresh_core::WindowId {
475 self.window_id
476 }
477
478 pub fn new(window_id: fresh_core::WindowId, root_uri: Option<Uri>) -> Self {
480 Self {
481 window_id,
482 handles: Vec::new(),
483 config: HashMap::new(),
484 universal_configs: Vec::new(),
485 root_uri,
486 per_language_root_uris: HashMap::new(),
487 runtime: None,
488 async_bridge: None,
489 long_running_spawner: None,
490 workspace_trust: None,
491 path_translation: None,
492 restart_attempts: HashMap::new(),
493 restart_cooldown: HashSet::new(),
494 pending_restarts: HashMap::new(),
495 allowed_languages: HashSet::new(),
496 disabled_languages: HashSet::new(),
497 globally_enabled: true,
498 }
499 }
500
501 pub fn set_globally_enabled(&mut self, enabled: bool) {
504 self.globally_enabled = enabled;
505 }
506
507 pub fn set_long_running_spawner(
516 &mut self,
517 spawner: std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>,
518 ) {
519 self.long_running_spawner = Some(spawner);
520 }
521
522 pub fn set_workspace_trust(
526 &mut self,
527 trust: std::sync::Arc<crate::services::workspace_trust::WorkspaceTrust>,
528 ) {
529 self.workspace_trust = Some(trust);
530 }
531
532 fn lsp_autostart_allowed(&self) -> bool {
536 use crate::services::workspace_trust::TrustLevel;
537 self.workspace_trust
538 .as_ref()
539 .map(|t| t.level() == TrustLevel::Trusted)
540 .unwrap_or(true)
541 }
542
543 pub fn set_path_translation(
548 &mut self,
549 translation: Option<crate::services::authority::PathTranslation>,
550 ) {
551 self.path_translation = translation;
552 }
553
554 pub fn command_exists_via_authority(&self, command: &str) -> bool {
568 if command.is_empty() {
569 return false;
570 }
571 let (Some(runtime), Some(spawner)) =
572 (self.runtime.as_ref(), self.long_running_spawner.as_ref())
573 else {
574 return crate::services::lsp::command_exists(command);
575 };
576 runtime.block_on(spawner.command_exists(command))
577 }
578
579 pub fn is_language_allowed(&self, language: &str) -> bool {
581 self.allowed_languages.contains(language)
582 }
583
584 pub fn allow_language(&mut self, language: &str) {
586 self.allowed_languages.insert(language.to_string());
587 tracing::info!("LSP language '{}' manually enabled", language);
588 }
589
590 pub fn allowed_languages(&self) -> &HashSet<String> {
592 &self.allowed_languages
593 }
594
595 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
597 self.config.get(language).map(|v| v.as_slice())
598 }
599
600 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
602 self.config.get(language).and_then(|v| v.first())
603 }
604
605 pub fn set_server_capabilities(
607 &mut self,
608 _language: &str,
609 server_name: &str,
610 mut capabilities: ServerCapabilitySummary,
611 ) {
612 capabilities.initialized = true;
613
614 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
615 sh.capabilities = capabilities;
616 }
617 }
618
619 pub fn apply_dynamic_capabilities(
629 &mut self,
630 server_name: &str,
631 register: bool,
632 registrations: &[(String, Option<serde_json::Value>)],
633 ) -> bool {
634 let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) else {
635 return false;
636 };
637 let mut changed = false;
638 for (method, options) in registrations {
639 if sh
640 .capabilities
641 .apply_dynamic_registration(method, options.as_ref(), register)
642 {
643 changed = true;
644 }
645 }
646 changed
647 }
648
649 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
651 self.get_handles(language).into_iter().find_map(|sh| {
652 if sh.feature_filter.allows(LspFeature::SemanticTokens)
653 && sh.has_capability(LspFeature::SemanticTokens)
654 {
655 sh.capabilities.semantic_tokens_legend.as_ref()
656 } else {
657 None
658 }
659 })
660 }
661
662 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
664 self.get_handles(language).iter().any(|sh| {
665 sh.feature_filter.allows(LspFeature::SemanticTokens)
666 && sh.capabilities.semantic_tokens_full
667 })
668 }
669
670 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
672 self.get_handles(language).iter().any(|sh| {
673 sh.feature_filter.allows(LspFeature::SemanticTokens)
674 && sh.capabilities.semantic_tokens_full_delta
675 })
676 }
677
678 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
680 self.get_handles(language).iter().any(|sh| {
681 sh.feature_filter.allows(LspFeature::SemanticTokens)
682 && sh.capabilities.semantic_tokens_range
683 })
684 }
685
686 pub fn folding_ranges_supported(&self, language: &str) -> bool {
688 self.get_handles(language).iter().any(|sh| {
689 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
690 })
691 }
692
693 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
695 let ch_str = ch.to_string();
696 self.get_handles(language).iter().any(|sh| {
697 sh.feature_filter.allows(LspFeature::Completion)
698 && sh
699 .capabilities
700 .completion_trigger_characters
701 .contains(&ch_str)
702 })
703 }
704
705 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
720 if self
722 .handles
723 .iter()
724 .any(|sh| sh.handle.scope().accepts(language))
725 {
726 self.ensure_universal_servers_running(file_path);
727 return LspSpawnResult::Spawned;
728 }
729
730 if !self.globally_enabled {
735 tracing::debug!(
736 "LSP for '{}' not auto-started: LSP is globally disabled (lsp_enabled=false)",
737 language
738 );
739 return LspSpawnResult::Disabled;
740 }
741
742 if self.runtime.is_none() || self.async_bridge.is_none() {
744 return LspSpawnResult::Failed;
745 }
746
747 if !self.lsp_autostart_allowed() && !self.allowed_languages.contains(language) {
754 tracing::info!(
755 "LSP for '{}' not auto-started: workspace is not trusted \
756 (trust the folder to enable language servers)",
757 language
758 );
759 return LspSpawnResult::NotAutoStart;
760 }
761
762 self.ensure_universal_servers_running(file_path);
764
765 let configs = match self.config.get(language) {
767 Some(configs) if !configs.is_empty() => configs,
768 _ => {
769 if self
771 .handles
772 .iter()
773 .any(|sh| sh.handle.scope().is_universal())
774 {
775 return LspSpawnResult::Spawned;
776 }
777 return LspSpawnResult::NotConfigured;
778 }
779 };
780
781 if !configs.iter().any(|c| c.enabled) {
783 if self
784 .handles
785 .iter()
786 .any(|sh| sh.handle.scope().is_universal())
787 {
788 return LspSpawnResult::Spawned;
789 }
790 return LspSpawnResult::Disabled;
791 }
792
793 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
795 if !any_auto_start && !self.allowed_languages.contains(language) {
796 if self
797 .handles
798 .iter()
799 .any(|sh| sh.handle.scope().is_universal())
800 {
801 return LspSpawnResult::Spawned;
802 }
803 return LspSpawnResult::NotAutoStart;
804 }
805
806 let spawned = self.force_spawn(language, file_path).is_some();
808
809 if spawned
810 || self
811 .handles
812 .iter()
813 .any(|sh| sh.handle.scope().is_universal())
814 {
815 LspSpawnResult::Spawned
816 } else {
817 LspSpawnResult::Failed
818 }
819 }
820
821 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
825 self.runtime = Some(runtime);
826 self.async_bridge = Some(async_bridge);
827 }
828
829 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
831 self.config.insert(language, vec![config]);
832 }
833
834 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
836 self.config.insert(language, configs);
837 }
838
839 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
841 self.config.entry(language).or_default().extend(configs);
842 }
843
844 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
849 self.universal_configs = configs;
850 }
851
852 pub fn configured_languages(&self) -> Vec<String> {
854 self.config.keys().cloned().collect()
855 }
856
857 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
862 self.root_uri = root_uri;
863 }
864
865 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
871 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
872 self.per_language_root_uris
873 .insert(language.to_string(), uri.clone());
874
875 if self
877 .handles
878 .iter()
879 .any(|sh| sh.handle.scope().accepts(language))
880 {
881 tracing::info!(
882 "Restarting {} LSP server with new root: {}",
883 language,
884 uri.as_str()
885 );
886 self.shutdown_server(language);
887 return true;
889 }
890 false
891 }
892
893 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
900 if let Some(uri) = self.per_language_root_uris.get(language) {
902 return Some(uri.clone());
903 }
904
905 if let Some(path) = file_path {
910 let markers = self
911 .config
912 .get(language)
913 .and_then(|configs| configs.first())
914 .map(|c| c.root_markers.as_slice())
915 .unwrap_or(&[]);
916 let root = detect_workspace_root(path, markers);
917 let mapped = self
918 .path_translation
919 .as_ref()
920 .and_then(|t| t.host_to_remote(&root))
921 .unwrap_or(root);
922 if let Some(uri) = path_to_uri(&mapped) {
923 return Some(uri);
924 }
925 }
926
927 self.root_uri.clone()
929 }
930
931 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
935 self.resolve_root_uri(language, None)
936 }
937
938 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
943 self.shutdown_all();
945
946 self.root_uri = new_root_uri;
948
949 self.restart_attempts.clear();
951 self.restart_cooldown.clear();
952 self.pending_restarts.clear();
953
954 tracing::info!(
958 "LSP manager reset for new project: {:?}",
959 self.root_uri.as_ref().map(|u| u.as_str())
960 );
961 }
962
963 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
966 self.handles
967 .iter()
968 .find(|sh| sh.handle.scope().accepts(language))
969 .map(|sh| &sh.handle)
970 }
971
972 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
975 self.handles
976 .iter_mut()
977 .find(|sh| sh.handle.scope().accepts(language))
978 .map(|sh| &mut sh.handle)
979 }
980
981 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
983 self.handles
984 .iter()
985 .filter(|sh| sh.handle.scope().accepts(language))
986 .collect()
987 }
988
989 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
991 self.handles
992 .iter_mut()
993 .filter(|sh| sh.handle.scope().accepts(language))
994 .collect()
995 }
996
997 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
1001 self.handles
1002 .iter()
1003 .find(|sh| sh.name == server_name)
1004 .map(|sh| sh.handle.scope())
1005 }
1006
1007 pub fn has_handles(&self, language: &str) -> bool {
1009 self.handles
1010 .iter()
1011 .any(|sh| sh.handle.scope().accepts(language))
1012 }
1013
1014 pub fn handle_count(&self, language: &str) -> usize {
1016 self.handles
1017 .iter()
1018 .filter(|sh| sh.handle.scope().accepts(language))
1019 .count()
1020 }
1021
1022 pub fn has_server_named(&self, server_name: &str) -> bool {
1024 self.handles.iter().any(|sh| sh.name == server_name)
1025 }
1026
1027 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
1033 self.handles
1034 .iter()
1035 .filter(|sh| sh.handle.scope().accepts(language))
1036 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1037 }
1038
1039 pub fn handle_for_feature_mut(
1043 &mut self,
1044 language: &str,
1045 feature: LspFeature,
1046 ) -> Option<&mut ServerHandle> {
1047 self.handles
1048 .iter_mut()
1049 .filter(|sh| sh.handle.scope().accepts(language))
1050 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1051 }
1052
1053 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
1057 self.handles
1058 .iter()
1059 .filter(|sh| sh.handle.scope().accepts(language))
1060 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1061 .collect()
1062 }
1063
1064 pub fn handles_for_feature_mut(
1068 &mut self,
1069 language: &str,
1070 feature: LspFeature,
1071 ) -> Vec<&mut ServerHandle> {
1072 self.handles
1073 .iter_mut()
1074 .filter(|sh| sh.handle.scope().accepts(language))
1075 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1076 .collect()
1077 }
1078
1079 fn spawn_decision(&mut self, language: &str) -> SpawnDecision {
1087 if self
1088 .handles
1089 .iter()
1090 .any(|sh| sh.handle.scope().accepts(language))
1091 {
1092 return SpawnDecision::Existing;
1093 }
1094 if self.restart_cooldown.contains(language) {
1095 return SpawnDecision::CooledDown;
1096 }
1097 if self.pending_restarts.contains_key(language) {
1098 return SpawnDecision::PendingBackoff;
1099 }
1100
1101 let now = Instant::now();
1102 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1103 let attempts = self
1104 .restart_attempts
1105 .entry(language.to_string())
1106 .or_default();
1107 attempts.retain(|t| now.duration_since(*t) < window);
1108
1109 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
1110 self.restart_cooldown.insert(language.to_string());
1111 tracing::warn!(
1112 "LSP server for {} has spawned {} times in {} minutes, entering cooldown",
1113 language,
1114 MAX_RESTARTS_IN_WINDOW,
1115 RESTART_WINDOW_SECS / 60
1116 );
1117 return SpawnDecision::CooledDown;
1118 }
1119
1120 attempts.push(now);
1121 SpawnDecision::Allow
1122 }
1123
1124 pub fn force_spawn(
1143 &mut self,
1144 language: &str,
1145 file_path: Option<&Path>,
1146 ) -> Option<&mut LspHandle> {
1147 tracing::debug!("force_spawn called for language: {}", language);
1148
1149 if self
1151 .handles
1152 .iter()
1153 .any(|sh| sh.handle.scope().accepts(language))
1154 {
1155 tracing::debug!("force_spawn: returning existing handle for {}", language);
1156 return self
1157 .handles
1158 .iter_mut()
1159 .find(|sh| sh.handle.scope().accepts(language))
1160 .map(|sh| &mut sh.handle);
1161 }
1162
1163 if self.disabled_languages.contains(language) {
1165 tracing::debug!(
1166 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
1167 language
1168 );
1169 return None;
1170 }
1171
1172 let configs = match self.config.get(language) {
1174 Some(configs) if !configs.is_empty() => configs.clone(),
1175 _ => {
1176 tracing::warn!(
1177 "force_spawn: no config found for language '{}', available configs: {:?}",
1178 language,
1179 self.config.keys().collect::<Vec<_>>()
1180 );
1181 return None;
1182 }
1183 };
1184
1185 match self.spawn_decision(language) {
1191 SpawnDecision::Existing => {
1192 return self
1195 .handles
1196 .iter_mut()
1197 .find(|sh| sh.handle.scope().accepts(language))
1198 .map(|sh| &mut sh.handle);
1199 }
1200 SpawnDecision::CooledDown => {
1201 tracing::debug!(
1202 "force_spawn: {} is in cooldown, refusing spawn (use Restart LSP command)",
1203 language
1204 );
1205 return None;
1206 }
1207 SpawnDecision::PendingBackoff => {
1208 tracing::debug!(
1209 "force_spawn: {} has a pending restart scheduled, not double-spawning",
1210 language
1211 );
1212 return None;
1213 }
1214 SpawnDecision::Allow => {}
1215 }
1216
1217 let runtime = match self.runtime.as_ref() {
1222 Some(r) => r.clone(),
1223 None => {
1224 tracing::error!("force_spawn: no tokio runtime available for {}", language);
1225 return None;
1226 }
1227 };
1228 let async_bridge = match self.async_bridge.as_ref() {
1229 Some(b) => b.clone(),
1230 None => {
1231 tracing::error!("force_spawn: no async bridge available for {}", language);
1232 return None;
1233 }
1234 };
1235 let long_running_spawner = match self.long_running_spawner.as_ref() {
1243 Some(s) => s.clone(),
1244 None => {
1245 tracing::warn!(
1246 "force_spawn: long-running spawner not wired for {} — \
1247 falling back to host-local spawn (normal for tests \
1248 that skip set_boot_authority)",
1249 language
1250 );
1251 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1252 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1253 std::sync::Arc::new(
1254 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1255 ),
1256 ))
1257 }
1258 };
1259
1260 let mut spawned_handles = Vec::new();
1261 let manually_allowed = self.allowed_languages.contains(language);
1262
1263 for config in &configs {
1264 if manually_allowed {
1265 } else {
1269 if !config.enabled || !config.auto_start {
1275 continue;
1276 }
1277 }
1278
1279 if config.command.is_empty() {
1280 tracing::warn!(
1281 "force_spawn: LSP command is empty for {} server '{}'",
1282 language,
1283 config.display_name()
1284 );
1285 continue;
1286 }
1287
1288 let server_name = config.display_name();
1289 tracing::info!(
1290 "Spawning LSP server '{}' for language: {}",
1291 server_name,
1292 language
1293 );
1294
1295 match LspHandle::spawn(
1296 &runtime,
1297 &config.command,
1298 &config.args,
1299 config.env.clone(),
1300 LanguageScope::single(language),
1301 server_name.clone(),
1302 &async_bridge,
1303 config.process_limits.clone(),
1304 config.language_id_overrides.clone(),
1305 long_running_spawner.clone(),
1306 ) {
1307 Ok(handle) => {
1308 let effective_root = self.resolve_root_uri(language, file_path);
1309 if let Err(e) =
1310 handle.initialize(effective_root, config.initialization_options.clone())
1311 {
1312 tracing::error!(
1313 "Failed to send initialize command for {} ({}): {}",
1314 language,
1315 server_name,
1316 e
1317 );
1318 continue;
1319 }
1320
1321 tracing::info!(
1322 "LSP initialization started for {} ({}), will be ready asynchronously",
1323 language,
1324 server_name
1325 );
1326
1327 spawned_handles.push(ServerHandle {
1328 name: server_name,
1329 handle,
1330 feature_filter: config.feature_filter(),
1331 capabilities: ServerCapabilitySummary::default(),
1332 });
1333 }
1334 Err(e) => {
1335 tracing::error!(
1336 "Failed to spawn LSP handle for {} ({}): {}",
1337 language,
1338 server_name,
1339 e
1340 );
1341 }
1342 }
1343 }
1344
1345 if spawned_handles.is_empty() {
1346 return None;
1347 }
1348
1349 self.handles.extend(spawned_handles);
1350 self.handles
1351 .iter_mut()
1352 .rev()
1353 .find(|sh| sh.handle.scope().accepts(language))
1354 .map(|sh| &mut sh.handle)
1355 }
1356
1357 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
1363 if !self.globally_enabled {
1367 return;
1368 }
1369 if self
1370 .handles
1371 .iter()
1372 .any(|sh| sh.handle.scope().is_universal())
1373 || self.universal_configs.is_empty()
1374 {
1375 return;
1376 }
1377
1378 let runtime = match self.runtime.as_ref() {
1379 Some(r) => r.clone(),
1380 None => return,
1381 };
1382 let async_bridge = match self.async_bridge.as_ref() {
1383 Some(b) => b.clone(),
1384 None => return,
1385 };
1386 let long_running_spawner =
1387 self.long_running_spawner
1388 .as_ref()
1389 .cloned()
1390 .unwrap_or_else(|| {
1391 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1392 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1393 std::sync::Arc::new(
1394 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1395 ),
1396 ))
1397 });
1398
1399 let mut spawned = Vec::new();
1400 for config in &self.universal_configs {
1401 if !config.enabled || !config.auto_start {
1402 continue;
1403 }
1404 if config.command.is_empty() {
1405 continue;
1406 }
1407
1408 let server_name = config.display_name();
1409 tracing::info!("Spawning universal LSP server '{}'", server_name);
1410
1411 match LspHandle::spawn(
1412 &runtime,
1413 &config.command,
1414 &config.args,
1415 config.env.clone(),
1416 LanguageScope::all(),
1417 server_name.clone(),
1418 &async_bridge,
1419 config.process_limits.clone(),
1420 config.language_id_overrides.clone(),
1421 long_running_spawner.clone(),
1422 ) {
1423 Ok(handle) => {
1424 let effective_root = file_path
1425 .and_then(|p| {
1426 let root = detect_workspace_root(p, &config.root_markers);
1427 path_to_uri(&root)
1428 })
1429 .or_else(|| self.root_uri.clone());
1430 if let Err(e) =
1431 handle.initialize(effective_root, config.initialization_options.clone())
1432 {
1433 tracing::error!(
1434 "Failed to initialize universal LSP server '{}': {}",
1435 server_name,
1436 e
1437 );
1438 continue;
1439 }
1440 tracing::info!(
1441 "Universal LSP server '{}' initialization started",
1442 server_name
1443 );
1444 spawned.push(ServerHandle {
1445 name: server_name,
1446 handle,
1447 feature_filter: config.feature_filter(),
1448 capabilities: ServerCapabilitySummary::default(),
1449 });
1450 }
1451 Err(e) => {
1452 tracing::error!(
1453 "Failed to spawn universal LSP server '{}': {}",
1454 server_name,
1455 e
1456 );
1457 }
1458 }
1459 }
1460
1461 self.handles.extend(spawned);
1462 }
1463
1464 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1468 if self
1470 .handles
1471 .iter()
1472 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1473 {
1474 let universals: Vec<ServerHandle> = {
1476 let mut drained = Vec::new();
1477 let mut i = 0;
1478 while i < self.handles.len() {
1479 if self.handles[i].handle.scope().is_universal() {
1480 drained.push(self.handles.remove(i));
1481 } else {
1482 i += 1;
1483 }
1484 }
1485 drained
1486 };
1487 for sh in universals {
1488 fire_and_forget(sh.handle.shutdown());
1489 }
1490 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1492 }
1493
1494 {
1496 let mut i = 0;
1497 while i < self.handles.len() {
1498 if !self.handles[i].handle.scope().is_universal()
1499 && self.handles[i].handle.scope().accepts(language)
1500 {
1501 let sh = self.handles.remove(i);
1502 fire_and_forget(sh.handle.shutdown());
1503 } else {
1504 i += 1;
1505 }
1506 }
1507 }
1508
1509 if self.disabled_languages.contains(language) {
1512 return format!(
1513 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1514 language
1515 );
1516 }
1517
1518 if self.restart_cooldown.contains(language) {
1520 return format!(
1521 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1522 language
1523 );
1524 }
1525
1526 let now = Instant::now();
1531 let attempt_number = self
1532 .restart_attempts
1533 .get(language)
1534 .map(|v| v.len())
1535 .unwrap_or(0);
1536
1537 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1539
1540 self.pending_restarts
1541 .insert(language.to_string(), restart_time);
1542
1543 tracing::info!(
1544 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1545 language,
1546 attempt_number + 1,
1547 MAX_RESTARTS_IN_WINDOW,
1548 delay_ms
1549 );
1550
1551 format!(
1552 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1553 language,
1554 attempt_number + 1,
1555 MAX_RESTARTS_IN_WINDOW,
1556 delay_ms / 1000
1557 )
1558 }
1559
1560 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1564 let now = Instant::now();
1565 let mut results = Vec::new();
1566
1567 let due_restarts: Vec<String> = self
1569 .pending_restarts
1570 .iter()
1571 .filter(|(_, time)| **time <= now)
1572 .map(|(lang, _)| lang.clone())
1573 .collect();
1574
1575 for language in due_restarts {
1576 self.pending_restarts.remove(&language);
1577
1578 if self.force_spawn(&language, None).is_some() {
1582 let message = format!("LSP server for {} restarted successfully", language);
1583 tracing::info!("{}", message);
1584 results.push((language, true, message));
1585 } else {
1586 let message = format!("Failed to restart LSP server for {}", language);
1587 tracing::error!("{}", message);
1588 results.push((language, false, message));
1589 }
1590 }
1591
1592 results
1593 }
1594
1595 pub fn is_in_cooldown(&self, language: &str) -> bool {
1597 self.restart_cooldown.contains(language)
1598 }
1599
1600 pub fn has_pending_restart(&self, language: &str) -> bool {
1602 self.pending_restarts.contains_key(language)
1603 }
1604
1605 pub fn clear_cooldown(&mut self, language: &str) {
1607 self.restart_cooldown.remove(language);
1608 self.restart_attempts.remove(language);
1609 self.pending_restarts.remove(language);
1610 tracing::info!("Cleared restart cooldown for {}", language);
1611 }
1612
1613 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1620 self.clear_cooldown(language);
1622
1623 self.disabled_languages.remove(language);
1625
1626 self.allowed_languages.insert(language.to_string());
1628
1629 {
1631 let mut i = 0;
1632 while i < self.handles.len() {
1633 if !self.handles[i].handle.scope().is_universal()
1634 && self.handles[i].handle.scope().accepts(language)
1635 {
1636 let sh = self.handles.remove(i);
1637 fire_and_forget(sh.handle.shutdown());
1638 } else {
1639 i += 1;
1640 }
1641 }
1642 }
1643
1644 if self.force_spawn(language, file_path).is_some() {
1646 let message = format!("LSP server for {} started", language);
1647 tracing::info!("{}", message);
1648 (true, message)
1649 } else {
1650 let message = format!("Failed to start LSP server for {}", language);
1651 tracing::error!("{}", message);
1652 (false, message)
1653 }
1654 }
1655
1656 pub fn manual_restart_server(
1661 &mut self,
1662 language: &str,
1663 server_name: &str,
1664 file_path: Option<&Path>,
1665 ) -> (bool, String) {
1666 self.clear_cooldown(language);
1667 self.disabled_languages.remove(language);
1668 self.allowed_languages.insert(language.to_string());
1669
1670 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1672 let sh = self.handles.remove(idx);
1673 fire_and_forget(sh.handle.shutdown());
1674 }
1675
1676 let is_universal = self
1678 .universal_configs
1679 .iter()
1680 .any(|c| c.display_name() == server_name);
1681 let config = if is_universal {
1682 self.universal_configs
1683 .iter()
1684 .find(|c| c.display_name() == server_name)
1685 .cloned()
1686 } else {
1687 self.config
1688 .get(language)
1689 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1690 .cloned()
1691 };
1692
1693 let Some(config) = config else {
1694 let message = format!(
1695 "No config found for server '{}' ({})",
1696 server_name, language
1697 );
1698 tracing::error!("{}", message);
1699 return (false, message);
1700 };
1701
1702 if config.command.is_empty() {
1703 let message = format!(
1704 "LSP command is empty for {} server '{}'",
1705 language, server_name
1706 );
1707 tracing::error!("{}", message);
1708 return (false, message);
1709 }
1710
1711 let runtime = match self.runtime.as_ref() {
1712 Some(r) => r.clone(),
1713 None => return (false, "No tokio runtime available".to_string()),
1714 };
1715 let async_bridge = match self.async_bridge.as_ref() {
1716 Some(b) => b.clone(),
1717 None => return (false, "No async bridge available".to_string()),
1718 };
1719 let long_running_spawner =
1720 self.long_running_spawner
1721 .as_ref()
1722 .cloned()
1723 .unwrap_or_else(|| {
1724 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1725 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1726 std::sync::Arc::new(
1727 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1728 ),
1729 ))
1730 });
1731
1732 let scope = if is_universal {
1733 LanguageScope::all()
1734 } else {
1735 LanguageScope::single(language)
1736 };
1737
1738 match LspHandle::spawn(
1739 &runtime,
1740 &config.command,
1741 &config.args,
1742 config.env.clone(),
1743 scope,
1744 server_name.to_string(),
1745 &async_bridge,
1746 config.process_limits.clone(),
1747 config.language_id_overrides.clone(),
1748 long_running_spawner,
1749 ) {
1750 Ok(handle) => {
1751 let effective_root = if is_universal {
1752 file_path
1753 .and_then(|p| {
1754 let root = detect_workspace_root(p, &config.root_markers);
1755 path_to_uri(&root)
1756 })
1757 .or_else(|| self.root_uri.clone())
1758 } else {
1759 self.resolve_root_uri(language, file_path)
1760 };
1761 if let Err(e) =
1762 handle.initialize(effective_root, config.initialization_options.clone())
1763 {
1764 let message = format!(
1765 "Failed to initialize LSP server '{}' for {}: {}",
1766 server_name, language, e
1767 );
1768 tracing::error!("{}", message);
1769 return (false, message);
1770 }
1771
1772 let sh = ServerHandle {
1773 name: server_name.to_string(),
1774 handle,
1775 feature_filter: config.feature_filter(),
1776 capabilities: ServerCapabilitySummary::default(),
1777 };
1778
1779 self.handles.push(sh);
1780
1781 let message = format!("LSP server '{}' for {} started", server_name, language);
1782 tracing::info!("{}", message);
1783 (true, message)
1784 }
1785 Err(e) => {
1786 let message = format!(
1787 "Failed to start LSP server '{}' for {}: {}",
1788 server_name, language, e
1789 );
1790 tracing::error!("{}", message);
1791 (false, message)
1792 }
1793 }
1794 }
1795
1796 pub fn restart_attempt_count(&self, language: &str) -> usize {
1798 let now = Instant::now();
1799 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1800 self.restart_attempts
1801 .get(language)
1802 .map(|attempts| {
1803 attempts
1804 .iter()
1805 .filter(|t| now.duration_since(**t) < window)
1806 .count()
1807 })
1808 .unwrap_or(0)
1809 }
1810
1811 pub fn running_servers(&self) -> Vec<String> {
1813 let mut labels: Vec<String> = self
1814 .handles
1815 .iter()
1816 .map(|sh| sh.handle.scope().label().to_string())
1817 .collect();
1818 labels.sort();
1819 labels.dedup();
1820 labels
1821 }
1822
1823 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1825 self.handles
1826 .iter()
1827 .filter(|sh| sh.handle.scope().accepts(language))
1828 .map(|sh| sh.name.clone())
1829 .collect()
1830 }
1831
1832 pub fn is_server_ready(&self, language: &str) -> bool {
1834 self.handles
1835 .iter()
1836 .filter(|sh| sh.handle.scope().accepts(language))
1837 .any(|sh| sh.handle.state().can_send_requests())
1838 }
1839
1840 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1845 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1846 tracing::warn!(
1847 "No running LSP server named '{}' found for {}",
1848 server_name,
1849 language
1850 );
1851 return false;
1852 };
1853
1854 let sh = self.handles.remove(idx);
1855 tracing::info!(
1856 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1857 sh.name,
1858 language
1859 );
1860 fire_and_forget(sh.handle.shutdown());
1861
1862 let has_remaining = self
1864 .handles
1865 .iter()
1866 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1867 if !has_remaining {
1868 self.disabled_languages.insert(language.to_string());
1869 self.pending_restarts.remove(language);
1870 self.restart_cooldown.remove(language);
1871 self.allowed_languages.remove(language);
1872 }
1873
1874 true
1875 }
1876
1877 pub fn shutdown_server(&mut self, language: &str) -> bool {
1882 let mut found = false;
1883 let mut i = 0;
1884 while i < self.handles.len() {
1885 if !self.handles[i].handle.scope().is_universal()
1886 && self.handles[i].handle.scope().accepts(language)
1887 {
1888 let sh = self.handles.remove(i);
1889 tracing::info!(
1890 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1891 sh.name,
1892 language
1893 );
1894 fire_and_forget(sh.handle.shutdown());
1895 found = true;
1896 } else {
1897 i += 1;
1898 }
1899 }
1900
1901 if found {
1902 self.disabled_languages.insert(language.to_string());
1903 self.pending_restarts.remove(language);
1904 self.restart_cooldown.remove(language);
1905 self.allowed_languages.remove(language);
1906 } else {
1907 tracing::warn!("No running LSP server found for {}", language);
1908 }
1909
1910 found
1911 }
1912
1913 pub fn shutdown_all(&mut self) {
1915 for sh in &self.handles {
1916 tracing::info!(
1917 "Shutting down LSP server '{}' ({})",
1918 sh.name,
1919 sh.handle.scope().label()
1920 );
1921 fire_and_forget(sh.handle.shutdown());
1922 }
1923 self.handles.clear();
1924 }
1925}
1926
1927impl Drop for LspManager {
1928 fn drop(&mut self) {
1929 self.shutdown_all();
1930 }
1931}
1932
1933pub fn detect_language(
1945 path: &std::path::Path,
1946 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1947) -> Option<String> {
1948 let detected = detect_language_by_config(path, languages);
1949
1950 if detected.as_deref() == Some("c")
1956 && path.extension().and_then(|e| e.to_str()) == Some("h")
1957 && languages.contains_key("cpp")
1958 && header_in_cpp_tree(path)
1959 {
1960 return Some("cpp".to_string());
1961 }
1962
1963 detected
1964}
1965
1966fn detect_language_by_config(
1968 path: &std::path::Path,
1969 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1970) -> Option<String> {
1971 use crate::primitives::glob_match::{
1972 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1973 };
1974
1975 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1976 for (language_name, lang_config) in languages {
1978 if lang_config
1979 .filenames
1980 .iter()
1981 .any(|f| !is_glob_pattern(f) && f == filename)
1982 {
1983 return Some(language_name.clone());
1984 }
1985 }
1986
1987 let path_str = path.to_str().unwrap_or("");
1991 for (language_name, lang_config) in languages {
1992 if lang_config.filenames.iter().any(|f| {
1993 if !is_glob_pattern(f) {
1994 return false;
1995 }
1996 if is_path_pattern(f) {
1997 path_glob_matches(f, path_str)
1998 } else {
1999 filename_glob_matches(f, filename)
2000 }
2001 }) {
2002 return Some(language_name.clone());
2003 }
2004 }
2005 }
2006
2007 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
2009 for (language_name, lang_config) in languages {
2010 if lang_config.extensions.iter().any(|ext| ext == extension) {
2011 return Some(language_name.clone());
2012 }
2013 }
2014 }
2015
2016 None
2017}
2018
2019fn header_in_cpp_tree(path: &std::path::Path) -> bool {
2049 let Some(start_dir) = path.parent() else {
2050 return false;
2051 };
2052
2053 if let Ok(entries) = std::fs::read_dir(start_dir) {
2055 for entry in entries.flatten() {
2056 let p = entry.path();
2057 let Some(ext) = p.extension().and_then(|e| e.to_str()) else {
2058 continue;
2059 };
2060 if matches!(
2061 ext,
2062 "cc" | "cpp" | "cxx" | "C" | "c++" | "hpp" | "hh" | "hxx"
2063 ) {
2064 return true;
2065 }
2066 }
2067 }
2068
2069 let mut current = Some(start_dir);
2073 let mut depth = 0u32;
2074 while let Some(dir) = current {
2075 let cc = dir.join("compile_commands.json");
2076 if cc.is_file() && compile_commands_has_cpp_marker(&cc) {
2077 return true;
2078 }
2079 if depth >= 10 {
2080 break;
2081 }
2082 depth += 1;
2083 current = dir.parent();
2084 }
2085
2086 false
2087}
2088
2089fn compile_commands_has_cpp_marker(path: &std::path::Path) -> bool {
2097 use std::io::Read;
2098 const MAX_READ: u64 = 1_048_576;
2099
2100 let Ok(file) = std::fs::File::open(path) else {
2101 return false;
2102 };
2103 let mut buf = Vec::with_capacity(64 * 1024);
2104 if file.take(MAX_READ).read_to_end(&mut buf).is_err() {
2105 return false;
2106 }
2107 let Ok(text) = std::str::from_utf8(&buf) else {
2108 return false;
2109 };
2110
2111 if text.contains("c++") {
2115 return true;
2116 }
2117 text.contains(".cpp") || text.contains(".cxx") || text.contains(".cc\"")
2120}
2121
2122#[cfg(test)]
2123mod tests {
2124 use super::*;
2125 use std::path::Path;
2126
2127 #[test]
2128 fn test_lsp_manager_new() {
2129 let root_uri: Option<Uri> = "file:///test".parse().ok();
2130 let manager = LspManager::new(fresh_core::WindowId(1), root_uri.clone());
2131
2132 assert_eq!(manager.handles.len(), 0);
2134 assert_eq!(manager.config.len(), 0);
2135 assert!(manager.root_uri.is_some());
2136 assert!(manager.runtime.is_none());
2137 assert!(manager.async_bridge.is_none());
2138 }
2139
2140 #[test]
2141 fn test_lsp_manager_set_language_config() {
2142 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2143
2144 let config = LspServerConfig {
2145 enabled: true,
2146 command: "rust-analyzer".to_string(),
2147 args: vec![],
2148 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2149 auto_start: false,
2150 initialization_options: None,
2151 env: Default::default(),
2152 language_id_overrides: Default::default(),
2153 name: None,
2154 only_features: None,
2155 except_features: None,
2156 root_markers: Default::default(),
2157 };
2158
2159 manager.set_language_config("rust".to_string(), config);
2160
2161 assert_eq!(manager.config.len(), 1);
2162 assert!(manager.config.contains_key("rust"));
2163 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
2164 }
2165
2166 #[test]
2167 fn test_lsp_manager_force_spawn_no_runtime() {
2168 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2169
2170 manager.set_language_config(
2172 "rust".to_string(),
2173 LspServerConfig {
2174 enabled: true,
2175 command: "rust-analyzer".to_string(),
2176 args: vec![],
2177 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2178 auto_start: false,
2179 initialization_options: None,
2180 env: Default::default(),
2181 language_id_overrides: Default::default(),
2182 name: None,
2183 only_features: None,
2184 except_features: None,
2185 root_markers: Default::default(),
2186 },
2187 );
2188
2189 let result = manager.force_spawn("rust", None);
2191 assert!(result.is_none());
2192 }
2193
2194 #[test]
2195 fn test_lsp_manager_force_spawn_no_config() {
2196 let rt = tokio::runtime::Runtime::new().unwrap();
2197 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2198 let async_bridge = AsyncBridge::new();
2199
2200 manager.set_runtime(rt.handle().clone(), async_bridge);
2201
2202 let result = manager.force_spawn("rust", None);
2204 assert!(result.is_none());
2205 }
2206
2207 #[test]
2208 fn test_lsp_manager_force_spawn_disabled_language() {
2209 let rt = tokio::runtime::Runtime::new().unwrap();
2210 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2211 let async_bridge = AsyncBridge::new();
2212
2213 manager.set_runtime(rt.handle().clone(), async_bridge);
2214
2215 manager.set_language_config(
2217 "rust".to_string(),
2218 LspServerConfig {
2219 enabled: false,
2220 command: String::new(), args: vec![],
2222 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2223 auto_start: false,
2224 initialization_options: None,
2225 env: Default::default(),
2226 language_id_overrides: Default::default(),
2227 name: None,
2228 only_features: None,
2229 except_features: None,
2230 root_markers: Default::default(),
2231 },
2232 );
2233
2234 let result = manager.force_spawn("rust", None);
2236 assert!(result.is_none());
2237 }
2238
2239 #[test]
2245 fn test_lsp_manager_try_spawn_returns_disabled_when_all_configs_disabled() {
2246 let rt = tokio::runtime::Runtime::new().unwrap();
2247 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2248 let async_bridge = AsyncBridge::new();
2249 manager.set_runtime(rt.handle().clone(), async_bridge);
2250
2251 manager.set_language_config(
2252 "rust".to_string(),
2253 LspServerConfig {
2254 enabled: false,
2255 command: String::new(),
2256 args: vec![],
2257 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2258 auto_start: false,
2259 initialization_options: None,
2260 env: Default::default(),
2261 language_id_overrides: Default::default(),
2262 name: None,
2263 only_features: None,
2264 except_features: None,
2265 root_markers: Default::default(),
2266 },
2267 );
2268
2269 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2270 }
2271
2272 #[test]
2277 fn test_lsp_manager_try_spawn_returns_disabled_when_globally_disabled() {
2278 let rt = tokio::runtime::Runtime::new().unwrap();
2279 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2280 let async_bridge = AsyncBridge::new();
2281 manager.set_runtime(rt.handle().clone(), async_bridge);
2282
2283 manager.set_language_config(
2284 "rust".to_string(),
2285 LspServerConfig {
2286 enabled: true,
2287 command: "rust-analyzer".to_string(),
2288 args: vec![],
2289 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2290 auto_start: true,
2291 initialization_options: None,
2292 env: Default::default(),
2293 language_id_overrides: Default::default(),
2294 name: None,
2295 only_features: None,
2296 except_features: None,
2297 root_markers: Default::default(),
2298 },
2299 );
2300
2301 manager.set_globally_enabled(false);
2302 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2303
2304 manager.set_globally_enabled(true);
2308 assert_ne!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2309 }
2310
2311 #[test]
2312 fn test_lsp_manager_shutdown_all() {
2313 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2314
2315 manager.shutdown_all();
2317 assert_eq!(manager.handles.len(), 0);
2318 }
2319
2320 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2321 let mut languages = std::collections::HashMap::new();
2322 languages.insert(
2323 "rust".to_string(),
2324 crate::config::LanguageConfig {
2325 extensions: vec!["rs".to_string()],
2326 filenames: vec![],
2327 grammar: "rust".to_string(),
2328 comment_prefix: Some("//".to_string()),
2329 auto_indent: true,
2330 auto_close: None,
2331 auto_surround: None,
2332 textmate_grammar: None,
2333 show_whitespace_tabs: false,
2334 line_wrap: None,
2335 wrap_column: None,
2336 page_view: None,
2337 page_width: None,
2338 use_tabs: None,
2339 tab_size: None,
2340 formatter: None,
2341 format_on_save: false,
2342 on_save: vec![],
2343 word_characters: None,
2344 indent: None,
2345 },
2346 );
2347 languages.insert(
2348 "javascript".to_string(),
2349 crate::config::LanguageConfig {
2350 extensions: vec!["js".to_string(), "jsx".to_string()],
2351 filenames: vec![],
2352 grammar: "javascript".to_string(),
2353 comment_prefix: Some("//".to_string()),
2354 auto_indent: true,
2355 auto_close: None,
2356 auto_surround: None,
2357 textmate_grammar: None,
2358 show_whitespace_tabs: false,
2359 line_wrap: None,
2360 wrap_column: None,
2361 page_view: None,
2362 page_width: None,
2363 use_tabs: None,
2364 tab_size: None,
2365 formatter: None,
2366 format_on_save: false,
2367 on_save: vec![],
2368 word_characters: None,
2369 indent: None,
2370 },
2371 );
2372 languages.insert(
2373 "csharp".to_string(),
2374 crate::config::LanguageConfig {
2375 extensions: vec!["cs".to_string()],
2376 filenames: vec![],
2377 grammar: "c_sharp".to_string(),
2378 comment_prefix: Some("//".to_string()),
2379 auto_indent: true,
2380 auto_close: None,
2381 auto_surround: None,
2382 textmate_grammar: None,
2383 show_whitespace_tabs: false,
2384 line_wrap: None,
2385 wrap_column: None,
2386 page_view: None,
2387 page_width: None,
2388 use_tabs: None,
2389 tab_size: None,
2390 formatter: None,
2391 format_on_save: false,
2392 on_save: vec![],
2393 word_characters: None,
2394 indent: None,
2395 },
2396 );
2397 languages
2398 }
2399
2400 #[test]
2401 fn test_detect_language_from_config() {
2402 let languages = test_languages();
2403
2404 assert_eq!(
2406 detect_language(Path::new("main.rs"), &languages),
2407 Some("rust".to_string())
2408 );
2409 assert_eq!(
2410 detect_language(Path::new("index.js"), &languages),
2411 Some("javascript".to_string())
2412 );
2413 assert_eq!(
2414 detect_language(Path::new("App.jsx"), &languages),
2415 Some("javascript".to_string())
2416 );
2417 assert_eq!(
2418 detect_language(Path::new("Program.cs"), &languages),
2419 Some("csharp".to_string())
2420 );
2421
2422 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
2424 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
2425 assert_eq!(detect_language(Path::new("file"), &languages), None);
2426 }
2427
2428 #[test]
2429 fn test_detect_language_no_extension() {
2430 let languages = test_languages();
2431 assert_eq!(detect_language(Path::new("README"), &languages), None);
2432 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
2433 }
2434
2435 #[test]
2436 fn test_detect_language_path_glob() {
2437 let mut languages = test_languages();
2438 languages.insert(
2439 "shell".to_string(),
2440 crate::config::LanguageConfig {
2441 extensions: vec!["sh".to_string()],
2442 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
2443 grammar: "bash".to_string(),
2444 comment_prefix: Some("#".to_string()),
2445 auto_indent: true,
2446 auto_close: None,
2447 auto_surround: None,
2448 textmate_grammar: None,
2449 show_whitespace_tabs: false,
2450 line_wrap: None,
2451 wrap_column: None,
2452 page_view: None,
2453 page_width: None,
2454 use_tabs: None,
2455 tab_size: None,
2456 formatter: None,
2457 format_on_save: false,
2458 on_save: vec![],
2459 word_characters: None,
2460 indent: None,
2461 },
2462 );
2463
2464 assert_eq!(
2466 detect_language(Path::new("/etc/rc.conf"), &languages),
2467 Some("shell".to_string())
2468 );
2469 assert_eq!(
2470 detect_language(Path::new("/etc/init/rc.local"), &languages),
2471 Some("shell".to_string())
2472 );
2473 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
2475
2476 assert_eq!(
2478 detect_language(Path::new("lfrc"), &languages),
2479 Some("shell".to_string())
2480 );
2481 }
2482
2483 #[test]
2484 fn test_detect_workspace_root_finds_marker_in_parent() {
2485 let tmp = tempfile::tempdir().unwrap();
2486 let project = tmp.path().join("myproject");
2487 let src = project.join("src");
2488 std::fs::create_dir_all(&src).unwrap();
2489 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2490 let file = src.join("main.rs");
2491 std::fs::write(&file, "").unwrap();
2492
2493 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
2494 assert_eq!(root, project);
2495 }
2496
2497 #[test]
2498 fn test_detect_workspace_root_finds_marker_two_levels_up() {
2499 let tmp = tempfile::tempdir().unwrap();
2500 let project = tmp.path().join("myproject");
2501 let deep = project.join("src").join("nested");
2502 std::fs::create_dir_all(&deep).unwrap();
2503 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2504 let file = deep.join("lib.rs");
2505 std::fs::write(&file, "").unwrap();
2506
2507 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
2508 assert_eq!(root, project);
2509 }
2510
2511 #[test]
2512 fn test_detect_workspace_root_no_marker_returns_parent() {
2513 let tmp = tempfile::tempdir().unwrap();
2514 let dir = tmp.path().join("somedir");
2515 std::fs::create_dir_all(&dir).unwrap();
2516 let file = dir.join("file.txt");
2517 std::fs::write(&file, "").unwrap();
2518
2519 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
2520 assert_eq!(root, dir);
2521 }
2522
2523 #[test]
2524 fn test_detect_workspace_root_empty_markers_returns_parent() {
2525 let tmp = tempfile::tempdir().unwrap();
2526 let dir = tmp.path().join("somedir");
2527 std::fs::create_dir_all(&dir).unwrap();
2528 let file = dir.join("file.txt");
2529 std::fs::write(&file, "").unwrap();
2530
2531 let root = detect_workspace_root(&file, &[]);
2532 assert_eq!(root, dir);
2533 }
2534
2535 #[test]
2536 fn test_detect_workspace_root_directory_marker() {
2537 let tmp = tempfile::tempdir().unwrap();
2538 let project = tmp.path().join("myproject");
2539 let src = project.join("src");
2540 std::fs::create_dir_all(&src).unwrap();
2541 std::fs::create_dir_all(project.join(".git")).unwrap();
2542 let file = src.join("main.rs");
2543 std::fs::write(&file, "").unwrap();
2544
2545 let root = detect_workspace_root(&file, &[".git".to_string()]);
2546 assert_eq!(root, project);
2547 }
2548
2549 fn c_cpp_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2554 use crate::config::LanguageConfig;
2555 let mut languages = std::collections::HashMap::new();
2556 let base = LanguageConfig {
2557 extensions: vec![],
2558 filenames: vec![],
2559 grammar: String::new(),
2560 comment_prefix: Some("//".to_string()),
2561 auto_indent: true,
2562 auto_close: None,
2563 auto_surround: None,
2564 textmate_grammar: None,
2565 show_whitespace_tabs: false,
2566 line_wrap: None,
2567 wrap_column: None,
2568 page_view: None,
2569 page_width: None,
2570 use_tabs: None,
2571 tab_size: None,
2572 formatter: None,
2573 format_on_save: false,
2574 on_save: vec![],
2575 word_characters: None,
2576 indent: None,
2577 };
2578 languages.insert(
2579 "c".to_string(),
2580 LanguageConfig {
2581 extensions: vec!["c".to_string(), "h".to_string()],
2582 grammar: "c".to_string(),
2583 ..base.clone()
2584 },
2585 );
2586 languages.insert(
2587 "cpp".to_string(),
2588 LanguageConfig {
2589 extensions: vec![
2590 "cpp".to_string(),
2591 "cc".to_string(),
2592 "cxx".to_string(),
2593 "hpp".to_string(),
2594 "hh".to_string(),
2595 "hxx".to_string(),
2596 ],
2597 grammar: "cpp".to_string(),
2598 ..base
2599 },
2600 );
2601 languages
2602 }
2603
2604 #[test]
2605 fn test_detect_language_h_stays_c_without_cpp_signals() {
2606 let languages = c_cpp_languages();
2610 assert_eq!(
2611 detect_language(Path::new("foo.h"), &languages),
2612 Some("c".to_string())
2613 );
2614 }
2615
2616 #[test]
2617 fn test_detect_language_h_promotes_to_cpp_with_sibling_cpp_source() {
2618 let tmp = tempfile::tempdir().unwrap();
2619 let project = tmp.path().join("proj");
2620 std::fs::create_dir_all(&project).unwrap();
2621 let header = project.join("widget.h");
2622 std::fs::write(&header, "").unwrap();
2623 std::fs::write(project.join("widget.cpp"), "").unwrap();
2625
2626 let languages = c_cpp_languages();
2627 assert_eq!(
2628 detect_language(&header, &languages),
2629 Some("cpp".to_string())
2630 );
2631 }
2632
2633 #[test]
2634 fn test_detect_language_h_promotes_to_cpp_with_sibling_hpp() {
2635 let tmp = tempfile::tempdir().unwrap();
2636 let project = tmp.path().join("proj");
2637 std::fs::create_dir_all(&project).unwrap();
2638 let header = project.join("a.h");
2639 std::fs::write(&header, "").unwrap();
2640 std::fs::write(project.join("b.hpp"), "").unwrap();
2642
2643 let languages = c_cpp_languages();
2644 assert_eq!(
2645 detect_language(&header, &languages),
2646 Some("cpp".to_string())
2647 );
2648 }
2649
2650 #[test]
2651 fn test_detect_language_h_promotes_to_cpp_with_ancestor_compile_commands() {
2652 let tmp = tempfile::tempdir().unwrap();
2653 let project = tmp.path().join("proj");
2654 let include = project.join("include").join("fmt");
2655 std::fs::create_dir_all(&include).unwrap();
2656 std::fs::write(
2660 project.join("compile_commands.json"),
2661 r#"[{"directory":"/proj","command":"/usr/bin/clang++ -std=c++17 -c src/format.cc","file":"src/format.cc"}]"#,
2662 ).unwrap();
2663 let header = include.join("format.h");
2664 std::fs::write(&header, "").unwrap();
2665
2666 let languages = c_cpp_languages();
2667 assert_eq!(
2668 detect_language(&header, &languages),
2669 Some("cpp".to_string())
2670 );
2671 }
2672
2673 #[test]
2674 fn test_detect_language_h_stays_c_with_pure_c_compile_commands() {
2675 let tmp = tempfile::tempdir().unwrap();
2678 let project = tmp.path().join("cproj");
2679 let include = project.join("include");
2680 std::fs::create_dir_all(&include).unwrap();
2681 std::fs::write(
2682 project.join("compile_commands.json"),
2683 r#"[{"directory":"/cproj","command":"/usr/bin/gcc -std=c11 -c src/lib.c","file":"src/lib.c"}]"#,
2684 )
2685 .unwrap();
2686 let header = include.join("lib.h");
2687 std::fs::write(&header, "").unwrap();
2688
2689 let languages = c_cpp_languages();
2690 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2691 }
2692
2693 #[test]
2694 fn test_detect_language_h_stays_c_in_pure_c_tree() {
2695 let tmp = tempfile::tempdir().unwrap();
2696 let project = tmp.path().join("cproj");
2697 std::fs::create_dir_all(&project).unwrap();
2698 let header = project.join("lib.h");
2699 std::fs::write(&header, "").unwrap();
2700 std::fs::write(project.join("lib.c"), "").unwrap();
2702
2703 let languages = c_cpp_languages();
2704 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2705 }
2706
2707 #[test]
2708 fn test_detect_language_h_stays_c_with_empty_compile_commands() {
2709 let tmp = tempfile::tempdir().unwrap();
2712 let project = tmp.path().join("proj");
2713 std::fs::create_dir_all(&project).unwrap();
2714 std::fs::write(project.join("compile_commands.json"), "[]").unwrap();
2715 let header = project.join("foo.h");
2716 std::fs::write(&header, "").unwrap();
2717
2718 let languages = c_cpp_languages();
2719 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2720 }
2721
2722 #[test]
2723 fn test_detect_language_h_promotes_on_cpp_std_flag_alone() {
2724 let tmp = tempfile::tempdir().unwrap();
2726 let project = tmp.path().join("proj");
2727 let include = project.join("include");
2728 std::fs::create_dir_all(&include).unwrap();
2729 std::fs::write(
2730 project.join("compile_commands.json"),
2731 r#"[{"directory":"/proj","command":"/usr/bin/clang -std=c++20 -c src/x.C","file":"src/x.C"}]"#,
2735 )
2736 .unwrap();
2737 let header = include.join("x.h");
2738 std::fs::write(&header, "").unwrap();
2739
2740 let languages = c_cpp_languages();
2741 assert_eq!(
2742 detect_language(&header, &languages),
2743 Some("cpp".to_string())
2744 );
2745 }
2746
2747 #[test]
2748 fn test_detect_language_c_source_never_promoted() {
2749 let tmp = tempfile::tempdir().unwrap();
2751 let project = tmp.path().join("proj");
2752 std::fs::create_dir_all(&project).unwrap();
2753 let source = project.join("legacy.c");
2754 std::fs::write(&source, "").unwrap();
2755 std::fs::write(project.join("main.cpp"), "").unwrap();
2756
2757 let languages = c_cpp_languages();
2758 assert_eq!(detect_language(&source, &languages), Some("c".to_string()));
2759 }
2760
2761 #[test]
2762 fn test_detect_language_h_no_promotion_without_cpp_config() {
2763 let tmp = tempfile::tempdir().unwrap();
2766 let project = tmp.path().join("proj");
2767 std::fs::create_dir_all(&project).unwrap();
2768 let header = project.join("widget.h");
2769 std::fs::write(&header, "").unwrap();
2770 std::fs::write(project.join("widget.cpp"), "").unwrap();
2771
2772 let mut languages = c_cpp_languages();
2773 languages.remove("cpp");
2774 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2775 }
2776
2777 #[test]
2778 fn test_path_to_uri_basic() {
2779 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
2780 assert_eq!(uri.as_str(), "file:///tmp/test");
2781 }
2782
2783 #[test]
2784 fn test_path_to_uri_with_spaces() {
2785 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
2786 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
2787 }
2788
2789 #[test]
2790 fn dynamic_registration_enables_then_disables_inlay_hints() {
2791 let mut caps = ServerCapabilitySummary::default();
2795 assert!(!caps.inlay_hints);
2796
2797 let recognized = caps.apply_dynamic_registration("textDocument/inlayHint", None, true);
2798 assert!(
2799 recognized,
2800 "inlayHint must be a recognized capability method"
2801 );
2802 assert!(
2803 caps.inlay_hints,
2804 "dynamic registration must enable inlay hints"
2805 );
2806
2807 let recognized = caps.apply_dynamic_registration("textDocument/inlayHint", None, false);
2808 assert!(recognized);
2809 assert!(!caps.inlay_hints, "unregister must disable inlay hints");
2810 }
2811
2812 #[test]
2813 fn dynamic_registration_ignores_unknown_methods() {
2814 let mut caps = ServerCapabilitySummary::default();
2818 let recognized =
2819 caps.apply_dynamic_registration("workspace/didChangeWatchedFiles", None, true);
2820 assert!(!recognized);
2821 }
2822
2823 #[test]
2824 fn dynamic_registration_parses_completion_options() {
2825 let mut caps = ServerCapabilitySummary::default();
2826 let opts = serde_json::json!({
2827 "triggerCharacters": [".", "::"],
2828 "resolveProvider": true,
2829 });
2830 let recognized =
2831 caps.apply_dynamic_registration("textDocument/completion", Some(&opts), true);
2832 assert!(recognized);
2833 assert!(caps.completion);
2834 assert!(caps.completion_resolve);
2835 assert_eq!(caps.completion_trigger_characters, vec![".", "::"]);
2836
2837 caps.apply_dynamic_registration("textDocument/completion", None, false);
2839 assert!(!caps.completion);
2840 assert!(!caps.completion_resolve);
2841 assert!(caps.completion_trigger_characters.is_empty());
2842 }
2843
2844 #[test]
2845 fn dynamic_registration_parses_semantic_tokens_legend() {
2846 let mut caps = ServerCapabilitySummary::default();
2847 let opts = serde_json::json!({
2848 "legend": {
2849 "tokenTypes": ["namespace", "type"],
2850 "tokenModifiers": ["declaration"],
2851 },
2852 "full": { "delta": true },
2853 "range": true,
2854 });
2855 let recognized =
2856 caps.apply_dynamic_registration("textDocument/semanticTokens", Some(&opts), true);
2857 assert!(recognized);
2858 assert!(caps.semantic_tokens_full);
2859 assert!(caps.semantic_tokens_full_delta);
2860 assert!(caps.semantic_tokens_range);
2861 let legend = caps
2862 .semantic_tokens_legend
2863 .as_ref()
2864 .expect("legend must be parsed from registration options");
2865 assert_eq!(legend.token_types.len(), 2);
2866
2867 caps.apply_dynamic_registration("textDocument/semanticTokens", None, false);
2868 assert!(!caps.semantic_tokens_full);
2869 assert!(caps.semantic_tokens_legend.is_none());
2870 }
2871
2872 #[test]
2873 fn apply_dynamic_capabilities_reports_change_only_for_known_methods() {
2874 let mut caps = ServerCapabilitySummary::default();
2877 let known = caps.apply_dynamic_registration("textDocument/hover", None, true);
2878 let unknown = caps.apply_dynamic_registration("some/unknownMethod", None, true);
2879 assert!(known);
2880 assert!(!unknown);
2881 assert!(caps.hover);
2882 }
2883}