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 Failed,
79}
80
81const MAX_RESTARTS_IN_WINDOW: usize = 5;
83const RESTART_WINDOW_SECS: u64 = 180; const RESTART_BACKOFF_BASE_MS: u64 = 1000; fn path_to_uri(path: &Path) -> Option<Uri> {
88 let abs = if path.is_absolute() {
89 path.to_path_buf()
90 } else {
91 std::env::current_dir().ok()?.join(path)
92 };
93 let encoded: String = abs
95 .components()
96 .filter_map(|c| match c {
97 std::path::Component::RootDir => None, std::path::Component::Normal(s) => {
99 let s = s.to_str()?;
100 let mut out = String::with_capacity(s.len() + 1);
101 out.push('/');
102 for b in s.bytes() {
103 if b.is_ascii_alphanumeric()
104 || matches!(
105 b,
106 b'-' | b'.'
107 | b'_'
108 | b'~'
109 | b'@'
110 | b'!'
111 | b'$'
112 | b'&'
113 | b'\''
114 | b'('
115 | b')'
116 | b'+'
117 | b','
118 | b';'
119 | b'='
120 )
121 {
122 out.push(b as char);
123 } else {
124 out.push_str(&format!("%{:02X}", b));
125 }
126 }
127 Some(out)
128 }
129 _ => None,
130 })
131 .collect();
132 format!("file://{}", encoded).parse().ok()
133}
134
135pub fn detect_workspace_root(file_path: &Path, root_markers: &[String]) -> std::path::PathBuf {
140 let file_dir = file_path.parent().unwrap_or(file_path).to_path_buf();
141
142 if root_markers.is_empty() {
143 return file_dir;
144 }
145
146 let mut dir = Some(file_dir.as_path());
147 while let Some(d) = dir {
148 for marker in root_markers {
149 if d.join(marker).exists() {
150 return d.to_path_buf();
151 }
152 }
153 dir = d.parent();
154 }
155
156 file_dir
157}
158
159#[derive(Debug, Clone, Default)]
166pub struct ServerCapabilitySummary {
167 pub initialized: bool,
170 pub hover: bool,
171 pub completion: bool,
172 pub completion_resolve: bool,
173 pub completion_trigger_characters: Vec<String>,
174 pub definition: bool,
175 pub references: bool,
176 pub document_formatting: bool,
177 pub document_range_formatting: bool,
178 pub rename: bool,
179 pub signature_help: bool,
180 pub inlay_hints: bool,
181 pub folding_ranges: bool,
182 pub semantic_tokens_full: bool,
183 pub semantic_tokens_full_delta: bool,
184 pub semantic_tokens_range: bool,
185 pub semantic_tokens_legend: Option<SemanticTokensLegend>,
186 pub document_highlight: bool,
187 pub code_action: bool,
188 pub code_action_resolve: bool,
189 pub document_symbols: bool,
190 pub workspace_symbols: bool,
191 pub diagnostics: bool,
192}
193
194pub struct ServerHandle {
198 pub name: String,
200 pub handle: LspHandle,
202 pub feature_filter: FeatureFilter,
204 pub capabilities: ServerCapabilitySummary,
206}
207
208impl ServerHandle {
209 pub fn has_capability(&self, feature: LspFeature) -> bool {
218 if !self.capabilities.initialized {
219 return false;
220 }
221 match feature {
222 LspFeature::Hover => self.capabilities.hover,
223 LspFeature::Completion => self.capabilities.completion,
224 LspFeature::Definition => self.capabilities.definition,
225 LspFeature::References => self.capabilities.references,
226 LspFeature::Format => {
227 self.capabilities.document_formatting || self.capabilities.document_range_formatting
228 }
229 LspFeature::Rename => self.capabilities.rename,
230 LspFeature::SignatureHelp => self.capabilities.signature_help,
231 LspFeature::InlayHints => self.capabilities.inlay_hints,
232 LspFeature::FoldingRange => self.capabilities.folding_ranges,
233 LspFeature::SemanticTokens => {
234 self.capabilities.semantic_tokens_full || self.capabilities.semantic_tokens_range
235 }
236 LspFeature::DocumentHighlight => self.capabilities.document_highlight,
237 LspFeature::CodeAction => self.capabilities.code_action,
238 LspFeature::DocumentSymbols => self.capabilities.document_symbols,
239 LspFeature::WorkspaceSymbols => self.capabilities.workspace_symbols,
240 LspFeature::Diagnostics => self.capabilities.diagnostics,
241 }
242 }
243}
244
245pub struct LspManager {
247 handles: Vec<ServerHandle>,
250
251 config: HashMap<String, Vec<LspServerConfig>>,
253
254 universal_configs: Vec<LspServerConfig>,
256
257 root_uri: Option<Uri>,
259
260 per_language_root_uris: HashMap<String, Uri>,
262
263 runtime: Option<tokio::runtime::Handle>,
265
266 async_bridge: Option<AsyncBridge>,
268
269 restart_attempts: HashMap<String, Vec<Instant>>,
271
272 restart_cooldown: HashSet<String>,
274
275 pending_restarts: HashMap<String, Instant>,
277
278 allowed_languages: HashSet<String>,
281
282 disabled_languages: HashSet<String>,
285}
286
287impl LspManager {
288 pub fn new(root_uri: Option<Uri>) -> Self {
290 Self {
291 handles: Vec::new(),
292 config: HashMap::new(),
293 universal_configs: Vec::new(),
294 root_uri,
295 per_language_root_uris: HashMap::new(),
296 runtime: None,
297 async_bridge: None,
298 restart_attempts: HashMap::new(),
299 restart_cooldown: HashSet::new(),
300 pending_restarts: HashMap::new(),
301 allowed_languages: HashSet::new(),
302 disabled_languages: HashSet::new(),
303 }
304 }
305
306 pub fn is_language_allowed(&self, language: &str) -> bool {
308 self.allowed_languages.contains(language)
309 }
310
311 pub fn allow_language(&mut self, language: &str) {
313 self.allowed_languages.insert(language.to_string());
314 tracing::info!("LSP language '{}' manually enabled", language);
315 }
316
317 pub fn allowed_languages(&self) -> &HashSet<String> {
319 &self.allowed_languages
320 }
321
322 pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
324 self.config.get(language).map(|v| v.as_slice())
325 }
326
327 pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
329 self.config.get(language).and_then(|v| v.first())
330 }
331
332 pub fn set_server_capabilities(
334 &mut self,
335 _language: &str,
336 server_name: &str,
337 mut capabilities: ServerCapabilitySummary,
338 ) {
339 capabilities.initialized = true;
340
341 if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
342 sh.capabilities = capabilities;
343 }
344 }
345
346 pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
348 self.get_handles(language).into_iter().find_map(|sh| {
349 if sh.feature_filter.allows(LspFeature::SemanticTokens)
350 && sh.has_capability(LspFeature::SemanticTokens)
351 {
352 sh.capabilities.semantic_tokens_legend.as_ref()
353 } else {
354 None
355 }
356 })
357 }
358
359 pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
361 self.get_handles(language).iter().any(|sh| {
362 sh.feature_filter.allows(LspFeature::SemanticTokens)
363 && sh.capabilities.semantic_tokens_full
364 })
365 }
366
367 pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
369 self.get_handles(language).iter().any(|sh| {
370 sh.feature_filter.allows(LspFeature::SemanticTokens)
371 && sh.capabilities.semantic_tokens_full_delta
372 })
373 }
374
375 pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
377 self.get_handles(language).iter().any(|sh| {
378 sh.feature_filter.allows(LspFeature::SemanticTokens)
379 && sh.capabilities.semantic_tokens_range
380 })
381 }
382
383 pub fn folding_ranges_supported(&self, language: &str) -> bool {
385 self.get_handles(language).iter().any(|sh| {
386 sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
387 })
388 }
389
390 pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
392 let ch_str = ch.to_string();
393 self.get_handles(language).iter().any(|sh| {
394 sh.feature_filter.allows(LspFeature::Completion)
395 && sh
396 .capabilities
397 .completion_trigger_characters
398 .contains(&ch_str)
399 })
400 }
401
402 pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
416 if self
418 .handles
419 .iter()
420 .any(|sh| sh.handle.scope().accepts(language))
421 {
422 self.ensure_universal_servers_running(file_path);
423 return LspSpawnResult::Spawned;
424 }
425
426 if self.runtime.is_none() || self.async_bridge.is_none() {
428 return LspSpawnResult::Failed;
429 }
430
431 self.ensure_universal_servers_running(file_path);
433
434 let configs = match self.config.get(language) {
436 Some(configs) if !configs.is_empty() => configs,
437 _ => {
438 if self
440 .handles
441 .iter()
442 .any(|sh| sh.handle.scope().is_universal())
443 {
444 return LspSpawnResult::Spawned;
445 }
446 return LspSpawnResult::NotConfigured;
447 }
448 };
449
450 if !configs.iter().any(|c| c.enabled) {
452 if self
453 .handles
454 .iter()
455 .any(|sh| sh.handle.scope().is_universal())
456 {
457 return LspSpawnResult::Spawned;
458 }
459 return LspSpawnResult::Failed;
460 }
461
462 let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
464 if !any_auto_start && !self.allowed_languages.contains(language) {
465 if self
466 .handles
467 .iter()
468 .any(|sh| sh.handle.scope().is_universal())
469 {
470 return LspSpawnResult::Spawned;
471 }
472 return LspSpawnResult::NotAutoStart;
473 }
474
475 let spawned = self.force_spawn(language, file_path).is_some();
477
478 if spawned
479 || self
480 .handles
481 .iter()
482 .any(|sh| sh.handle.scope().is_universal())
483 {
484 LspSpawnResult::Spawned
485 } else {
486 LspSpawnResult::Failed
487 }
488 }
489
490 pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
494 self.runtime = Some(runtime);
495 self.async_bridge = Some(async_bridge);
496 }
497
498 pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
500 self.config.insert(language, vec![config]);
501 }
502
503 pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
505 self.config.insert(language, configs);
506 }
507
508 pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
510 self.config.entry(language).or_default().extend(configs);
511 }
512
513 pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
518 self.universal_configs = configs;
519 }
520
521 pub fn configured_languages(&self) -> Vec<String> {
523 self.config.keys().cloned().collect()
524 }
525
526 pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
531 self.root_uri = root_uri;
532 }
533
534 pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
540 tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
541 self.per_language_root_uris
542 .insert(language.to_string(), uri.clone());
543
544 if self
546 .handles
547 .iter()
548 .any(|sh| sh.handle.scope().accepts(language))
549 {
550 tracing::info!(
551 "Restarting {} LSP server with new root: {}",
552 language,
553 uri.as_str()
554 );
555 self.shutdown_server(language);
556 return true;
558 }
559 false
560 }
561
562 pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
569 if let Some(uri) = self.per_language_root_uris.get(language) {
571 return Some(uri.clone());
572 }
573
574 if let Some(path) = file_path {
576 let markers = self
577 .config
578 .get(language)
579 .and_then(|configs| configs.first())
580 .map(|c| c.root_markers.as_slice())
581 .unwrap_or(&[]);
582 let root = detect_workspace_root(path, markers);
583 if let Some(uri) = path_to_uri(&root) {
584 return Some(uri);
585 }
586 }
587
588 self.root_uri.clone()
590 }
591
592 pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
596 self.resolve_root_uri(language, None)
597 }
598
599 pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
604 self.shutdown_all();
606
607 self.root_uri = new_root_uri;
609
610 self.restart_attempts.clear();
612 self.restart_cooldown.clear();
613 self.pending_restarts.clear();
614
615 tracing::info!(
619 "LSP manager reset for new project: {:?}",
620 self.root_uri.as_ref().map(|u| u.as_str())
621 );
622 }
623
624 pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
627 self.handles
628 .iter()
629 .find(|sh| sh.handle.scope().accepts(language))
630 .map(|sh| &sh.handle)
631 }
632
633 pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
636 self.handles
637 .iter_mut()
638 .find(|sh| sh.handle.scope().accepts(language))
639 .map(|sh| &mut sh.handle)
640 }
641
642 pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
644 self.handles
645 .iter()
646 .filter(|sh| sh.handle.scope().accepts(language))
647 .collect()
648 }
649
650 pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
652 self.handles
653 .iter_mut()
654 .filter(|sh| sh.handle.scope().accepts(language))
655 .collect()
656 }
657
658 pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
662 self.handles
663 .iter()
664 .find(|sh| sh.name == server_name)
665 .map(|sh| sh.handle.scope())
666 }
667
668 pub fn has_handles(&self, language: &str) -> bool {
670 self.handles
671 .iter()
672 .any(|sh| sh.handle.scope().accepts(language))
673 }
674
675 pub fn handle_count(&self, language: &str) -> usize {
677 self.handles
678 .iter()
679 .filter(|sh| sh.handle.scope().accepts(language))
680 .count()
681 }
682
683 pub fn has_server_named(&self, server_name: &str) -> bool {
685 self.handles.iter().any(|sh| sh.name == server_name)
686 }
687
688 pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
694 self.handles
695 .iter()
696 .filter(|sh| sh.handle.scope().accepts(language))
697 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
698 }
699
700 pub fn handle_for_feature_mut(
704 &mut self,
705 language: &str,
706 feature: LspFeature,
707 ) -> Option<&mut ServerHandle> {
708 self.handles
709 .iter_mut()
710 .filter(|sh| sh.handle.scope().accepts(language))
711 .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
712 }
713
714 pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
718 self.handles
719 .iter()
720 .filter(|sh| sh.handle.scope().accepts(language))
721 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
722 .collect()
723 }
724
725 pub fn handles_for_feature_mut(
729 &mut self,
730 language: &str,
731 feature: LspFeature,
732 ) -> Vec<&mut ServerHandle> {
733 self.handles
734 .iter_mut()
735 .filter(|sh| sh.handle.scope().accepts(language))
736 .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
737 .collect()
738 }
739
740 pub fn force_spawn(
759 &mut self,
760 language: &str,
761 file_path: Option<&Path>,
762 ) -> Option<&mut LspHandle> {
763 tracing::debug!("force_spawn called for language: {}", language);
764
765 if self
767 .handles
768 .iter()
769 .any(|sh| sh.handle.scope().accepts(language))
770 {
771 tracing::debug!("force_spawn: returning existing handle for {}", language);
772 return self
773 .handles
774 .iter_mut()
775 .find(|sh| sh.handle.scope().accepts(language))
776 .map(|sh| &mut sh.handle);
777 }
778
779 if self.disabled_languages.contains(language) {
781 tracing::debug!(
782 "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
783 language
784 );
785 return None;
786 }
787
788 let configs = match self.config.get(language) {
790 Some(configs) if !configs.is_empty() => configs.clone(),
791 _ => {
792 tracing::warn!(
793 "force_spawn: no config found for language '{}', available configs: {:?}",
794 language,
795 self.config.keys().collect::<Vec<_>>()
796 );
797 return None;
798 }
799 };
800
801 let runtime = match self.runtime.as_ref() {
803 Some(r) => r.clone(),
804 None => {
805 tracing::error!("force_spawn: no tokio runtime available for {}", language);
806 return None;
807 }
808 };
809 let async_bridge = match self.async_bridge.as_ref() {
810 Some(b) => b.clone(),
811 None => {
812 tracing::error!("force_spawn: no async bridge available for {}", language);
813 return None;
814 }
815 };
816
817 let mut spawned_handles = Vec::new();
818 let manually_allowed = self.allowed_languages.contains(language);
819
820 for config in &configs {
821 if manually_allowed {
822 } else {
826 if !config.enabled || !config.auto_start {
832 continue;
833 }
834 }
835
836 if config.command.is_empty() {
837 tracing::warn!(
838 "force_spawn: LSP command is empty for {} server '{}'",
839 language,
840 config.display_name()
841 );
842 continue;
843 }
844
845 let server_name = config.display_name();
846 tracing::info!(
847 "Spawning LSP server '{}' for language: {}",
848 server_name,
849 language
850 );
851
852 match LspHandle::spawn(
853 &runtime,
854 &config.command,
855 &config.args,
856 config.env.clone(),
857 LanguageScope::single(language),
858 server_name.clone(),
859 &async_bridge,
860 config.process_limits.clone(),
861 config.language_id_overrides.clone(),
862 ) {
863 Ok(handle) => {
864 let effective_root = self.resolve_root_uri(language, file_path);
865 if let Err(e) =
866 handle.initialize(effective_root, config.initialization_options.clone())
867 {
868 tracing::error!(
869 "Failed to send initialize command for {} ({}): {}",
870 language,
871 server_name,
872 e
873 );
874 continue;
875 }
876
877 tracing::info!(
878 "LSP initialization started for {} ({}), will be ready asynchronously",
879 language,
880 server_name
881 );
882
883 spawned_handles.push(ServerHandle {
884 name: server_name,
885 handle,
886 feature_filter: config.feature_filter(),
887 capabilities: ServerCapabilitySummary::default(),
888 });
889 }
890 Err(e) => {
891 tracing::error!(
892 "Failed to spawn LSP handle for {} ({}): {}",
893 language,
894 server_name,
895 e
896 );
897 }
898 }
899 }
900
901 if spawned_handles.is_empty() {
902 return None;
903 }
904
905 self.handles.extend(spawned_handles);
906 self.handles
907 .iter_mut()
908 .rev()
909 .find(|sh| sh.handle.scope().accepts(language))
910 .map(|sh| &mut sh.handle)
911 }
912
913 fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
919 if self
920 .handles
921 .iter()
922 .any(|sh| sh.handle.scope().is_universal())
923 || self.universal_configs.is_empty()
924 {
925 return;
926 }
927
928 let runtime = match self.runtime.as_ref() {
929 Some(r) => r.clone(),
930 None => return,
931 };
932 let async_bridge = match self.async_bridge.as_ref() {
933 Some(b) => b.clone(),
934 None => return,
935 };
936
937 let mut spawned = Vec::new();
938 for config in &self.universal_configs {
939 if !config.enabled || !config.auto_start {
940 continue;
941 }
942 if config.command.is_empty() {
943 continue;
944 }
945
946 let server_name = config.display_name();
947 tracing::info!("Spawning universal LSP server '{}'", server_name);
948
949 match LspHandle::spawn(
950 &runtime,
951 &config.command,
952 &config.args,
953 config.env.clone(),
954 LanguageScope::all(),
955 server_name.clone(),
956 &async_bridge,
957 config.process_limits.clone(),
958 config.language_id_overrides.clone(),
959 ) {
960 Ok(handle) => {
961 let effective_root = file_path
962 .map(|p| {
963 let root = detect_workspace_root(p, &config.root_markers);
964 path_to_uri(&root)
965 })
966 .flatten()
967 .or_else(|| self.root_uri.clone());
968 if let Err(e) =
969 handle.initialize(effective_root, config.initialization_options.clone())
970 {
971 tracing::error!(
972 "Failed to initialize universal LSP server '{}': {}",
973 server_name,
974 e
975 );
976 continue;
977 }
978 tracing::info!(
979 "Universal LSP server '{}' initialization started",
980 server_name
981 );
982 spawned.push(ServerHandle {
983 name: server_name,
984 handle,
985 feature_filter: config.feature_filter(),
986 capabilities: ServerCapabilitySummary::default(),
987 });
988 }
989 Err(e) => {
990 tracing::error!(
991 "Failed to spawn universal LSP server '{}': {}",
992 server_name,
993 e
994 );
995 }
996 }
997 }
998
999 self.handles.extend(spawned);
1000 }
1001
1002 pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1006 if self
1008 .handles
1009 .iter()
1010 .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1011 {
1012 let universals: Vec<ServerHandle> = {
1014 let mut drained = Vec::new();
1015 let mut i = 0;
1016 while i < self.handles.len() {
1017 if self.handles[i].handle.scope().is_universal() {
1018 drained.push(self.handles.remove(i));
1019 } else {
1020 i += 1;
1021 }
1022 }
1023 drained
1024 };
1025 for sh in universals {
1026 fire_and_forget(sh.handle.shutdown());
1027 }
1028 return "Universal LSP server crashed. It will restart on next file open.".to_string();
1030 }
1031
1032 {
1034 let mut i = 0;
1035 while i < self.handles.len() {
1036 if !self.handles[i].handle.scope().is_universal()
1037 && self.handles[i].handle.scope().accepts(language)
1038 {
1039 let sh = self.handles.remove(i);
1040 fire_and_forget(sh.handle.shutdown());
1041 } else {
1042 i += 1;
1043 }
1044 }
1045 }
1046
1047 if self.disabled_languages.contains(language) {
1050 return format!(
1051 "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1052 language
1053 );
1054 }
1055
1056 if self.restart_cooldown.contains(language) {
1058 return format!(
1059 "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1060 language
1061 );
1062 }
1063
1064 let now = Instant::now();
1066 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1067 let attempts = self
1068 .restart_attempts
1069 .entry(language.to_string())
1070 .or_default();
1071 attempts.retain(|t| now.duration_since(*t) < window);
1072
1073 if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
1075 self.restart_cooldown.insert(language.to_string());
1076 tracing::warn!(
1077 "LSP server for {} has crashed {} times in {} minutes, entering cooldown",
1078 language,
1079 MAX_RESTARTS_IN_WINDOW,
1080 RESTART_WINDOW_SECS / 60
1081 );
1082 return format!(
1083 "LSP server for {} has crashed too many times ({} in {} min). Use 'Restart LSP Server' command to manually restart.",
1084 language,
1085 MAX_RESTARTS_IN_WINDOW,
1086 RESTART_WINDOW_SECS / 60
1087 );
1088 }
1089
1090 let attempt_number = attempts.len();
1092 let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); let restart_time = now + Duration::from_millis(delay_ms);
1094
1095 self.pending_restarts
1097 .insert(language.to_string(), restart_time);
1098
1099 tracing::info!(
1100 "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1101 language,
1102 attempt_number + 1,
1103 MAX_RESTARTS_IN_WINDOW,
1104 delay_ms
1105 );
1106
1107 format!(
1108 "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1109 language,
1110 attempt_number + 1,
1111 MAX_RESTARTS_IN_WINDOW,
1112 delay_ms / 1000
1113 )
1114 }
1115
1116 pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1120 let now = Instant::now();
1121 let mut results = Vec::new();
1122
1123 let due_restarts: Vec<String> = self
1125 .pending_restarts
1126 .iter()
1127 .filter(|(_, time)| **time <= now)
1128 .map(|(lang, _)| lang.clone())
1129 .collect();
1130
1131 for language in due_restarts {
1132 self.pending_restarts.remove(&language);
1133
1134 self.restart_attempts
1136 .entry(language.clone())
1137 .or_default()
1138 .push(now);
1139
1140 if self.force_spawn(&language, None).is_some() {
1142 let message = format!("LSP server for {} restarted successfully", language);
1143 tracing::info!("{}", message);
1144 results.push((language, true, message));
1145 } else {
1146 let message = format!("Failed to restart LSP server for {}", language);
1147 tracing::error!("{}", message);
1148 results.push((language, false, message));
1149 }
1150 }
1151
1152 results
1153 }
1154
1155 pub fn is_in_cooldown(&self, language: &str) -> bool {
1157 self.restart_cooldown.contains(language)
1158 }
1159
1160 pub fn has_pending_restart(&self, language: &str) -> bool {
1162 self.pending_restarts.contains_key(language)
1163 }
1164
1165 pub fn clear_cooldown(&mut self, language: &str) {
1167 self.restart_cooldown.remove(language);
1168 self.restart_attempts.remove(language);
1169 self.pending_restarts.remove(language);
1170 tracing::info!("Cleared restart cooldown for {}", language);
1171 }
1172
1173 pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1180 self.clear_cooldown(language);
1182
1183 self.disabled_languages.remove(language);
1185
1186 self.allowed_languages.insert(language.to_string());
1188
1189 {
1191 let mut i = 0;
1192 while i < self.handles.len() {
1193 if !self.handles[i].handle.scope().is_universal()
1194 && self.handles[i].handle.scope().accepts(language)
1195 {
1196 let sh = self.handles.remove(i);
1197 fire_and_forget(sh.handle.shutdown());
1198 } else {
1199 i += 1;
1200 }
1201 }
1202 }
1203
1204 if self.force_spawn(language, file_path).is_some() {
1206 let message = format!("LSP server for {} started", language);
1207 tracing::info!("{}", message);
1208 (true, message)
1209 } else {
1210 let message = format!("Failed to start LSP server for {}", language);
1211 tracing::error!("{}", message);
1212 (false, message)
1213 }
1214 }
1215
1216 pub fn manual_restart_server(
1221 &mut self,
1222 language: &str,
1223 server_name: &str,
1224 file_path: Option<&Path>,
1225 ) -> (bool, String) {
1226 self.clear_cooldown(language);
1227 self.disabled_languages.remove(language);
1228 self.allowed_languages.insert(language.to_string());
1229
1230 if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1232 let sh = self.handles.remove(idx);
1233 fire_and_forget(sh.handle.shutdown());
1234 }
1235
1236 let is_universal = self
1238 .universal_configs
1239 .iter()
1240 .any(|c| c.display_name() == server_name);
1241 let config = if is_universal {
1242 self.universal_configs
1243 .iter()
1244 .find(|c| c.display_name() == server_name)
1245 .cloned()
1246 } else {
1247 self.config
1248 .get(language)
1249 .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1250 .cloned()
1251 };
1252
1253 let Some(config) = config else {
1254 let message = format!(
1255 "No config found for server '{}' ({})",
1256 server_name, language
1257 );
1258 tracing::error!("{}", message);
1259 return (false, message);
1260 };
1261
1262 if config.command.is_empty() {
1263 let message = format!(
1264 "LSP command is empty for {} server '{}'",
1265 language, server_name
1266 );
1267 tracing::error!("{}", message);
1268 return (false, message);
1269 }
1270
1271 let runtime = match self.runtime.as_ref() {
1272 Some(r) => r.clone(),
1273 None => return (false, "No tokio runtime available".to_string()),
1274 };
1275 let async_bridge = match self.async_bridge.as_ref() {
1276 Some(b) => b.clone(),
1277 None => return (false, "No async bridge available".to_string()),
1278 };
1279
1280 let scope = if is_universal {
1281 LanguageScope::all()
1282 } else {
1283 LanguageScope::single(language)
1284 };
1285
1286 match LspHandle::spawn(
1287 &runtime,
1288 &config.command,
1289 &config.args,
1290 config.env.clone(),
1291 scope,
1292 server_name.to_string(),
1293 &async_bridge,
1294 config.process_limits.clone(),
1295 config.language_id_overrides.clone(),
1296 ) {
1297 Ok(handle) => {
1298 let effective_root = if is_universal {
1299 file_path
1300 .map(|p| {
1301 let root = detect_workspace_root(p, &config.root_markers);
1302 path_to_uri(&root)
1303 })
1304 .flatten()
1305 .or_else(|| self.root_uri.clone())
1306 } else {
1307 self.resolve_root_uri(language, file_path)
1308 };
1309 if let Err(e) =
1310 handle.initialize(effective_root, config.initialization_options.clone())
1311 {
1312 let message = format!(
1313 "Failed to initialize LSP server '{}' for {}: {}",
1314 server_name, language, e
1315 );
1316 tracing::error!("{}", message);
1317 return (false, message);
1318 }
1319
1320 let sh = ServerHandle {
1321 name: server_name.to_string(),
1322 handle,
1323 feature_filter: config.feature_filter(),
1324 capabilities: ServerCapabilitySummary::default(),
1325 };
1326
1327 self.handles.push(sh);
1328
1329 let message = format!("LSP server '{}' for {} started", server_name, language);
1330 tracing::info!("{}", message);
1331 (true, message)
1332 }
1333 Err(e) => {
1334 let message = format!(
1335 "Failed to start LSP server '{}' for {}: {}",
1336 server_name, language, e
1337 );
1338 tracing::error!("{}", message);
1339 (false, message)
1340 }
1341 }
1342 }
1343
1344 pub fn restart_attempt_count(&self, language: &str) -> usize {
1346 let now = Instant::now();
1347 let window = Duration::from_secs(RESTART_WINDOW_SECS);
1348 self.restart_attempts
1349 .get(language)
1350 .map(|attempts| {
1351 attempts
1352 .iter()
1353 .filter(|t| now.duration_since(**t) < window)
1354 .count()
1355 })
1356 .unwrap_or(0)
1357 }
1358
1359 pub fn running_servers(&self) -> Vec<String> {
1361 let mut labels: Vec<String> = self
1362 .handles
1363 .iter()
1364 .map(|sh| sh.handle.scope().label().to_string())
1365 .collect();
1366 labels.sort();
1367 labels.dedup();
1368 labels
1369 }
1370
1371 pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1373 self.handles
1374 .iter()
1375 .filter(|sh| sh.handle.scope().accepts(language))
1376 .map(|sh| sh.name.clone())
1377 .collect()
1378 }
1379
1380 pub fn is_server_ready(&self, language: &str) -> bool {
1382 self.handles
1383 .iter()
1384 .filter(|sh| sh.handle.scope().accepts(language))
1385 .any(|sh| sh.handle.state().can_send_requests())
1386 }
1387
1388 pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1393 let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1394 tracing::warn!(
1395 "No running LSP server named '{}' found for {}",
1396 server_name,
1397 language
1398 );
1399 return false;
1400 };
1401
1402 let sh = self.handles.remove(idx);
1403 tracing::info!(
1404 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1405 sh.name,
1406 language
1407 );
1408 fire_and_forget(sh.handle.shutdown());
1409
1410 let has_remaining = self
1412 .handles
1413 .iter()
1414 .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1415 if !has_remaining {
1416 self.disabled_languages.insert(language.to_string());
1417 self.pending_restarts.remove(language);
1418 self.restart_cooldown.remove(language);
1419 self.allowed_languages.remove(language);
1420 }
1421
1422 true
1423 }
1424
1425 pub fn shutdown_server(&mut self, language: &str) -> bool {
1430 let mut found = false;
1431 let mut i = 0;
1432 while i < self.handles.len() {
1433 if !self.handles[i].handle.scope().is_universal()
1434 && self.handles[i].handle.scope().accepts(language)
1435 {
1436 let sh = self.handles.remove(i);
1437 tracing::info!(
1438 "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1439 sh.name,
1440 language
1441 );
1442 fire_and_forget(sh.handle.shutdown());
1443 found = true;
1444 } else {
1445 i += 1;
1446 }
1447 }
1448
1449 if found {
1450 self.disabled_languages.insert(language.to_string());
1451 self.pending_restarts.remove(language);
1452 self.restart_cooldown.remove(language);
1453 self.allowed_languages.remove(language);
1454 } else {
1455 tracing::warn!("No running LSP server found for {}", language);
1456 }
1457
1458 found
1459 }
1460
1461 pub fn shutdown_all(&mut self) {
1463 for sh in &self.handles {
1464 tracing::info!(
1465 "Shutting down LSP server '{}' ({})",
1466 sh.name,
1467 sh.handle.scope().label()
1468 );
1469 fire_and_forget(sh.handle.shutdown());
1470 }
1471 self.handles.clear();
1472 }
1473}
1474
1475impl Drop for LspManager {
1476 fn drop(&mut self) {
1477 self.shutdown_all();
1478 }
1479}
1480
1481pub fn detect_language(
1488 path: &std::path::Path,
1489 languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1490) -> Option<String> {
1491 use crate::primitives::glob_match::{
1492 filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1493 };
1494
1495 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1496 for (language_name, lang_config) in languages {
1498 if lang_config
1499 .filenames
1500 .iter()
1501 .any(|f| !is_glob_pattern(f) && f == filename)
1502 {
1503 return Some(language_name.clone());
1504 }
1505 }
1506
1507 let path_str = path.to_str().unwrap_or("");
1511 for (language_name, lang_config) in languages {
1512 if lang_config.filenames.iter().any(|f| {
1513 if !is_glob_pattern(f) {
1514 return false;
1515 }
1516 if is_path_pattern(f) {
1517 path_glob_matches(f, path_str)
1518 } else {
1519 filename_glob_matches(f, filename)
1520 }
1521 }) {
1522 return Some(language_name.clone());
1523 }
1524 }
1525 }
1526
1527 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1529 for (language_name, lang_config) in languages {
1530 if lang_config.extensions.iter().any(|ext| ext == extension) {
1531 return Some(language_name.clone());
1532 }
1533 }
1534 }
1535
1536 None
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541 use super::*;
1542 use std::path::Path;
1543
1544 #[test]
1545 fn test_lsp_manager_new() {
1546 let root_uri: Option<Uri> = "file:///test".parse().ok();
1547 let manager = LspManager::new(root_uri.clone());
1548
1549 assert_eq!(manager.handles.len(), 0);
1551 assert_eq!(manager.config.len(), 0);
1552 assert!(manager.root_uri.is_some());
1553 assert!(manager.runtime.is_none());
1554 assert!(manager.async_bridge.is_none());
1555 }
1556
1557 #[test]
1558 fn test_lsp_manager_set_language_config() {
1559 let mut manager = LspManager::new(None);
1560
1561 let config = LspServerConfig {
1562 enabled: true,
1563 command: "rust-analyzer".to_string(),
1564 args: vec![],
1565 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1566 auto_start: false,
1567 initialization_options: None,
1568 env: Default::default(),
1569 language_id_overrides: Default::default(),
1570 name: None,
1571 only_features: None,
1572 except_features: None,
1573 root_markers: Default::default(),
1574 };
1575
1576 manager.set_language_config("rust".to_string(), config);
1577
1578 assert_eq!(manager.config.len(), 1);
1579 assert!(manager.config.contains_key("rust"));
1580 assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1581 }
1582
1583 #[test]
1584 fn test_lsp_manager_force_spawn_no_runtime() {
1585 let mut manager = LspManager::new(None);
1586
1587 manager.set_language_config(
1589 "rust".to_string(),
1590 LspServerConfig {
1591 enabled: true,
1592 command: "rust-analyzer".to_string(),
1593 args: vec![],
1594 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1595 auto_start: false,
1596 initialization_options: None,
1597 env: Default::default(),
1598 language_id_overrides: Default::default(),
1599 name: None,
1600 only_features: None,
1601 except_features: None,
1602 root_markers: Default::default(),
1603 },
1604 );
1605
1606 let result = manager.force_spawn("rust", None);
1608 assert!(result.is_none());
1609 }
1610
1611 #[test]
1612 fn test_lsp_manager_force_spawn_no_config() {
1613 let rt = tokio::runtime::Runtime::new().unwrap();
1614 let mut manager = LspManager::new(None);
1615 let async_bridge = AsyncBridge::new();
1616
1617 manager.set_runtime(rt.handle().clone(), async_bridge);
1618
1619 let result = manager.force_spawn("rust", None);
1621 assert!(result.is_none());
1622 }
1623
1624 #[test]
1625 fn test_lsp_manager_force_spawn_disabled_language() {
1626 let rt = tokio::runtime::Runtime::new().unwrap();
1627 let mut manager = LspManager::new(None);
1628 let async_bridge = AsyncBridge::new();
1629
1630 manager.set_runtime(rt.handle().clone(), async_bridge);
1631
1632 manager.set_language_config(
1634 "rust".to_string(),
1635 LspServerConfig {
1636 enabled: false,
1637 command: String::new(), args: vec![],
1639 process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1640 auto_start: false,
1641 initialization_options: None,
1642 env: Default::default(),
1643 language_id_overrides: Default::default(),
1644 name: None,
1645 only_features: None,
1646 except_features: None,
1647 root_markers: Default::default(),
1648 },
1649 );
1650
1651 let result = manager.force_spawn("rust", None);
1653 assert!(result.is_none());
1654 }
1655
1656 #[test]
1657 fn test_lsp_manager_shutdown_all() {
1658 let mut manager = LspManager::new(None);
1659
1660 manager.shutdown_all();
1662 assert_eq!(manager.handles.len(), 0);
1663 }
1664
1665 fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
1666 let mut languages = std::collections::HashMap::new();
1667 languages.insert(
1668 "rust".to_string(),
1669 crate::config::LanguageConfig {
1670 extensions: vec!["rs".to_string()],
1671 filenames: vec![],
1672 grammar: "rust".to_string(),
1673 comment_prefix: Some("//".to_string()),
1674 auto_indent: true,
1675 auto_close: None,
1676 auto_surround: None,
1677 textmate_grammar: None,
1678 show_whitespace_tabs: false,
1679 line_wrap: None,
1680 wrap_column: None,
1681 page_view: None,
1682 page_width: None,
1683 use_tabs: None,
1684 tab_size: None,
1685 formatter: None,
1686 format_on_save: false,
1687 on_save: vec![],
1688 word_characters: None,
1689 },
1690 );
1691 languages.insert(
1692 "javascript".to_string(),
1693 crate::config::LanguageConfig {
1694 extensions: vec!["js".to_string(), "jsx".to_string()],
1695 filenames: vec![],
1696 grammar: "javascript".to_string(),
1697 comment_prefix: Some("//".to_string()),
1698 auto_indent: true,
1699 auto_close: None,
1700 auto_surround: None,
1701 textmate_grammar: None,
1702 show_whitespace_tabs: false,
1703 line_wrap: None,
1704 wrap_column: None,
1705 page_view: None,
1706 page_width: None,
1707 use_tabs: None,
1708 tab_size: None,
1709 formatter: None,
1710 format_on_save: false,
1711 on_save: vec![],
1712 word_characters: None,
1713 },
1714 );
1715 languages.insert(
1716 "csharp".to_string(),
1717 crate::config::LanguageConfig {
1718 extensions: vec!["cs".to_string()],
1719 filenames: vec![],
1720 grammar: "c_sharp".to_string(),
1721 comment_prefix: Some("//".to_string()),
1722 auto_indent: true,
1723 auto_close: None,
1724 auto_surround: None,
1725 textmate_grammar: None,
1726 show_whitespace_tabs: false,
1727 line_wrap: None,
1728 wrap_column: None,
1729 page_view: None,
1730 page_width: None,
1731 use_tabs: None,
1732 tab_size: None,
1733 formatter: None,
1734 format_on_save: false,
1735 on_save: vec![],
1736 word_characters: None,
1737 },
1738 );
1739 languages
1740 }
1741
1742 #[test]
1743 fn test_detect_language_from_config() {
1744 let languages = test_languages();
1745
1746 assert_eq!(
1748 detect_language(Path::new("main.rs"), &languages),
1749 Some("rust".to_string())
1750 );
1751 assert_eq!(
1752 detect_language(Path::new("index.js"), &languages),
1753 Some("javascript".to_string())
1754 );
1755 assert_eq!(
1756 detect_language(Path::new("App.jsx"), &languages),
1757 Some("javascript".to_string())
1758 );
1759 assert_eq!(
1760 detect_language(Path::new("Program.cs"), &languages),
1761 Some("csharp".to_string())
1762 );
1763
1764 assert_eq!(detect_language(Path::new("main.py"), &languages), None);
1766 assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
1767 assert_eq!(detect_language(Path::new("file"), &languages), None);
1768 }
1769
1770 #[test]
1771 fn test_detect_language_no_extension() {
1772 let languages = test_languages();
1773 assert_eq!(detect_language(Path::new("README"), &languages), None);
1774 assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
1775 }
1776
1777 #[test]
1778 fn test_detect_language_path_glob() {
1779 let mut languages = test_languages();
1780 languages.insert(
1781 "shell".to_string(),
1782 crate::config::LanguageConfig {
1783 extensions: vec!["sh".to_string()],
1784 filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
1785 grammar: "bash".to_string(),
1786 comment_prefix: Some("#".to_string()),
1787 auto_indent: true,
1788 auto_close: None,
1789 auto_surround: None,
1790 textmate_grammar: None,
1791 show_whitespace_tabs: false,
1792 line_wrap: None,
1793 wrap_column: None,
1794 page_view: None,
1795 page_width: None,
1796 use_tabs: None,
1797 tab_size: None,
1798 formatter: None,
1799 format_on_save: false,
1800 on_save: vec![],
1801 word_characters: None,
1802 },
1803 );
1804
1805 assert_eq!(
1807 detect_language(Path::new("/etc/rc.conf"), &languages),
1808 Some("shell".to_string())
1809 );
1810 assert_eq!(
1811 detect_language(Path::new("/etc/init/rc.local"), &languages),
1812 Some("shell".to_string())
1813 );
1814 assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
1816
1817 assert_eq!(
1819 detect_language(Path::new("lfrc"), &languages),
1820 Some("shell".to_string())
1821 );
1822 }
1823
1824 #[test]
1825 fn test_detect_workspace_root_finds_marker_in_parent() {
1826 let tmp = tempfile::tempdir().unwrap();
1827 let project = tmp.path().join("myproject");
1828 let src = project.join("src");
1829 std::fs::create_dir_all(&src).unwrap();
1830 std::fs::write(project.join("Cargo.toml"), "").unwrap();
1831 let file = src.join("main.rs");
1832 std::fs::write(&file, "").unwrap();
1833
1834 let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
1835 assert_eq!(root, project);
1836 }
1837
1838 #[test]
1839 fn test_detect_workspace_root_finds_marker_two_levels_up() {
1840 let tmp = tempfile::tempdir().unwrap();
1841 let project = tmp.path().join("myproject");
1842 let deep = project.join("src").join("nested");
1843 std::fs::create_dir_all(&deep).unwrap();
1844 std::fs::write(project.join("Cargo.toml"), "").unwrap();
1845 let file = deep.join("lib.rs");
1846 std::fs::write(&file, "").unwrap();
1847
1848 let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
1849 assert_eq!(root, project);
1850 }
1851
1852 #[test]
1853 fn test_detect_workspace_root_no_marker_returns_parent() {
1854 let tmp = tempfile::tempdir().unwrap();
1855 let dir = tmp.path().join("somedir");
1856 std::fs::create_dir_all(&dir).unwrap();
1857 let file = dir.join("file.txt");
1858 std::fs::write(&file, "").unwrap();
1859
1860 let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
1861 assert_eq!(root, dir);
1862 }
1863
1864 #[test]
1865 fn test_detect_workspace_root_empty_markers_returns_parent() {
1866 let tmp = tempfile::tempdir().unwrap();
1867 let dir = tmp.path().join("somedir");
1868 std::fs::create_dir_all(&dir).unwrap();
1869 let file = dir.join("file.txt");
1870 std::fs::write(&file, "").unwrap();
1871
1872 let root = detect_workspace_root(&file, &[]);
1873 assert_eq!(root, dir);
1874 }
1875
1876 #[test]
1877 fn test_detect_workspace_root_directory_marker() {
1878 let tmp = tempfile::tempdir().unwrap();
1879 let project = tmp.path().join("myproject");
1880 let src = project.join("src");
1881 std::fs::create_dir_all(&src).unwrap();
1882 std::fs::create_dir_all(project.join(".git")).unwrap();
1883 let file = src.join("main.rs");
1884 std::fs::write(&file, "").unwrap();
1885
1886 let root = detect_workspace_root(&file, &[".git".to_string()]);
1887 assert_eq!(root, project);
1888 }
1889
1890 #[test]
1891 fn test_path_to_uri_basic() {
1892 let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
1893 assert_eq!(uri.as_str(), "file:///tmp/test");
1894 }
1895
1896 #[test]
1897 fn test_path_to_uri_with_spaces() {
1898 let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
1899 assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
1900 }
1901}