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
462impl LspManager {
463 pub fn window_id(&self) -> fresh_core::WindowId {
465 self.window_id
466 }
467
468 pub fn new(window_id: fresh_core::WindowId, root_uri: Option<Uri>) -> Self {
470 Self {
471 window_id,
472 handles: Vec::new(),
473 config: HashMap::new(),
474 universal_configs: Vec::new(),
475 root_uri,
476 per_language_root_uris: HashMap::new(),
477 runtime: None,
478 async_bridge: None,
479 long_running_spawner: None,
480 workspace_trust: None,
481 path_translation: None,
482 restart_attempts: HashMap::new(),
483 restart_cooldown: HashSet::new(),
484 pending_restarts: HashMap::new(),
485 allowed_languages: HashSet::new(),
486 disabled_languages: HashSet::new(),
487 }
488 }
489
490 pub fn set_long_running_spawner(
499 &mut self,
500 spawner: std::sync::Arc<dyn crate::services::remote::LongRunningSpawner>,
501 ) {
502 self.long_running_spawner = Some(spawner);
503 }
504
505 pub fn set_workspace_trust(
509 &mut self,
510 trust: std::sync::Arc<crate::services::workspace_trust::WorkspaceTrust>,
511 ) {
512 self.workspace_trust = Some(trust);
513 }
514
515 fn lsp_autostart_allowed(&self) -> bool {
519 use crate::services::workspace_trust::TrustLevel;
520 self.workspace_trust
521 .as_ref()
522 .map(|t| t.level() == TrustLevel::Trusted)
523 .unwrap_or(true)
524 }
525
526 pub fn set_path_translation(
531 &mut self,
532 translation: Option<crate::services::authority::PathTranslation>,
533 ) {
534 self.path_translation = translation;
535 }
536
537 pub fn command_exists_via_authority(&self, command: &str) -> bool {
551 if command.is_empty() {
552 return false;
553 }
554 let (Some(runtime), Some(spawner)) =
555 (self.runtime.as_ref(), self.long_running_spawner.as_ref())
556 else {
557 return crate::services::lsp::command_exists(command);
558 };
559 runtime.block_on(spawner.command_exists(command))
560 }
561
562 pub fn is_language_allowed(&self, language: &str) -> bool {
564 self.allowed_languages.contains(language)
565 }
566
567 pub fn allow_language(&mut self, language: &str) {
569 self.allowed_languages.insert(language.to_string());
570 tracing::info!("LSP language '{}' manually enabled", language);
571 }
572
573 pub fn allowed_languages(&self) -> &HashSet<String> {
575 &self.allowed_languages
576 }
577
578 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
580 self.config.get(language).map(|v| v.as_slice())
581 }
582
583 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
585 self.config.get(language).and_then(|v| v.first())
586 }
587
588 pub fn set_server_capabilities(
590 &mut self,
591 _language: &str,
592 server_name: &str,
593 mut capabilities: ServerCapabilitySummary,
594 ) {
595 capabilities.initialized = true;
596
597 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
598 sh.capabilities = capabilities;
599 }
600 }
601
602 pub fn apply_dynamic_capabilities(
612 &mut self,
613 server_name: &str,
614 register: bool,
615 registrations: &[(String, Option<serde_json::Value>)],
616 ) -> bool {
617 let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) else {
618 return false;
619 };
620 let mut changed = false;
621 for (method, options) in registrations {
622 if sh
623 .capabilities
624 .apply_dynamic_registration(method, options.as_ref(), register)
625 {
626 changed = true;
627 }
628 }
629 changed
630 }
631
632 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
634 self.get_handles(language).into_iter().find_map(|sh| {
635 if sh.feature_filter.allows(LspFeature::SemanticTokens)
636 && sh.has_capability(LspFeature::SemanticTokens)
637 {
638 sh.capabilities.semantic_tokens_legend.as_ref()
639 } else {
640 None
641 }
642 })
643 }
644
645 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
647 self.get_handles(language).iter().any(|sh| {
648 sh.feature_filter.allows(LspFeature::SemanticTokens)
649 && sh.capabilities.semantic_tokens_full
650 })
651 }
652
653 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
655 self.get_handles(language).iter().any(|sh| {
656 sh.feature_filter.allows(LspFeature::SemanticTokens)
657 && sh.capabilities.semantic_tokens_full_delta
658 })
659 }
660
661 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
663 self.get_handles(language).iter().any(|sh| {
664 sh.feature_filter.allows(LspFeature::SemanticTokens)
665 && sh.capabilities.semantic_tokens_range
666 })
667 }
668
669 pub fn folding_ranges_supported(&self, language: &str) -> bool {
671 self.get_handles(language).iter().any(|sh| {
672 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
673 })
674 }
675
676 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
678 let ch_str = ch.to_string();
679 self.get_handles(language).iter().any(|sh| {
680 sh.feature_filter.allows(LspFeature::Completion)
681 && sh
682 .capabilities
683 .completion_trigger_characters
684 .contains(&ch_str)
685 })
686 }
687
688 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
703 if self
705 .handles
706 .iter()
707 .any(|sh| sh.handle.scope().accepts(language))
708 {
709 self.ensure_universal_servers_running(file_path);
710 return LspSpawnResult::Spawned;
711 }
712
713 if self.runtime.is_none() || self.async_bridge.is_none() {
715 return LspSpawnResult::Failed;
716 }
717
718 if !self.lsp_autostart_allowed() && !self.allowed_languages.contains(language) {
725 tracing::info!(
726 "LSP for '{}' not auto-started: workspace is not trusted \
727 (trust the folder to enable language servers)",
728 language
729 );
730 return LspSpawnResult::NotAutoStart;
731 }
732
733 self.ensure_universal_servers_running(file_path);
735
736 let configs = match self.config.get(language) {
738 Some(configs) if !configs.is_empty() => configs,
739 _ => {
740 if self
742 .handles
743 .iter()
744 .any(|sh| sh.handle.scope().is_universal())
745 {
746 return LspSpawnResult::Spawned;
747 }
748 return LspSpawnResult::NotConfigured;
749 }
750 };
751
752 if !configs.iter().any(|c| c.enabled) {
754 if self
755 .handles
756 .iter()
757 .any(|sh| sh.handle.scope().is_universal())
758 {
759 return LspSpawnResult::Spawned;
760 }
761 return LspSpawnResult::Disabled;
762 }
763
764 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
766 if !any_auto_start && !self.allowed_languages.contains(language) {
767 if self
768 .handles
769 .iter()
770 .any(|sh| sh.handle.scope().is_universal())
771 {
772 return LspSpawnResult::Spawned;
773 }
774 return LspSpawnResult::NotAutoStart;
775 }
776
777 let spawned = self.force_spawn(language, file_path).is_some();
779
780 if spawned
781 || self
782 .handles
783 .iter()
784 .any(|sh| sh.handle.scope().is_universal())
785 {
786 LspSpawnResult::Spawned
787 } else {
788 LspSpawnResult::Failed
789 }
790 }
791
792 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
796 self.runtime = Some(runtime);
797 self.async_bridge = Some(async_bridge);
798 }
799
800 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
802 self.config.insert(language, vec![config]);
803 }
804
805 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
807 self.config.insert(language, configs);
808 }
809
810 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
812 self.config.entry(language).or_default().extend(configs);
813 }
814
815 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
820 self.universal_configs = configs;
821 }
822
823 pub fn configured_languages(&self) -> Vec<String> {
825 self.config.keys().cloned().collect()
826 }
827
828 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
833 self.root_uri = root_uri;
834 }
835
836 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
842 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
843 self.per_language_root_uris
844 .insert(language.to_string(), uri.clone());
845
846 if self
848 .handles
849 .iter()
850 .any(|sh| sh.handle.scope().accepts(language))
851 {
852 tracing::info!(
853 "Restarting {} LSP server with new root: {}",
854 language,
855 uri.as_str()
856 );
857 self.shutdown_server(language);
858 return true;
860 }
861 false
862 }
863
864 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
871 if let Some(uri) = self.per_language_root_uris.get(language) {
873 return Some(uri.clone());
874 }
875
876 if let Some(path) = file_path {
881 let markers = self
882 .config
883 .get(language)
884 .and_then(|configs| configs.first())
885 .map(|c| c.root_markers.as_slice())
886 .unwrap_or(&[]);
887 let root = detect_workspace_root(path, markers);
888 let mapped = self
889 .path_translation
890 .as_ref()
891 .and_then(|t| t.host_to_remote(&root))
892 .unwrap_or(root);
893 if let Some(uri) = path_to_uri(&mapped) {
894 return Some(uri);
895 }
896 }
897
898 self.root_uri.clone()
900 }
901
902 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
906 self.resolve_root_uri(language, None)
907 }
908
909 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
914 self.shutdown_all();
916
917 self.root_uri = new_root_uri;
919
920 self.restart_attempts.clear();
922 self.restart_cooldown.clear();
923 self.pending_restarts.clear();
924
925 tracing::info!(
929 "LSP manager reset for new project: {:?}",
930 self.root_uri.as_ref().map(|u| u.as_str())
931 );
932 }
933
934 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
937 self.handles
938 .iter()
939 .find(|sh| sh.handle.scope().accepts(language))
940 .map(|sh| &sh.handle)
941 }
942
943 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
946 self.handles
947 .iter_mut()
948 .find(|sh| sh.handle.scope().accepts(language))
949 .map(|sh| &mut sh.handle)
950 }
951
952 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
954 self.handles
955 .iter()
956 .filter(|sh| sh.handle.scope().accepts(language))
957 .collect()
958 }
959
960 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
962 self.handles
963 .iter_mut()
964 .filter(|sh| sh.handle.scope().accepts(language))
965 .collect()
966 }
967
968 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
972 self.handles
973 .iter()
974 .find(|sh| sh.name == server_name)
975 .map(|sh| sh.handle.scope())
976 }
977
978 pub fn has_handles(&self, language: &str) -> bool {
980 self.handles
981 .iter()
982 .any(|sh| sh.handle.scope().accepts(language))
983 }
984
985 pub fn handle_count(&self, language: &str) -> usize {
987 self.handles
988 .iter()
989 .filter(|sh| sh.handle.scope().accepts(language))
990 .count()
991 }
992
993 pub fn has_server_named(&self, server_name: &str) -> bool {
995 self.handles.iter().any(|sh| sh.name == server_name)
996 }
997
998 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
1004 self.handles
1005 .iter()
1006 .filter(|sh| sh.handle.scope().accepts(language))
1007 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1008 }
1009
1010 pub fn handle_for_feature_mut(
1014 &mut self,
1015 language: &str,
1016 feature: LspFeature,
1017 ) -> Option<&mut ServerHandle> {
1018 self.handles
1019 .iter_mut()
1020 .filter(|sh| sh.handle.scope().accepts(language))
1021 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1022 }
1023
1024 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
1028 self.handles
1029 .iter()
1030 .filter(|sh| sh.handle.scope().accepts(language))
1031 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1032 .collect()
1033 }
1034
1035 pub fn handles_for_feature_mut(
1039 &mut self,
1040 language: &str,
1041 feature: LspFeature,
1042 ) -> Vec<&mut ServerHandle> {
1043 self.handles
1044 .iter_mut()
1045 .filter(|sh| sh.handle.scope().accepts(language))
1046 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
1047 .collect()
1048 }
1049
1050 fn spawn_decision(&mut self, language: &str) -> SpawnDecision {
1058 if self
1059 .handles
1060 .iter()
1061 .any(|sh| sh.handle.scope().accepts(language))
1062 {
1063 return SpawnDecision::Existing;
1064 }
1065 if self.restart_cooldown.contains(language) {
1066 return SpawnDecision::CooledDown;
1067 }
1068 if self.pending_restarts.contains_key(language) {
1069 return SpawnDecision::PendingBackoff;
1070 }
1071
1072 let now = Instant::now();
1073 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1074 let attempts = self
1075 .restart_attempts
1076 .entry(language.to_string())
1077 .or_default();
1078 attempts.retain(|t| now.duration_since(*t) < window);
1079
1080 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
1081 self.restart_cooldown.insert(language.to_string());
1082 tracing::warn!(
1083 "LSP server for {} has spawned {} times in {} minutes, entering cooldown",
1084 language,
1085 MAX_RESTARTS_IN_WINDOW,
1086 RESTART_WINDOW_SECS / 60
1087 );
1088 return SpawnDecision::CooledDown;
1089 }
1090
1091 attempts.push(now);
1092 SpawnDecision::Allow
1093 }
1094
1095 pub fn force_spawn(
1114 &mut self,
1115 language: &str,
1116 file_path: Option<&Path>,
1117 ) -> Option<&mut LspHandle> {
1118 tracing::debug!("force_spawn called for language: {}", language);
1119
1120 if self
1122 .handles
1123 .iter()
1124 .any(|sh| sh.handle.scope().accepts(language))
1125 {
1126 tracing::debug!("force_spawn: returning existing handle for {}", language);
1127 return self
1128 .handles
1129 .iter_mut()
1130 .find(|sh| sh.handle.scope().accepts(language))
1131 .map(|sh| &mut sh.handle);
1132 }
1133
1134 if self.disabled_languages.contains(language) {
1136 tracing::debug!(
1137 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
1138 language
1139 );
1140 return None;
1141 }
1142
1143 let configs = match self.config.get(language) {
1145 Some(configs) if !configs.is_empty() => configs.clone(),
1146 _ => {
1147 tracing::warn!(
1148 "force_spawn: no config found for language '{}', available configs: {:?}",
1149 language,
1150 self.config.keys().collect::<Vec<_>>()
1151 );
1152 return None;
1153 }
1154 };
1155
1156 match self.spawn_decision(language) {
1162 SpawnDecision::Existing => {
1163 return self
1166 .handles
1167 .iter_mut()
1168 .find(|sh| sh.handle.scope().accepts(language))
1169 .map(|sh| &mut sh.handle);
1170 }
1171 SpawnDecision::CooledDown => {
1172 tracing::debug!(
1173 "force_spawn: {} is in cooldown, refusing spawn (use Restart LSP command)",
1174 language
1175 );
1176 return None;
1177 }
1178 SpawnDecision::PendingBackoff => {
1179 tracing::debug!(
1180 "force_spawn: {} has a pending restart scheduled, not double-spawning",
1181 language
1182 );
1183 return None;
1184 }
1185 SpawnDecision::Allow => {}
1186 }
1187
1188 let runtime = match self.runtime.as_ref() {
1193 Some(r) => r.clone(),
1194 None => {
1195 tracing::error!("force_spawn: no tokio runtime available for {}", language);
1196 return None;
1197 }
1198 };
1199 let async_bridge = match self.async_bridge.as_ref() {
1200 Some(b) => b.clone(),
1201 None => {
1202 tracing::error!("force_spawn: no async bridge available for {}", language);
1203 return None;
1204 }
1205 };
1206 let long_running_spawner = match self.long_running_spawner.as_ref() {
1214 Some(s) => s.clone(),
1215 None => {
1216 tracing::warn!(
1217 "force_spawn: long-running spawner not wired for {} — \
1218 falling back to host-local spawn (normal for tests \
1219 that skip set_boot_authority)",
1220 language
1221 );
1222 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1223 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1224 std::sync::Arc::new(
1225 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1226 ),
1227 ))
1228 }
1229 };
1230
1231 let mut spawned_handles = Vec::new();
1232 let manually_allowed = self.allowed_languages.contains(language);
1233
1234 for config in &configs {
1235 if manually_allowed {
1236 } else {
1240 if !config.enabled || !config.auto_start {
1246 continue;
1247 }
1248 }
1249
1250 if config.command.is_empty() {
1251 tracing::warn!(
1252 "force_spawn: LSP command is empty for {} server '{}'",
1253 language,
1254 config.display_name()
1255 );
1256 continue;
1257 }
1258
1259 let server_name = config.display_name();
1260 tracing::info!(
1261 "Spawning LSP server '{}' for language: {}",
1262 server_name,
1263 language
1264 );
1265
1266 match LspHandle::spawn(
1267 &runtime,
1268 &config.command,
1269 &config.args,
1270 config.env.clone(),
1271 LanguageScope::single(language),
1272 server_name.clone(),
1273 &async_bridge,
1274 config.process_limits.clone(),
1275 config.language_id_overrides.clone(),
1276 long_running_spawner.clone(),
1277 ) {
1278 Ok(handle) => {
1279 let effective_root = self.resolve_root_uri(language, file_path);
1280 if let Err(e) =
1281 handle.initialize(effective_root, config.initialization_options.clone())
1282 {
1283 tracing::error!(
1284 "Failed to send initialize command for {} ({}): {}",
1285 language,
1286 server_name,
1287 e
1288 );
1289 continue;
1290 }
1291
1292 tracing::info!(
1293 "LSP initialization started for {} ({}), will be ready asynchronously",
1294 language,
1295 server_name
1296 );
1297
1298 spawned_handles.push(ServerHandle {
1299 name: server_name,
1300 handle,
1301 feature_filter: config.feature_filter(),
1302 capabilities: ServerCapabilitySummary::default(),
1303 });
1304 }
1305 Err(e) => {
1306 tracing::error!(
1307 "Failed to spawn LSP handle for {} ({}): {}",
1308 language,
1309 server_name,
1310 e
1311 );
1312 }
1313 }
1314 }
1315
1316 if spawned_handles.is_empty() {
1317 return None;
1318 }
1319
1320 self.handles.extend(spawned_handles);
1321 self.handles
1322 .iter_mut()
1323 .rev()
1324 .find(|sh| sh.handle.scope().accepts(language))
1325 .map(|sh| &mut sh.handle)
1326 }
1327
1328 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
1334 if self
1335 .handles
1336 .iter()
1337 .any(|sh| sh.handle.scope().is_universal())
1338 || self.universal_configs.is_empty()
1339 {
1340 return;
1341 }
1342
1343 let runtime = match self.runtime.as_ref() {
1344 Some(r) => r.clone(),
1345 None => return,
1346 };
1347 let async_bridge = match self.async_bridge.as_ref() {
1348 Some(b) => b.clone(),
1349 None => return,
1350 };
1351 let long_running_spawner =
1352 self.long_running_spawner
1353 .as_ref()
1354 .cloned()
1355 .unwrap_or_else(|| {
1356 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1357 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1358 std::sync::Arc::new(
1359 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1360 ),
1361 ))
1362 });
1363
1364 let mut spawned = Vec::new();
1365 for config in &self.universal_configs {
1366 if !config.enabled || !config.auto_start {
1367 continue;
1368 }
1369 if config.command.is_empty() {
1370 continue;
1371 }
1372
1373 let server_name = config.display_name();
1374 tracing::info!("Spawning universal LSP server '{}'", server_name);
1375
1376 match LspHandle::spawn(
1377 &runtime,
1378 &config.command,
1379 &config.args,
1380 config.env.clone(),
1381 LanguageScope::all(),
1382 server_name.clone(),
1383 &async_bridge,
1384 config.process_limits.clone(),
1385 config.language_id_overrides.clone(),
1386 long_running_spawner.clone(),
1387 ) {
1388 Ok(handle) => {
1389 let effective_root = file_path
1390 .and_then(|p| {
1391 let root = detect_workspace_root(p, &config.root_markers);
1392 path_to_uri(&root)
1393 })
1394 .or_else(|| self.root_uri.clone());
1395 if let Err(e) =
1396 handle.initialize(effective_root, config.initialization_options.clone())
1397 {
1398 tracing::error!(
1399 "Failed to initialize universal LSP server '{}': {}",
1400 server_name,
1401 e
1402 );
1403 continue;
1404 }
1405 tracing::info!(
1406 "Universal LSP server '{}' initialization started",
1407 server_name
1408 );
1409 spawned.push(ServerHandle {
1410 name: server_name,
1411 handle,
1412 feature_filter: config.feature_filter(),
1413 capabilities: ServerCapabilitySummary::default(),
1414 });
1415 }
1416 Err(e) => {
1417 tracing::error!(
1418 "Failed to spawn universal LSP server '{}': {}",
1419 server_name,
1420 e
1421 );
1422 }
1423 }
1424 }
1425
1426 self.handles.extend(spawned);
1427 }
1428
1429 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1433 if self
1435 .handles
1436 .iter()
1437 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1438 {
1439 let universals: Vec<ServerHandle> = {
1441 let mut drained = Vec::new();
1442 let mut i = 0;
1443 while i < self.handles.len() {
1444 if self.handles[i].handle.scope().is_universal() {
1445 drained.push(self.handles.remove(i));
1446 } else {
1447 i += 1;
1448 }
1449 }
1450 drained
1451 };
1452 for sh in universals {
1453 fire_and_forget(sh.handle.shutdown());
1454 }
1455 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1457 }
1458
1459 {
1461 let mut i = 0;
1462 while i < self.handles.len() {
1463 if !self.handles[i].handle.scope().is_universal()
1464 && self.handles[i].handle.scope().accepts(language)
1465 {
1466 let sh = self.handles.remove(i);
1467 fire_and_forget(sh.handle.shutdown());
1468 } else {
1469 i += 1;
1470 }
1471 }
1472 }
1473
1474 if self.disabled_languages.contains(language) {
1477 return format!(
1478 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1479 language
1480 );
1481 }
1482
1483 if self.restart_cooldown.contains(language) {
1485 return format!(
1486 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1487 language
1488 );
1489 }
1490
1491 let now = Instant::now();
1496 let attempt_number = self
1497 .restart_attempts
1498 .get(language)
1499 .map(|v| v.len())
1500 .unwrap_or(0);
1501
1502 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1504
1505 self.pending_restarts
1506 .insert(language.to_string(), restart_time);
1507
1508 tracing::info!(
1509 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1510 language,
1511 attempt_number + 1,
1512 MAX_RESTARTS_IN_WINDOW,
1513 delay_ms
1514 );
1515
1516 format!(
1517 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1518 language,
1519 attempt_number + 1,
1520 MAX_RESTARTS_IN_WINDOW,
1521 delay_ms / 1000
1522 )
1523 }
1524
1525 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1529 let now = Instant::now();
1530 let mut results = Vec::new();
1531
1532 let due_restarts: Vec<String> = self
1534 .pending_restarts
1535 .iter()
1536 .filter(|(_, time)| **time <= now)
1537 .map(|(lang, _)| lang.clone())
1538 .collect();
1539
1540 for language in due_restarts {
1541 self.pending_restarts.remove(&language);
1542
1543 if self.force_spawn(&language, None).is_some() {
1547 let message = format!("LSP server for {} restarted successfully", language);
1548 tracing::info!("{}", message);
1549 results.push((language, true, message));
1550 } else {
1551 let message = format!("Failed to restart LSP server for {}", language);
1552 tracing::error!("{}", message);
1553 results.push((language, false, message));
1554 }
1555 }
1556
1557 results
1558 }
1559
1560 pub fn is_in_cooldown(&self, language: &str) -> bool {
1562 self.restart_cooldown.contains(language)
1563 }
1564
1565 pub fn has_pending_restart(&self, language: &str) -> bool {
1567 self.pending_restarts.contains_key(language)
1568 }
1569
1570 pub fn clear_cooldown(&mut self, language: &str) {
1572 self.restart_cooldown.remove(language);
1573 self.restart_attempts.remove(language);
1574 self.pending_restarts.remove(language);
1575 tracing::info!("Cleared restart cooldown for {}", language);
1576 }
1577
1578 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1585 self.clear_cooldown(language);
1587
1588 self.disabled_languages.remove(language);
1590
1591 self.allowed_languages.insert(language.to_string());
1593
1594 {
1596 let mut i = 0;
1597 while i < self.handles.len() {
1598 if !self.handles[i].handle.scope().is_universal()
1599 && self.handles[i].handle.scope().accepts(language)
1600 {
1601 let sh = self.handles.remove(i);
1602 fire_and_forget(sh.handle.shutdown());
1603 } else {
1604 i += 1;
1605 }
1606 }
1607 }
1608
1609 if self.force_spawn(language, file_path).is_some() {
1611 let message = format!("LSP server for {} started", language);
1612 tracing::info!("{}", message);
1613 (true, message)
1614 } else {
1615 let message = format!("Failed to start LSP server for {}", language);
1616 tracing::error!("{}", message);
1617 (false, message)
1618 }
1619 }
1620
1621 pub fn manual_restart_server(
1626 &mut self,
1627 language: &str,
1628 server_name: &str,
1629 file_path: Option<&Path>,
1630 ) -> (bool, String) {
1631 self.clear_cooldown(language);
1632 self.disabled_languages.remove(language);
1633 self.allowed_languages.insert(language.to_string());
1634
1635 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1637 let sh = self.handles.remove(idx);
1638 fire_and_forget(sh.handle.shutdown());
1639 }
1640
1641 let is_universal = self
1643 .universal_configs
1644 .iter()
1645 .any(|c| c.display_name() == server_name);
1646 let config = if is_universal {
1647 self.universal_configs
1648 .iter()
1649 .find(|c| c.display_name() == server_name)
1650 .cloned()
1651 } else {
1652 self.config
1653 .get(language)
1654 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1655 .cloned()
1656 };
1657
1658 let Some(config) = config else {
1659 let message = format!(
1660 "No config found for server '{}' ({})",
1661 server_name, language
1662 );
1663 tracing::error!("{}", message);
1664 return (false, message);
1665 };
1666
1667 if config.command.is_empty() {
1668 let message = format!(
1669 "LSP command is empty for {} server '{}'",
1670 language, server_name
1671 );
1672 tracing::error!("{}", message);
1673 return (false, message);
1674 }
1675
1676 let runtime = match self.runtime.as_ref() {
1677 Some(r) => r.clone(),
1678 None => return (false, "No tokio runtime available".to_string()),
1679 };
1680 let async_bridge = match self.async_bridge.as_ref() {
1681 Some(b) => b.clone(),
1682 None => return (false, "No async bridge available".to_string()),
1683 };
1684 let long_running_spawner =
1685 self.long_running_spawner
1686 .as_ref()
1687 .cloned()
1688 .unwrap_or_else(|| {
1689 std::sync::Arc::new(crate::services::remote::LocalLongRunningSpawner::new(
1690 std::sync::Arc::new(crate::services::env_provider::EnvProvider::inactive()),
1691 std::sync::Arc::new(
1692 crate::services::workspace_trust::WorkspaceTrust::permissive(),
1693 ),
1694 ))
1695 });
1696
1697 let scope = if is_universal {
1698 LanguageScope::all()
1699 } else {
1700 LanguageScope::single(language)
1701 };
1702
1703 match LspHandle::spawn(
1704 &runtime,
1705 &config.command,
1706 &config.args,
1707 config.env.clone(),
1708 scope,
1709 server_name.to_string(),
1710 &async_bridge,
1711 config.process_limits.clone(),
1712 config.language_id_overrides.clone(),
1713 long_running_spawner,
1714 ) {
1715 Ok(handle) => {
1716 let effective_root = if is_universal {
1717 file_path
1718 .and_then(|p| {
1719 let root = detect_workspace_root(p, &config.root_markers);
1720 path_to_uri(&root)
1721 })
1722 .or_else(|| self.root_uri.clone())
1723 } else {
1724 self.resolve_root_uri(language, file_path)
1725 };
1726 if let Err(e) =
1727 handle.initialize(effective_root, config.initialization_options.clone())
1728 {
1729 let message = format!(
1730 "Failed to initialize LSP server '{}' for {}: {}",
1731 server_name, language, e
1732 );
1733 tracing::error!("{}", message);
1734 return (false, message);
1735 }
1736
1737 let sh = ServerHandle {
1738 name: server_name.to_string(),
1739 handle,
1740 feature_filter: config.feature_filter(),
1741 capabilities: ServerCapabilitySummary::default(),
1742 };
1743
1744 self.handles.push(sh);
1745
1746 let message = format!("LSP server '{}' for {} started", server_name, language);
1747 tracing::info!("{}", message);
1748 (true, message)
1749 }
1750 Err(e) => {
1751 let message = format!(
1752 "Failed to start LSP server '{}' for {}: {}",
1753 server_name, language, e
1754 );
1755 tracing::error!("{}", message);
1756 (false, message)
1757 }
1758 }
1759 }
1760
1761 pub fn restart_attempt_count(&self, language: &str) -> usize {
1763 let now = Instant::now();
1764 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1765 self.restart_attempts
1766 .get(language)
1767 .map(|attempts| {
1768 attempts
1769 .iter()
1770 .filter(|t| now.duration_since(**t) < window)
1771 .count()
1772 })
1773 .unwrap_or(0)
1774 }
1775
1776 pub fn running_servers(&self) -> Vec<String> {
1778 let mut labels: Vec<String> = self
1779 .handles
1780 .iter()
1781 .map(|sh| sh.handle.scope().label().to_string())
1782 .collect();
1783 labels.sort();
1784 labels.dedup();
1785 labels
1786 }
1787
1788 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1790 self.handles
1791 .iter()
1792 .filter(|sh| sh.handle.scope().accepts(language))
1793 .map(|sh| sh.name.clone())
1794 .collect()
1795 }
1796
1797 pub fn is_server_ready(&self, language: &str) -> bool {
1799 self.handles
1800 .iter()
1801 .filter(|sh| sh.handle.scope().accepts(language))
1802 .any(|sh| sh.handle.state().can_send_requests())
1803 }
1804
1805 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1810 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1811 tracing::warn!(
1812 "No running LSP server named '{}' found for {}",
1813 server_name,
1814 language
1815 );
1816 return false;
1817 };
1818
1819 let sh = self.handles.remove(idx);
1820 tracing::info!(
1821 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1822 sh.name,
1823 language
1824 );
1825 fire_and_forget(sh.handle.shutdown());
1826
1827 let has_remaining = self
1829 .handles
1830 .iter()
1831 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1832 if !has_remaining {
1833 self.disabled_languages.insert(language.to_string());
1834 self.pending_restarts.remove(language);
1835 self.restart_cooldown.remove(language);
1836 self.allowed_languages.remove(language);
1837 }
1838
1839 true
1840 }
1841
1842 pub fn shutdown_server(&mut self, language: &str) -> bool {
1847 let mut found = false;
1848 let mut i = 0;
1849 while i < self.handles.len() {
1850 if !self.handles[i].handle.scope().is_universal()
1851 && self.handles[i].handle.scope().accepts(language)
1852 {
1853 let sh = self.handles.remove(i);
1854 tracing::info!(
1855 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1856 sh.name,
1857 language
1858 );
1859 fire_and_forget(sh.handle.shutdown());
1860 found = true;
1861 } else {
1862 i += 1;
1863 }
1864 }
1865
1866 if found {
1867 self.disabled_languages.insert(language.to_string());
1868 self.pending_restarts.remove(language);
1869 self.restart_cooldown.remove(language);
1870 self.allowed_languages.remove(language);
1871 } else {
1872 tracing::warn!("No running LSP server found for {}", language);
1873 }
1874
1875 found
1876 }
1877
1878 pub fn shutdown_all(&mut self) {
1880 for sh in &self.handles {
1881 tracing::info!(
1882 "Shutting down LSP server '{}' ({})",
1883 sh.name,
1884 sh.handle.scope().label()
1885 );
1886 fire_and_forget(sh.handle.shutdown());
1887 }
1888 self.handles.clear();
1889 }
1890}
1891
1892impl Drop for LspManager {
1893 fn drop(&mut self) {
1894 self.shutdown_all();
1895 }
1896}
1897
1898pub fn detect_language(
1910 path: &std::path::Path,
1911 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1912) -> Option<String> {
1913 let detected = detect_language_by_config(path, languages);
1914
1915 if detected.as_deref() == Some("c")
1921 && path.extension().and_then(|e| e.to_str()) == Some("h")
1922 && languages.contains_key("cpp")
1923 && header_in_cpp_tree(path)
1924 {
1925 return Some("cpp".to_string());
1926 }
1927
1928 detected
1929}
1930
1931fn detect_language_by_config(
1933 path: &std::path::Path,
1934 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1935) -> Option<String> {
1936 use crate::primitives::glob_match::{
1937 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1938 };
1939
1940 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1941 for (language_name, lang_config) in languages {
1943 if lang_config
1944 .filenames
1945 .iter()
1946 .any(|f| !is_glob_pattern(f) && f == filename)
1947 {
1948 return Some(language_name.clone());
1949 }
1950 }
1951
1952 let path_str = path.to_str().unwrap_or("");
1956 for (language_name, lang_config) in languages {
1957 if lang_config.filenames.iter().any(|f| {
1958 if !is_glob_pattern(f) {
1959 return false;
1960 }
1961 if is_path_pattern(f) {
1962 path_glob_matches(f, path_str)
1963 } else {
1964 filename_glob_matches(f, filename)
1965 }
1966 }) {
1967 return Some(language_name.clone());
1968 }
1969 }
1970 }
1971
1972 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1974 for (language_name, lang_config) in languages {
1975 if lang_config.extensions.iter().any(|ext| ext == extension) {
1976 return Some(language_name.clone());
1977 }
1978 }
1979 }
1980
1981 None
1982}
1983
1984fn header_in_cpp_tree(path: &std::path::Path) -> bool {
2014 let Some(start_dir) = path.parent() else {
2015 return false;
2016 };
2017
2018 if let Ok(entries) = std::fs::read_dir(start_dir) {
2020 for entry in entries.flatten() {
2021 let p = entry.path();
2022 let Some(ext) = p.extension().and_then(|e| e.to_str()) else {
2023 continue;
2024 };
2025 if matches!(
2026 ext,
2027 "cc" | "cpp" | "cxx" | "C" | "c++" | "hpp" | "hh" | "hxx"
2028 ) {
2029 return true;
2030 }
2031 }
2032 }
2033
2034 let mut current = Some(start_dir);
2038 let mut depth = 0u32;
2039 while let Some(dir) = current {
2040 let cc = dir.join("compile_commands.json");
2041 if cc.is_file() && compile_commands_has_cpp_marker(&cc) {
2042 return true;
2043 }
2044 if depth >= 10 {
2045 break;
2046 }
2047 depth += 1;
2048 current = dir.parent();
2049 }
2050
2051 false
2052}
2053
2054fn compile_commands_has_cpp_marker(path: &std::path::Path) -> bool {
2062 use std::io::Read;
2063 const MAX_READ: u64 = 1_048_576;
2064
2065 let Ok(file) = std::fs::File::open(path) else {
2066 return false;
2067 };
2068 let mut buf = Vec::with_capacity(64 * 1024);
2069 if file.take(MAX_READ).read_to_end(&mut buf).is_err() {
2070 return false;
2071 }
2072 let Ok(text) = std::str::from_utf8(&buf) else {
2073 return false;
2074 };
2075
2076 if text.contains("c++") {
2080 return true;
2081 }
2082 text.contains(".cpp") || text.contains(".cxx") || text.contains(".cc\"")
2085}
2086
2087#[cfg(test)]
2088mod tests {
2089 use super::*;
2090 use std::path::Path;
2091
2092 #[test]
2093 fn test_lsp_manager_new() {
2094 let root_uri: Option<Uri> = "file:///test".parse().ok();
2095 let manager = LspManager::new(fresh_core::WindowId(1), root_uri.clone());
2096
2097 assert_eq!(manager.handles.len(), 0);
2099 assert_eq!(manager.config.len(), 0);
2100 assert!(manager.root_uri.is_some());
2101 assert!(manager.runtime.is_none());
2102 assert!(manager.async_bridge.is_none());
2103 }
2104
2105 #[test]
2106 fn test_lsp_manager_set_language_config() {
2107 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2108
2109 let config = LspServerConfig {
2110 enabled: true,
2111 command: "rust-analyzer".to_string(),
2112 args: vec![],
2113 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2114 auto_start: false,
2115 initialization_options: None,
2116 env: Default::default(),
2117 language_id_overrides: Default::default(),
2118 name: None,
2119 only_features: None,
2120 except_features: None,
2121 root_markers: Default::default(),
2122 };
2123
2124 manager.set_language_config("rust".to_string(), config);
2125
2126 assert_eq!(manager.config.len(), 1);
2127 assert!(manager.config.contains_key("rust"));
2128 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
2129 }
2130
2131 #[test]
2132 fn test_lsp_manager_force_spawn_no_runtime() {
2133 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2134
2135 manager.set_language_config(
2137 "rust".to_string(),
2138 LspServerConfig {
2139 enabled: true,
2140 command: "rust-analyzer".to_string(),
2141 args: vec![],
2142 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2143 auto_start: false,
2144 initialization_options: None,
2145 env: Default::default(),
2146 language_id_overrides: Default::default(),
2147 name: None,
2148 only_features: None,
2149 except_features: None,
2150 root_markers: Default::default(),
2151 },
2152 );
2153
2154 let result = manager.force_spawn("rust", None);
2156 assert!(result.is_none());
2157 }
2158
2159 #[test]
2160 fn test_lsp_manager_force_spawn_no_config() {
2161 let rt = tokio::runtime::Runtime::new().unwrap();
2162 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2163 let async_bridge = AsyncBridge::new();
2164
2165 manager.set_runtime(rt.handle().clone(), async_bridge);
2166
2167 let result = manager.force_spawn("rust", None);
2169 assert!(result.is_none());
2170 }
2171
2172 #[test]
2173 fn test_lsp_manager_force_spawn_disabled_language() {
2174 let rt = tokio::runtime::Runtime::new().unwrap();
2175 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2176 let async_bridge = AsyncBridge::new();
2177
2178 manager.set_runtime(rt.handle().clone(), async_bridge);
2179
2180 manager.set_language_config(
2182 "rust".to_string(),
2183 LspServerConfig {
2184 enabled: false,
2185 command: String::new(), args: vec![],
2187 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
2188 auto_start: false,
2189 initialization_options: None,
2190 env: Default::default(),
2191 language_id_overrides: Default::default(),
2192 name: None,
2193 only_features: None,
2194 except_features: None,
2195 root_markers: Default::default(),
2196 },
2197 );
2198
2199 let result = manager.force_spawn("rust", None);
2201 assert!(result.is_none());
2202 }
2203
2204 #[test]
2210 fn test_lsp_manager_try_spawn_returns_disabled_when_all_configs_disabled() {
2211 let rt = tokio::runtime::Runtime::new().unwrap();
2212 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2213 let async_bridge = AsyncBridge::new();
2214 manager.set_runtime(rt.handle().clone(), async_bridge);
2215
2216 manager.set_language_config(
2217 "rust".to_string(),
2218 LspServerConfig {
2219 enabled: false,
2220 command: String::new(),
2221 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 assert_eq!(manager.try_spawn("rust", None), LspSpawnResult::Disabled);
2235 }
2236
2237 #[test]
2238 fn test_lsp_manager_shutdown_all() {
2239 let mut manager = LspManager::new(fresh_core::WindowId(1), None);
2240
2241 manager.shutdown_all();
2243 assert_eq!(manager.handles.len(), 0);
2244 }
2245
2246 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2247 let mut languages = std::collections::HashMap::new();
2248 languages.insert(
2249 "rust".to_string(),
2250 crate::config::LanguageConfig {
2251 extensions: vec!["rs".to_string()],
2252 filenames: vec![],
2253 grammar: "rust".to_string(),
2254 comment_prefix: Some("//".to_string()),
2255 auto_indent: true,
2256 auto_close: None,
2257 auto_surround: None,
2258 textmate_grammar: None,
2259 show_whitespace_tabs: false,
2260 line_wrap: None,
2261 wrap_column: None,
2262 page_view: None,
2263 page_width: None,
2264 use_tabs: None,
2265 tab_size: None,
2266 formatter: None,
2267 format_on_save: false,
2268 on_save: vec![],
2269 word_characters: None,
2270 },
2271 );
2272 languages.insert(
2273 "javascript".to_string(),
2274 crate::config::LanguageConfig {
2275 extensions: vec!["js".to_string(), "jsx".to_string()],
2276 filenames: vec![],
2277 grammar: "javascript".to_string(),
2278 comment_prefix: Some("//".to_string()),
2279 auto_indent: true,
2280 auto_close: None,
2281 auto_surround: None,
2282 textmate_grammar: None,
2283 show_whitespace_tabs: false,
2284 line_wrap: None,
2285 wrap_column: None,
2286 page_view: None,
2287 page_width: None,
2288 use_tabs: None,
2289 tab_size: None,
2290 formatter: None,
2291 format_on_save: false,
2292 on_save: vec![],
2293 word_characters: None,
2294 },
2295 );
2296 languages.insert(
2297 "csharp".to_string(),
2298 crate::config::LanguageConfig {
2299 extensions: vec!["cs".to_string()],
2300 filenames: vec![],
2301 grammar: "c_sharp".to_string(),
2302 comment_prefix: Some("//".to_string()),
2303 auto_indent: true,
2304 auto_close: None,
2305 auto_surround: None,
2306 textmate_grammar: None,
2307 show_whitespace_tabs: false,
2308 line_wrap: None,
2309 wrap_column: None,
2310 page_view: None,
2311 page_width: None,
2312 use_tabs: None,
2313 tab_size: None,
2314 formatter: None,
2315 format_on_save: false,
2316 on_save: vec![],
2317 word_characters: None,
2318 },
2319 );
2320 languages
2321 }
2322
2323 #[test]
2324 fn test_detect_language_from_config() {
2325 let languages = test_languages();
2326
2327 assert_eq!(
2329 detect_language(Path::new("main.rs"), &languages),
2330 Some("rust".to_string())
2331 );
2332 assert_eq!(
2333 detect_language(Path::new("index.js"), &languages),
2334 Some("javascript".to_string())
2335 );
2336 assert_eq!(
2337 detect_language(Path::new("App.jsx"), &languages),
2338 Some("javascript".to_string())
2339 );
2340 assert_eq!(
2341 detect_language(Path::new("Program.cs"), &languages),
2342 Some("csharp".to_string())
2343 );
2344
2345 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
2347 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
2348 assert_eq!(detect_language(Path::new("file"), &languages), None);
2349 }
2350
2351 #[test]
2352 fn test_detect_language_no_extension() {
2353 let languages = test_languages();
2354 assert_eq!(detect_language(Path::new("README"), &languages), None);
2355 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
2356 }
2357
2358 #[test]
2359 fn test_detect_language_path_glob() {
2360 let mut languages = test_languages();
2361 languages.insert(
2362 "shell".to_string(),
2363 crate::config::LanguageConfig {
2364 extensions: vec!["sh".to_string()],
2365 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
2366 grammar: "bash".to_string(),
2367 comment_prefix: Some("#".to_string()),
2368 auto_indent: true,
2369 auto_close: None,
2370 auto_surround: None,
2371 textmate_grammar: None,
2372 show_whitespace_tabs: false,
2373 line_wrap: None,
2374 wrap_column: None,
2375 page_view: None,
2376 page_width: None,
2377 use_tabs: None,
2378 tab_size: None,
2379 formatter: None,
2380 format_on_save: false,
2381 on_save: vec![],
2382 word_characters: None,
2383 },
2384 );
2385
2386 assert_eq!(
2388 detect_language(Path::new("/etc/rc.conf"), &languages),
2389 Some("shell".to_string())
2390 );
2391 assert_eq!(
2392 detect_language(Path::new("/etc/init/rc.local"), &languages),
2393 Some("shell".to_string())
2394 );
2395 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
2397
2398 assert_eq!(
2400 detect_language(Path::new("lfrc"), &languages),
2401 Some("shell".to_string())
2402 );
2403 }
2404
2405 #[test]
2406 fn test_detect_workspace_root_finds_marker_in_parent() {
2407 let tmp = tempfile::tempdir().unwrap();
2408 let project = tmp.path().join("myproject");
2409 let src = project.join("src");
2410 std::fs::create_dir_all(&src).unwrap();
2411 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2412 let file = src.join("main.rs");
2413 std::fs::write(&file, "").unwrap();
2414
2415 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
2416 assert_eq!(root, project);
2417 }
2418
2419 #[test]
2420 fn test_detect_workspace_root_finds_marker_two_levels_up() {
2421 let tmp = tempfile::tempdir().unwrap();
2422 let project = tmp.path().join("myproject");
2423 let deep = project.join("src").join("nested");
2424 std::fs::create_dir_all(&deep).unwrap();
2425 std::fs::write(project.join("Cargo.toml"), "").unwrap();
2426 let file = deep.join("lib.rs");
2427 std::fs::write(&file, "").unwrap();
2428
2429 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
2430 assert_eq!(root, project);
2431 }
2432
2433 #[test]
2434 fn test_detect_workspace_root_no_marker_returns_parent() {
2435 let tmp = tempfile::tempdir().unwrap();
2436 let dir = tmp.path().join("somedir");
2437 std::fs::create_dir_all(&dir).unwrap();
2438 let file = dir.join("file.txt");
2439 std::fs::write(&file, "").unwrap();
2440
2441 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
2442 assert_eq!(root, dir);
2443 }
2444
2445 #[test]
2446 fn test_detect_workspace_root_empty_markers_returns_parent() {
2447 let tmp = tempfile::tempdir().unwrap();
2448 let dir = tmp.path().join("somedir");
2449 std::fs::create_dir_all(&dir).unwrap();
2450 let file = dir.join("file.txt");
2451 std::fs::write(&file, "").unwrap();
2452
2453 let root = detect_workspace_root(&file, &[]);
2454 assert_eq!(root, dir);
2455 }
2456
2457 #[test]
2458 fn test_detect_workspace_root_directory_marker() {
2459 let tmp = tempfile::tempdir().unwrap();
2460 let project = tmp.path().join("myproject");
2461 let src = project.join("src");
2462 std::fs::create_dir_all(&src).unwrap();
2463 std::fs::create_dir_all(project.join(".git")).unwrap();
2464 let file = src.join("main.rs");
2465 std::fs::write(&file, "").unwrap();
2466
2467 let root = detect_workspace_root(&file, &[".git".to_string()]);
2468 assert_eq!(root, project);
2469 }
2470
2471 fn c_cpp_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
2476 use crate::config::LanguageConfig;
2477 let mut languages = std::collections::HashMap::new();
2478 let base = LanguageConfig {
2479 extensions: vec![],
2480 filenames: vec![],
2481 grammar: String::new(),
2482 comment_prefix: Some("//".to_string()),
2483 auto_indent: true,
2484 auto_close: None,
2485 auto_surround: None,
2486 textmate_grammar: None,
2487 show_whitespace_tabs: false,
2488 line_wrap: None,
2489 wrap_column: None,
2490 page_view: None,
2491 page_width: None,
2492 use_tabs: None,
2493 tab_size: None,
2494 formatter: None,
2495 format_on_save: false,
2496 on_save: vec![],
2497 word_characters: None,
2498 };
2499 languages.insert(
2500 "c".to_string(),
2501 LanguageConfig {
2502 extensions: vec!["c".to_string(), "h".to_string()],
2503 grammar: "c".to_string(),
2504 ..base.clone()
2505 },
2506 );
2507 languages.insert(
2508 "cpp".to_string(),
2509 LanguageConfig {
2510 extensions: vec![
2511 "cpp".to_string(),
2512 "cc".to_string(),
2513 "cxx".to_string(),
2514 "hpp".to_string(),
2515 "hh".to_string(),
2516 "hxx".to_string(),
2517 ],
2518 grammar: "cpp".to_string(),
2519 ..base
2520 },
2521 );
2522 languages
2523 }
2524
2525 #[test]
2526 fn test_detect_language_h_stays_c_without_cpp_signals() {
2527 let languages = c_cpp_languages();
2531 assert_eq!(
2532 detect_language(Path::new("foo.h"), &languages),
2533 Some("c".to_string())
2534 );
2535 }
2536
2537 #[test]
2538 fn test_detect_language_h_promotes_to_cpp_with_sibling_cpp_source() {
2539 let tmp = tempfile::tempdir().unwrap();
2540 let project = tmp.path().join("proj");
2541 std::fs::create_dir_all(&project).unwrap();
2542 let header = project.join("widget.h");
2543 std::fs::write(&header, "").unwrap();
2544 std::fs::write(project.join("widget.cpp"), "").unwrap();
2546
2547 let languages = c_cpp_languages();
2548 assert_eq!(
2549 detect_language(&header, &languages),
2550 Some("cpp".to_string())
2551 );
2552 }
2553
2554 #[test]
2555 fn test_detect_language_h_promotes_to_cpp_with_sibling_hpp() {
2556 let tmp = tempfile::tempdir().unwrap();
2557 let project = tmp.path().join("proj");
2558 std::fs::create_dir_all(&project).unwrap();
2559 let header = project.join("a.h");
2560 std::fs::write(&header, "").unwrap();
2561 std::fs::write(project.join("b.hpp"), "").unwrap();
2563
2564 let languages = c_cpp_languages();
2565 assert_eq!(
2566 detect_language(&header, &languages),
2567 Some("cpp".to_string())
2568 );
2569 }
2570
2571 #[test]
2572 fn test_detect_language_h_promotes_to_cpp_with_ancestor_compile_commands() {
2573 let tmp = tempfile::tempdir().unwrap();
2574 let project = tmp.path().join("proj");
2575 let include = project.join("include").join("fmt");
2576 std::fs::create_dir_all(&include).unwrap();
2577 std::fs::write(
2581 project.join("compile_commands.json"),
2582 r#"[{"directory":"/proj","command":"/usr/bin/clang++ -std=c++17 -c src/format.cc","file":"src/format.cc"}]"#,
2583 ).unwrap();
2584 let header = include.join("format.h");
2585 std::fs::write(&header, "").unwrap();
2586
2587 let languages = c_cpp_languages();
2588 assert_eq!(
2589 detect_language(&header, &languages),
2590 Some("cpp".to_string())
2591 );
2592 }
2593
2594 #[test]
2595 fn test_detect_language_h_stays_c_with_pure_c_compile_commands() {
2596 let tmp = tempfile::tempdir().unwrap();
2599 let project = tmp.path().join("cproj");
2600 let include = project.join("include");
2601 std::fs::create_dir_all(&include).unwrap();
2602 std::fs::write(
2603 project.join("compile_commands.json"),
2604 r#"[{"directory":"/cproj","command":"/usr/bin/gcc -std=c11 -c src/lib.c","file":"src/lib.c"}]"#,
2605 )
2606 .unwrap();
2607 let header = include.join("lib.h");
2608 std::fs::write(&header, "").unwrap();
2609
2610 let languages = c_cpp_languages();
2611 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2612 }
2613
2614 #[test]
2615 fn test_detect_language_h_stays_c_in_pure_c_tree() {
2616 let tmp = tempfile::tempdir().unwrap();
2617 let project = tmp.path().join("cproj");
2618 std::fs::create_dir_all(&project).unwrap();
2619 let header = project.join("lib.h");
2620 std::fs::write(&header, "").unwrap();
2621 std::fs::write(project.join("lib.c"), "").unwrap();
2623
2624 let languages = c_cpp_languages();
2625 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2626 }
2627
2628 #[test]
2629 fn test_detect_language_h_stays_c_with_empty_compile_commands() {
2630 let tmp = tempfile::tempdir().unwrap();
2633 let project = tmp.path().join("proj");
2634 std::fs::create_dir_all(&project).unwrap();
2635 std::fs::write(project.join("compile_commands.json"), "[]").unwrap();
2636 let header = project.join("foo.h");
2637 std::fs::write(&header, "").unwrap();
2638
2639 let languages = c_cpp_languages();
2640 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2641 }
2642
2643 #[test]
2644 fn test_detect_language_h_promotes_on_cpp_std_flag_alone() {
2645 let tmp = tempfile::tempdir().unwrap();
2647 let project = tmp.path().join("proj");
2648 let include = project.join("include");
2649 std::fs::create_dir_all(&include).unwrap();
2650 std::fs::write(
2651 project.join("compile_commands.json"),
2652 r#"[{"directory":"/proj","command":"/usr/bin/clang -std=c++20 -c src/x.C","file":"src/x.C"}]"#,
2656 )
2657 .unwrap();
2658 let header = include.join("x.h");
2659 std::fs::write(&header, "").unwrap();
2660
2661 let languages = c_cpp_languages();
2662 assert_eq!(
2663 detect_language(&header, &languages),
2664 Some("cpp".to_string())
2665 );
2666 }
2667
2668 #[test]
2669 fn test_detect_language_c_source_never_promoted() {
2670 let tmp = tempfile::tempdir().unwrap();
2672 let project = tmp.path().join("proj");
2673 std::fs::create_dir_all(&project).unwrap();
2674 let source = project.join("legacy.c");
2675 std::fs::write(&source, "").unwrap();
2676 std::fs::write(project.join("main.cpp"), "").unwrap();
2677
2678 let languages = c_cpp_languages();
2679 assert_eq!(detect_language(&source, &languages), Some("c".to_string()));
2680 }
2681
2682 #[test]
2683 fn test_detect_language_h_no_promotion_without_cpp_config() {
2684 let tmp = tempfile::tempdir().unwrap();
2687 let project = tmp.path().join("proj");
2688 std::fs::create_dir_all(&project).unwrap();
2689 let header = project.join("widget.h");
2690 std::fs::write(&header, "").unwrap();
2691 std::fs::write(project.join("widget.cpp"), "").unwrap();
2692
2693 let mut languages = c_cpp_languages();
2694 languages.remove("cpp");
2695 assert_eq!(detect_language(&header, &languages), Some("c".to_string()));
2696 }
2697
2698 #[test]
2699 fn test_path_to_uri_basic() {
2700 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
2701 assert_eq!(uri.as_str(), "file:///tmp/test");
2702 }
2703
2704 #[test]
2705 fn test_path_to_uri_with_spaces() {
2706 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
2707 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
2708 }
2709
2710 #[test]
2711 fn dynamic_registration_enables_then_disables_inlay_hints() {
2712 let mut caps = ServerCapabilitySummary::default();
2716 assert!(!caps.inlay_hints);
2717
2718 let recognized = caps.apply_dynamic_registration("textDocument/inlayHint", None, true);
2719 assert!(
2720 recognized,
2721 "inlayHint must be a recognized capability method"
2722 );
2723 assert!(
2724 caps.inlay_hints,
2725 "dynamic registration must enable inlay hints"
2726 );
2727
2728 let recognized = caps.apply_dynamic_registration("textDocument/inlayHint", None, false);
2729 assert!(recognized);
2730 assert!(!caps.inlay_hints, "unregister must disable inlay hints");
2731 }
2732
2733 #[test]
2734 fn dynamic_registration_ignores_unknown_methods() {
2735 let mut caps = ServerCapabilitySummary::default();
2739 let recognized =
2740 caps.apply_dynamic_registration("workspace/didChangeWatchedFiles", None, true);
2741 assert!(!recognized);
2742 }
2743
2744 #[test]
2745 fn dynamic_registration_parses_completion_options() {
2746 let mut caps = ServerCapabilitySummary::default();
2747 let opts = serde_json::json!({
2748 "triggerCharacters": [".", "::"],
2749 "resolveProvider": true,
2750 });
2751 let recognized =
2752 caps.apply_dynamic_registration("textDocument/completion", Some(&opts), true);
2753 assert!(recognized);
2754 assert!(caps.completion);
2755 assert!(caps.completion_resolve);
2756 assert_eq!(caps.completion_trigger_characters, vec![".", "::"]);
2757
2758 caps.apply_dynamic_registration("textDocument/completion", None, false);
2760 assert!(!caps.completion);
2761 assert!(!caps.completion_resolve);
2762 assert!(caps.completion_trigger_characters.is_empty());
2763 }
2764
2765 #[test]
2766 fn dynamic_registration_parses_semantic_tokens_legend() {
2767 let mut caps = ServerCapabilitySummary::default();
2768 let opts = serde_json::json!({
2769 "legend": {
2770 "tokenTypes": ["namespace", "type"],
2771 "tokenModifiers": ["declaration"],
2772 },
2773 "full": { "delta": true },
2774 "range": true,
2775 });
2776 let recognized =
2777 caps.apply_dynamic_registration("textDocument/semanticTokens", Some(&opts), true);
2778 assert!(recognized);
2779 assert!(caps.semantic_tokens_full);
2780 assert!(caps.semantic_tokens_full_delta);
2781 assert!(caps.semantic_tokens_range);
2782 let legend = caps
2783 .semantic_tokens_legend
2784 .as_ref()
2785 .expect("legend must be parsed from registration options");
2786 assert_eq!(legend.token_types.len(), 2);
2787
2788 caps.apply_dynamic_registration("textDocument/semanticTokens", None, false);
2789 assert!(!caps.semantic_tokens_full);
2790 assert!(caps.semantic_tokens_legend.is_none());
2791 }
2792
2793 #[test]
2794 fn apply_dynamic_capabilities_reports_change_only_for_known_methods() {
2795 let mut caps = ServerCapabilitySummary::default();
2798 let known = caps.apply_dynamic_registration("textDocument/hover", None, true);
2799 let unknown = caps.apply_dynamic_registration("some/unknownMethod", None, true);
2800 assert!(known);
2801 assert!(!unknown);
2802 assert!(caps.hover);
2803 }
2804}