1use std::borrow::Cow;
17use std::collections::{HashMap, HashSet};
18use std::fmt;
19
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22
23use super::language_id::LanguageId;
24use super::server::{LaunchCommand, LspServerConfig};
25use super::text_newtype::impl_text_newtype;
26use crate::error::{ConfigError, Result};
27
28#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
30#[error("server id cannot be blank (omit `name` to default to the language id)")]
31pub struct InvalidServerId;
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
53#[serde(try_from = "String", into = "String")]
54#[schemars(with = "String")]
55pub struct ServerId(Cow<'static, str>);
56
57impl_text_newtype!(ServerId, InvalidServerId, non_blank, "server id");
58
59impl From<LanguageId> for ServerId {
60 fn from(id: LanguageId) -> Self {
61 Self(Cow::Owned(id.into()))
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91#[non_exhaustive]
92pub enum ToolKind {
93 Hover,
95 Definition,
97 TypeDefinition,
99 Implementation,
101 References,
103 Diagnostics,
105 Rename,
107 Completions,
109 SignatureHelp,
111 DocumentSymbols,
113 WorkspaceSymbols,
115 FormatDocument,
117 CodeActions,
119 CallHierarchy,
121 InlayHints,
123 Declaration,
125 TypeHierarchy,
127 DocumentHighlights,
129 FormatRange,
131 SelectionRange,
133 FoldingRange,
135}
136
137impl ToolKind {
138 pub const ALL: &[Self] = &[
141 Self::Hover,
142 Self::Definition,
143 Self::TypeDefinition,
144 Self::Implementation,
145 Self::References,
146 Self::Diagnostics,
147 Self::Rename,
148 Self::Completions,
149 Self::SignatureHelp,
150 Self::DocumentSymbols,
151 Self::WorkspaceSymbols,
152 Self::FormatDocument,
153 Self::CodeActions,
154 Self::CallHierarchy,
155 Self::InlayHints,
156 Self::Declaration,
157 Self::TypeHierarchy,
158 Self::DocumentHighlights,
159 Self::FormatRange,
160 Self::SelectionRange,
161 Self::FoldingRange,
162 ];
163
164 #[must_use]
166 pub const fn as_str(&self) -> &'static str {
167 match self {
168 Self::Hover => "hover",
169 Self::Definition => "definition",
170 Self::TypeDefinition => "type_definition",
171 Self::Implementation => "implementation",
172 Self::References => "references",
173 Self::Diagnostics => "diagnostics",
174 Self::Rename => "rename",
175 Self::Completions => "completions",
176 Self::SignatureHelp => "signature_help",
177 Self::DocumentSymbols => "document_symbols",
178 Self::WorkspaceSymbols => "workspace_symbols",
179 Self::FormatDocument => "format_document",
180 Self::CodeActions => "code_actions",
181 Self::CallHierarchy => "call_hierarchy",
182 Self::InlayHints => "inlay_hints",
183 Self::Declaration => "declaration",
184 Self::TypeHierarchy => "type_hierarchy",
185 Self::DocumentHighlights => "document_highlights",
186 Self::FormatRange => "format_range",
187 Self::SelectionRange => "selection_range",
188 Self::FoldingRange => "folding_range",
189 }
190 }
191}
192
193impl std::fmt::Display for ToolKind {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.write_str(self.as_str())
196 }
197}
198
199#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
201pub enum InvalidToolSet {
202 #[error("handles cannot be empty (omit `handles` for a catch-all server)")]
204 Empty,
205 #[error("duplicate tool '{0}' in `handles`")]
207 Duplicate(ToolKind),
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(try_from = "Vec<ToolKind>", into = "Vec<ToolKind>")]
227pub struct ToolSet(Vec<ToolKind>);
228
229impl ToolSet {
230 pub fn new(tools: Vec<ToolKind>) -> std::result::Result<Self, InvalidToolSet> {
237 if tools.is_empty() {
238 return Err(InvalidToolSet::Empty);
239 }
240 let mut seen = HashSet::new();
241 if let Some(tool) = tools.iter().find(|tool| !seen.insert(**tool)) {
242 return Err(InvalidToolSet::Duplicate(*tool));
243 }
244 Ok(Self(tools))
245 }
246
247 #[must_use]
249 pub fn single(tool: ToolKind) -> Self {
250 Self(vec![tool])
251 }
252
253 #[must_use]
255 pub fn contains(&self, tool: ToolKind) -> bool {
256 self.0.contains(&tool)
257 }
258
259 pub fn iter(&self) -> std::slice::Iter<'_, ToolKind> {
261 self.0.iter()
262 }
263}
264
265impl<'a> IntoIterator for &'a ToolSet {
266 type Item = &'a ToolKind;
267 type IntoIter = std::slice::Iter<'a, ToolKind>;
268
269 fn into_iter(self) -> Self::IntoIter {
270 self.iter()
271 }
272}
273
274impl TryFrom<Vec<ToolKind>> for ToolSet {
275 type Error = InvalidToolSet;
276
277 fn try_from(tools: Vec<ToolKind>) -> std::result::Result<Self, Self::Error> {
278 Self::new(tools)
279 }
280}
281
282impl From<ToolSet> for Vec<ToolKind> {
283 fn from(set: ToolSet) -> Self {
284 set.0
285 }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct EntrySummary {
314 pub language: LanguageId,
316 pub command: LaunchCommand,
318 pub arg_count: usize,
323}
324
325impl EntrySummary {
326 fn of(cfg: &LspServerConfig) -> Self {
327 Self {
328 language: cfg.language_id.clone(),
329 command: cfg.command.clone(),
330 arg_count: cfg.args.len(),
331 }
332 }
333}
334
335impl fmt::Display for EntrySummary {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 write!(
338 f,
339 "language '{}', command '{}'",
340 self.language, self.command
341 )?;
342 if self.arg_count > 0 {
343 let noun = if self.arg_count == 1 { "arg" } else { "args" };
344 write!(f, ", {} {noun}", self.arg_count)?;
345 }
346 Ok(())
347 }
348}
349
350#[derive(Debug, Default, Clone)]
352struct LanguageRoutes {
353 explicit: HashMap<ToolKind, ServerId>,
355 default: Option<ServerId>,
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363#[non_exhaustive]
364pub enum NoServerReason {
365 NothingRegistered,
373 NoClaimant,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub enum ServerSettlement {
392 Pending,
394 Registered,
396 Failed,
398}
399
400#[derive(Debug, Default, Clone)]
407pub struct ToolRouter {
408 by_language: HashMap<LanguageId, LanguageRoutes>,
409 order: Vec<ServerId>,
412}
413
414impl ToolRouter {
415 pub fn from_configs<'a, I>(cfgs: I) -> Result<Self>
434 where
435 I: IntoIterator<Item = &'a LspServerConfig>,
436 {
437 let mut by_language: HashMap<LanguageId, LanguageRoutes> = HashMap::new();
438 let mut order: Vec<ServerId> = Vec::new();
439 let mut seen_ids: HashMap<ServerId, &LspServerConfig> = HashMap::new();
440
441 for cfg in cfgs {
442 let id = cfg.id();
443
444 if let Some(previous) = seen_ids.get(&id) {
445 return Err(ConfigError::DuplicateServerId {
446 id,
447 first: Box::new(EntrySummary::of(previous)),
448 second: Box::new(EntrySummary::of(cfg)),
449 }
450 .into());
451 }
452 seen_ids.insert(id.clone(), cfg);
453 order.push(id.clone());
454
455 let routes = by_language.entry(cfg.language_id.clone()).or_default();
456
457 match &cfg.handles {
458 None => {
459 if let Some(existing) = &routes.default {
460 return Err(ConfigError::TwoCatchAllServers {
461 language: cfg.language_id.clone(),
462 existing: existing.clone(),
463 id,
464 }
465 .into());
466 }
467 routes.default = Some(id);
468 }
469 Some(tools) => {
470 for tool in tools {
471 if let Some(existing) = routes.explicit.get(tool) {
472 return Err(ConfigError::ToolClaimedTwice {
473 tool: *tool,
474 language: cfg.language_id.clone(),
475 existing: existing.clone(),
476 id,
477 }
478 .into());
479 }
480 routes.explicit.insert(*tool, id.clone());
481 }
482 }
483 }
484 }
485
486 for (language, routes) in &by_language {
492 if routes.default.is_none() {
493 let uncovered: Vec<&str> = ToolKind::ALL
494 .iter()
495 .filter(|t| !routes.explicit.contains_key(t))
496 .map(ToolKind::as_str)
497 .collect();
498 if !uncovered.is_empty() {
499 tracing::warn!(
500 "language '{language}' has no catch-all server and does not claim: {}",
501 uncovered.join(", ")
502 );
503 }
504 }
505 }
506
507 Ok(Self { by_language, order })
508 }
509
510 #[must_use]
516 pub fn catch_all<I>(entries: I) -> Self
517 where
518 I: IntoIterator<Item = (ServerId, LanguageId)>,
519 {
520 let mut by_language: HashMap<LanguageId, LanguageRoutes> = HashMap::new();
521 let mut order = Vec::new();
522 for (id, language) in entries {
523 order.push(id.clone());
524 by_language.entry(language).or_default().default = Some(id);
525 }
526 Self { by_language, order }
527 }
528
529 pub fn rebind(&mut self, settlement: impl Fn(&ServerId) -> ServerSettlement) {
555 for routes in self.by_language.values_mut() {
556 let catch_all = routes.default.clone().map(|id| (settlement(&id), id));
557
558 let dead: Vec<ToolKind> = routes
559 .explicit
560 .iter()
561 .filter(|(_, id)| settlement(id) == ServerSettlement::Failed)
562 .map(|(tool, _)| *tool)
563 .collect();
564 for tool in dead {
565 match &catch_all {
566 Some((ServerSettlement::Registered, id)) => {
567 routes.explicit.insert(tool, id.clone());
568 }
569 Some((ServerSettlement::Pending, _)) => {}
570 Some((ServerSettlement::Failed, _)) | None => {
571 routes.explicit.remove(&tool);
572 }
573 }
574 }
575
576 if matches!(catch_all, Some((ServerSettlement::Failed, _))) {
577 routes.default = None;
578 }
579 }
580
581 self.order
582 .retain(|id| settlement(id) != ServerSettlement::Failed);
583 }
584
585 pub fn rebind_to_registered(&mut self, registered: &HashSet<ServerId>) {
588 self.rebind(|id| {
589 if registered.contains(id) {
590 ServerSettlement::Registered
591 } else {
592 ServerSettlement::Failed
593 }
594 });
595 }
596
597 #[must_use]
600 pub fn languages_routed_to(&self, id: &ServerId) -> Vec<LanguageId> {
601 let mut languages: Vec<LanguageId> = self
602 .by_language
603 .iter()
604 .filter(|(_, routes)| {
605 routes.default.as_ref() == Some(id) || routes.explicit.values().any(|v| v == id)
606 })
607 .map(|(language, _)| language.clone())
608 .collect();
609 languages.sort_unstable();
610 languages
611 }
612
613 #[must_use]
615 pub fn catch_all_for(&self, language_id: &str) -> Option<&ServerId> {
616 self.by_language.get(language_id)?.default.as_ref()
617 }
618
619 #[must_use]
624 pub fn resolve(&self, language_id: &str, tool: ToolKind) -> Option<&ServerId> {
625 let routes = self.by_language.get(language_id)?;
626 routes.explicit.get(&tool).or(routes.default.as_ref())
627 }
628
629 pub fn resolve_any(&self, tool: ToolKind) -> std::result::Result<&ServerId, NoServerReason> {
651 let claims_explicitly = |id: &ServerId| {
652 self.by_language
653 .values()
654 .any(|r| r.explicit.get(&tool) == Some(id))
655 };
656 let is_catch_all = |id: &ServerId| {
657 self.by_language
658 .values()
659 .any(|r| r.default.as_ref() == Some(id))
660 };
661
662 self.order
663 .iter()
664 .find(|id| claims_explicitly(id))
665 .or_else(|| self.order.iter().find(|id| is_catch_all(id)))
666 .ok_or(if self.order.is_empty() {
667 NoServerReason::NothingRegistered
668 } else {
669 NoServerReason::NoClaimant
670 })
671 }
672
673 #[must_use]
684 pub fn has_language(&self, language_id: &str) -> bool {
685 self.by_language
686 .get(language_id)
687 .is_some_and(|r| r.default.is_some() || !r.explicit.is_empty())
688 }
689
690 #[must_use]
707 pub fn configured_languages(&self) -> Vec<LanguageId> {
708 let mut languages: Vec<LanguageId> = self.by_language.keys().cloned().collect();
709 languages.sort_unstable();
710 languages
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use std::assert_matches;
717
718 use super::*;
719 use crate::config::{ServerCommand, TimeoutSecs};
720 use crate::error::Error;
721
722 #[test]
723 fn test_server_id_new_rejects_blank_and_deserialization_agrees() {
724 assert_eq!(ServerId::new(""), Err(InvalidServerId));
725 assert_eq!(ServerId::new(" \t"), Err(InvalidServerId));
726 assert_eq!("pyright".parse::<ServerId>().unwrap().as_str(), "pyright");
727 assert!(serde_json::from_str::<ServerId>("\" \"").is_err());
728 assert_eq!(
729 serde_json::from_str::<ServerId>("\"pylsp\"").unwrap(),
730 ServerId::from_static("pylsp")
731 );
732 }
733
734 #[test]
735 #[should_panic(expected = "server id must be ASCII and not blank")]
736 fn test_server_id_from_static_panics_on_blank() {
737 let _ = ServerId::from_static(" ");
738 }
739
740 #[test]
741 fn test_server_id_static_and_owned_compare_and_serialize_alike() {
742 let owned = ServerId::new("rust".to_owned()).unwrap();
743 assert_eq!(owned, ServerId::from_static("rust"));
744 assert_eq!(owned, "rust");
745 assert_eq!(serde_json::to_string(&owned).unwrap(), "\"rust\"");
746 assert_eq!(String::from(owned), "rust");
747 }
748
749 #[test]
750 fn test_tool_set_rejects_empty_and_duplicates() {
751 assert_eq!(ToolSet::new(vec![]), Err(InvalidToolSet::Empty));
752 assert_eq!(
753 ToolSet::new(vec![ToolKind::Hover, ToolKind::Rename, ToolKind::Hover]),
754 Err(InvalidToolSet::Duplicate(ToolKind::Hover))
755 );
756 }
757
758 #[test]
759 fn test_tool_set_preserves_order_and_round_trips() {
760 let set = ToolSet::new(vec![ToolKind::Rename, ToolKind::Hover]).unwrap();
761 assert_eq!(
762 set.iter().copied().collect::<Vec<_>>(),
763 [ToolKind::Rename, ToolKind::Hover]
764 );
765 assert!(set.contains(ToolKind::Hover));
766 assert!(!set.contains(ToolKind::Definition));
767 let json = serde_json::to_string(&set).unwrap();
768 assert_eq!(json, r#"["rename","hover"]"#);
769 assert_eq!(serde_json::from_str::<ToolSet>(&json).unwrap(), set);
770 assert!(serde_json::from_str::<ToolSet>("[]").is_err());
771 }
772
773 fn cfg(
774 language_id: &str,
775 name: Option<&str>,
776 handles: Option<Vec<ToolKind>>,
777 ) -> LspServerConfig {
778 LspServerConfig {
779 language_id: LanguageId::new(language_id).unwrap(),
780 command: ServerCommand::from_static("cmd").into(),
781 args: crate::config::ServerArgs::default(),
782 env: crate::config::ServerEnv::default(),
783 file_patterns: vec![],
784 initialization_options: None,
785 settings: None,
786 timeout_seconds: TimeoutSecs::new(30).unwrap(),
787 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
788 heuristics: None,
789 name: name.map(|id| ServerId::new(id).unwrap()),
790 handles: handles.map(|tools| ToolSet::new(tools).unwrap()),
791 indexing: crate::bridge::IndexingPolicy::Auto,
792 }
793 }
794
795 #[test]
796 fn test_resolve_explicit_wins_over_catch_all() {
797 let configs = vec![
798 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
799 cfg("python", Some("pylsp"), None),
800 ];
801 let router = ToolRouter::from_configs(&configs).unwrap();
802 assert_eq!(
803 router.resolve("python", ToolKind::Hover),
804 Some(&ServerId::from_static("pyright"))
805 );
806 assert_eq!(
807 router.resolve("python", ToolKind::Diagnostics),
808 Some(&ServerId::from_static("pylsp"))
809 );
810 }
811
812 #[test]
813 fn test_resolve_no_catch_all_unclaimed_is_none() {
814 let configs = vec![cfg("python", Some("pyright"), Some(vec![ToolKind::Hover]))];
815 let router = ToolRouter::from_configs(&configs).unwrap();
816 assert_eq!(router.resolve("python", ToolKind::Diagnostics), None);
817 }
818
819 #[test]
820 fn test_resolve_any_explicit_claimer_beats_catch_all_declared_first() {
821 let configs = vec![
822 cfg("python", Some("python-narrow"), Some(vec![ToolKind::Hover])),
823 cfg("rust", Some("rust-catch-all"), None),
824 ];
825 let router = ToolRouter::from_configs(&configs).unwrap();
826 assert_eq!(
830 router.resolve_any(ToolKind::WorkspaceSymbols),
831 Ok(&ServerId::from_static("rust-catch-all"))
832 );
833 }
834
835 #[test]
836 fn test_resolve_any_prefers_explicit_claimer_over_catch_all() {
837 let configs = vec![
838 cfg("rust", Some("rust-catch-all"), None),
839 cfg(
840 "python",
841 Some("python-explicit"),
842 Some(vec![ToolKind::WorkspaceSymbols]),
843 ),
844 ];
845 let router = ToolRouter::from_configs(&configs).unwrap();
846 assert_eq!(
847 router.resolve_any(ToolKind::WorkspaceSymbols),
848 Ok(&ServerId::from_static("python-explicit"))
849 );
850 }
851
852 #[test]
853 fn test_from_configs_rejects_duplicate_server_id_across_languages() {
854 let configs = vec![
855 cfg("python", None, None),
856 cfg("typescript", Some("python"), None),
857 ];
858 let err = ToolRouter::from_configs(&configs).unwrap_err();
859 assert_matches!(err, Error::Config(ConfigError::DuplicateServerId { .. }));
860 }
861
862 #[test]
863 fn test_from_configs_duplicate_server_id_error_distinguishes_entries() {
864 let configs = vec![
870 LspServerConfig {
871 language_id: LanguageId::from_static("rust"),
872 command: ServerCommand::from_static("rust-analyzer").into(),
873 args: crate::config::ServerArgs::default(),
874 env: crate::config::ServerEnv::default(),
875 file_patterns: vec![],
876 initialization_options: None,
877 settings: None,
878 timeout_seconds: TimeoutSecs::new(30).unwrap(),
879 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
880 heuristics: None,
881 name: None,
882 handles: None,
883 indexing: crate::bridge::IndexingPolicy::Auto,
884 },
885 LspServerConfig {
886 language_id: LanguageId::from_static("rust"),
887 command: ServerCommand::from_static("rust-analyzer").into(),
888 args: crate::config::ServerArgs::try_from(vec![
889 "--dummy-second-instance".to_string(),
890 ])
891 .unwrap(),
892 env: crate::config::ServerEnv::default(),
893 file_patterns: vec![],
894 initialization_options: None,
895 settings: None,
896 timeout_seconds: TimeoutSecs::new(30).unwrap(),
897 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
898 heuristics: None,
899 name: None,
900 handles: None,
901 indexing: crate::bridge::IndexingPolicy::Auto,
902 },
903 ];
904 let err = ToolRouter::from_configs(&configs).unwrap_err();
905 let Error::Config(config_err) = err else {
906 panic!("expected Config, got {err:?}");
907 };
908 let msg = config_err.to_string();
909 assert!(!msg.contains("entry #"), "message was: {msg}");
913 assert!(msg.contains("rust-analyzer"), "message was: {msg}");
914 assert!(msg.contains(", 1 arg"), "message was: {msg}");
915 assert!(!msg.contains("1 args"), "message was: {msg}");
916 assert!(
917 !msg.contains("--dummy-second-instance"),
918 "message was: {msg}"
919 );
920 }
921
922 #[test]
923 fn test_entry_summary_pluralises_the_argument_count() {
924 let summary = |arg_count| EntrySummary {
925 language: LanguageId::from_static("rust"),
926 command: ServerCommand::from_static("ra").into(),
927 arg_count,
928 };
929 assert_eq!(summary(0).to_string(), "language 'rust', command 'ra'");
930 assert_eq!(
931 summary(1).to_string(),
932 "language 'rust', command 'ra', 1 arg"
933 );
934 assert_eq!(
935 summary(2).to_string(),
936 "language 'rust', command 'ra', 2 args"
937 );
938 }
939
940 #[test]
941 fn test_from_configs_duplicate_server_id_error_never_prints_arguments() {
942 let secret_args = |args: &[&str]| {
943 crate::config::ServerArgs::try_from(
944 args.iter()
945 .map(|arg| (*arg).to_string())
946 .collect::<Vec<_>>(),
947 )
948 .unwrap()
949 };
950 let mut first = cfg("rust", None, None);
951 first.args = secret_args(&["--stdio"]);
952 let mut second = cfg("rust", None, None);
953 second.args = secret_args(&["--api-key=SECRET"]);
954 let err = ToolRouter::from_configs(&[first, second]).unwrap_err();
955 for rendered in [
956 format!("{err}"),
957 format!("{err:?}"),
958 crate::error::ErrorChain(&err).to_string(),
959 ] {
960 assert!(!rendered.contains("SECRET"), "leaked: {rendered}");
961 }
962 }
963
964 #[test]
965 fn test_from_configs_duplicate_server_id_error_identical_entries_still_reports() {
966 let configs = vec![cfg("rust", None, None), cfg("rust", None, None)];
971 let err = ToolRouter::from_configs(&configs).unwrap_err();
972 let Error::Config(config_err) = err else {
973 panic!("expected Config, got {err:?}");
974 };
975 let msg = config_err.to_string();
976 assert!(!msg.contains("entry #"), "message was: {msg}");
977 assert!(
978 msg.contains("duplicate server id 'rust'"),
979 "message was: {msg}"
980 );
981 }
982
983 #[test]
984 fn test_from_configs_rejects_two_catch_alls() {
985 let configs = vec![
986 cfg("python", Some("a"), None),
987 cfg("python", Some("b"), None),
988 ];
989 let err = ToolRouter::from_configs(&configs).unwrap_err();
990 assert_matches!(err, Error::Config(ConfigError::TwoCatchAllServers { .. }));
991 }
992
993 #[test]
994 fn test_from_configs_rejects_duplicate_tool_claim() {
995 let configs = vec![
996 cfg("python", Some("a"), Some(vec![ToolKind::Hover])),
997 cfg("python", Some("b"), Some(vec![ToolKind::Hover])),
998 ];
999 let err = ToolRouter::from_configs(&configs).unwrap_err();
1000 assert_matches!(err, Error::Config(ConfigError::ToolClaimedTwice { .. }));
1001 }
1002
1003 #[test]
1004 fn test_rebind_to_registered_dead_server_with_live_catch_all() {
1005 let configs = vec![
1006 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
1007 cfg("python", Some("pylsp"), None),
1008 ];
1009 let mut router = ToolRouter::from_configs(&configs).unwrap();
1010 let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("pylsp")]);
1011 router.rebind_to_registered(®istered);
1012
1013 assert_eq!(
1014 router.resolve("python", ToolKind::Hover),
1015 Some(&ServerId::from_static("pylsp"))
1016 );
1017 }
1018
1019 #[test]
1020 fn test_rebind_to_registered_dead_server_no_catch_all_drops_route() {
1021 let configs = vec![
1022 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
1023 cfg("python", Some("pylsp"), Some(vec![ToolKind::Diagnostics])),
1024 ];
1025 let mut router = ToolRouter::from_configs(&configs).unwrap();
1026 let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("pylsp")]);
1027 router.rebind_to_registered(®istered);
1028
1029 assert_eq!(router.resolve("python", ToolKind::Hover), None);
1032 assert_eq!(
1033 router.resolve("python", ToolKind::Diagnostics),
1034 Some(&ServerId::from_static("pylsp"))
1035 );
1036 }
1037
1038 #[test]
1039 fn test_rebind_to_registered_all_failed_drops_everything() {
1040 let configs = vec![cfg("rust", None, None)];
1041 let mut router = ToolRouter::from_configs(&configs).unwrap();
1042 router.rebind_to_registered(&HashSet::new());
1043 assert_eq!(router.resolve("rust", ToolKind::Hover), None);
1044 assert_eq!(
1045 router.resolve_any(ToolKind::Hover),
1046 Err(NoServerReason::NothingRegistered)
1047 );
1048 assert!(!router.has_language("rust"));
1052 }
1053
1054 fn explicit_and_catch_all_router() -> ToolRouter {
1055 ToolRouter::from_configs(&[
1056 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
1057 cfg("python", Some("pylsp"), None),
1058 ])
1059 .unwrap()
1060 }
1061
1062 #[test]
1063 fn rebind_keeps_pending_routes_and_order() {
1064 let mut router = ToolRouter::from_configs(&[
1065 cfg("rust", Some("slow"), None),
1066 cfg("python", Some("fast"), None),
1067 ])
1068 .unwrap();
1069
1070 router.rebind(|id| {
1071 if id.as_str() == "fast" {
1072 ServerSettlement::Registered
1073 } else {
1074 ServerSettlement::Pending
1075 }
1076 });
1077
1078 assert_eq!(
1079 router.resolve("rust", ToolKind::Hover),
1080 Some(&ServerId::from_static("slow"))
1081 );
1082 assert_eq!(
1083 router.resolve_any(ToolKind::Hover),
1084 Ok(&ServerId::from_static("slow"))
1085 );
1086 }
1087
1088 #[test]
1089 fn dead_explicit_route_waits_for_pending_catch_all_and_drops_when_it_fails() {
1090 let settle = |pyright, pylsp| {
1091 let mut router = explicit_and_catch_all_router();
1092 router.rebind(|id| {
1093 if id.as_str() == "pyright" {
1094 pyright
1095 } else {
1096 pylsp
1097 }
1098 });
1099 router.resolve("python", ToolKind::Hover).cloned()
1100 };
1101
1102 let pending_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Pending);
1103 assert_eq!(pending_catch_all, Some(ServerId::from_static("pyright")));
1104 let registered_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Registered);
1105 assert_eq!(registered_catch_all, Some(ServerId::from_static("pylsp")));
1106 let failed_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Failed);
1107 assert_eq!(failed_catch_all, None);
1108 }
1109
1110 #[test]
1111 fn rebind_without_pending_servers_matches_the_batch_rule_for_every_outcome() {
1112 for pyright_up in [false, true] {
1113 for pylsp_up in [false, true] {
1114 let registered: HashSet<ServerId> = [("pyright", pyright_up), ("pylsp", pylsp_up)]
1115 .into_iter()
1116 .filter(|(_, up)| *up)
1117 .map(|(id, _)| ServerId::new(id).unwrap())
1118 .collect();
1119 let mut router = explicit_and_catch_all_router();
1120 router.rebind_to_registered(®istered);
1121
1122 let hover = if pyright_up {
1123 Some("pyright")
1124 } else if pylsp_up {
1125 Some("pylsp")
1126 } else {
1127 None
1128 };
1129 assert_eq!(
1130 router
1131 .resolve("python", ToolKind::Hover)
1132 .map(ServerId::as_str),
1133 hover,
1134 "pyright up: {pyright_up}, pylsp up: {pylsp_up}"
1135 );
1136 assert_eq!(
1137 router
1138 .resolve("python", ToolKind::Diagnostics)
1139 .map(ServerId::as_str),
1140 pylsp_up.then_some("pylsp")
1141 );
1142 }
1143 }
1144 }
1145
1146 #[test]
1147 fn test_rebind_prunes_order_for_resolve_any() {
1148 let configs = vec![cfg("rust", Some("a"), None), cfg("python", Some("b"), None)];
1149 let mut router = ToolRouter::from_configs(&configs).unwrap();
1150 let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("b")]);
1151 router.rebind_to_registered(®istered);
1152 assert_eq!(
1153 router.resolve_any(ToolKind::Hover),
1154 Ok(&ServerId::from_static("b"))
1155 );
1156 }
1157
1158 #[test]
1159 fn test_resolve_any_no_claimant_does_not_fall_back_to_arbitrary_server() {
1160 let configs = vec![cfg("python", Some("pyright"), Some(vec![ToolKind::Hover]))];
1164 let router = ToolRouter::from_configs(&configs).unwrap();
1165 assert_eq!(
1166 router.resolve_any(ToolKind::WorkspaceSymbols),
1167 Err(NoServerReason::NoClaimant)
1168 );
1169 }
1170
1171 #[test]
1172 fn test_has_language() {
1173 let configs = vec![cfg("rust", None, None)];
1174 let router = ToolRouter::from_configs(&configs).unwrap();
1175 assert!(router.has_language("rust"));
1176 assert!(!router.has_language("python"));
1177 }
1178
1179 #[test]
1180 fn test_catch_all_helper_registers_two_entries() {
1181 let router = ToolRouter::catch_all([
1182 (
1183 ServerId::from_static("ts"),
1184 LanguageId::from_static("typescript"),
1185 ),
1186 (
1187 ServerId::from_static("tsx"),
1188 LanguageId::from_static("typescriptreact"),
1189 ),
1190 ]);
1191 assert_eq!(
1192 router.resolve("typescript", ToolKind::Hover),
1193 Some(&ServerId::from_static("ts"))
1194 );
1195 assert_eq!(
1196 router.resolve("typescriptreact", ToolKind::Hover),
1197 Some(&ServerId::from_static("tsx"))
1198 );
1199 }
1200
1201 #[test]
1202 fn test_tool_kind_as_str_and_all_len() {
1203 assert_eq!(ToolKind::Hover.as_str(), "hover");
1204 assert_eq!(ToolKind::CallHierarchy.as_str(), "call_hierarchy");
1205 assert_eq!(ToolKind::ALL.len(), 21);
1206
1207 let unique_names: std::collections::HashSet<&str> =
1211 ToolKind::ALL.iter().map(ToolKind::as_str).collect();
1212 assert_eq!(unique_names.len(), ToolKind::ALL.len());
1213 }
1214
1215 #[test]
1216 fn test_server_id_display_and_as_str() {
1217 let id = ServerId::from_static("pyright");
1218 assert_eq!(id.as_str(), "pyright");
1219 assert_eq!(id.to_string(), "pyright");
1220 }
1221}