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