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