1use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use super::Translator;
8use super::positioned::DisclosedDocument;
9use crate::bridge::resources::{DiagnosticsResourceUri, parse_uri};
10use crate::bridge::{ClientPath, InFlightGuard, LinePresence, Position, WorkspacePath};
11use crate::config::{
12 FileKey, LanguageId, NoServerReason, ServerId, ToolKind, ToolRouter, base_language_id,
13};
14use crate::error::{Error, Result, ServerSpawnFailure};
15use crate::lsp::LspClient;
16use crate::util::lock_std;
17
18const FLUSH_PENDING_CLOSES_DEADLINE: std::time::Duration = std::time::Duration::from_secs(1);
20
21#[derive(Debug)]
30#[must_use = "dropping a PreparedDocument makes its document evictable mid-request"]
31pub(super) struct PreparedDocument {
32 server_id: ServerId,
33 client: LspClient,
34 uri: lsp_types::Uri,
35 path: PathBuf,
36 _in_flight: InFlightGuard,
37}
38
39impl PreparedDocument {
40 pub(super) const fn server_id(&self) -> &ServerId {
41 &self.server_id
42 }
43
44 pub(super) const fn client(&self) -> &LspClient {
45 &self.client
46 }
47
48 pub(super) const fn uri(&self) -> &lsp_types::Uri {
49 &self.uri
50 }
51
52 pub(super) fn path(&self) -> &Path {
53 &self.path
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59enum IndexingWait {
60 Wait,
61 Skip,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub(super) enum IndexingGate {
103 Required(Capability),
107 FileLocal(FileLocalCapability),
118}
119
120impl IndexingGate {
121 pub(super) fn capability(self) -> Capability {
123 match self {
124 Self::Required(capability) => capability,
125 Self::FileLocal(capability) => capability.into(),
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub(super) enum FileLocalCapability {
134 DocumentSymbols,
136 FoldingRange,
138 SelectionRange,
140 DocumentHighlights,
142 FormatDocument,
144 FormatRange,
146}
147
148impl From<FileLocalCapability> for Capability {
149 fn from(capability: FileLocalCapability) -> Self {
150 match capability {
151 FileLocalCapability::DocumentSymbols => Self::DocumentSymbols,
152 FileLocalCapability::FoldingRange => Self::FoldingRange,
153 FileLocalCapability::SelectionRange => Self::SelectionRange,
154 FileLocalCapability::DocumentHighlights => Self::DocumentHighlights,
155 FileLocalCapability::FormatDocument => Self::FormatDocument,
156 FileLocalCapability::FormatRange => Self::FormatRange,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub(super) enum DisclosedCapability {
166 SignatureHelp,
168 InlayHints,
170 CallHierarchy,
172 TypeHierarchy,
174}
175
176impl From<DisclosedCapability> for Capability {
177 fn from(capability: DisclosedCapability) -> Self {
178 match capability {
179 DisclosedCapability::SignatureHelp => Self::SignatureHelp,
180 DisclosedCapability::InlayHints => Self::InlayHints,
181 DisclosedCapability::CallHierarchy => Self::CallHierarchy,
182 DisclosedCapability::TypeHierarchy => Self::TypeHierarchy,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Capability {
195 Completions,
197 SignatureHelp,
199 InlayHints,
201 Hover,
203 Definition,
205 References,
207 Implementation,
209 TypeDefinition,
211 CallHierarchy,
214 Rename,
216 FormatDocument,
218 CodeActions,
220 DocumentSymbols,
222 WorkspaceSymbols,
224 Declaration,
226 TypeHierarchy,
229 PrepareRename,
233 DocumentHighlights,
235 FormatRange,
237 SelectionRange,
239 FoldingRange,
241}
242
243#[expect(
244 clippy::indexing_slicing,
245 reason = "compile-time check; the loop condition keeps `i` below `ALL.len()`"
246)]
247const _: () = {
248 assert!(Capability::ALL.len() == Capability::FoldingRange as usize + 1);
249 assert!(Capability::ALL.len() <= u32::BITS as usize);
250 let mut i = 0;
251 while i < Capability::ALL.len() {
252 assert!(Capability::ALL[i] as usize == i);
253 i += 1;
254 }
255};
256
257impl Capability {
258 pub(crate) const ALL: [Self; 21] = [
260 Self::Completions,
261 Self::SignatureHelp,
262 Self::InlayHints,
263 Self::Hover,
264 Self::Definition,
265 Self::References,
266 Self::Implementation,
267 Self::TypeDefinition,
268 Self::CallHierarchy,
269 Self::Rename,
270 Self::FormatDocument,
271 Self::CodeActions,
272 Self::DocumentSymbols,
273 Self::WorkspaceSymbols,
274 Self::Declaration,
275 Self::TypeHierarchy,
276 Self::PrepareRename,
277 Self::DocumentHighlights,
278 Self::FormatRange,
279 Self::SelectionRange,
280 Self::FoldingRange,
281 ];
282
283 pub(crate) const fn tool_kind(self) -> ToolKind {
286 match self {
287 Self::Completions => ToolKind::Completions,
288 Self::SignatureHelp => ToolKind::SignatureHelp,
289 Self::InlayHints => ToolKind::InlayHints,
290 Self::Hover => ToolKind::Hover,
291 Self::Definition => ToolKind::Definition,
292 Self::References => ToolKind::References,
293 Self::Implementation => ToolKind::Implementation,
294 Self::TypeDefinition => ToolKind::TypeDefinition,
295 Self::CallHierarchy => ToolKind::CallHierarchy,
296 Self::Rename | Self::PrepareRename => ToolKind::Rename,
297 Self::FormatDocument => ToolKind::FormatDocument,
298 Self::CodeActions => ToolKind::CodeActions,
299 Self::DocumentSymbols => ToolKind::DocumentSymbols,
300 Self::WorkspaceSymbols => ToolKind::WorkspaceSymbols,
301 Self::Declaration => ToolKind::Declaration,
302 Self::TypeHierarchy => ToolKind::TypeHierarchy,
303 Self::DocumentHighlights => ToolKind::DocumentHighlights,
304 Self::FormatRange => ToolKind::FormatRange,
305 Self::SelectionRange => ToolKind::SelectionRange,
306 Self::FoldingRange => ToolKind::FoldingRange,
307 }
308 }
309
310 pub(crate) const fn is_primary(self) -> bool {
314 !matches!(self, Self::PrepareRename)
315 }
316
317 pub(crate) const fn for_tool(tool: ToolKind) -> Option<Self> {
322 Self::find_gating(&Self::ALL, tool)
323 }
324
325 const fn find_gating(candidates: &[Self], tool: ToolKind) -> Option<Self> {
326 match candidates {
327 [first, rest @ ..] => {
329 if first.is_primary() && first.tool_kind() as u8 == tool as u8 {
330 Some(*first)
331 } else {
332 Self::find_gating(rest, tool)
333 }
334 }
335 [] => None,
336 }
337 }
338
339 pub(crate) const fn is_available(self, caps: Option<&lsp_types::ServerCapabilities>) -> bool {
344 match caps {
345 Some(caps) => self.is_supported(caps),
346 None => true,
347 }
348 }
349
350 pub(crate) const fn name(self) -> &'static str {
353 match self {
354 Self::Completions => "completionProvider",
355 Self::SignatureHelp => "signatureHelpProvider",
356 Self::InlayHints => "inlayHintProvider",
357 Self::Hover => "hoverProvider",
358 Self::Definition => "definitionProvider",
359 Self::References => "referencesProvider",
360 Self::Implementation => "implementationProvider",
361 Self::TypeDefinition => "typeDefinitionProvider",
362 Self::CallHierarchy => "callHierarchyProvider",
363 Self::Rename => "renameProvider",
364 Self::FormatDocument => "documentFormattingProvider",
365 Self::CodeActions => "codeActionProvider",
366 Self::DocumentSymbols => "documentSymbolProvider",
367 Self::WorkspaceSymbols => "workspaceSymbolProvider",
368 Self::Declaration => "declarationProvider",
369 Self::TypeHierarchy => "typeHierarchyProvider",
370 Self::PrepareRename => "renameProvider.prepareProvider",
371 Self::DocumentHighlights => "documentHighlightProvider",
372 Self::FormatRange => "documentRangeFormattingProvider",
373 Self::SelectionRange => "selectionRangeProvider",
374 Self::FoldingRange => "foldingRangeProvider",
375 }
376 }
377
378 #[allow(clippy::too_many_lines, reason = "one match arm per capability")]
380 pub(crate) const fn is_supported(self, caps: &lsp_types::ServerCapabilities) -> bool {
381 match self {
382 Self::Completions => caps.completion_provider.is_some(),
383 Self::SignatureHelp => caps.signature_help_provider.is_some(),
384 Self::InlayHints => matches!(
385 caps.inlay_hint_provider,
386 Some(
387 lsp_types::InlayHintProvider::Bool(true)
388 | lsp_types::InlayHintProvider::InlayHintOptions(_)
389 | lsp_types::InlayHintProvider::InlayHintRegistrationOptions(_)
390 )
391 ),
392 Self::Hover => matches!(
393 caps.hover_provider,
394 Some(
395 lsp_types::HoverProvider::Bool(true)
396 | lsp_types::HoverProvider::HoverOptions(_)
397 )
398 ),
399 Self::Definition => matches!(
400 caps.definition_provider,
401 Some(
402 lsp_types::DefinitionProvider::Bool(true)
403 | lsp_types::DefinitionProvider::DefinitionOptions(_)
404 )
405 ),
406 Self::References => matches!(
407 caps.references_provider,
408 Some(
409 lsp_types::ReferencesProvider::Bool(true)
410 | lsp_types::ReferencesProvider::ReferenceOptions(_)
411 )
412 ),
413 Self::Implementation => matches!(
414 caps.implementation_provider,
415 Some(
416 lsp_types::ImplementationProvider::Bool(true)
417 | lsp_types::ImplementationProvider::ImplementationOptions(_)
418 | lsp_types::ImplementationProvider::ImplementationRegistrationOptions(_)
419 )
420 ),
421 Self::TypeDefinition => matches!(
422 caps.type_definition_provider,
423 Some(
424 lsp_types::TypeDefinitionProvider::Bool(true)
425 | lsp_types::TypeDefinitionProvider::TypeDefinitionOptions(_)
426 | lsp_types::TypeDefinitionProvider::TypeDefinitionRegistrationOptions(_)
427 )
428 ),
429 Self::CallHierarchy => matches!(
430 caps.call_hierarchy_provider,
431 Some(
432 lsp_types::CallHierarchyProvider::Bool(true)
433 | lsp_types::CallHierarchyProvider::CallHierarchyOptions(_)
434 | lsp_types::CallHierarchyProvider::CallHierarchyRegistrationOptions(_)
435 )
436 ),
437 Self::Rename => matches!(
438 caps.rename_provider,
439 Some(
440 lsp_types::RenameProvider::Bool(true)
441 | lsp_types::RenameProvider::RenameOptions(_)
442 )
443 ),
444 Self::FormatDocument => matches!(
445 caps.document_formatting_provider,
446 Some(
447 lsp_types::DocumentFormattingProvider::Bool(true)
448 | lsp_types::DocumentFormattingProvider::DocumentFormattingOptions(_)
449 )
450 ),
451 Self::CodeActions => matches!(
452 caps.code_action_provider,
453 Some(
454 lsp_types::CodeActionProvider::Bool(true)
455 | lsp_types::CodeActionProvider::CodeActionOptions(_)
456 )
457 ),
458 Self::DocumentSymbols => matches!(
459 caps.document_symbol_provider,
460 Some(
461 lsp_types::DocumentSymbolProvider::Bool(true)
462 | lsp_types::DocumentSymbolProvider::DocumentSymbolOptions(_)
463 )
464 ),
465 Self::WorkspaceSymbols => matches!(
466 caps.workspace_symbol_provider,
467 Some(
468 lsp_types::WorkspaceSymbolProvider::Bool(true)
469 | lsp_types::WorkspaceSymbolProvider::WorkspaceSymbolOptions(_)
470 )
471 ),
472 Self::Declaration => matches!(
473 caps.declaration_provider,
474 Some(
475 lsp_types::DeclarationProvider::Bool(true)
476 | lsp_types::DeclarationProvider::DeclarationOptions(_)
477 | lsp_types::DeclarationProvider::DeclarationRegistrationOptions(_)
478 )
479 ),
480 Self::TypeHierarchy => matches!(
481 caps.type_hierarchy_provider,
482 Some(
483 lsp_types::TypeHierarchyProvider::Bool(true)
484 | lsp_types::TypeHierarchyProvider::TypeHierarchyOptions(_)
485 | lsp_types::TypeHierarchyProvider::TypeHierarchyRegistrationOptions(_)
486 )
487 ),
488 Self::PrepareRename => matches!(
489 caps.rename_provider,
490 Some(lsp_types::RenameProvider::RenameOptions(
491 lsp_types::RenameOptions {
492 prepare_provider: Some(true),
493 ..
494 }
495 ))
496 ),
497 Self::DocumentHighlights => matches!(
498 caps.document_highlight_provider,
499 Some(
500 lsp_types::DocumentHighlightProvider::Bool(true)
501 | lsp_types::DocumentHighlightProvider::DocumentHighlightOptions(_)
502 )
503 ),
504 Self::FormatRange => matches!(
505 caps.document_range_formatting_provider,
506 Some(
507 lsp_types::DocumentRangeFormattingProvider::Bool(true)
508 | lsp_types::DocumentRangeFormattingProvider::DocumentRangeFormattingOptions(_)
509 )
510 ),
511 Self::SelectionRange => matches!(
512 caps.selection_range_provider,
513 Some(
514 lsp_types::SelectionRangeProvider::Bool(true)
515 | lsp_types::SelectionRangeProvider::SelectionRangeOptions(_)
516 | lsp_types::SelectionRangeProvider::SelectionRangeRegistrationOptions(_)
517 )
518 ),
519 Self::FoldingRange => matches!(
520 caps.folding_range_provider,
521 Some(
522 lsp_types::FoldingRangeProvider::Bool(true)
523 | lsp_types::FoldingRangeProvider::FoldingRangeOptions(_)
524 | lsp_types::FoldingRangeProvider::FoldingRangeRegistrationOptions(_)
525 )
526 ),
527 }
528 }
529}
530
531impl std::fmt::Display for Capability {
532 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533 f.write_str(self.name())
534 }
535}
536
537impl serde::Serialize for Capability {
538 fn serialize<S: serde::Serializer>(
539 &self,
540 serializer: S,
541 ) -> std::result::Result<S::Ok, S::Error> {
542 serializer.serialize_str(self.name())
543 }
544}
545
546impl schemars::JsonSchema for Capability {
548 fn schema_name() -> std::borrow::Cow<'static, str> {
549 "Capability".into()
550 }
551
552 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
553 schemars::json_schema!({
554 "type": "string",
555 "description": "LSP server capability the tool is gated on."
556 })
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
567pub(super) struct LanguageCandidates {
568 language: LanguageId,
569 base: Option<LanguageId>,
570}
571
572impl LanguageCandidates {
573 pub(super) fn new(language: LanguageId) -> Self {
574 let base = base_language_id(&language);
575 Self { language, base }
576 }
577
578 pub(super) const fn language(&self) -> &LanguageId {
579 &self.language
580 }
581
582 pub(super) fn iter(&self) -> impl Iterator<Item = &LanguageId> {
583 std::iter::once(&self.language).chain(self.base.as_ref())
584 }
585}
586
587#[derive(Debug)]
591pub(super) enum RouteLookup<T> {
592 Registered(ServerId, T),
594 Initializing(ServerId),
596 Failed(Box<ServerSpawnFailure>),
600 Dangling {
603 language: LanguageId,
604 server_id: ServerId,
605 },
606 Unrouted,
608}
609
610pub(super) fn lookup_route<T>(
619 candidates: &LanguageCandidates,
620 resolve: impl Fn(&str) -> Option<ServerId>,
621 registered: impl Fn(&ServerId) -> Option<T>,
622 is_expected: impl Fn(&ServerId) -> bool,
623 pending_catch_all: impl Fn(&str) -> Option<ServerId>,
624 startup_failure: impl Fn(&ServerId) -> Option<ServerSpawnFailure>,
625) -> RouteLookup<T> {
626 for language in candidates.iter() {
627 let Some(server_id) = resolve(language.as_str()) else {
628 continue;
629 };
630 return if let Some(found) = registered(&server_id) {
631 RouteLookup::Registered(server_id, found)
632 } else if is_expected(&server_id) {
633 RouteLookup::Initializing(server_id)
634 } else if let Some(catch_all) = pending_catch_all(language.as_str()) {
635 RouteLookup::Initializing(catch_all)
636 } else if let Some(failure) = startup_failure(&server_id) {
637 RouteLookup::Failed(Box::new(failure))
638 } else {
639 RouteLookup::Dangling {
640 language: language.clone(),
641 server_id,
642 }
643 };
644 }
645 RouteLookup::Unrouted
646}
647
648fn pending_catch_all(
651 router: &ToolRouter,
652 servers: &super::servers::Servers,
653 language: &str,
654) -> Option<ServerId> {
655 router
656 .catch_all_for(language)
657 .filter(|catch_all| servers.is_expected(catch_all))
658 .cloned()
659}
660
661#[derive(Debug)]
668pub enum DiagnosticsRoute {
669 Live(ServerId),
671 Initializing(ServerId),
673 FailedToStart(Box<ServerSpawnFailure>),
675 Unrouted,
677}
678
679impl DiagnosticsRoute {
680 pub(crate) const fn server_id(&self) -> Option<&ServerId> {
682 match self {
683 Self::Live(id) | Self::Initializing(id) => Some(id),
684 Self::FailedToStart(_) | Self::Unrouted => None,
685 }
686 }
687
688 pub(crate) const fn is_failed(&self) -> bool {
690 matches!(self, Self::FailedToStart(_))
691 }
692
693 pub(crate) fn into_startup_failure(self) -> Option<Box<ServerSpawnFailure>> {
698 match self {
699 Self::FailedToStart(failure) => Some(failure),
700 Self::Live(_) | Self::Initializing(_) | Self::Unrouted => None,
701 }
702 }
703
704 pub(crate) fn into_read_result(self) -> Result<Option<ServerId>> {
712 match self {
713 Self::Live(id) => Ok(Some(id)),
714 Self::Unrouted => Ok(None),
715 Self::Initializing(server_id) => Err(Error::ServerInitializing { server_id }),
716 Self::FailedToStart(failure) => Err(Error::ServerFailedToStart(failure)),
717 }
718 }
719}
720
721#[derive(Debug, PartialEq, Eq)]
723pub(super) enum WorkspaceRouteLookup {
724 Registered(ServerId),
726 Initializing(ServerId),
728 Dangling(ServerId),
730 AllInitializing,
732 NothingConfigured,
734 NoClaimant,
736}
737
738pub(super) fn lookup_workspace_route(
743 resolve: impl FnOnce() -> std::result::Result<ServerId, NoServerReason>,
744 is_registered: impl Fn(&ServerId) -> bool,
745 is_expected: impl Fn(&ServerId) -> bool,
746 expected_is_empty: impl FnOnce() -> bool,
747) -> WorkspaceRouteLookup {
748 match resolve() {
749 Ok(id) if is_registered(&id) => WorkspaceRouteLookup::Registered(id),
750 Ok(id) if is_expected(&id) => WorkspaceRouteLookup::Initializing(id),
751 Ok(id) => WorkspaceRouteLookup::Dangling(id),
752 Err(NoServerReason::NothingRegistered) if expected_is_empty() => {
753 WorkspaceRouteLookup::NothingConfigured
754 }
755 Err(NoServerReason::NothingRegistered) => WorkspaceRouteLookup::AllInitializing,
756 Err(NoServerReason::NoClaimant) => WorkspaceRouteLookup::NoClaimant,
757 }
758}
759
760pub(super) fn check_capability(
763 server_id: &ServerId,
764 caps: Option<&lsp_types::ServerCapabilities>,
765 capability: Capability,
766) -> Result<()> {
767 capability
768 .is_available(caps)
769 .ok_or_else(|| Error::CapabilityNotSupported {
770 server_id: server_id.clone(),
771 capability,
772 })
773}
774
775impl Translator {
776 pub(crate) async fn validate_path(&self, path: &ClientPath) -> Result<WorkspacePath> {
784 self.workspace_roots.validate(path).await
785 }
786
787 pub(super) async fn resolve_client_for_file(
796 &self,
797 path: &Path,
798 tool: ToolKind,
799 ) -> Result<(ServerId, LspClient)> {
800 let (id, client) = self.client_for_file(path, tool)?;
801 self.respawn_if_dead(&id).await?;
802 let client = lock_std(&self.servers).client(&id).unwrap_or(client);
803 Ok((id, client))
804 }
805
806 pub(super) fn client_for_file(
819 &self,
820 path: &Path,
821 tool: ToolKind,
822 ) -> Result<(ServerId, LspClient)> {
823 let candidates = self.language_candidates(path);
824 let router = self.router_snapshot();
825 let lookup = {
826 let servers = lock_std(&self.servers);
827 lookup_route(
828 &candidates,
829 |lang| router.resolve(lang, tool).cloned(),
830 |id| servers.client(id),
831 |id| servers.is_expected(id),
832 |lang| pending_catch_all(&router, &servers, lang),
833 |id| servers.failure(id).cloned(),
834 )
835 };
836 match lookup {
837 RouteLookup::Registered(id, client) => Ok((id, client)),
838 RouteLookup::Initializing(server_id) => Err(Error::ServerInitializing { server_id }),
842 RouteLookup::Failed(failure) => Err(Error::ServerFailedToStart(failure)),
843 RouteLookup::Dangling {
850 language,
851 server_id,
852 } => {
853 tracing::error!(
854 "router route names server '{server_id}' for tool '{tool}' that is neither \
855 registered nor expected"
856 );
857 Err(Error::NoServerForTool {
858 language_id: language,
859 tool,
860 })
861 }
862 RouteLookup::Unrouted => {
863 if let Some(failure) = self.startup_failure_for_candidates(&candidates, tool) {
864 return Err(Error::ServerFailedToStart(Box::new(failure)));
865 }
866 let has_language = {
867 let router = lock_std(&self.router);
868 candidates
869 .iter()
870 .any(|lang| router.has_language(lang.as_str()))
871 };
872 let language = candidates.language().clone();
873 if has_language {
874 Err(Error::NoServerForTool {
875 language_id: language,
876 tool,
877 })
878 } else {
879 Err(Error::NoServerForLanguage {
880 language,
881 file: FileKey::of(path),
882 patterns: Arc::clone(&self.file_patterns),
883 })
884 }
885 }
886 }
887 }
888
889 pub(super) fn language_candidates(&self, path: &Path) -> LanguageCandidates {
891 LanguageCandidates::new(self.language_map.detect(path))
892 }
893
894 fn startup_failure_for_candidates(
900 &self,
901 candidates: &LanguageCandidates,
902 tool: ToolKind,
903 ) -> Option<ServerSpawnFailure> {
904 candidates.iter().find_map(|lang| {
905 let id = self.configured_router.resolve(lang.as_str(), tool)?;
906 self.startup_failure(id)
907 })
908 }
909
910 #[must_use]
922 pub(crate) fn diagnostics_route_for_path(&self, path: &Path) -> DiagnosticsRoute {
923 let candidates = self.language_candidates(path);
924 let router = self.router_snapshot();
925 let lookup = {
926 let servers = lock_std(&self.servers);
927 lookup_route(
928 &candidates,
929 |lang| router.resolve(lang, ToolKind::Diagnostics).cloned(),
930 |id| servers.client(id).map(|_| ()),
931 |id| servers.is_expected(id),
932 |lang| pending_catch_all(&router, &servers, lang),
933 |id| servers.failure(id).cloned(),
934 )
935 };
936 match lookup {
937 RouteLookup::Registered(id, ()) => DiagnosticsRoute::Live(id),
938 RouteLookup::Initializing(id) => DiagnosticsRoute::Initializing(id),
939 RouteLookup::Failed(failure) => DiagnosticsRoute::FailedToStart(failure),
940 RouteLookup::Dangling { server_id, .. } => {
941 tracing::error!(
942 "router diagnostics route names server '{server_id}' that is neither \
943 registered nor expected"
944 );
945 DiagnosticsRoute::Unrouted
946 }
947 RouteLookup::Unrouted => self
948 .startup_failure_for_candidates(&candidates, ToolKind::Diagnostics)
949 .map_or(DiagnosticsRoute::Unrouted, |failure| {
950 DiagnosticsRoute::FailedToStart(Box::new(failure))
951 }),
952 }
953 }
954
955 #[must_use]
958 pub(crate) fn diagnostics_route_for_uri(
959 &self,
960 uri: &DiagnosticsResourceUri,
961 ) -> DiagnosticsRoute {
962 parse_uri(uri.as_str()).map_or(DiagnosticsRoute::Unrouted, |path| {
963 self.diagnostics_route_for_path(path.as_path())
964 })
965 }
966
967 async fn resolve_validated_client_for_file(
976 &self,
977 file_path: &ClientPath,
978 tool: ToolKind,
979 ) -> Result<(ServerId, LspClient, PathBuf)> {
980 let validated_path = self.validate_path(file_path).await?;
981 let (server_id, client) = self
982 .resolve_client_for_file(validated_path.as_path(), tool)
983 .await?;
984 Ok((server_id, client, validated_path.into_path_buf()))
985 }
986
987 async fn resolve_validated_client_for_path(
994 &self,
995 path: &WorkspacePath,
996 tool: ToolKind,
997 ) -> Result<(ServerId, LspClient, PathBuf)> {
998 let (server_id, client) = self.resolve_client_for_file(path.as_path(), tool).await?;
999 Ok((server_id, client, path.as_path().to_path_buf()))
1000 }
1001
1002 #[cfg(test)]
1021 pub(super) async fn prepare_document(
1022 &self,
1023 file_path: &ClientPath,
1024 tool: ToolKind,
1025 ) -> Result<PreparedDocument> {
1026 let path = self.validate_path(file_path).await?;
1027 self.prepare_document_for_path(&path, tool).await
1028 }
1029
1030 pub(super) async fn prepare_document_for_path(
1032 &self,
1033 path: &WorkspacePath,
1034 tool: ToolKind,
1035 ) -> Result<PreparedDocument> {
1036 let (server_id, client, validated_path) =
1037 self.resolve_validated_client_for_path(path, tool).await?;
1038 self.open_prepared(server_id, client, &validated_path).await
1039 }
1040
1041 async fn open_prepared(
1047 &self,
1048 server_id: ServerId,
1049 client: LspClient,
1050 validated_path: &Path,
1051 ) -> Result<PreparedDocument> {
1052 let in_flight = self.document_tracker.mark_in_flight(validated_path);
1053 let result = self
1060 .document_tracker
1061 .ensure_open(validated_path, &server_id, &client)
1062 .await;
1063 self.flush_pending_closes().await;
1064 let uri = result?;
1065 Ok(PreparedDocument {
1066 server_id,
1067 client,
1068 uri,
1069 path: validated_path.to_path_buf(),
1070 _in_flight: in_flight,
1071 })
1072 }
1073
1074 pub(super) fn require_line_in_document(
1085 &self,
1086 doc: &PreparedDocument,
1087 position: Position,
1088 ) -> Result<()> {
1089 let line = position.line();
1090 match self
1091 .document_tracker
1092 .line_presence(doc.path(), line.get().saturating_sub(1))
1093 {
1094 LinePresence::Present => Ok(()),
1095 LinePresence::Beyond { last_line } => {
1096 Err(Error::PositionBeyondDocument { line, last_line })
1097 }
1098 LinePresence::Untracked => {
1099 tracing::debug!(
1100 line = line.get(),
1101 "skipping the line check for an untracked document"
1102 );
1103 Ok(())
1104 }
1105 }
1106 }
1107
1108 pub(super) async fn prepare_gated_document(
1124 &self,
1125 file_path: &ClientPath,
1126 indexing_gate: IndexingGate,
1127 ) -> Result<PreparedDocument> {
1128 let (server_id, client, validated_path) = self
1129 .resolve_validated_client_for_file(file_path, indexing_gate.capability().tool_kind())
1130 .await?;
1131 self.finish_prepare_gated_document(server_id, client, validated_path, indexing_gate)
1132 .await
1133 }
1134
1135 pub(super) async fn prepare_positioned_document(
1148 &self,
1149 file_path: &ClientPath,
1150 indexing_gate: IndexingGate,
1151 positions: &[Position],
1152 ) -> Result<PreparedDocument> {
1153 let doc = self
1154 .prepare_gated_document(file_path, indexing_gate)
1155 .await?;
1156 self.require_lines_in_document(&doc, positions)?;
1157 Ok(doc)
1158 }
1159
1160 pub(super) async fn prepare_gated_document_for_path(
1165 &self,
1166 path: &WorkspacePath,
1167 indexing_gate: IndexingGate,
1168 ) -> Result<PreparedDocument> {
1169 let (server_id, client, validated_path) = self
1170 .resolve_validated_client_for_path(path, indexing_gate.capability().tool_kind())
1171 .await?;
1172 self.finish_prepare_gated_document(server_id, client, validated_path, indexing_gate)
1173 .await
1174 }
1175
1176 pub(super) async fn prepare_disclosed_document(
1188 &self,
1189 file_path: &ClientPath,
1190 capability: DisclosedCapability,
1191 positions: &[Position],
1192 ) -> Result<DisclosedDocument<'_>> {
1193 let capability = Capability::from(capability);
1194 let (server_id, client, validated_path) = self
1195 .resolve_validated_client_for_file(file_path, capability.tool_kind())
1196 .await?;
1197 let doc = self
1198 .open_checked(
1199 server_id,
1200 client,
1201 &validated_path,
1202 capability,
1203 IndexingWait::Skip,
1204 )
1205 .await?;
1206 self.require_lines_in_document(&doc, positions)?;
1207 Ok(DisclosedDocument::new(self, doc))
1208 }
1209
1210 pub(super) async fn require_routed_capability(
1218 &self,
1219 file_path: &ClientPath,
1220 capability: Capability,
1221 ) -> Result<()> {
1222 let (server_id, _client, _path) = self
1223 .resolve_validated_client_for_file(file_path, capability.tool_kind())
1224 .await?;
1225 self.require_capability(&server_id, capability)
1226 }
1227
1228 async fn finish_prepare_gated_document(
1232 &self,
1233 server_id: ServerId,
1234 client: LspClient,
1235 validated_path: PathBuf,
1236 indexing_gate: IndexingGate,
1237 ) -> Result<PreparedDocument> {
1238 let wait = match indexing_gate {
1239 IndexingGate::Required(_) => IndexingWait::Wait,
1240 IndexingGate::FileLocal(_) => IndexingWait::Skip,
1241 };
1242 self.open_checked(
1243 server_id,
1244 client,
1245 &validated_path,
1246 indexing_gate.capability(),
1247 wait,
1248 )
1249 .await
1250 }
1251
1252 async fn open_checked(
1255 &self,
1256 server_id: ServerId,
1257 client: LspClient,
1258 validated_path: &Path,
1259 capability: Capability,
1260 wait: IndexingWait,
1261 ) -> Result<PreparedDocument> {
1262 self.require_capability(&server_id, capability)?;
1263 if wait == IndexingWait::Wait {
1264 self.wait_for_indexing_ready(&server_id).await?;
1265 }
1266 self.open_prepared(server_id, client, validated_path).await
1267 }
1268
1269 fn require_lines_in_document(
1270 &self,
1271 doc: &PreparedDocument,
1272 positions: &[Position],
1273 ) -> Result<()> {
1274 positions
1275 .iter()
1276 .try_for_each(|position| self.require_line_in_document(doc, *position))
1277 }
1278
1279 async fn flush_pending_closes(&self) {
1303 let flush = async {
1304 for path in self.document_tracker.pending_close_paths() {
1305 let Some(claim) = self.document_tracker.try_claim_pending_close(&path) else {
1306 continue;
1307 };
1308 for server_id in &claim.servers {
1309 let Some(client) = lock_std(&self.servers).client(server_id) else {
1310 continue;
1311 };
1312 if let Err(err) = client
1313 .notify_typed::<lsp_types::DidCloseTextDocumentNotification>(
1314 lsp_types::DidCloseTextDocumentParams {
1315 text_document: lsp_types::TextDocumentIdentifier {
1316 uri: claim.uri.clone(),
1317 },
1318 },
1319 )
1320 .await
1321 {
1322 tracing::warn!(
1323 %server_id,
1324 path = %claim.path.display(),
1325 error = %err,
1326 "failed to notify evicted document's server of textDocument/didClose; \
1327 dropping the close"
1328 );
1329 }
1330 }
1331 }
1332 };
1333 if tokio::time::timeout(FLUSH_PENDING_CLOSES_DEADLINE, flush)
1334 .await
1335 .is_err()
1336 {
1337 tracing::warn!(
1338 deadline = ?FLUSH_PENDING_CLOSES_DEADLINE,
1339 "flushing evicted documents' didClose notifications hit the deadline; paths not yet \
1340 reached stay pending, the in-flight path's remaining closes are dropped"
1341 );
1342 }
1343 }
1344
1345 pub(super) fn require_capability(
1370 &self,
1371 server_id: &ServerId,
1372 capability: Capability,
1373 ) -> Result<()> {
1374 let servers = lock_std(&self.servers);
1375 check_capability(
1376 server_id,
1377 servers
1378 .server(server_id)
1379 .map(crate::lsp::LspServer::capabilities),
1380 capability,
1381 )
1382 }
1383
1384 pub(super) fn code_action_resolve_supported(&self, server_id: &ServerId) -> bool {
1398 let servers = lock_std(&self.servers);
1399 matches!(
1400 servers
1401 .server(server_id)
1402 .map(crate::lsp::LspServer::capabilities)
1403 .and_then(|caps| caps.code_action_provider.as_ref()),
1404 Some(lsp_types::CodeActionProvider::CodeActionOptions(
1405 lsp_types::CodeActionOptions {
1406 resolve_provider: Some(true),
1407 ..
1408 }
1409 ))
1410 )
1411 }
1412
1413 pub(super) async fn parse_file_uri(&self, uri: &lsp_types::Uri) -> Result<WorkspacePath> {
1423 let path = crate::bridge::state::uri_to_path(uri).ok_or_else(|| {
1424 Error::InvalidToolParams(format!(
1425 "Invalid URI, expected an absolute file:// URI but got: {}",
1426 uri.as_ref()
1427 ))
1428 })?;
1429
1430 self.validate_path(&ClientPath::try_from(path)?).await
1431 }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436 use std::collections::{HashMap, HashSet};
1437 use std::sync::Arc;
1438 use std::{assert_matches, fs};
1439
1440 use tempfile::TempDir;
1441 use tokio::io::BufReader;
1442 use tokio::sync::Mutex;
1443 use tokio::time::{Duration, timeout};
1444 use url::Url;
1445
1446 use super::*;
1447 use crate::bridge::translator::dto::Position;
1448 use crate::bridge::translator::testing::*;
1449 use crate::bridge::{NotificationCache, ResultContext, WorkspaceRoots};
1450 use crate::config::{
1451 DocumentLimit, FileExtension, FilePattern, IndexingReadyTimeoutSecs, LanguageId,
1452 LanguageMap, LspServerConfig, PositionEncodings, SearchDepth, ServerCommand, SizeLimit,
1453 TimeoutSecs, ToolRouter, ToolSet,
1454 };
1455 use crate::error::Error;
1456 use crate::lsp::LspServer;
1457 use crate::test_lsp::client_path;
1458
1459 type JsonValue = serde_json::Value;
1460
1461 #[test]
1462 fn test_indexing_gate_capability_is_the_gated_capability() {
1463 assert_eq!(
1464 IndexingGate::Required(Capability::Hover).capability(),
1465 Capability::Hover
1466 );
1467 let file_local = [
1468 (
1469 FileLocalCapability::DocumentSymbols,
1470 Capability::DocumentSymbols,
1471 ),
1472 (FileLocalCapability::FoldingRange, Capability::FoldingRange),
1473 (
1474 FileLocalCapability::SelectionRange,
1475 Capability::SelectionRange,
1476 ),
1477 (
1478 FileLocalCapability::DocumentHighlights,
1479 Capability::DocumentHighlights,
1480 ),
1481 (
1482 FileLocalCapability::FormatDocument,
1483 Capability::FormatDocument,
1484 ),
1485 (FileLocalCapability::FormatRange, Capability::FormatRange),
1486 ];
1487 for (file_local, capability) in file_local {
1488 assert_eq!(IndexingGate::FileLocal(file_local).capability(), capability);
1489 }
1490 }
1491
1492 #[test]
1493 fn test_client_for_file_server_initializing_when_expected() {
1494 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
1498 let lang = LanguageMap::default().detect(&path);
1499 let id = ServerId::from(lang.clone());
1500
1501 let translator = Translator::new().with_router(ToolRouter::catch_all([(
1502 id.clone(),
1503 LanguageId::new(lang).unwrap(),
1504 )]));
1505 let mut expected = HashSet::new();
1506 expected.insert(id.clone());
1507 translator.set_expected_servers(expected);
1508
1509 let err = translator
1510 .client_for_file(&path, ToolKind::Hover)
1511 .unwrap_err();
1512 assert_matches!(err, Error::ServerInitializing { server_id } if server_id == id);
1513 }
1514
1515 #[test]
1516 fn test_client_for_file_no_server_when_not_expected() {
1517 let translator = Translator::new();
1520 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
1521 let lang = translator.language_map.detect(&path);
1522
1523 let err = translator
1524 .client_for_file(&path, ToolKind::Hover)
1525 .unwrap_err();
1526 assert_matches!(err, Error::NoServerForLanguage { ref language, .. } if *language == lang);
1527 }
1528
1529 use crate::test_lsp::test_extensions;
1530
1531 fn not_found_failure(id: &ServerId, language: &str, command: &str) -> ServerSpawnFailure {
1532 ServerSpawnFailure {
1533 server_id: id.clone(),
1534 language_id: LanguageId::new(language).unwrap(),
1535 command: ServerCommand::new(command).unwrap(),
1536 reason: crate::error::StartupFailure::Spawn(Arc::new(Error::ServerNotFound {
1537 command: ServerCommand::new(command).unwrap(),
1538 source: std::io::Error::from(std::io::ErrorKind::NotFound),
1539 })),
1540 }
1541 }
1542
1543 fn router_config(
1544 language: &str,
1545 name: &str,
1546 handles: Option<Vec<ToolKind>>,
1547 ) -> LspServerConfig {
1548 LspServerConfig {
1549 language_id: LanguageId::new(language).unwrap(),
1550 command: ServerCommand::from_static("sh").into(),
1551 args: vec![],
1552 env: crate::config::ServerEnv::default(),
1553 file_patterns: vec![],
1554 initialization_options: None,
1555 settings: None,
1556 timeout_seconds: TimeoutSecs::new(5).unwrap(),
1557 request_timeout_seconds: TimeoutSecs::new(5).unwrap(),
1558 heuristics: None,
1559 name: Some(ServerId::new(name).unwrap()),
1560 handles: handles.map(|tools| ToolSet::new(tools).unwrap()),
1561 indexing: crate::bridge::IndexingPolicy::Auto,
1562 }
1563 }
1564
1565 #[test]
1569 fn test_client_for_file_reports_startup_failure_of_sole_server() {
1570 let id = ServerId::from_static("rust");
1571 let translator = Translator::new()
1572 .with_extensions(test_extensions())
1573 .with_router(ToolRouter::catch_all([(
1574 id.clone(),
1575 LanguageId::from_static("rust"),
1576 )]));
1577 translator.record_startup_failures(&[not_found_failure(&id, "rust", "rust-analyzer")]);
1578 translator.rebind_router(&HashSet::new());
1579 translator.clear_expected_servers();
1580
1581 let err = translator
1582 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1583 .unwrap_err();
1584
1585 let Error::ServerFailedToStart(failure) = &err else {
1586 panic!("expected ServerFailedToStart, got {err:?}");
1587 };
1588 assert_eq!(failure.server_id, id);
1589 let message = err.to_string();
1590 assert!(
1591 message.contains("rustup component add rust-analyzer"),
1592 "{message}"
1593 );
1594 }
1595
1596 #[test]
1597 fn test_client_for_file_ignores_startup_failure_of_other_language() {
1598 let id = ServerId::from_static("rust");
1599 let translator = Translator::new()
1600 .with_extensions(test_extensions())
1601 .with_router(ToolRouter::catch_all([(
1602 id.clone(),
1603 LanguageId::from_static("rust"),
1604 )]));
1605 translator.record_startup_failures(&[not_found_failure(&id, "rust", "rust-analyzer")]);
1606 translator.rebind_router(&HashSet::new());
1607 translator.clear_expected_servers();
1608
1609 let err = translator
1610 .client_for_file(Path::new("/ws/script.py"), ToolKind::Hover)
1611 .unwrap_err();
1612
1613 assert_matches!(err, Error::NoServerForLanguage { .. }, "got {err:?}");
1614 }
1615
1616 #[tokio::test]
1619 async fn test_client_for_file_live_catch_all_wins_over_failed_explicit_server() {
1620 let configs = [
1621 router_config("rust", "hover-only", Some(vec![ToolKind::Hover])),
1622 router_config("rust", "catch-all", None),
1623 ];
1624 let router = ToolRouter::from_configs(configs.iter()).unwrap();
1625 let hover_id = ServerId::from_static("hover-only");
1626 let live_id = ServerId::from_static("catch-all");
1627
1628 let translator = Translator::new()
1629 .with_extensions(test_extensions())
1630 .with_router(router);
1631 let (client, _server) = fake_lsp_client();
1632 translator.register_client(live_id.clone(), client);
1633 translator.record_startup_failures(&[not_found_failure(&hover_id, "rust", "sh")]);
1634 translator.rebind_router(&HashSet::from([live_id.clone()]));
1635 translator.clear_expected_servers();
1636
1637 let (id, _client) = translator
1638 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1639 .unwrap();
1640
1641 assert_eq!(id, live_id);
1642 }
1643
1644 #[tokio::test]
1647 async fn test_client_for_file_reports_failed_catch_all_for_unclaimed_tool() {
1648 let configs = [
1649 router_config("rust", "hover-only", Some(vec![ToolKind::Hover])),
1650 router_config("rust", "catch-all", None),
1651 ];
1652 let router = ToolRouter::from_configs(configs.iter()).unwrap();
1653 let hover_id = ServerId::from_static("hover-only");
1654 let failed_id = ServerId::from_static("catch-all");
1655
1656 let translator = Translator::new()
1657 .with_extensions(test_extensions())
1658 .with_router(router);
1659 let (client, _server) = fake_lsp_client();
1660 translator.register_client(hover_id.clone(), client);
1661 translator.record_startup_failures(&[not_found_failure(&failed_id, "rust", "sh")]);
1662 translator.rebind_router(&HashSet::from([hover_id]));
1663 translator.clear_expected_servers();
1664
1665 let err = translator
1666 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Definition)
1667 .unwrap_err();
1668
1669 assert_matches!(&err, Error::ServerFailedToStart(f) if f.server_id == failed_id,
1670 "got {err:?}"
1671 );
1672 }
1673
1674 #[test]
1677 fn test_client_for_file_reports_startup_failure_through_react_base_language() {
1678 let id = ServerId::from_static("typescript");
1679 let translator = Translator::new()
1680 .with_extensions(test_extensions())
1681 .with_router(ToolRouter::catch_all([(
1682 id.clone(),
1683 LanguageId::from_static("typescript"),
1684 )]));
1685 translator.record_startup_failures(&[not_found_failure(
1686 &id,
1687 "typescript",
1688 "typescript-language-server",
1689 )]);
1690 translator.rebind_router(&HashSet::new());
1691 translator.clear_expected_servers();
1692
1693 let err = translator
1694 .client_for_file(Path::new("/ws/app.tsx"), ToolKind::Hover)
1695 .unwrap_err();
1696
1697 assert_matches!(&err, Error::ServerFailedToStart(f) if f.server_id == id,
1698 "got {err:?}"
1699 );
1700 }
1701
1702 #[tokio::test]
1703 async fn test_validate_path_no_workspace_roots_rejects_any_path() {
1704 let translator = Translator::new();
1705 let temp_dir = TempDir::new().unwrap();
1706 let test_file = temp_dir.path().join("test.rs");
1707 fs::write(&test_file, "fn main() {}").unwrap();
1708
1709 let result = translator.validate_path(&client_path(test_file)).await;
1711 assert_matches!(result, Err(Error::NoWorkspaceRoots(_)));
1712 }
1713
1714 #[tokio::test]
1715 async fn test_validate_path_within_workspace() {
1716 let mut translator = Translator::new();
1717 let temp_dir = TempDir::new().unwrap();
1718 let workspace_root = temp_dir.path().to_path_buf();
1719 translator.set_workspace_roots(WorkspaceRoots::from_paths(&[workspace_root]).unwrap());
1720
1721 let test_file = temp_dir.path().join("test.rs");
1722 fs::write(&test_file, "fn main() {}").unwrap();
1723
1724 let result = translator.validate_path(&client_path(test_file)).await;
1725 assert!(result.is_ok());
1726 }
1727
1728 #[tokio::test]
1729 async fn test_validate_path_outside_workspace() {
1730 let mut translator = Translator::new();
1731 let temp_dir1 = TempDir::new().unwrap();
1732 let temp_dir2 = TempDir::new().unwrap();
1733
1734 translator.set_workspace_roots(
1736 WorkspaceRoots::from_paths(&[temp_dir1.path().to_path_buf()]).unwrap(),
1737 );
1738
1739 let test_file = temp_dir2.path().join("test.rs");
1741 fs::write(&test_file, "fn main() {}").unwrap();
1742
1743 let result = translator.validate_path(&client_path(test_file)).await;
1744 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1745 }
1746
1747 #[test]
1750 fn test_validate_path_nonexistent_outside_path_is_outside_workspace() {
1751 let root = TempDir::new().unwrap();
1752 let outside = TempDir::new().unwrap();
1753 let roots = WorkspaceRoots::from_paths(&[root.path().to_path_buf()]).unwrap();
1754
1755 let result = roots
1756 .validate_blocking(&client_path(outside.path().join("missing.rs")))
1757 .map(crate::bridge::WorkspacePath::into_path_buf);
1758
1759 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1760 }
1761
1762 #[test]
1763 fn test_validate_path_dotdot_escape_is_outside_workspace() {
1764 let root = TempDir::new().unwrap();
1765 let roots = WorkspaceRoots::from_paths(&[root.path().to_path_buf()]).unwrap();
1766
1767 let result = roots
1768 .validate_blocking(&client_path(root.path().join("../escape.rs")))
1769 .map(crate::bridge::WorkspacePath::into_path_buf);
1770
1771 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1772 }
1773
1774 #[test]
1775 fn test_validate_path_dot_segments_inside_root_are_accepted() {
1776 let root = TempDir::new().unwrap();
1777 fs::create_dir(root.path().join("a")).unwrap();
1778 fs::write(root.path().join("b.rs"), "").unwrap();
1779 let roots = WorkspaceRoots::from_paths(&[root.path().to_path_buf()]).unwrap();
1780
1781 let result = roots
1782 .validate_blocking(&client_path(root.path().join("./a/../b.rs")))
1783 .map(crate::bridge::WorkspacePath::into_path_buf);
1784
1785 assert_eq!(
1786 result.unwrap(),
1787 dunce::canonicalize(root.path().join("b.rs")).unwrap()
1788 );
1789 }
1790
1791 #[cfg(unix)]
1794 #[test]
1795 fn test_validate_path_symlink_alias_of_root_is_accepted() {
1796 let AliasFixture {
1797 dir: _dir,
1798 real,
1799 alias,
1800 roots,
1801 } = alias_fixture();
1802 fs::write(real.join("a.rs"), "").unwrap();
1803
1804 let result = roots
1805 .validate_blocking(&client_path(alias.join("a.rs")))
1806 .map(crate::bridge::WorkspacePath::into_path_buf);
1807
1808 assert_eq!(
1809 result.unwrap(),
1810 dunce::canonicalize(real.join("a.rs")).unwrap()
1811 );
1812 }
1813
1814 #[cfg(unix)]
1817 #[test]
1818 fn test_validate_path_dotdot_after_alias_root_is_outside_workspace() {
1819 let AliasFixture {
1820 dir, alias, roots, ..
1821 } = alias_fixture();
1822 fs::write(dir.path().join("outside.rs"), "").unwrap();
1823
1824 let result = roots
1825 .validate_blocking(&client_path(alias.join("../outside.rs")))
1826 .map(crate::bridge::WorkspacePath::into_path_buf);
1827
1828 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1829 }
1830
1831 #[cfg(unix)]
1834 #[test]
1835 fn test_validate_path_missing_file_under_alias_root_is_file_io() {
1836 let AliasFixture {
1837 dir: _dir,
1838 alias,
1839 roots,
1840 ..
1841 } = alias_fixture();
1842
1843 let result = roots
1844 .validate_blocking(&client_path(alias.join("missing.rs")))
1845 .map(crate::bridge::WorkspacePath::into_path_buf);
1846
1847 assert_matches!(result, Err(Error::FileIo { .. }), "{result:?}");
1848 }
1849
1850 #[cfg(unix)]
1853 #[test]
1854 fn test_validate_path_escaping_symlink_under_alias_root_is_outside_workspace() {
1855 let AliasFixture {
1856 dir: _dir,
1857 real,
1858 alias,
1859 roots,
1860 } = alias_fixture();
1861 let outside = TempDir::new().unwrap();
1862 fs::write(outside.path().join("secret.rs"), "").unwrap();
1863 std::os::unix::fs::symlink(outside.path(), real.join("link")).unwrap();
1864
1865 let result = roots
1866 .validate_blocking(&client_path(alias.join("link/secret.rs")))
1867 .map(crate::bridge::WorkspacePath::into_path_buf);
1868
1869 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1870 }
1871
1872 #[cfg(unix)]
1875 #[test]
1876 fn test_validate_path_symlink_escaping_root_is_outside_workspace() {
1877 let root = TempDir::new().unwrap();
1878 let outside = TempDir::new().unwrap();
1879 fs::write(outside.path().join("secret.rs"), "").unwrap();
1880 std::os::unix::fs::symlink(outside.path(), root.path().join("link")).unwrap();
1881 let roots = WorkspaceRoots::from_paths(&[root.path().to_path_buf()]).unwrap();
1882
1883 let result = roots
1884 .validate_blocking(&client_path(root.path().join("link/secret.rs")))
1885 .map(crate::bridge::WorkspacePath::into_path_buf);
1886
1887 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1888 }
1889
1890 #[tokio::test]
1902 async fn test_handle_hover_blocked_when_path_outside_workspace() {
1903 let workspace_dir = TempDir::new().unwrap();
1904 let outside_dir = TempDir::new().unwrap();
1905 let server_id = ServerId::from_static("rust");
1906 let (translator, _server) = translator_with_capabilities(
1907 &workspace_dir,
1908 &server_id,
1909 lsp_types::ServerCapabilities {
1910 hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
1911 ..Default::default()
1912 },
1913 );
1914
1915 let outside_path = outside_dir.path().join("outside.rs");
1916 fs::write(&outside_path, "fn outside() {}").unwrap();
1917
1918 let result = translator
1919 .handle_hover(
1920 client_path(outside_path.to_string_lossy().into_owned()),
1921 pos(1, 1),
1922 )
1923 .await;
1924
1925 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
1926 }
1927
1928 #[tokio::test]
1929 async fn test_parse_file_uri_invalid_scheme() {
1930 let translator = Translator::new();
1931 let uri: lsp_types::Uri = lsp_types::Uri::from("http://example.com/file.rs");
1932 let result = translator.parse_file_uri(&uri).await;
1933 assert_matches!(result, Err(Error::InvalidToolParams(_)));
1934 }
1935
1936 #[tokio::test]
1937 async fn test_parse_file_uri_valid_scheme() {
1938 let mut translator = Translator::new();
1939 let temp_dir = TempDir::new().unwrap();
1940 translator.set_workspace_roots(
1941 WorkspaceRoots::from_paths(&[temp_dir.path().to_path_buf()]).unwrap(),
1942 );
1943 let test_file = temp_dir.path().join("test.rs");
1944 fs::write(&test_file, "fn main() {}").unwrap();
1945
1946 let file_url = Url::from_file_path(&test_file).unwrap();
1948 let uri: lsp_types::Uri = lsp_types::Uri::from(file_url.as_str());
1949 let result = translator.parse_file_uri(&uri).await;
1950 assert!(result.is_ok());
1951 }
1952
1953 #[tokio::test]
1958 async fn test_parse_file_uri_percent_decodes_space_and_non_ascii() {
1959 let mut translator = Translator::new();
1960 let temp_dir = TempDir::new().unwrap();
1961 translator.set_workspace_roots(
1962 WorkspaceRoots::from_paths(&[temp_dir.path().to_path_buf()]).unwrap(),
1963 );
1964 let test_file = temp_dir.path().join("my file café.rs");
1965 fs::write(&test_file, "fn main() {}").unwrap();
1966
1967 let file_url = Url::from_file_path(&test_file).unwrap();
1968 assert!(
1969 file_url.as_str().contains("%20"),
1970 "test fixture must exercise percent-encoding"
1971 );
1972 let uri: lsp_types::Uri = lsp_types::Uri::from(file_url.as_str());
1973 let result = translator.parse_file_uri(&uri).await.unwrap();
1974 assert_eq!(result.as_path(), dunce::canonicalize(&test_file).unwrap());
1975 }
1976
1977 #[tokio::test]
1981 async fn test_parse_file_uri_rejects_authority() {
1982 let translator = Translator::new();
1983 let uri: lsp_types::Uri = lsp_types::Uri::from("file://host/some/path.rs");
1984 let result = translator.parse_file_uri(&uri).await;
1985 assert_matches!(result, Err(Error::InvalidToolParams(_)));
1986 }
1987
1988 #[test]
1989 fn test_client_for_file_uses_custom_extension() {
1990 let temp_dir = TempDir::new().unwrap();
1991 let test_file = temp_dir.path().join("script.nu");
1992 fs::write(&test_file, "echo hello").unwrap();
1993
1994 let mut extension_map = HashMap::new();
1995 extension_map.insert(
1996 FileExtension::from_static("nu"),
1997 LanguageId::from_static("nushell"),
1998 );
1999
2000 let translator = Translator::new().with_extensions(extension_map);
2001
2002 let result = translator.client_for_file(&test_file, ToolKind::Hover);
2003
2004 assert!(result.is_err());
2005 if let Err(Error::NoServerForLanguage { language, file, .. }) = result {
2006 assert_eq!(language, "nushell");
2007 assert_eq!(file, FileKey::Extension(FileExtension::from_static("nu")));
2008 } else {
2009 panic!("Expected NoServerForLanguage(nushell) error");
2010 }
2011 }
2012
2013 #[test]
2014 fn test_client_for_file_reports_an_extensionless_name() {
2015 let mut language_map = LanguageMap::default();
2016 language_map.insert(
2017 crate::config::PatternTarget::Name(crate::config::FileName::from_static("Makefile")),
2018 LanguageId::from_static("make"),
2019 );
2020 let translator = Translator::new().with_extensions(language_map);
2021
2022 let result = translator.client_for_file(Path::new("/ws/Makefile"), ToolKind::Hover);
2023 assert_matches!(
2024 result,
2025 Err(Error::NoServerForLanguage { language, .. }) if language == "make"
2026 );
2027
2028 let result = translator.client_for_file(Path::new("/ws/Dockerfile"), ToolKind::Hover);
2029 assert_matches!(
2030 result,
2031 Err(Error::NoServerForLanguage { file: FileKey::Name(name), .. }) if name == "Dockerfile"
2032 );
2033 }
2034
2035 #[test]
2036 fn test_client_for_file_falls_back_to_default() {
2037 let temp_dir = TempDir::new().unwrap();
2038 let test_file = temp_dir.path().join("unknown.xyz");
2039 fs::write(&test_file, "content").unwrap();
2040
2041 let mut extension_map = HashMap::new();
2042 extension_map.insert(
2043 FileExtension::from_static("rs"),
2044 LanguageId::from_static("rust"),
2045 );
2046
2047 let translator = Translator::new()
2048 .with_extensions(extension_map)
2049 .with_file_patterns([FilePattern::from_static("**/*.rs")]);
2050
2051 let result = translator.client_for_file(&test_file, ToolKind::Hover);
2052
2053 assert!(result.is_err());
2054 if let Err(Error::NoServerForLanguage {
2055 language,
2056 file,
2057 patterns,
2058 }) = result
2059 {
2060 assert_eq!(language, "plaintext");
2061 assert_eq!(file, FileKey::Extension(FileExtension::from_static("xyz")));
2062 assert_eq!(&*patterns, [FilePattern::from_static("**/*.rs")]);
2063 } else {
2064 panic!("Expected NoServerForLanguage(plaintext) error");
2065 }
2066 }
2067
2068 #[test]
2069 fn test_client_for_file_routes_tsx_to_typescript_server() {
2070 let temp_dir = TempDir::new().unwrap();
2071 let test_file = temp_dir.path().join("component.tsx");
2072 fs::write(&test_file, "export const Component = () => <div />").unwrap();
2073
2074 let mut extension_map = HashMap::new();
2075 extension_map.insert(
2076 FileExtension::from_static("tsx"),
2077 LanguageId::from_static("typescriptreact"),
2078 );
2079
2080 let translator = Translator::new()
2081 .with_extensions(extension_map)
2082 .with_router(ToolRouter::catch_all([(
2083 ServerId::from_static("typescript"),
2084 LanguageId::from_static("typescript"),
2085 )]));
2086 translator.register_client(
2087 ServerId::from_static("typescript"),
2088 LspClient::new(crate::config::LspServerConfig::typescript()),
2089 );
2090
2091 let (_id, client) = translator
2092 .client_for_file(&test_file, ToolKind::Hover)
2093 .unwrap();
2094 assert_eq!(client.language_id(), "typescript");
2095 }
2096
2097 #[test]
2098 fn test_client_for_file_prefers_exact_react_server() {
2099 let temp_dir = TempDir::new().unwrap();
2100 let test_file = temp_dir.path().join("component.tsx");
2101 fs::write(&test_file, "export const Component = () => <div />").unwrap();
2102
2103 let mut extension_map = HashMap::new();
2104 extension_map.insert(
2105 FileExtension::from_static("tsx"),
2106 LanguageId::from_static("typescriptreact"),
2107 );
2108
2109 let typescript_react_config = crate::config::LspServerConfig {
2110 language_id: LanguageId::from_static("typescriptreact"),
2111 command: ServerCommand::from_static("typescript-language-server").into(),
2112 args: vec!["--stdio".to_string()],
2113 env: crate::config::ServerEnv::default(),
2114 file_patterns: vec![FilePattern::from_static("**/*.tsx")],
2115 initialization_options: None,
2116 settings: None,
2117 timeout_seconds: TimeoutSecs::new(30).unwrap(),
2118 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
2119 heuristics: None,
2120 name: None,
2121 handles: None,
2122 indexing: crate::bridge::IndexingPolicy::Auto,
2123 };
2124
2125 let translator = Translator::new()
2126 .with_extensions(extension_map)
2127 .with_router(ToolRouter::catch_all([
2128 (
2129 ServerId::from_static("typescript"),
2130 LanguageId::from_static("typescript"),
2131 ),
2132 (
2133 ServerId::from_static("typescriptreact"),
2134 LanguageId::from_static("typescriptreact"),
2135 ),
2136 ]));
2137 translator.register_client(
2138 ServerId::from_static("typescript"),
2139 LspClient::new(crate::config::LspServerConfig::typescript()),
2140 );
2141 translator.register_client(
2142 ServerId::from(LanguageId::from_static("typescriptreact")),
2143 LspClient::new(typescript_react_config),
2144 );
2145
2146 let (_id, client) = translator
2147 .client_for_file(&test_file, ToolKind::Hover)
2148 .unwrap();
2149 assert_eq!(client.language_id(), "typescriptreact");
2150 }
2151
2152 #[test]
2156 fn test_diagnostics_route_for_path_live_when_registered() {
2157 let id = ServerId::from_static("rust");
2158 let translator = Translator::new()
2159 .with_extensions(test_extensions())
2160 .with_router(ToolRouter::catch_all([(
2161 id.clone(),
2162 LanguageId::from_static("rust"),
2163 )]));
2164 translator.register_client(
2165 id.clone(),
2166 LspClient::new(crate::config::LspServerConfig::rust_analyzer()),
2167 );
2168
2169 let route = translator.diagnostics_route_for_path(Path::new("/ws/main.rs"));
2170
2171 assert_matches!(&route, DiagnosticsRoute::Live(live) if *live == id,
2172 "{route:?}"
2173 );
2174 assert_eq!(route.server_id(), Some(&id));
2175 assert_eq!(route.into_read_result().unwrap(), Some(id));
2176 }
2177
2178 #[test]
2181 fn test_diagnostics_route_for_path_initializing_while_expected() {
2182 let id = ServerId::from_static("rust");
2183 let translator = Translator::new()
2184 .with_extensions(test_extensions())
2185 .with_router(ToolRouter::catch_all([(
2186 id.clone(),
2187 LanguageId::from_static("rust"),
2188 )]));
2189 translator.set_expected_servers(HashSet::from([id.clone()]));
2190
2191 let route = translator.diagnostics_route_for_path(Path::new("/ws/main.rs"));
2192
2193 assert_matches!(&route, DiagnosticsRoute::Initializing(i) if *i == id,
2194 "{route:?}"
2195 );
2196 assert_eq!(route.server_id(), Some(&id));
2197 let err = route.into_read_result().unwrap_err();
2198 assert_matches!(&err, Error::ServerInitializing { server_id } if *server_id == id,
2199 "{err:?}"
2200 );
2201 }
2202
2203 #[test]
2205 fn test_diagnostics_route_for_path_failed_to_start() {
2206 let id = ServerId::from_static("rust");
2207 let translator = Translator::new()
2208 .with_extensions(test_extensions())
2209 .with_router(ToolRouter::catch_all([(
2210 id.clone(),
2211 LanguageId::from_static("rust"),
2212 )]));
2213 translator.record_startup_failures(&[not_found_failure(&id, "rust", "rust-analyzer")]);
2214 translator.rebind_router(&HashSet::new());
2215 translator.clear_expected_servers();
2216
2217 let route = translator.diagnostics_route_for_path(Path::new("/ws/main.rs"));
2218
2219 assert!(route.is_failed(), "{route:?}");
2220 assert_eq!(route.server_id(), None);
2221 let err = route.into_read_result().unwrap_err();
2222 assert_matches!(&err, Error::ServerFailedToStart(f) if f.server_id == id,
2223 "{err:?}"
2224 );
2225 }
2226
2227 #[test]
2230 fn test_diagnostics_route_for_path_unrouted_reads_empty() {
2231 let id = ServerId::from_static("rust");
2232 let translator = Translator::new()
2233 .with_extensions(test_extensions())
2234 .with_router(ToolRouter::catch_all([(
2235 id.clone(),
2236 LanguageId::from_static("rust"),
2237 )]));
2238 translator.record_startup_failures(&[not_found_failure(&id, "rust", "rust-analyzer")]);
2239 translator.rebind_router(&HashSet::new());
2240 translator.clear_expected_servers();
2241
2242 let route = translator.diagnostics_route_for_path(Path::new("/ws/script.py"));
2243
2244 assert_matches!(route, DiagnosticsRoute::Unrouted, "{route:?}");
2245 assert_eq!(route.into_read_result().unwrap(), None);
2246 }
2247
2248 #[test]
2251 fn test_diagnostics_route_for_path_dangling_is_unrouted() {
2252 let id = ServerId::from_static("rust");
2253 let translator = Translator::new()
2254 .with_extensions(test_extensions())
2255 .with_router(ToolRouter::catch_all([(
2256 id,
2257 LanguageId::from_static("rust"),
2258 )]));
2259
2260 let route = translator.diagnostics_route_for_path(Path::new("/ws/main.rs"));
2261
2262 assert_matches!(route, DiagnosticsRoute::Unrouted, "{route:?}");
2263 }
2264
2265 #[tokio::test]
2268 async fn test_diagnostics_route_for_path_live_catch_all_wins_over_failed_explicit_server() {
2269 let configs = [
2270 router_config("rust", "diag-only", Some(vec![ToolKind::Diagnostics])),
2271 router_config("rust", "catch-all", None),
2272 ];
2273 let router = ToolRouter::from_configs(configs.iter()).unwrap();
2274 let failed_id = ServerId::from_static("diag-only");
2275 let live_id = ServerId::from_static("catch-all");
2276 let translator = Translator::new()
2277 .with_extensions(test_extensions())
2278 .with_router(router);
2279 let (client, _server) = fake_lsp_client();
2280 translator.register_client(live_id.clone(), client);
2281 translator.record_startup_failures(&[not_found_failure(&failed_id, "rust", "sh")]);
2282 translator.rebind_router(&HashSet::from([live_id.clone()]));
2283 translator.clear_expected_servers();
2284
2285 let route = translator.diagnostics_route_for_path(Path::new("/ws/main.rs"));
2286
2287 assert_matches!(&route, DiagnosticsRoute::Live(id) if *id == live_id,
2288 "{route:?}"
2289 );
2290 }
2291
2292 #[test]
2294 fn test_diagnostics_route_for_path_reports_failure_through_react_base_language() {
2295 let id = ServerId::from_static("typescript");
2296 let translator = Translator::new()
2297 .with_extensions(test_extensions())
2298 .with_router(ToolRouter::catch_all([(
2299 id.clone(),
2300 LanguageId::from_static("typescript"),
2301 )]));
2302 translator.record_startup_failures(&[not_found_failure(
2303 &id,
2304 "typescript",
2305 "typescript-language-server",
2306 )]);
2307 translator.rebind_router(&HashSet::new());
2308 translator.clear_expected_servers();
2309
2310 let route = translator.diagnostics_route_for_path(Path::new("/ws/app.tsx"));
2311
2312 assert_matches!(&route, DiagnosticsRoute::FailedToStart(f) if f.server_id == id,
2313 "{route:?}"
2314 );
2315 }
2316
2317 #[test]
2320 fn test_diagnostics_route_for_path_ignores_failure_of_server_without_diagnostics_route() {
2321 let configs = [router_config(
2322 "rust",
2323 "hover-only",
2324 Some(vec![ToolKind::Hover]),
2325 )];
2326 let router = ToolRouter::from_configs(configs.iter()).unwrap();
2327 let failed = ServerId::from_static("hover-only");
2328 let translator = Translator::new()
2329 .with_extensions(test_extensions())
2330 .with_router(router);
2331 translator.record_startup_failures(&[not_found_failure(&failed, "rust", "sh")]);
2332 translator.rebind_router(&HashSet::new());
2333 translator.clear_expected_servers();
2334
2335 let route = translator.diagnostics_route_for_path(Path::new("/ws/main.rs"));
2336
2337 assert_matches!(route, DiagnosticsRoute::Unrouted, "{route:?}");
2338 }
2339
2340 #[tokio::test]
2342 async fn test_handle_diagnostics_reports_failed_server_start() {
2343 let dir = TempDir::new().unwrap();
2344 let file = dunce::canonicalize(dir.path()).unwrap().join("main.rs");
2345 fs::write(&file, "fn main() {}").unwrap();
2346 let id = ServerId::from_static("rust");
2347 let mut translator = Translator::new()
2348 .with_extensions(test_extensions())
2349 .with_router(ToolRouter::catch_all([(
2350 id.clone(),
2351 LanguageId::from_static("rust"),
2352 )]));
2353 translator
2354 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
2355 translator.record_startup_failures(&[not_found_failure(&id, "rust", "sh")]);
2356 translator.rebind_router(&HashSet::new());
2357 translator.clear_expected_servers();
2358 let cache = Mutex::new(crate::bridge::NotificationCache::new());
2359
2360 let err = translator
2361 .handle_diagnostics(
2362 client_path(file.to_string_lossy().into_owned()),
2363 ResultContext::None,
2364 &cache,
2365 )
2366 .await
2367 .unwrap_err();
2368
2369 assert_matches!(&err, Error::ServerFailedToStart(f) if f.server_id == id,
2370 "got {err:?}"
2371 );
2372 }
2373
2374 #[cfg(unix)]
2377 struct AliasFixture {
2378 dir: TempDir,
2379 real: PathBuf,
2380 alias: PathBuf,
2381 roots: WorkspaceRoots,
2382 }
2383
2384 #[cfg(unix)]
2385 fn alias_fixture() -> AliasFixture {
2386 let dir = TempDir::new().unwrap();
2387 let real = dir.path().join("real");
2388 fs::create_dir(&real).unwrap();
2389 let alias = dir.path().join("alias");
2390 std::os::unix::fs::symlink(&real, &alias).unwrap();
2391 let roots = WorkspaceRoots::from_paths(std::slice::from_ref(&alias)).unwrap();
2392 AliasFixture {
2393 dir,
2394 real,
2395 alias,
2396 roots,
2397 }
2398 }
2399
2400 #[test]
2401 fn test_client_for_file_routes_jsx_to_javascript_server() {
2402 let temp_dir = TempDir::new().unwrap();
2403 let test_file = temp_dir.path().join("component.jsx");
2404 fs::write(&test_file, "export const Component = () => <div />").unwrap();
2405
2406 let mut extension_map = HashMap::new();
2407 extension_map.insert(
2408 FileExtension::from_static("jsx"),
2409 LanguageId::from_static("javascriptreact"),
2410 );
2411
2412 let javascript_config = crate::config::LspServerConfig {
2413 language_id: LanguageId::from_static("javascript"),
2414 command: ServerCommand::from_static("typescript-language-server").into(),
2415 args: vec!["--stdio".to_string()],
2416 env: crate::config::ServerEnv::default(),
2417 file_patterns: vec![
2418 FilePattern::from_static("**/*.js"),
2419 FilePattern::from_static("**/*.jsx"),
2420 ],
2421 initialization_options: None,
2422 settings: None,
2423 timeout_seconds: TimeoutSecs::new(30).unwrap(),
2424 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
2425 heuristics: None,
2426 name: None,
2427 handles: None,
2428 indexing: crate::bridge::IndexingPolicy::Auto,
2429 };
2430 let translator = Translator::new()
2431 .with_extensions(extension_map)
2432 .with_router(ToolRouter::catch_all([(
2433 ServerId::from_static("javascript"),
2434 LanguageId::from_static("javascript"),
2435 )]));
2436 translator.register_client(
2437 ServerId::from_static("javascript"),
2438 LspClient::new(javascript_config),
2439 );
2440
2441 let (_id, client) = translator
2442 .client_for_file(&test_file, ToolKind::Hover)
2443 .unwrap();
2444 assert_eq!(client.language_id(), "javascript");
2445 }
2446
2447 #[tokio::test]
2448 async fn test_serve_initializes_translator_with_extensions() {
2449 use crate::config::{LanguageExtensionMapping, WorkspaceConfig};
2450
2451 let language_extensions = vec![
2452 LanguageExtensionMapping {
2453 extensions: vec![FileExtension::from_static("nu")],
2454 language_id: LanguageId::from_static("nushell"),
2455 },
2456 LanguageExtensionMapping {
2457 extensions: vec![FileExtension::from_static("rs")],
2458 language_id: LanguageId::from_static("rust"),
2459 },
2460 ];
2461
2462 let config = crate::config::ServerConfig {
2463 mcp: crate::config::McpConfig::default(),
2464 workspace: WorkspaceConfig {
2465 roots: vec![crate::config::ConfiguredRoot::new("/tmp/test-workspace").unwrap()],
2466 position_encodings: PositionEncodings::DEFAULT,
2467 language_extensions: language_extensions.clone(),
2468 heuristics_max_depth: SearchDepth::DEFAULT,
2469 max_documents: DocumentLimit::DEFAULT,
2470 max_file_size: SizeLimit::DEFAULT,
2471 indexing_ready_timeout_seconds: IndexingReadyTimeoutSecs::DEFAULT,
2472 max_concurrent_server_starts: crate::config::ServerStartConcurrency::DEFAULT,
2473 },
2474 lsp_servers: vec![],
2475 project_config_status: crate::ProjectConfigStatus::NotIgnored,
2476 workspace_trust: crate::config::WorkspaceTrust::default(),
2477 };
2478
2479 let language_map = config.build_effective_language_map();
2480 assert_eq!(language_map.detect(Path::new("a.nu")), "nushell");
2481 assert_eq!(language_map.detect(Path::new("a.rs")), "rust");
2482
2483 let result = crate::serve(config).await;
2486 if let Err(ref err) = result {
2487 assert!(
2488 !matches!(err, crate::error::Error::AllServersFailedToInit { .. }),
2489 "serve() must not report init failures for empty lsp_servers config"
2490 );
2491 }
2492 }
2493
2494 #[tokio::test]
2495 async fn test_concurrent_handlers_on_different_files_do_not_serialize() {
2496 let dir = TempDir::new().unwrap();
2504 let mut extensions = HashMap::new();
2505 extensions.insert(
2506 FileExtension::from_static("aa"),
2507 LanguageId::from_static("lang_a"),
2508 );
2509 extensions.insert(
2510 FileExtension::from_static("bb"),
2511 LanguageId::from_static("lang_b"),
2512 );
2513
2514 let mut translator =
2515 Translator::new()
2516 .with_extensions(extensions)
2517 .with_router(ToolRouter::catch_all([
2518 (
2519 ServerId::from_static("lang_a"),
2520 LanguageId::from_static("lang_a"),
2521 ),
2522 (
2523 ServerId::from_static("lang_b"),
2524 LanguageId::from_static("lang_b"),
2525 ),
2526 ]));
2527 translator
2528 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
2529
2530 let (client_a, mut server_a) = fake_lsp_client();
2531 let (client_b, mut server_b) = fake_lsp_client();
2532 translator.register_client(ServerId::from_static("lang_a"), client_a);
2533 translator.register_client(ServerId::from_static("lang_b"), client_b);
2534
2535 let path_a = dir.path().join("file.aa");
2536 let path_b = dir.path().join("file.bb");
2537 fs::write(&path_a, "content a").unwrap();
2538 fs::write(&path_b, "content b").unwrap();
2539
2540 let translator = Arc::new(translator);
2541
2542 let slow = {
2546 let translator = Arc::clone(&translator);
2547 let path = path_a.to_string_lossy().to_string();
2548 tokio::spawn(async move {
2549 translator
2550 .handle_hover(client_path(path), Position::at(1, 1))
2551 .await
2552 })
2553 };
2554
2555 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
2559 let opened_a = read_framed_message(&mut wire_a).await;
2560 assert_eq!(opened_a["method"], "textDocument/didOpen");
2561 let hover_request_a = read_framed_message(&mut wire_a).await;
2562 assert_eq!(hover_request_a["method"], "textDocument/hover");
2563
2564 let fast = {
2566 let translator = Arc::clone(&translator);
2567 let path = path_b.to_string_lossy().to_string();
2568 tokio::spawn(async move {
2569 translator
2570 .handle_hover(client_path(path), Position::at(1, 1))
2571 .await
2572 })
2573 };
2574
2575 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2576 let opened_b = read_framed_message(&mut wire_b).await;
2577 assert_eq!(opened_b["method"], "textDocument/didOpen");
2578 let hover_request_b = read_framed_message(&mut wire_b).await;
2579 assert_eq!(hover_request_b["method"], "textDocument/hover");
2580 write_response(
2581 &mut server_b.read_half_stdin,
2582 &hover_request_b["id"],
2583 JsonValue::Null,
2584 )
2585 .await;
2586
2587 let fast_result = timeout(Duration::from_secs(2), fast)
2588 .await
2589 .expect("fast call must not be blocked by the slow in-flight request")
2590 .unwrap();
2591 assert!(fast_result.is_ok());
2592
2593 assert!(
2594 !slow.is_finished(),
2595 "slow call should still be waiting on its (never-sent) response"
2596 );
2597 slow.abort();
2598 }
2599
2600 #[tokio::test]
2601 async fn test_concurrent_ensure_open_same_path_sends_single_did_open() {
2602 let dir = TempDir::new().unwrap();
2606 let mut extensions = HashMap::new();
2607 extensions.insert(
2608 FileExtension::from_static("aa"),
2609 LanguageId::from_static("lang_a"),
2610 );
2611
2612 let mut translator =
2613 Translator::new()
2614 .with_extensions(extensions)
2615 .with_router(ToolRouter::catch_all([(
2616 ServerId::from_static("lang_a"),
2617 LanguageId::from_static("lang_a"),
2618 )]));
2619 translator
2620 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
2621
2622 let (client, mut server) = fake_lsp_client();
2623 translator.register_client(ServerId::from_static("lang_a"), client);
2624
2625 let path = dir.path().join("file.aa");
2626 fs::write(&path, "content").unwrap();
2627
2628 let concurrent_calls = 4;
2629
2630 let translator = Arc::new(translator);
2631 let path_str = path.to_string_lossy().to_string();
2632
2633 let handles: Vec<_> = (0..concurrent_calls)
2634 .map(|_| {
2635 let translator = Arc::clone(&translator);
2636 let path_str = path_str.clone();
2637 tokio::spawn(async move {
2638 translator
2639 .handle_hover(client_path(path_str), Position::at(1, 1))
2640 .await
2641 })
2642 })
2643 .collect();
2644
2645 let mut wire = BufReader::new(&mut server.write_stdout);
2646 let opened = read_framed_message(&mut wire).await;
2647 assert_eq!(opened["method"], "textDocument/didOpen");
2648
2649 for _ in 0..concurrent_calls {
2650 let request = read_framed_message(&mut wire).await;
2651 assert_eq!(
2652 request["method"], "textDocument/hover",
2653 "no second didOpen must appear ahead of the hover requests"
2654 );
2655 write_response(&mut server.read_half_stdin, &request["id"], JsonValue::Null).await;
2656 }
2657
2658 for handle in handles {
2659 let result = timeout(Duration::from_secs(2), handle)
2660 .await
2661 .expect("handler call should not hang")
2662 .unwrap();
2663 assert!(result.is_ok());
2664 }
2665 }
2666
2667 fn single_document_translator(dir: &TempDir) -> (Translator, crate::test_lsp::FakeServer) {
2670 use crate::bridge::state::ResourceLimits;
2671
2672 let mut extensions = HashMap::new();
2673 extensions.insert(
2674 FileExtension::from_static("aa"),
2675 LanguageId::from_static("lang_a"),
2676 );
2677
2678 let mut translator = Translator::new()
2679 .with_extensions(extensions)
2680 .with_router(ToolRouter::catch_all([(
2681 ServerId::from_static("lang_a"),
2682 LanguageId::from_static("lang_a"),
2683 )]))
2684 .with_resource_limits(ResourceLimits {
2685 max_documents: DocumentLimit::new(1),
2686 max_file_size: SizeLimit::UNLIMITED,
2687 });
2688 translator
2689 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
2690
2691 let (client, server) = fake_lsp_client();
2692 translator.register_client(ServerId::from_static("lang_a"), client);
2693 (translator, server)
2694 }
2695
2696 #[tokio::test]
2699 async fn test_flush_pending_closes_drops_failed_servers_debt() {
2700 let dir = TempDir::new().unwrap();
2701 let (translator, mut server_a) = single_document_translator(&dir);
2702 let (client_d, _server_d) = fake_lsp_client();
2703
2704 let tracker = &translator.document_tracker;
2705 let path = dir.path().join("p.aa");
2706 std::fs::write(&path, "p").unwrap();
2707 let other = dir.path().join("q.aa");
2708 std::fs::write(&other, "q").unwrap();
2709 let client_a = lock_std(&translator.servers)
2710 .client(&ServerId::from_static("lang_a"))
2711 .unwrap();
2712 tracker
2713 .ensure_open(&path, &ServerId::from_static("lang_a"), &client_a)
2714 .await
2715 .unwrap();
2716 tracker
2717 .ensure_open(&path, &ServerId::from_static("lang_d"), &client_d)
2718 .await
2719 .unwrap();
2720 tracker
2721 .ensure_open(&other, &ServerId::from_static("lang_a"), &client_a)
2722 .await
2723 .unwrap();
2724
2725 translator.register_client(ServerId::from_static("lang_d"), client_d.clone());
2726 client_d.shutdown().await.unwrap();
2727
2728 translator.flush_pending_closes().await;
2729
2730 let mut wire = tokio::io::BufReader::new(&mut server_a.write_stdout);
2731 for method in [
2732 "textDocument/didOpen",
2733 "textDocument/didOpen",
2734 "textDocument/didClose",
2735 ] {
2736 assert_eq!(read_framed_message(&mut wire).await["method"], method);
2737 }
2738 assert!(
2739 tracker.pending_close_paths().is_empty(),
2740 "a failed close is dropped, not re-owed"
2741 );
2742 }
2743
2744 #[tokio::test]
2750 async fn test_prepare_document_sends_didclose_for_evicted_document() {
2751 let dir = TempDir::new().unwrap();
2752 let (translator, mut server) = single_document_translator(&dir);
2753
2754 let path_a = dir.path().join("a.aa");
2755 fs::write(&path_a, "content a").unwrap();
2756 let path_b = dir.path().join("b.aa");
2757 fs::write(&path_b, "content b").unwrap();
2758
2759 drop(
2760 translator
2761 .prepare_document(
2762 &client_path(path_a.to_string_lossy().into_owned()),
2763 ToolKind::Hover,
2764 )
2765 .await
2766 .unwrap(),
2767 );
2768
2769 let mut wire = BufReader::new(&mut server.write_stdout);
2770 let opened_a = read_framed_message(&mut wire).await;
2771 assert_eq!(opened_a["method"], "textDocument/didOpen");
2772
2773 drop(
2774 translator
2775 .prepare_document(
2776 &client_path(path_b.to_string_lossy().into_owned()),
2777 ToolKind::Hover,
2778 )
2779 .await
2780 .unwrap(),
2781 );
2782
2783 let opened_b = read_framed_message(&mut wire).await;
2784 assert_eq!(opened_b["method"], "textDocument/didOpen");
2785 let closed_a = read_framed_message(&mut wire).await;
2786 assert_eq!(
2787 closed_a["method"], "textDocument/didClose",
2788 "evicting `a` to make room for `b` under max_documents: 1 must notify its server"
2789 );
2790 assert_eq!(
2791 closed_a["params"]["textDocument"]["uri"], opened_a["params"]["textDocument"]["uri"],
2792 "the didClose must name the evicted document, not the newly opened one"
2793 );
2794 }
2795
2796 #[tokio::test]
2802 async fn test_prepared_document_blocks_eviction_until_dropped() {
2803 let dir = TempDir::new().unwrap();
2804 let (translator, _server) = single_document_translator(&dir);
2805
2806 let path_a = dir.path().join("a.aa");
2807 fs::write(&path_a, "content a").unwrap();
2808 let path_b = dir.path().join("b.aa");
2809 fs::write(&path_b, "content b").unwrap();
2810
2811 let doc_a = translator
2812 .prepare_document(
2813 &client_path(path_a.to_string_lossy().into_owned()),
2814 ToolKind::Hover,
2815 )
2816 .await
2817 .unwrap();
2818
2819 let err = translator
2820 .prepare_document(
2821 &client_path(path_b.to_string_lossy().into_owned()),
2822 ToolKind::Hover,
2823 )
2824 .await
2825 .unwrap_err();
2826 assert_matches!(err, Error::DocumentLimitExceeded { .. });
2827
2828 drop(doc_a);
2829 drop(
2830 translator
2831 .prepare_document(
2832 &client_path(path_b.to_string_lossy().into_owned()),
2833 ToolKind::Hover,
2834 )
2835 .await
2836 .unwrap(),
2837 );
2838 }
2839
2840 #[tokio::test]
2843 async fn test_two_prepared_documents_on_same_path_are_refcounted() {
2844 let dir = TempDir::new().unwrap();
2845 let (translator, _server) = single_document_translator(&dir);
2846
2847 let path_a = dir.path().join("a.aa");
2848 fs::write(&path_a, "content a").unwrap();
2849 let path_b = dir.path().join("b.aa");
2850 fs::write(&path_b, "content b").unwrap();
2851 let canonical_a = dunce::canonicalize(&path_a).unwrap();
2852
2853 let first = translator
2854 .prepare_document(
2855 &client_path(path_a.to_string_lossy().into_owned()),
2856 ToolKind::Hover,
2857 )
2858 .await
2859 .unwrap();
2860 let second = translator
2861 .prepare_document(
2862 &client_path(path_a.to_string_lossy().into_owned()),
2863 ToolKind::Hover,
2864 )
2865 .await
2866 .unwrap();
2867 assert_eq!(translator.document_tracker.in_flight_count(&canonical_a), 2);
2868
2869 drop(first);
2870 let err = translator
2871 .prepare_document(
2872 &client_path(path_b.to_string_lossy().into_owned()),
2873 ToolKind::Hover,
2874 )
2875 .await
2876 .unwrap_err();
2877 assert_matches!(err, Error::DocumentLimitExceeded { .. });
2878
2879 drop(second);
2880 assert_eq!(translator.document_tracker.in_flight_count(&canonical_a), 0);
2881 drop(
2882 translator
2883 .prepare_document(
2884 &client_path(path_b.to_string_lossy().into_owned()),
2885 ToolKind::Hover,
2886 )
2887 .await
2888 .unwrap(),
2889 );
2890 }
2891
2892 #[tokio::test]
2895 async fn test_prepare_document_releases_in_flight_guard_when_ensure_open_fails() {
2896 let dir = TempDir::new().unwrap();
2897 let mut extensions = HashMap::new();
2898 extensions.insert(
2899 FileExtension::from_static("bb"),
2900 LanguageId::from_static("lang_b"),
2901 );
2902
2903 let mut translator =
2904 Translator::new()
2905 .with_extensions(extensions)
2906 .with_router(ToolRouter::catch_all([(
2907 ServerId::from_static("lang_b"),
2908 LanguageId::from_static("lang_b"),
2909 )]));
2910 translator
2911 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
2912
2913 let (client, _server) = fake_lsp_client();
2914 translator.register_client(ServerId::from_static("lang_b"), client.clone());
2915
2916 let path_b = dir.path().join("b.bb");
2917 fs::write(&path_b, "content b").unwrap();
2918 let canonical_b = dunce::canonicalize(&path_b).unwrap();
2919
2920 client.shutdown().await.unwrap();
2921 let err = translator
2922 .prepare_document(
2923 &client_path(path_b.to_string_lossy().into_owned()),
2924 ToolKind::Hover,
2925 )
2926 .await
2927 .unwrap_err();
2928
2929 assert_matches!(err, Error::ServerTerminated);
2930 assert_eq!(translator.document_tracker.in_flight_count(&canonical_b), 0);
2931 }
2932
2933 #[tokio::test]
2942 async fn test_prepare_document_still_sends_didclose_when_ensure_open_itself_fails() {
2943 use crate::bridge::state::ResourceLimits;
2944
2945 let dir = TempDir::new().unwrap();
2946 let mut extensions = HashMap::new();
2947 extensions.insert(
2948 FileExtension::from_static("aa"),
2949 LanguageId::from_static("lang_a"),
2950 );
2951 extensions.insert(
2952 FileExtension::from_static("bb"),
2953 LanguageId::from_static("lang_b"),
2954 );
2955
2956 let mut translator = Translator::new()
2957 .with_extensions(extensions)
2958 .with_router(ToolRouter::catch_all([
2959 (
2960 ServerId::from_static("lang_a"),
2961 LanguageId::from_static("lang_a"),
2962 ),
2963 (
2964 ServerId::from_static("lang_b"),
2965 LanguageId::from_static("lang_b"),
2966 ),
2967 ]))
2968 .with_resource_limits(ResourceLimits {
2969 max_documents: DocumentLimit::new(1),
2970 max_file_size: SizeLimit::UNLIMITED,
2971 });
2972 translator
2973 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
2974
2975 let (client_a, mut server_a) = fake_lsp_client();
2976 translator.register_client(ServerId::from_static("lang_a"), client_a);
2977 let (client_b, _server_b) = fake_lsp_client();
2978 translator.register_client(ServerId::from_static("lang_b"), client_b.clone());
2979
2980 let path_a = dir.path().join("a.aa");
2981 fs::write(&path_a, "content a").unwrap();
2982 let path_b = dir.path().join("b.bb");
2983 fs::write(&path_b, "content b").unwrap();
2984
2985 drop(
2986 translator
2987 .prepare_document(
2988 &client_path(path_a.to_string_lossy().into_owned()),
2989 ToolKind::Hover,
2990 )
2991 .await
2992 .unwrap(),
2993 );
2994
2995 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
2996 let opened_a = read_framed_message(&mut wire_a).await;
2997 assert_eq!(opened_a["method"], "textDocument/didOpen");
2998
2999 client_b.shutdown().await.unwrap();
3004
3005 let err = translator
3006 .prepare_document(
3007 &client_path(path_b.to_string_lossy().into_owned()),
3008 ToolKind::Hover,
3009 )
3010 .await
3011 .unwrap_err();
3012 assert_matches!(err, Error::ServerTerminated);
3013
3014 let closed_a = read_framed_message(&mut wire_a).await;
3018 assert_eq!(closed_a["method"], "textDocument/didClose");
3019 assert_eq!(
3020 closed_a["params"]["textDocument"]["uri"],
3021 opened_a["params"]["textDocument"]["uri"]
3022 );
3023 }
3024
3025 #[tokio::test]
3030 async fn test_dispatch_routes_hover_and_diagnostics_to_different_servers() {
3031 let dir = TempDir::new().unwrap();
3032 let mut extensions = HashMap::new();
3033 extensions.insert(
3034 FileExtension::from_static("py"),
3035 LanguageId::from_static("python"),
3036 );
3037
3038 let pyright_id = ServerId::from_static("pyright");
3039 let pylsp_id = ServerId::from_static("pylsp");
3040 let pyright = LspServerConfig {
3041 language_id: LanguageId::from_static("python"),
3042 command: ServerCommand::from_static("pyright-langserver").into(),
3043 args: vec![],
3044 env: crate::config::ServerEnv::default(),
3045 file_patterns: vec![],
3046 initialization_options: None,
3047 settings: None,
3048 timeout_seconds: TimeoutSecs::new(30).unwrap(),
3049 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
3050 heuristics: None,
3051 name: Some(ServerId::from_static("pyright")),
3052 handles: Some(ToolSet::single(ToolKind::Hover)),
3053 indexing: crate::bridge::IndexingPolicy::Auto,
3054 };
3055 let pylsp = LspServerConfig {
3056 command: ServerCommand::from_static("pylsp").into(),
3057 name: Some(ServerId::from_static("pylsp")),
3058 handles: Some(ToolSet::single(ToolKind::Diagnostics)),
3059 ..pyright.clone()
3060 };
3061 let configs = vec![pyright, pylsp];
3062 let router = ToolRouter::from_configs(&configs).unwrap();
3063
3064 let mut translator = Translator::new()
3065 .with_extensions(extensions)
3066 .with_router(router);
3067 translator
3068 .set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
3069
3070 let (client_pyright, mut server_pyright) = fake_lsp_client();
3071 let (client_pylsp, mut server_pylsp) = fake_lsp_client();
3072 translator.register_client(pyright_id, client_pyright);
3073 translator.register_client(pylsp_id, client_pylsp);
3074
3075 let path = dir.path().join("main.py");
3076 fs::write(&path, "x = 1").unwrap();
3077 let file = client_path(&path);
3078
3079 let translator = Arc::new(translator);
3080
3081 let rename_result = translator
3084 .handle_rename(
3085 file.clone(),
3086 pos(1, 1),
3087 crate::bridge::NewName::try_new("renamed").unwrap(),
3088 )
3089 .await;
3090 assert_matches!(
3091 rename_result,
3092 Err(Error::NoServerForTool {
3093 tool: ToolKind::Rename,
3094 ..
3095 }),
3096 "expected NoServerForTool for rename, got {rename_result:?}"
3097 );
3098
3099 let hover = {
3101 let translator = Arc::clone(&translator);
3102 let file = file.clone();
3103 tokio::spawn(async move { translator.handle_hover(file, pos(1, 1)).await })
3104 };
3105 let mut wire_pyright = BufReader::new(&mut server_pyright.write_stdout);
3106 let opened = read_framed_message(&mut wire_pyright).await;
3107 assert_eq!(opened["method"], "textDocument/didOpen");
3108 let hover_request = read_framed_message(&mut wire_pyright).await;
3109 assert_eq!(hover_request["method"], "textDocument/hover");
3110 write_response(
3111 &mut server_pyright.read_half_stdin,
3112 &hover_request["id"],
3113 JsonValue::Null,
3114 )
3115 .await;
3116 hover
3117 .await
3118 .unwrap()
3119 .expect("hover routed to pyright must succeed");
3120
3121 let diagnostics = {
3125 let translator = Arc::clone(&translator);
3126 let notification_cache = Arc::new(Mutex::new(NotificationCache::new()));
3127 tokio::spawn(async move {
3128 translator
3129 .handle_diagnostics(file, ResultContext::None, ¬ification_cache)
3130 .await
3131 })
3132 };
3133 let mut wire_pylsp = BufReader::new(&mut server_pylsp.write_stdout);
3134 let opened = read_framed_message(&mut wire_pylsp).await;
3135 assert_eq!(opened["method"], "textDocument/didOpen");
3136 let diag_request = read_framed_message(&mut wire_pylsp).await;
3137 assert_eq!(diag_request["method"], "textDocument/diagnostic");
3138 diagnostics.abort();
3141 }
3142
3143 #[test]
3147 fn test_require_capability_ok_when_server_not_registered() {
3148 let translator = Translator::new();
3149 let result =
3150 translator.require_capability(&ServerId::from_static("rust"), Capability::Rename);
3151 assert!(result.is_ok());
3152 }
3153
3154 #[tokio::test]
3155 async fn test_require_capability_ok_when_capability_present() {
3156 let translator = Translator::new();
3157 let server_id = ServerId::from_static("rust");
3158 let caps = lsp_types::ServerCapabilities {
3159 rename_provider: Some(lsp_types::RenameProvider::Bool(true)),
3160 ..Default::default()
3161 };
3162 translator.register_server(server_id.clone(), LspServer::new_for_test(caps));
3163
3164 let result = translator.require_capability(&server_id, Capability::Rename);
3165 assert!(result.is_ok());
3166 }
3167
3168 #[tokio::test]
3169 async fn test_require_capability_err_when_capability_absent() {
3170 let translator = Translator::new();
3171 let server_id = ServerId::from_static("rust");
3172 let caps = lsp_types::ServerCapabilities::default();
3173 translator.register_server(server_id.clone(), LspServer::new_for_test(caps));
3174
3175 let result = translator.require_capability(&server_id, Capability::Rename);
3176 assert_matches!(
3177 result,
3178 Err(Error::CapabilityNotSupported {
3179 capability: Capability::Rename,
3180 ..
3181 })
3182 );
3183 }
3184
3185 #[tokio::test]
3186 async fn test_handle_rename_blocked_when_capability_not_supported() {
3187 let dir = TempDir::new().unwrap();
3188 let server_id = ServerId::from_static("rust");
3189 let (translator, _server) = translator_with_capabilities(
3190 &dir,
3191 &server_id,
3192 lsp_types::ServerCapabilities::default(),
3193 );
3194
3195 let path = dir.path().join("main.rs");
3196 fs::write(&path, "fn main() {}").unwrap();
3197
3198 let result = translator
3199 .handle_rename(
3200 client_path(path.to_string_lossy().into_owned()),
3201 Position::at(1, 1),
3202 crate::bridge::NewName::try_new("renamed").unwrap(),
3203 )
3204 .await;
3205
3206 assert_matches!(
3207 result,
3208 Err(Error::CapabilityNotSupported {
3209 capability: Capability::Rename,
3210 ..
3211 })
3212 );
3213 }
3214
3215 #[tokio::test]
3216 async fn test_handle_code_actions_blocked_when_capability_not_supported() {
3217 let dir = TempDir::new().unwrap();
3218 let server_id = ServerId::from_static("rust");
3219 let (translator, _server) = translator_with_capabilities(
3220 &dir,
3221 &server_id,
3222 lsp_types::ServerCapabilities::default(),
3223 );
3224
3225 let path = dir.path().join("main.rs");
3226 fs::write(&path, "fn main() {}").unwrap();
3227
3228 let result = translator
3229 .handle_code_actions(
3230 client_path(path.to_string_lossy().into_owned()),
3231 bounded(Position::at(1, 1), Position::at(1, 5)),
3232 None,
3233 )
3234 .await;
3235
3236 assert_matches!(
3237 result,
3238 Err(Error::CapabilityNotSupported {
3239 capability: Capability::CodeActions,
3240 ..
3241 })
3242 );
3243 }
3244
3245 #[tokio::test]
3246 async fn test_handle_signature_help_blocked_when_capability_not_supported() {
3247 let dir = TempDir::new().unwrap();
3248 let server_id = ServerId::from_static("rust");
3249 let (translator, _server) = translator_with_capabilities(
3250 &dir,
3251 &server_id,
3252 lsp_types::ServerCapabilities::default(),
3253 );
3254
3255 let path = dir.path().join("main.rs");
3256 fs::write(&path, "fn main() {}").unwrap();
3257
3258 let result = translator
3259 .handle_signature_help(
3260 client_path(path.to_string_lossy().into_owned()),
3261 Position::at(1, 1),
3262 )
3263 .await;
3264
3265 assert_matches!(
3266 result,
3267 Err(Error::CapabilityNotSupported {
3268 capability: Capability::SignatureHelp,
3269 ..
3270 })
3271 );
3272 }
3273
3274 #[tokio::test]
3278 async fn test_handle_incoming_calls_blocked_when_capability_not_supported() {
3279 let dir = TempDir::new().unwrap();
3280 let server_id = ServerId::from_static("rust");
3281 let (translator, _server) = translator_with_capabilities(
3282 &dir,
3283 &server_id,
3284 lsp_types::ServerCapabilities::default(),
3285 );
3286
3287 let path = dir.path().join("main.rs");
3288 fs::write(&path, "fn main() {}").unwrap();
3289 let uri = Url::from_file_path(&path).unwrap().to_string();
3290
3291 let item = serde_json::json!({
3292 "name": "test_function",
3293 "kind": 12,
3294 "uri": uri,
3295 "range": {
3296 "start": {"line": 1, "character": 1},
3297 "end": {"line": 1, "character": 10}
3298 },
3299 "selectionRange": {
3300 "start": {"line": 1, "character": 1},
3301 "end": {"line": 1, "character": 10}
3302 }
3303 });
3304
3305 let result = translator
3306 .handle_incoming_calls(checked_hierarchy_item(item))
3307 .await;
3308
3309 assert_matches!(
3310 result,
3311 Err(Error::CapabilityNotSupported {
3312 capability: Capability::CallHierarchy,
3313 ..
3314 })
3315 );
3316 }
3317
3318 #[tokio::test]
3319 async fn test_handle_outgoing_calls_blocked_when_capability_not_supported() {
3320 let dir = TempDir::new().unwrap();
3321 let server_id = ServerId::from_static("rust");
3322 let (translator, _server) = translator_with_capabilities(
3323 &dir,
3324 &server_id,
3325 lsp_types::ServerCapabilities::default(),
3326 );
3327
3328 let path = dir.path().join("main.rs");
3329 fs::write(&path, "fn main() {}").unwrap();
3330 let uri = Url::from_file_path(&path).unwrap().to_string();
3331
3332 let item = serde_json::json!({
3333 "name": "test_function",
3334 "kind": 12,
3335 "uri": uri,
3336 "range": {
3337 "start": {"line": 1, "character": 1},
3338 "end": {"line": 1, "character": 10}
3339 },
3340 "selectionRange": {
3341 "start": {"line": 1, "character": 1},
3342 "end": {"line": 1, "character": 10}
3343 }
3344 });
3345
3346 let result = translator
3347 .handle_outgoing_calls(checked_hierarchy_item(item))
3348 .await;
3349
3350 assert_matches!(
3351 result,
3352 Err(Error::CapabilityNotSupported {
3353 capability: Capability::CallHierarchy,
3354 ..
3355 })
3356 );
3357 }
3358
3359 #[tokio::test]
3360 async fn test_handle_format_document_blocked_when_capability_not_supported() {
3361 let dir = TempDir::new().unwrap();
3362 let server_id = ServerId::from_static("rust");
3363 let (translator, _server) = translator_with_capabilities(
3364 &dir,
3365 &server_id,
3366 lsp_types::ServerCapabilities::default(),
3367 );
3368
3369 let path = dir.path().join("main.rs");
3370 fs::write(&path, "fn main() {}").unwrap();
3371
3372 let result = translator
3373 .handle_format_document(
3374 client_path(path.to_string_lossy().into_owned()),
3375 crate::bridge::TabSize::default(),
3376 true,
3377 )
3378 .await;
3379
3380 assert_matches!(
3381 result,
3382 Err(Error::CapabilityNotSupported {
3383 capability: Capability::FormatDocument,
3384 ..
3385 })
3386 );
3387 }
3388
3389 #[tokio::test]
3390 async fn test_handle_call_hierarchy_prepare_blocked_when_capability_not_supported() {
3391 let dir = TempDir::new().unwrap();
3392 let server_id = ServerId::from_static("rust");
3393 let (translator, _server) = translator_with_capabilities(
3394 &dir,
3395 &server_id,
3396 lsp_types::ServerCapabilities::default(),
3397 );
3398
3399 let path = dir.path().join("main.rs");
3400 fs::write(&path, "fn main() {}").unwrap();
3401
3402 let result = translator
3403 .handle_call_hierarchy_prepare(
3404 client_path(path.to_string_lossy().into_owned()),
3405 Position::at(1, 1),
3406 )
3407 .await;
3408
3409 assert_matches!(
3410 result,
3411 Err(Error::CapabilityNotSupported {
3412 capability: Capability::CallHierarchy,
3413 ..
3414 })
3415 );
3416 }
3417
3418 #[tokio::test]
3419 async fn test_handle_inlay_hints_blocked_when_capability_not_supported() {
3420 let dir = TempDir::new().unwrap();
3421 let server_id = ServerId::from_static("rust");
3422 let (translator, _server) = translator_with_capabilities(
3423 &dir,
3424 &server_id,
3425 lsp_types::ServerCapabilities::default(),
3426 );
3427
3428 let path = dir.path().join("main.rs");
3429 fs::write(&path, "fn main() {}").unwrap();
3430
3431 let result = translator
3432 .handle_inlay_hints(
3433 client_path(path.to_string_lossy().into_owned()),
3434 span(Position::at(1, 1), Position::at(10, 1)),
3435 )
3436 .await;
3437
3438 assert_matches!(
3439 result,
3440 Err(Error::CapabilityNotSupported {
3441 capability: Capability::InlayHints,
3442 ..
3443 })
3444 );
3445 }
3446
3447 #[tokio::test]
3448 async fn test_handle_hover_blocked_when_capability_not_supported() {
3449 let dir = TempDir::new().unwrap();
3450 let server_id = ServerId::from_static("rust");
3451 let (translator, _server) = translator_with_capabilities(
3452 &dir,
3453 &server_id,
3454 lsp_types::ServerCapabilities::default(),
3455 );
3456
3457 let path = dir.path().join("main.rs");
3458 fs::write(&path, "fn main() {}").unwrap();
3459
3460 let result = translator
3461 .handle_hover(
3462 client_path(path.to_string_lossy().into_owned()),
3463 Position::at(1, 1),
3464 )
3465 .await;
3466
3467 assert_matches!(
3468 result,
3469 Err(Error::CapabilityNotSupported {
3470 capability: Capability::Hover,
3471 ..
3472 })
3473 );
3474 }
3475
3476 #[tokio::test]
3477 async fn test_handle_definition_blocked_when_capability_not_supported() {
3478 let dir = TempDir::new().unwrap();
3479 let server_id = ServerId::from_static("rust");
3480 let (translator, _server) = translator_with_capabilities(
3481 &dir,
3482 &server_id,
3483 lsp_types::ServerCapabilities::default(),
3484 );
3485
3486 let path = dir.path().join("main.rs");
3487 fs::write(&path, "fn main() {}").unwrap();
3488
3489 let result = translator
3490 .handle_definition(
3491 client_path(path.to_string_lossy().into_owned()),
3492 Position::at(1, 1),
3493 ResultContext::None,
3494 )
3495 .await;
3496
3497 assert_matches!(
3498 result,
3499 Err(Error::CapabilityNotSupported {
3500 capability: Capability::Definition,
3501 ..
3502 })
3503 );
3504 }
3505
3506 #[tokio::test]
3507 async fn test_handle_references_blocked_when_capability_not_supported() {
3508 let dir = TempDir::new().unwrap();
3509 let server_id = ServerId::from_static("rust");
3510 let (translator, _server) = translator_with_capabilities(
3511 &dir,
3512 &server_id,
3513 lsp_types::ServerCapabilities::default(),
3514 );
3515
3516 let path = dir.path().join("main.rs");
3517 fs::write(&path, "fn main() {}").unwrap();
3518
3519 let result = translator
3520 .handle_references(
3521 client_path(path.to_string_lossy().into_owned()),
3522 Position::at(1, 1),
3523 false,
3524 ResultContext::None,
3525 )
3526 .await;
3527
3528 assert_matches!(
3529 result,
3530 Err(Error::CapabilityNotSupported {
3531 capability: Capability::References,
3532 ..
3533 })
3534 );
3535 }
3536
3537 #[tokio::test]
3538 async fn test_handle_completions_blocked_when_capability_not_supported() {
3539 let dir = TempDir::new().unwrap();
3540 let server_id = ServerId::from_static("rust");
3541 let (translator, _server) = translator_with_capabilities(
3542 &dir,
3543 &server_id,
3544 lsp_types::ServerCapabilities::default(),
3545 );
3546
3547 let path = dir.path().join("main.rs");
3548 fs::write(&path, "fn main() {}").unwrap();
3549
3550 let result = translator
3551 .handle_completions(
3552 client_path(path.to_string_lossy().into_owned()),
3553 Position::at(1, 1),
3554 None,
3555 )
3556 .await;
3557
3558 assert_matches!(
3559 result,
3560 Err(Error::CapabilityNotSupported {
3561 capability: Capability::Completions,
3562 ..
3563 })
3564 );
3565 }
3566
3567 #[tokio::test]
3568 async fn test_handle_document_symbols_blocked_when_capability_not_supported() {
3569 let dir = TempDir::new().unwrap();
3570 let server_id = ServerId::from_static("rust");
3571 let (translator, _server) = translator_with_capabilities(
3572 &dir,
3573 &server_id,
3574 lsp_types::ServerCapabilities::default(),
3575 );
3576
3577 let path = dir.path().join("main.rs");
3578 fs::write(&path, "fn main() {}").unwrap();
3579
3580 let result = translator
3581 .handle_document_symbols(client_path(path.to_string_lossy().into_owned()))
3582 .await;
3583
3584 assert_matches!(
3585 result,
3586 Err(Error::CapabilityNotSupported {
3587 capability: Capability::DocumentSymbols,
3588 ..
3589 })
3590 );
3591 }
3592
3593 #[tokio::test]
3594 async fn test_handle_workspace_symbol_blocked_when_capability_not_supported() {
3595 let dir = TempDir::new().unwrap();
3596 let server_id = ServerId::from_static("rust");
3597 let (translator, _server) = translator_with_capabilities(
3598 &dir,
3599 &server_id,
3600 lsp_types::ServerCapabilities::default(),
3601 );
3602
3603 let result = translator
3604 .handle_workspace_symbol("main".to_string(), None, 100)
3605 .await;
3606
3607 assert_matches!(
3608 result,
3609 Err(Error::CapabilityNotSupported {
3610 capability: Capability::WorkspaceSymbols,
3611 ..
3612 })
3613 );
3614 }
3615
3616 #[tokio::test]
3617 async fn test_handle_implementation_blocked_when_capability_not_supported() {
3618 let dir = TempDir::new().unwrap();
3619 let server_id = ServerId::from_static("rust");
3620 let (translator, _server) = translator_with_capabilities(
3621 &dir,
3622 &server_id,
3623 lsp_types::ServerCapabilities::default(),
3624 );
3625
3626 let path = dir.path().join("main.rs");
3627 fs::write(&path, "fn main() {}").unwrap();
3628
3629 let result = translator
3630 .handle_implementation(
3631 client_path(path.to_string_lossy().into_owned()),
3632 Position::at(1, 1),
3633 ResultContext::None,
3634 )
3635 .await;
3636
3637 assert_matches!(
3638 result,
3639 Err(Error::CapabilityNotSupported {
3640 capability: Capability::Implementation,
3641 ..
3642 })
3643 );
3644 }
3645
3646 #[tokio::test]
3647 async fn test_handle_type_definition_blocked_when_capability_not_supported() {
3648 let dir = TempDir::new().unwrap();
3649 let server_id = ServerId::from_static("rust");
3650 let (translator, _server) = translator_with_capabilities(
3651 &dir,
3652 &server_id,
3653 lsp_types::ServerCapabilities::default(),
3654 );
3655
3656 let path = dir.path().join("main.rs");
3657 fs::write(&path, "fn main() {}").unwrap();
3658
3659 let result = translator
3660 .handle_type_definition(
3661 client_path(path.to_string_lossy().into_owned()),
3662 Position::at(1, 1),
3663 ResultContext::None,
3664 )
3665 .await;
3666
3667 assert_matches!(
3668 result,
3669 Err(Error::CapabilityNotSupported {
3670 capability: Capability::TypeDefinition,
3671 ..
3672 })
3673 );
3674 }
3675
3676 #[tokio::test]
3677 async fn test_handle_declaration_blocked_when_capability_not_supported() {
3678 let dir = TempDir::new().unwrap();
3679 let server_id = ServerId::from_static("rust");
3680 let (translator, _server) = translator_with_capabilities(
3681 &dir,
3682 &server_id,
3683 lsp_types::ServerCapabilities::default(),
3684 );
3685
3686 let path = dir.path().join("main.rs");
3687 fs::write(&path, "fn main() {}").unwrap();
3688
3689 let result = translator
3690 .handle_declaration(
3691 client_path(&path),
3692 Position::at(1, 1),
3693 crate::bridge::ResultContext::None,
3694 )
3695 .await;
3696
3697 assert_matches!(
3698 result,
3699 Err(Error::CapabilityNotSupported {
3700 capability: Capability::Declaration,
3701 ..
3702 })
3703 );
3704 }
3705
3706 #[tokio::test]
3710 async fn test_require_capability_err_when_capability_explicitly_false() {
3711 let translator = Translator::new();
3712 let server_id = ServerId::from_static("rust");
3713 let caps = lsp_types::ServerCapabilities {
3714 rename_provider: Some(lsp_types::RenameProvider::Bool(false)),
3715 ..Default::default()
3716 };
3717 translator.register_server(server_id.clone(), LspServer::new_for_test(caps));
3718
3719 let result = translator.require_capability(&server_id, Capability::Rename);
3720 assert_matches!(
3721 result,
3722 Err(Error::CapabilityNotSupported {
3723 capability: Capability::Rename,
3724 ..
3725 })
3726 );
3727 }
3728
3729 #[tokio::test]
3735 async fn test_handle_rename_proceeds_when_capability_supported() {
3736 let dir = TempDir::new().unwrap();
3737 let server_id = ServerId::from_static("rust");
3738 let caps = lsp_types::ServerCapabilities {
3739 rename_provider: Some(lsp_types::RenameProvider::Bool(true)),
3740 ..Default::default()
3741 };
3742 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
3743
3744 let path = dir.path().join("main.rs");
3745 fs::write(&path, "fn main() {}").unwrap();
3746 let path_str = path.to_string_lossy().to_string();
3747
3748 let translator = Arc::new(translator);
3749 let handle = {
3750 let translator = Arc::clone(&translator);
3751 tokio::spawn(async move {
3752 translator
3753 .handle_rename(
3754 client_path(path_str),
3755 Position::at(1, 1),
3756 crate::bridge::NewName::try_new("renamed").unwrap(),
3757 )
3758 .await
3759 })
3760 };
3761
3762 let mut wire = BufReader::new(&mut server.write_stdout);
3763 let opened = read_framed_message(&mut wire).await;
3764 assert_eq!(opened["method"], "textDocument/didOpen");
3765 let rename_request = read_framed_message(&mut wire).await;
3766 assert_eq!(rename_request["method"], "textDocument/rename");
3767 write_response(
3768 &mut server.read_half_stdin,
3769 &rename_request["id"],
3770 JsonValue::Null,
3771 )
3772 .await;
3773
3774 let result = timeout(Duration::from_secs(2), handle)
3775 .await
3776 .expect("handler call should not hang")
3777 .unwrap();
3778
3779 assert!(
3780 !matches!(result, Err(Error::CapabilityNotSupported { .. })),
3781 "capability is supported, gate must not block dispatch, got {result:?}"
3782 );
3783 assert!(
3784 result.is_ok(),
3785 "fake server answered, expected Ok: {result:?}"
3786 );
3787 }
3788
3789 fn assert_gates_on_provider_forms<P>(
3791 supported: impl Fn(Option<P>) -> bool,
3792 forms: impl IntoIterator<Item = P>,
3793 disabled: P,
3794 ) {
3795 for form in forms {
3796 assert!(supported(Some(form)));
3797 }
3798 assert!(!supported(Some(disabled)));
3799 assert!(!supported(None));
3800 }
3801
3802 #[test]
3803 fn selection_range_capability_gates_on_every_provider_form() {
3804 use lsp_types::SelectionRangeProvider as Provider;
3805
3806 assert_gates_on_provider_forms(
3807 |provider| {
3808 Capability::SelectionRange.is_supported(&lsp_types::ServerCapabilities {
3809 selection_range_provider: provider,
3810 ..Default::default()
3811 })
3812 },
3813 [
3814 Provider::Bool(true),
3815 Provider::SelectionRangeOptions(lsp_types::SelectionRangeOptions::default()),
3816 Provider::SelectionRangeRegistrationOptions(
3817 lsp_types::SelectionRangeRegistrationOptions::default(),
3818 ),
3819 ],
3820 Provider::Bool(false),
3821 );
3822 }
3823
3824 #[test]
3825 fn folding_range_capability_gates_on_every_provider_form() {
3826 use lsp_types::FoldingRangeProvider as Provider;
3827
3828 assert_gates_on_provider_forms(
3829 |provider| {
3830 Capability::FoldingRange.is_supported(&lsp_types::ServerCapabilities {
3831 folding_range_provider: provider,
3832 ..Default::default()
3833 })
3834 },
3835 [
3836 Provider::Bool(true),
3837 Provider::FoldingRangeOptions(lsp_types::FoldingRangeOptions::default()),
3838 Provider::FoldingRangeRegistrationOptions(
3839 lsp_types::FoldingRangeRegistrationOptions::default(),
3840 ),
3841 ],
3842 Provider::Bool(false),
3843 );
3844 }
3845}