1use std::collections::{HashMap, HashSet};
12use std::sync::Arc;
13
14use lsp_types::ServerCapabilities;
15use schemars::JsonSchema;
16use serde::Serialize;
17
18use super::Translator;
19use super::pull_support::PullSupport;
20use super::routing::{
21 Capability, LanguageCandidates, RouteLookup, WorkspaceRouteLookup, lookup_route,
22 lookup_workspace_route,
23};
24use crate::bridge::ClientPath;
25use crate::config::{LanguageId, LanguageMap, ServerId, ToolKind, ToolRouter};
26use crate::error::Result;
27use crate::util::lock_std;
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
34#[serde(tag = "status", rename_all = "snake_case")]
35pub enum RouteSupport {
36 Supported {
38 server: ServerId,
40 },
41 PushOnly {
44 server: ServerId,
46 },
47 CapabilityNotAdvertised {
49 server: ServerId,
51 capability: Capability,
53 },
54 Initializing,
57 NoServer,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66pub struct CapabilitySet(u32);
67
68impl CapabilitySet {
69 pub fn of(caps: &ServerCapabilities) -> Self {
71 Capability::ALL
72 .into_iter()
73 .filter(|capability| capability.is_supported(caps))
74 .fold(Self::default(), |set, capability| {
75 Self(set.0 | 1 << capability as u32)
76 })
77 }
78
79 pub const fn contains(self, capability: Capability) -> bool {
81 self.0 & (1 << capability as u32) != 0
82 }
83}
84
85#[derive(Debug)]
91pub struct ToolSupportSnapshot {
92 language_map: Arc<LanguageMap>,
93 router: Arc<ToolRouter>,
94 expected: HashSet<ServerId>,
95 running: HashMap<ServerId, RunningSupport>,
96 registered: HashSet<ServerId>,
97}
98
99#[derive(Debug, Clone, Copy)]
102struct RunningSupport {
103 caps: CapabilitySet,
104 pull: PullSupport,
105}
106
107impl ToolSupportSnapshot {
108 pub(crate) fn languages(&self) -> Vec<LanguageId> {
113 let configured = self.router.configured_languages();
114 let mut languages: Vec<LanguageId> = self
115 .language_map
116 .languages()
117 .filter(|language| {
118 LanguageCandidates::new((*language).clone())
119 .iter()
120 .any(|candidate| configured.contains(candidate))
121 })
122 .cloned()
123 .collect();
124 languages.sort_unstable();
125 languages.dedup();
126 languages
127 }
128
129 #[cfg(test)]
132 pub(crate) fn document_support(&self, language: &LanguageId, tool: ToolKind) -> RouteSupport {
133 self.document_support_gated(language, tool, Capability::for_tool(tool))
134 }
135
136 pub(crate) fn document_support_gated(
141 &self,
142 language: &LanguageId,
143 tool: ToolKind,
144 capability: Option<Capability>,
145 ) -> RouteSupport {
146 let lookup = lookup_route(
147 &LanguageCandidates::new(language.clone()),
148 |lang| self.router.resolve(lang, tool).cloned(),
149 |id| self.registered.contains(id).then_some(()),
150 |id| self.expected.contains(id),
151 |lang| {
152 self.router
153 .catch_all_for(lang)
154 .filter(|id| self.expected.contains(*id))
155 .cloned()
156 },
157 |_| None,
158 );
159 match lookup {
160 RouteLookup::Registered(server, ()) => {
161 match (tool, self.registered_support(server, capability)) {
162 (ToolKind::Diagnostics, RouteSupport::Supported { server })
163 if self.answers_from_push_cache(&server) =>
164 {
165 RouteSupport::PushOnly { server }
166 }
167 (_, support) => support,
168 }
169 }
170 RouteLookup::Initializing(_) => RouteSupport::Initializing,
171 RouteLookup::Failed(_) | RouteLookup::Dangling { .. } | RouteLookup::Unrouted => {
172 RouteSupport::NoServer
173 }
174 }
175 }
176
177 pub(crate) fn workspace_support(&self, tool: ToolKind) -> RouteSupport {
179 let lookup = lookup_workspace_route(
180 || self.router.resolve_any(tool).cloned(),
181 |id| self.registered.contains(id),
182 |id| self.expected.contains(id),
183 || self.expected.is_empty(),
184 );
185 match lookup {
186 WorkspaceRouteLookup::Registered(server) => {
187 self.registered_support(server, Capability::for_tool(tool))
188 }
189 WorkspaceRouteLookup::Initializing(_) | WorkspaceRouteLookup::AllInitializing => {
190 RouteSupport::Initializing
191 }
192 WorkspaceRouteLookup::Dangling(_)
193 | WorkspaceRouteLookup::NothingConfigured
194 | WorkspaceRouteLookup::NoClaimant => RouteSupport::NoServer,
195 }
196 }
197
198 fn answers_from_push_cache(&self, server: &ServerId) -> bool {
201 matches!(
202 self.running.get(server).map(|running| running.pull),
203 Some(PullSupport::Probing | PullSupport::Unsupported)
204 )
205 }
206
207 fn registered_support(&self, server: ServerId, capability: Option<Capability>) -> RouteSupport {
208 let Some(RunningSupport { caps, .. }) = self.running.get(&server) else {
209 return RouteSupport::Initializing;
210 };
211 match capability {
212 Some(capability) if !caps.contains(capability) => {
213 RouteSupport::CapabilityNotAdvertised { server, capability }
214 }
215 _ => RouteSupport::Supported { server },
216 }
217 }
218}
219
220impl Translator {
221 pub(crate) fn tool_support_snapshot(&self) -> ToolSupportSnapshot {
227 let servers = lock_std(&self.servers);
228 let expected = servers.expected();
229 let running = servers
230 .running_servers()
231 .map(|(id, server)| {
232 (
233 id.clone(),
234 RunningSupport {
235 caps: CapabilitySet::of(server.capabilities()),
236 pull: servers.pull_support(id),
237 },
238 )
239 })
240 .collect();
241 let registered = servers
242 .ids()
243 .filter(|id| servers.client(id).is_some())
244 .cloned()
245 .collect();
246 drop(servers);
247 let router = self.router_snapshot();
251 ToolSupportSnapshot {
252 language_map: Arc::clone(&self.language_map),
253 router,
254 expected,
255 running,
256 registered,
257 }
258 }
259
260 pub(crate) async fn language_for_path(&self, path: &ClientPath) -> Result<LanguageId> {
266 let validated = self.validate_path(path).await?;
267 Ok(self
268 .language_candidates(validated.as_path())
269 .language()
270 .clone())
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use tempfile::TempDir;
277
278 use super::super::testing::translator_with_capabilities;
279 use super::*;
280 use crate::config::FileExtension;
281 use crate::test_lsp::client_path;
282
283 fn rust_language_map() -> Arc<LanguageMap> {
284 Arc::new(LanguageMap::from(HashMap::from([(
285 FileExtension::from_static("rs"),
286 LanguageId::from_static("rust"),
287 )])))
288 }
289
290 fn rust_caps(hover: bool) -> ServerCapabilities {
291 ServerCapabilities {
292 hover_provider: hover.then_some(lsp_types::HoverProvider::Bool(true)),
293 ..Default::default()
294 }
295 }
296
297 fn snapshot(
298 registered: &[&str],
299 expected: &[&str],
300 caps: &[(&str, ServerCapabilities)],
301 ) -> ToolSupportSnapshot {
302 let ids = |names: &[&str]| names.iter().map(|n| ServerId::new(*n).unwrap()).collect();
303 ToolSupportSnapshot {
304 language_map: rust_language_map(),
305 router: Arc::new(ToolRouter::catch_all([(
306 ServerId::from_static("rust"),
307 LanguageId::from_static("rust"),
308 )])),
309 expected: ids(expected),
310 running: caps
311 .iter()
312 .map(|(id, caps)| {
313 (
314 ServerId::new(*id).unwrap(),
315 RunningSupport {
316 caps: CapabilitySet::of(caps),
317 pull: PullSupport::Advertised,
318 },
319 )
320 })
321 .collect(),
322 registered: ids(registered),
323 }
324 }
325
326 #[test]
327 fn capability_tool_kind_round_trips() {
328 for capability in Capability::ALL {
329 if capability.is_primary() {
330 assert_eq!(
331 Capability::for_tool(capability.tool_kind()),
332 Some(capability)
333 );
334 } else {
335 assert_ne!(
336 Capability::for_tool(capability.tool_kind()),
337 Some(capability)
338 );
339 }
340 }
341 assert_eq!(Capability::for_tool(ToolKind::Diagnostics), None);
342 for tool in ToolKind::ALL {
343 let expected = usize::from(*tool != ToolKind::Diagnostics);
344 assert_eq!(
345 Capability::ALL
346 .iter()
347 .filter(|c| c.tool_kind() == *tool && c.is_primary())
348 .count(),
349 expected,
350 "{tool}"
351 );
352 }
353 }
354
355 #[test]
356 fn prepare_rename_shares_the_rename_route_with_its_own_capability() {
357 let rename_only = ServerCapabilities {
358 rename_provider: Some(lsp_types::RenameProvider::Bool(true)),
359 ..Default::default()
360 };
361 let with_prepare = ServerCapabilities {
362 rename_provider: Some(lsp_types::RenameProvider::RenameOptions(
363 lsp_types::RenameOptions {
364 prepare_provider: Some(true),
365 ..Default::default()
366 },
367 )),
368 ..Default::default()
369 };
370 let server = ServerId::from_static("rust");
371 let snap = snapshot(&["rust"], &[], &[("rust", rename_only)]);
372 assert_eq!(
373 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Rename),
374 RouteSupport::Supported {
375 server: server.clone()
376 }
377 );
378 assert_eq!(
379 snap.document_support_gated(
380 &LanguageId::from_static("rust"),
381 ToolKind::Rename,
382 Some(Capability::PrepareRename)
383 ),
384 RouteSupport::CapabilityNotAdvertised {
385 server: server.clone(),
386 capability: Capability::PrepareRename,
387 }
388 );
389 let snap = snapshot(&["rust"], &[], &[("rust", with_prepare)]);
390 assert_eq!(
391 snap.document_support_gated(
392 &LanguageId::from_static("rust"),
393 ToolKind::Rename,
394 Some(Capability::PrepareRename)
395 ),
396 RouteSupport::Supported { server }
397 );
398 }
399
400 #[test]
401 fn capability_set_fits_every_capability() {
402 assert!(Capability::ALL.len() > 16);
403 let caps = ServerCapabilities {
404 document_range_formatting_provider: Some(
405 lsp_types::DocumentRangeFormattingProvider::Bool(true),
406 ),
407 ..Default::default()
408 };
409 assert!(CapabilitySet::of(&caps).contains(Capability::FormatRange));
410 }
411
412 #[test]
413 fn registered_server_with_capability_is_supported() {
414 let snap = snapshot(&["rust"], &[], &[("rust", rust_caps(true))]);
415 assert_eq!(
416 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
417 RouteSupport::Supported {
418 server: ServerId::from_static("rust")
419 }
420 );
421 }
422
423 #[test]
424 fn registered_server_without_capability_is_not_advertised() {
425 let snap = snapshot(&["rust"], &[], &[("rust", rust_caps(false))]);
426 assert_eq!(
427 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
428 RouteSupport::CapabilityNotAdvertised {
429 server: ServerId::from_static("rust"),
430 capability: Capability::Hover,
431 }
432 );
433 assert_eq!(
434 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Diagnostics),
435 RouteSupport::Supported {
436 server: ServerId::from_static("rust")
437 }
438 );
439 }
440
441 fn snapshot_with_pull(pull: PullSupport) -> ToolSupportSnapshot {
442 ToolSupportSnapshot {
443 language_map: rust_language_map(),
444 running: HashMap::from([(
445 ServerId::from_static("rust"),
446 RunningSupport {
447 caps: CapabilitySet::of(&rust_caps(true)),
448 pull,
449 },
450 )]),
451 ..snapshot(&["rust"], &[], &[("rust", rust_caps(true))])
452 }
453 }
454
455 #[test]
459 fn diagnostics_route_is_push_only_unless_pull_is_advertised_or_answered() {
460 let language = LanguageId::from_static("rust");
461 let server = ServerId::from_static("rust");
462 for (pull, push_only) in [
463 (PullSupport::Advertised, false),
464 (PullSupport::Answers, false),
465 (PullSupport::Probing, true),
466 (PullSupport::Unsupported, true),
467 ] {
468 let support =
469 snapshot_with_pull(pull).document_support(&language, ToolKind::Diagnostics);
470 let expected = if push_only {
471 RouteSupport::PushOnly {
472 server: server.clone(),
473 }
474 } else {
475 RouteSupport::Supported {
476 server: server.clone(),
477 }
478 };
479 assert_eq!(support, expected, "{pull:?}");
480 }
481 assert_eq!(
482 snapshot_with_pull(PullSupport::Unsupported)
483 .document_support(&language, ToolKind::Hover),
484 RouteSupport::Supported { server },
485 "only the diagnostics route is affected"
486 );
487 }
488
489 #[test]
490 fn client_without_server_capabilities_is_initializing() {
491 let snap = snapshot(&["rust"], &[], &[]);
492 assert_eq!(
493 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
494 RouteSupport::Initializing
495 );
496 }
497
498 #[test]
499 fn expected_but_unregistered_server_is_initializing() {
500 let snap = snapshot(&[], &["rust"], &[]);
501 assert_eq!(
502 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
503 RouteSupport::Initializing
504 );
505 assert_eq!(
506 snap.workspace_support(ToolKind::WorkspaceSymbols),
507 RouteSupport::Initializing
508 );
509 }
510
511 #[test]
512 fn capability_set_holds_exactly_the_advertised_capabilities() {
513 let set = CapabilitySet::of(&rust_caps(true));
514 for capability in Capability::ALL {
515 assert_eq!(
516 set.contains(capability),
517 capability == Capability::Hover,
518 "{capability:?}"
519 );
520 }
521 assert_eq!(
522 CapabilitySet::of(&ServerCapabilities::default()),
523 CapabilitySet::default()
524 );
525 }
526
527 #[tokio::test]
531 async fn server_is_never_misreported_across_registration_and_restart_steps() {
532 use crate::lsp::LspServer;
533
534 let id = ServerId::from_static("rust");
535 let translator = Translator::new().with_router(ToolRouter::catch_all([(
536 id.clone(),
537 LanguageId::from_static("rust"),
538 )]));
539 let check = |step: &str| {
540 let snap = translator.tool_support_snapshot();
541 assert_ne!(
542 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
543 RouteSupport::NoServer,
544 "document route, {step}"
545 );
546 assert_ne!(
547 snap.workspace_support(ToolKind::WorkspaceSymbols),
548 RouteSupport::NoServer,
549 "workspace route, {step}"
550 );
551 };
552
553 translator.set_expected_servers(HashSet::from([id.clone()]));
554 check("expected");
555 translator.register_server(id.clone(), LspServer::new_for_test(rust_caps(true)));
556 check("registered");
557 let held = lock_std(&translator.servers).take_for_restart(&id);
558 check("taken for restart");
559 let refused = lock_std(&translator.servers).restore(&id, held.unwrap());
560 assert!(refused.is_none());
561 check("restored");
562 translator.clear_expected_servers();
563 check("expectation cleared");
564 assert_eq!(
565 translator
566 .tool_support_snapshot()
567 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
568 RouteSupport::Supported { server: id }
569 );
570 }
571
572 #[test]
573 fn language_without_live_route_is_listed_as_no_server() {
574 let mut router = ToolRouter::catch_all([(
575 ServerId::from_static("rust"),
576 LanguageId::from_static("rust"),
577 )]);
578 router.rebind_to_registered(&HashSet::new());
579 let snap = ToolSupportSnapshot {
580 language_map: rust_language_map(),
581 router: Arc::new(router),
582 expected: HashSet::new(),
583 running: HashMap::new(),
584 registered: HashSet::new(),
585 };
586 assert_eq!(snap.languages(), ["rust"]);
587 assert_eq!(
588 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
589 RouteSupport::NoServer
590 );
591 }
592
593 #[test]
594 fn test_languages_list_mapped_languages_with_a_route_including_the_react_base() {
595 let mut language_map = LanguageMap::from(HashMap::from([
596 (
597 FileExtension::from_static("ts"),
598 LanguageId::from_static("typescript"),
599 ),
600 (
601 FileExtension::from_static("tsx"),
602 LanguageId::from_static("typescriptreact"),
603 ),
604 (
605 FileExtension::from_static("py"),
606 LanguageId::from_static("python"),
607 ),
608 ]));
609 language_map.insert(
610 crate::config::PatternTarget::Name(crate::config::FileName::from_static("Makefile")),
611 LanguageId::from_static("make"),
612 );
613 let snap = ToolSupportSnapshot {
614 language_map: Arc::new(language_map),
615 router: Arc::new(ToolRouter::catch_all([
616 (
617 ServerId::from_static("ts"),
618 LanguageId::from_static("typescript"),
619 ),
620 (
621 ServerId::from_static("make-lsp"),
622 LanguageId::from_static("make"),
623 ),
624 (
625 ServerId::from_static("unmapped"),
626 LanguageId::from_static("zig"),
627 ),
628 ])),
629 expected: HashSet::new(),
630 running: HashMap::new(),
631 registered: HashSet::new(),
632 };
633 assert_eq!(snap.languages(), ["make", "typescript", "typescriptreact"]);
634 }
635
636 #[test]
637 fn nothing_configured_workspace_support_is_no_server() {
638 let snap = ToolSupportSnapshot {
639 language_map: rust_language_map(),
640 router: Arc::new(ToolRouter::default()),
641 expected: HashSet::new(),
642 running: HashMap::new(),
643 registered: HashSet::new(),
644 };
645 assert!(snap.languages().is_empty());
646 assert_eq!(
647 snap.workspace_support(ToolKind::WorkspaceSymbols),
648 RouteSupport::NoServer
649 );
650 }
651
652 #[tokio::test]
653 async fn translator_snapshot_reflects_registered_capabilities() {
654 let dir = TempDir::new().unwrap();
655 let id = ServerId::from_static("rust");
656 let (translator, _fake) = translator_with_capabilities(&dir, &id, rust_caps(true));
657 let snap = translator.tool_support_snapshot();
658 assert_eq!(
659 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
660 RouteSupport::Supported { server: id.clone() }
661 );
662 assert_eq!(
663 snap.document_support(&LanguageId::from_static("rust"), ToolKind::Rename),
664 RouteSupport::CapabilityNotAdvertised {
665 server: id,
666 capability: Capability::Rename,
667 }
668 );
669 }
670
671 #[tokio::test]
672 async fn language_for_path_validates_and_detects() {
673 let dir = TempDir::new().unwrap();
674 let (translator, _fake) = translator_with_capabilities(
675 &dir,
676 &ServerId::from_static("rust"),
677 ServerCapabilities::default(),
678 );
679 let file = dir.path().join("a.rs");
680 std::fs::write(&file, "").unwrap();
681 assert_eq!(
682 translator
683 .language_for_path(&client_path(&file))
684 .await
685 .unwrap(),
686 "rust"
687 );
688 assert!(
689 translator
690 .language_for_path(&client_path("/definitely/missing.rs"))
691 .await
692 .is_err()
693 );
694 }
695}