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 args: Vec<String>,
320}
321
322impl EntrySummary {
323 fn of(cfg: &LspServerConfig) -> Self {
324 Self {
325 language: cfg.language_id.clone(),
326 command: cfg.command.clone(),
327 args: cfg.args.clone(),
328 }
329 }
330}
331
332impl fmt::Display for EntrySummary {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 write!(
335 f,
336 "language '{}', command '{}'",
337 self.language, self.command
338 )?;
339 if !self.args.is_empty() {
340 write!(f, ", args {:?}", self.args)?;
341 }
342 Ok(())
343 }
344}
345
346#[derive(Debug, Default, Clone)]
348struct LanguageRoutes {
349 explicit: HashMap<ToolKind, ServerId>,
351 default: Option<ServerId>,
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359#[non_exhaustive]
360pub enum NoServerReason {
361 NothingRegistered,
369 NoClaimant,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum ServerSettlement {
388 Pending,
390 Registered,
392 Failed,
394}
395
396#[derive(Debug, Default, Clone)]
403pub struct ToolRouter {
404 by_language: HashMap<LanguageId, LanguageRoutes>,
405 order: Vec<ServerId>,
408}
409
410impl ToolRouter {
411 pub fn from_configs<'a, I>(cfgs: I) -> Result<Self>
430 where
431 I: IntoIterator<Item = &'a LspServerConfig>,
432 {
433 let mut by_language: HashMap<LanguageId, LanguageRoutes> = HashMap::new();
434 let mut order: Vec<ServerId> = Vec::new();
435 let mut seen_ids: HashMap<ServerId, &LspServerConfig> = HashMap::new();
436
437 for cfg in cfgs {
438 let id = cfg.id();
439
440 if let Some(previous) = seen_ids.get(&id) {
441 return Err(ConfigError::DuplicateServerId {
442 id,
443 first: Box::new(EntrySummary::of(previous)),
444 second: Box::new(EntrySummary::of(cfg)),
445 }
446 .into());
447 }
448 seen_ids.insert(id.clone(), cfg);
449 order.push(id.clone());
450
451 let routes = by_language.entry(cfg.language_id.clone()).or_default();
452
453 match &cfg.handles {
454 None => {
455 if let Some(existing) = &routes.default {
456 return Err(ConfigError::TwoCatchAllServers {
457 language: cfg.language_id.clone(),
458 existing: existing.clone(),
459 id,
460 }
461 .into());
462 }
463 routes.default = Some(id);
464 }
465 Some(tools) => {
466 for tool in tools {
467 if let Some(existing) = routes.explicit.get(tool) {
468 return Err(ConfigError::ToolClaimedTwice {
469 tool: *tool,
470 language: cfg.language_id.clone(),
471 existing: existing.clone(),
472 id,
473 }
474 .into());
475 }
476 routes.explicit.insert(*tool, id.clone());
477 }
478 }
479 }
480 }
481
482 for (language, routes) in &by_language {
488 if routes.default.is_none() {
489 let uncovered: Vec<&str> = ToolKind::ALL
490 .iter()
491 .filter(|t| !routes.explicit.contains_key(t))
492 .map(ToolKind::as_str)
493 .collect();
494 if !uncovered.is_empty() {
495 tracing::warn!(
496 "language '{language}' has no catch-all server and does not claim: {}",
497 uncovered.join(", ")
498 );
499 }
500 }
501 }
502
503 Ok(Self { by_language, order })
504 }
505
506 #[must_use]
512 pub fn catch_all<I>(entries: I) -> Self
513 where
514 I: IntoIterator<Item = (ServerId, LanguageId)>,
515 {
516 let mut by_language: HashMap<LanguageId, LanguageRoutes> = HashMap::new();
517 let mut order = Vec::new();
518 for (id, language) in entries {
519 order.push(id.clone());
520 by_language.entry(language).or_default().default = Some(id);
521 }
522 Self { by_language, order }
523 }
524
525 pub fn rebind(&mut self, settlement: impl Fn(&ServerId) -> ServerSettlement) {
551 for routes in self.by_language.values_mut() {
552 let catch_all = routes.default.clone().map(|id| (settlement(&id), id));
553
554 let dead: Vec<ToolKind> = routes
555 .explicit
556 .iter()
557 .filter(|(_, id)| settlement(id) == ServerSettlement::Failed)
558 .map(|(tool, _)| *tool)
559 .collect();
560 for tool in dead {
561 match &catch_all {
562 Some((ServerSettlement::Registered, id)) => {
563 routes.explicit.insert(tool, id.clone());
564 }
565 Some((ServerSettlement::Pending, _)) => {}
566 Some((ServerSettlement::Failed, _)) | None => {
567 routes.explicit.remove(&tool);
568 }
569 }
570 }
571
572 if matches!(catch_all, Some((ServerSettlement::Failed, _))) {
573 routes.default = None;
574 }
575 }
576
577 self.order
578 .retain(|id| settlement(id) != ServerSettlement::Failed);
579 }
580
581 pub fn rebind_to_registered(&mut self, registered: &HashSet<ServerId>) {
584 self.rebind(|id| {
585 if registered.contains(id) {
586 ServerSettlement::Registered
587 } else {
588 ServerSettlement::Failed
589 }
590 });
591 }
592
593 #[must_use]
596 pub fn languages_routed_to(&self, id: &ServerId) -> Vec<LanguageId> {
597 let mut languages: Vec<LanguageId> = self
598 .by_language
599 .iter()
600 .filter(|(_, routes)| {
601 routes.default.as_ref() == Some(id) || routes.explicit.values().any(|v| v == id)
602 })
603 .map(|(language, _)| language.clone())
604 .collect();
605 languages.sort_unstable();
606 languages
607 }
608
609 #[must_use]
611 pub fn catch_all_for(&self, language_id: &str) -> Option<&ServerId> {
612 self.by_language.get(language_id)?.default.as_ref()
613 }
614
615 #[must_use]
620 pub fn resolve(&self, language_id: &str, tool: ToolKind) -> Option<&ServerId> {
621 let routes = self.by_language.get(language_id)?;
622 routes.explicit.get(&tool).or(routes.default.as_ref())
623 }
624
625 pub fn resolve_any(&self, tool: ToolKind) -> std::result::Result<&ServerId, NoServerReason> {
647 let claims_explicitly = |id: &ServerId| {
648 self.by_language
649 .values()
650 .any(|r| r.explicit.get(&tool) == Some(id))
651 };
652 let is_catch_all = |id: &ServerId| {
653 self.by_language
654 .values()
655 .any(|r| r.default.as_ref() == Some(id))
656 };
657
658 self.order
659 .iter()
660 .find(|id| claims_explicitly(id))
661 .or_else(|| self.order.iter().find(|id| is_catch_all(id)))
662 .ok_or(if self.order.is_empty() {
663 NoServerReason::NothingRegistered
664 } else {
665 NoServerReason::NoClaimant
666 })
667 }
668
669 #[must_use]
680 pub fn has_language(&self, language_id: &str) -> bool {
681 self.by_language
682 .get(language_id)
683 .is_some_and(|r| r.default.is_some() || !r.explicit.is_empty())
684 }
685
686 #[must_use]
703 pub fn configured_languages(&self) -> Vec<LanguageId> {
704 let mut languages: Vec<LanguageId> = self.by_language.keys().cloned().collect();
705 languages.sort_unstable();
706 languages
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use std::assert_matches;
713
714 use super::*;
715 use crate::config::{ServerCommand, TimeoutSecs};
716 use crate::error::Error;
717
718 #[test]
719 fn test_server_id_new_rejects_blank_and_deserialization_agrees() {
720 assert_eq!(ServerId::new(""), Err(InvalidServerId));
721 assert_eq!(ServerId::new(" \t"), Err(InvalidServerId));
722 assert_eq!("pyright".parse::<ServerId>().unwrap().as_str(), "pyright");
723 assert!(serde_json::from_str::<ServerId>("\" \"").is_err());
724 assert_eq!(
725 serde_json::from_str::<ServerId>("\"pylsp\"").unwrap(),
726 ServerId::from_static("pylsp")
727 );
728 }
729
730 #[test]
731 #[should_panic(expected = "server id must be ASCII and not blank")]
732 fn test_server_id_from_static_panics_on_blank() {
733 let _ = ServerId::from_static(" ");
734 }
735
736 #[test]
737 fn test_server_id_static_and_owned_compare_and_serialize_alike() {
738 let owned = ServerId::new("rust".to_owned()).unwrap();
739 assert_eq!(owned, ServerId::from_static("rust"));
740 assert_eq!(owned, "rust");
741 assert_eq!(serde_json::to_string(&owned).unwrap(), "\"rust\"");
742 assert_eq!(String::from(owned), "rust");
743 }
744
745 #[test]
746 fn test_tool_set_rejects_empty_and_duplicates() {
747 assert_eq!(ToolSet::new(vec![]), Err(InvalidToolSet::Empty));
748 assert_eq!(
749 ToolSet::new(vec![ToolKind::Hover, ToolKind::Rename, ToolKind::Hover]),
750 Err(InvalidToolSet::Duplicate(ToolKind::Hover))
751 );
752 }
753
754 #[test]
755 fn test_tool_set_preserves_order_and_round_trips() {
756 let set = ToolSet::new(vec![ToolKind::Rename, ToolKind::Hover]).unwrap();
757 assert_eq!(
758 set.iter().copied().collect::<Vec<_>>(),
759 [ToolKind::Rename, ToolKind::Hover]
760 );
761 assert!(set.contains(ToolKind::Hover));
762 assert!(!set.contains(ToolKind::Definition));
763 let json = serde_json::to_string(&set).unwrap();
764 assert_eq!(json, r#"["rename","hover"]"#);
765 assert_eq!(serde_json::from_str::<ToolSet>(&json).unwrap(), set);
766 assert!(serde_json::from_str::<ToolSet>("[]").is_err());
767 }
768
769 fn cfg(
770 language_id: &str,
771 name: Option<&str>,
772 handles: Option<Vec<ToolKind>>,
773 ) -> LspServerConfig {
774 LspServerConfig {
775 language_id: LanguageId::new(language_id).unwrap(),
776 command: ServerCommand::from_static("cmd").into(),
777 args: vec![],
778 env: crate::config::ServerEnv::default(),
779 file_patterns: vec![],
780 initialization_options: None,
781 settings: None,
782 timeout_seconds: TimeoutSecs::new(30).unwrap(),
783 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
784 heuristics: None,
785 name: name.map(|id| ServerId::new(id).unwrap()),
786 handles: handles.map(|tools| ToolSet::new(tools).unwrap()),
787 indexing: crate::bridge::IndexingPolicy::Auto,
788 }
789 }
790
791 #[test]
792 fn test_resolve_explicit_wins_over_catch_all() {
793 let configs = vec![
794 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
795 cfg("python", Some("pylsp"), None),
796 ];
797 let router = ToolRouter::from_configs(&configs).unwrap();
798 assert_eq!(
799 router.resolve("python", ToolKind::Hover),
800 Some(&ServerId::from_static("pyright"))
801 );
802 assert_eq!(
803 router.resolve("python", ToolKind::Diagnostics),
804 Some(&ServerId::from_static("pylsp"))
805 );
806 }
807
808 #[test]
809 fn test_resolve_no_catch_all_unclaimed_is_none() {
810 let configs = vec![cfg("python", Some("pyright"), Some(vec![ToolKind::Hover]))];
811 let router = ToolRouter::from_configs(&configs).unwrap();
812 assert_eq!(router.resolve("python", ToolKind::Diagnostics), None);
813 }
814
815 #[test]
816 fn test_resolve_any_explicit_claimer_beats_catch_all_declared_first() {
817 let configs = vec![
818 cfg("python", Some("python-narrow"), Some(vec![ToolKind::Hover])),
819 cfg("rust", Some("rust-catch-all"), None),
820 ];
821 let router = ToolRouter::from_configs(&configs).unwrap();
822 assert_eq!(
826 router.resolve_any(ToolKind::WorkspaceSymbols),
827 Ok(&ServerId::from_static("rust-catch-all"))
828 );
829 }
830
831 #[test]
832 fn test_resolve_any_prefers_explicit_claimer_over_catch_all() {
833 let configs = vec![
834 cfg("rust", Some("rust-catch-all"), None),
835 cfg(
836 "python",
837 Some("python-explicit"),
838 Some(vec![ToolKind::WorkspaceSymbols]),
839 ),
840 ];
841 let router = ToolRouter::from_configs(&configs).unwrap();
842 assert_eq!(
843 router.resolve_any(ToolKind::WorkspaceSymbols),
844 Ok(&ServerId::from_static("python-explicit"))
845 );
846 }
847
848 #[test]
849 fn test_from_configs_rejects_duplicate_server_id_across_languages() {
850 let configs = vec![
851 cfg("python", None, None),
852 cfg("typescript", Some("python"), None),
853 ];
854 let err = ToolRouter::from_configs(&configs).unwrap_err();
855 assert_matches!(err, Error::Config(ConfigError::DuplicateServerId { .. }));
856 }
857
858 #[test]
859 fn test_from_configs_duplicate_server_id_error_distinguishes_entries() {
860 let configs = vec![
866 LspServerConfig {
867 language_id: LanguageId::from_static("rust"),
868 command: ServerCommand::from_static("rust-analyzer").into(),
869 args: vec![],
870 env: crate::config::ServerEnv::default(),
871 file_patterns: vec![],
872 initialization_options: None,
873 settings: None,
874 timeout_seconds: TimeoutSecs::new(30).unwrap(),
875 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
876 heuristics: None,
877 name: None,
878 handles: None,
879 indexing: crate::bridge::IndexingPolicy::Auto,
880 },
881 LspServerConfig {
882 language_id: LanguageId::from_static("rust"),
883 command: ServerCommand::from_static("rust-analyzer").into(),
884 args: vec!["--dummy-second-instance".to_string()],
885 env: crate::config::ServerEnv::default(),
886 file_patterns: vec![],
887 initialization_options: None,
888 settings: None,
889 timeout_seconds: TimeoutSecs::new(30).unwrap(),
890 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
891 heuristics: None,
892 name: None,
893 handles: None,
894 indexing: crate::bridge::IndexingPolicy::Auto,
895 },
896 ];
897 let err = ToolRouter::from_configs(&configs).unwrap_err();
898 let Error::Config(config_err) = err else {
899 panic!("expected Config, got {err:?}");
900 };
901 let msg = config_err.to_string();
902 assert!(!msg.contains("entry #"), "message was: {msg}");
906 assert!(msg.contains("rust-analyzer"), "message was: {msg}");
907 assert!(
908 msg.contains("--dummy-second-instance"),
909 "message was: {msg}"
910 );
911 }
912
913 #[test]
914 fn test_from_configs_duplicate_server_id_error_identical_entries_still_reports() {
915 let configs = vec![cfg("rust", None, None), cfg("rust", None, None)];
920 let err = ToolRouter::from_configs(&configs).unwrap_err();
921 let Error::Config(config_err) = err else {
922 panic!("expected Config, got {err:?}");
923 };
924 let msg = config_err.to_string();
925 assert!(!msg.contains("entry #"), "message was: {msg}");
926 assert!(
927 msg.contains("duplicate server id 'rust'"),
928 "message was: {msg}"
929 );
930 }
931
932 #[test]
933 fn test_from_configs_rejects_two_catch_alls() {
934 let configs = vec![
935 cfg("python", Some("a"), None),
936 cfg("python", Some("b"), None),
937 ];
938 let err = ToolRouter::from_configs(&configs).unwrap_err();
939 assert_matches!(err, Error::Config(ConfigError::TwoCatchAllServers { .. }));
940 }
941
942 #[test]
943 fn test_from_configs_rejects_duplicate_tool_claim() {
944 let configs = vec![
945 cfg("python", Some("a"), Some(vec![ToolKind::Hover])),
946 cfg("python", Some("b"), Some(vec![ToolKind::Hover])),
947 ];
948 let err = ToolRouter::from_configs(&configs).unwrap_err();
949 assert_matches!(err, Error::Config(ConfigError::ToolClaimedTwice { .. }));
950 }
951
952 #[test]
953 fn test_rebind_to_registered_dead_server_with_live_catch_all() {
954 let configs = vec![
955 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
956 cfg("python", Some("pylsp"), None),
957 ];
958 let mut router = ToolRouter::from_configs(&configs).unwrap();
959 let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("pylsp")]);
960 router.rebind_to_registered(®istered);
961
962 assert_eq!(
963 router.resolve("python", ToolKind::Hover),
964 Some(&ServerId::from_static("pylsp"))
965 );
966 }
967
968 #[test]
969 fn test_rebind_to_registered_dead_server_no_catch_all_drops_route() {
970 let configs = vec![
971 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
972 cfg("python", Some("pylsp"), Some(vec![ToolKind::Diagnostics])),
973 ];
974 let mut router = ToolRouter::from_configs(&configs).unwrap();
975 let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("pylsp")]);
976 router.rebind_to_registered(®istered);
977
978 assert_eq!(router.resolve("python", ToolKind::Hover), None);
981 assert_eq!(
982 router.resolve("python", ToolKind::Diagnostics),
983 Some(&ServerId::from_static("pylsp"))
984 );
985 }
986
987 #[test]
988 fn test_rebind_to_registered_all_failed_drops_everything() {
989 let configs = vec![cfg("rust", None, None)];
990 let mut router = ToolRouter::from_configs(&configs).unwrap();
991 router.rebind_to_registered(&HashSet::new());
992 assert_eq!(router.resolve("rust", ToolKind::Hover), None);
993 assert_eq!(
994 router.resolve_any(ToolKind::Hover),
995 Err(NoServerReason::NothingRegistered)
996 );
997 assert!(!router.has_language("rust"));
1001 }
1002
1003 fn explicit_and_catch_all_router() -> ToolRouter {
1004 ToolRouter::from_configs(&[
1005 cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
1006 cfg("python", Some("pylsp"), None),
1007 ])
1008 .unwrap()
1009 }
1010
1011 #[test]
1012 fn rebind_keeps_pending_routes_and_order() {
1013 let mut router = ToolRouter::from_configs(&[
1014 cfg("rust", Some("slow"), None),
1015 cfg("python", Some("fast"), None),
1016 ])
1017 .unwrap();
1018
1019 router.rebind(|id| {
1020 if id.as_str() == "fast" {
1021 ServerSettlement::Registered
1022 } else {
1023 ServerSettlement::Pending
1024 }
1025 });
1026
1027 assert_eq!(
1028 router.resolve("rust", ToolKind::Hover),
1029 Some(&ServerId::from_static("slow"))
1030 );
1031 assert_eq!(
1032 router.resolve_any(ToolKind::Hover),
1033 Ok(&ServerId::from_static("slow"))
1034 );
1035 }
1036
1037 #[test]
1038 fn dead_explicit_route_waits_for_pending_catch_all_and_drops_when_it_fails() {
1039 let settle = |pyright, pylsp| {
1040 let mut router = explicit_and_catch_all_router();
1041 router.rebind(|id| {
1042 if id.as_str() == "pyright" {
1043 pyright
1044 } else {
1045 pylsp
1046 }
1047 });
1048 router.resolve("python", ToolKind::Hover).cloned()
1049 };
1050
1051 let pending_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Pending);
1052 assert_eq!(pending_catch_all, Some(ServerId::from_static("pyright")));
1053 let registered_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Registered);
1054 assert_eq!(registered_catch_all, Some(ServerId::from_static("pylsp")));
1055 let failed_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Failed);
1056 assert_eq!(failed_catch_all, None);
1057 }
1058
1059 #[test]
1060 fn rebind_without_pending_servers_matches_the_batch_rule_for_every_outcome() {
1061 for pyright_up in [false, true] {
1062 for pylsp_up in [false, true] {
1063 let registered: HashSet<ServerId> = [("pyright", pyright_up), ("pylsp", pylsp_up)]
1064 .into_iter()
1065 .filter(|(_, up)| *up)
1066 .map(|(id, _)| ServerId::new(id).unwrap())
1067 .collect();
1068 let mut router = explicit_and_catch_all_router();
1069 router.rebind_to_registered(®istered);
1070
1071 let hover = if pyright_up {
1072 Some("pyright")
1073 } else if pylsp_up {
1074 Some("pylsp")
1075 } else {
1076 None
1077 };
1078 assert_eq!(
1079 router
1080 .resolve("python", ToolKind::Hover)
1081 .map(ServerId::as_str),
1082 hover,
1083 "pyright up: {pyright_up}, pylsp up: {pylsp_up}"
1084 );
1085 assert_eq!(
1086 router
1087 .resolve("python", ToolKind::Diagnostics)
1088 .map(ServerId::as_str),
1089 pylsp_up.then_some("pylsp")
1090 );
1091 }
1092 }
1093 }
1094
1095 #[test]
1096 fn test_rebind_prunes_order_for_resolve_any() {
1097 let configs = vec![cfg("rust", Some("a"), None), cfg("python", Some("b"), None)];
1098 let mut router = ToolRouter::from_configs(&configs).unwrap();
1099 let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("b")]);
1100 router.rebind_to_registered(®istered);
1101 assert_eq!(
1102 router.resolve_any(ToolKind::Hover),
1103 Ok(&ServerId::from_static("b"))
1104 );
1105 }
1106
1107 #[test]
1108 fn test_resolve_any_no_claimant_does_not_fall_back_to_arbitrary_server() {
1109 let configs = vec![cfg("python", Some("pyright"), Some(vec![ToolKind::Hover]))];
1113 let router = ToolRouter::from_configs(&configs).unwrap();
1114 assert_eq!(
1115 router.resolve_any(ToolKind::WorkspaceSymbols),
1116 Err(NoServerReason::NoClaimant)
1117 );
1118 }
1119
1120 #[test]
1121 fn test_has_language() {
1122 let configs = vec![cfg("rust", None, None)];
1123 let router = ToolRouter::from_configs(&configs).unwrap();
1124 assert!(router.has_language("rust"));
1125 assert!(!router.has_language("python"));
1126 }
1127
1128 #[test]
1129 fn test_catch_all_helper_registers_two_entries() {
1130 let router = ToolRouter::catch_all([
1131 (
1132 ServerId::from_static("ts"),
1133 LanguageId::from_static("typescript"),
1134 ),
1135 (
1136 ServerId::from_static("tsx"),
1137 LanguageId::from_static("typescriptreact"),
1138 ),
1139 ]);
1140 assert_eq!(
1141 router.resolve("typescript", ToolKind::Hover),
1142 Some(&ServerId::from_static("ts"))
1143 );
1144 assert_eq!(
1145 router.resolve("typescriptreact", ToolKind::Hover),
1146 Some(&ServerId::from_static("tsx"))
1147 );
1148 }
1149
1150 #[test]
1151 fn test_tool_kind_as_str_and_all_len() {
1152 assert_eq!(ToolKind::Hover.as_str(), "hover");
1153 assert_eq!(ToolKind::CallHierarchy.as_str(), "call_hierarchy");
1154 assert_eq!(ToolKind::ALL.len(), 21);
1155
1156 let unique_names: std::collections::HashSet<&str> =
1160 ToolKind::ALL.iter().map(ToolKind::as_str).collect();
1161 assert_eq!(unique_names.len(), ToolKind::ALL.len());
1162 }
1163
1164 #[test]
1165 fn test_server_id_display_and_as_str() {
1166 let id = ServerId::from_static("pyright");
1167 assert_eq!(id.as_str(), "pyright");
1168 assert_eq!(id.to_string(), "pyright");
1169 }
1170}