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