use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::language_id::LanguageId;
use super::server::{LaunchCommand, LspServerConfig};
use super::text_newtype::impl_text_newtype;
use crate::error::{ConfigError, Result};
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("server id cannot be blank (omit `name` to default to the language id)")]
pub struct InvalidServerId;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(try_from = "String", into = "String")]
#[schemars(with = "String")]
pub struct ServerId(Cow<'static, str>);
impl_text_newtype!(ServerId, InvalidServerId, non_blank, "server id");
impl From<LanguageId> for ServerId {
fn from(id: LanguageId) -> Self {
Self(Cow::Owned(id.into()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ToolKind {
Hover,
Definition,
TypeDefinition,
Implementation,
References,
Diagnostics,
Rename,
Completions,
SignatureHelp,
DocumentSymbols,
WorkspaceSymbols,
FormatDocument,
CodeActions,
CallHierarchy,
InlayHints,
Declaration,
TypeHierarchy,
DocumentHighlights,
FormatRange,
SelectionRange,
FoldingRange,
}
impl ToolKind {
pub const ALL: &[Self] = &[
Self::Hover,
Self::Definition,
Self::TypeDefinition,
Self::Implementation,
Self::References,
Self::Diagnostics,
Self::Rename,
Self::Completions,
Self::SignatureHelp,
Self::DocumentSymbols,
Self::WorkspaceSymbols,
Self::FormatDocument,
Self::CodeActions,
Self::CallHierarchy,
Self::InlayHints,
Self::Declaration,
Self::TypeHierarchy,
Self::DocumentHighlights,
Self::FormatRange,
Self::SelectionRange,
Self::FoldingRange,
];
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Hover => "hover",
Self::Definition => "definition",
Self::TypeDefinition => "type_definition",
Self::Implementation => "implementation",
Self::References => "references",
Self::Diagnostics => "diagnostics",
Self::Rename => "rename",
Self::Completions => "completions",
Self::SignatureHelp => "signature_help",
Self::DocumentSymbols => "document_symbols",
Self::WorkspaceSymbols => "workspace_symbols",
Self::FormatDocument => "format_document",
Self::CodeActions => "code_actions",
Self::CallHierarchy => "call_hierarchy",
Self::InlayHints => "inlay_hints",
Self::Declaration => "declaration",
Self::TypeHierarchy => "type_hierarchy",
Self::DocumentHighlights => "document_highlights",
Self::FormatRange => "format_range",
Self::SelectionRange => "selection_range",
Self::FoldingRange => "folding_range",
}
}
}
impl std::fmt::Display for ToolKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidToolSet {
#[error("handles cannot be empty (omit `handles` for a catch-all server)")]
Empty,
#[error("duplicate tool '{0}' in `handles`")]
Duplicate(ToolKind),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Vec<ToolKind>", into = "Vec<ToolKind>")]
pub struct ToolSet(Vec<ToolKind>);
impl ToolSet {
pub fn new(tools: Vec<ToolKind>) -> std::result::Result<Self, InvalidToolSet> {
if tools.is_empty() {
return Err(InvalidToolSet::Empty);
}
let mut seen = HashSet::new();
if let Some(tool) = tools.iter().find(|tool| !seen.insert(**tool)) {
return Err(InvalidToolSet::Duplicate(*tool));
}
Ok(Self(tools))
}
#[must_use]
pub fn single(tool: ToolKind) -> Self {
Self(vec![tool])
}
#[must_use]
pub fn contains(&self, tool: ToolKind) -> bool {
self.0.contains(&tool)
}
pub fn iter(&self) -> std::slice::Iter<'_, ToolKind> {
self.0.iter()
}
}
impl<'a> IntoIterator for &'a ToolSet {
type Item = &'a ToolKind;
type IntoIter = std::slice::Iter<'a, ToolKind>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl TryFrom<Vec<ToolKind>> for ToolSet {
type Error = InvalidToolSet;
fn try_from(tools: Vec<ToolKind>) -> std::result::Result<Self, Self::Error> {
Self::new(tools)
}
}
impl From<ToolSet> for Vec<ToolKind> {
fn from(set: ToolSet) -> Self {
set.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntrySummary {
pub language: LanguageId,
pub command: LaunchCommand,
pub arg_count: usize,
}
impl EntrySummary {
fn of(cfg: &LspServerConfig) -> Self {
Self {
language: cfg.language_id.clone(),
command: cfg.command.clone(),
arg_count: cfg.args.len(),
}
}
}
impl fmt::Display for EntrySummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"language '{}', command '{}'",
self.language, self.command
)?;
if self.arg_count > 0 {
let noun = if self.arg_count == 1 { "arg" } else { "args" };
write!(f, ", {} {noun}", self.arg_count)?;
}
Ok(())
}
}
#[derive(Debug, Default, Clone)]
struct LanguageRoutes {
explicit: HashMap<ToolKind, ServerId>,
default: Option<ServerId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NoServerReason {
NothingRegistered,
NoClaimant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerSettlement {
Pending,
Registered,
Failed,
}
#[derive(Debug, Default, Clone)]
pub struct ToolRouter {
by_language: HashMap<LanguageId, LanguageRoutes>,
order: Vec<ServerId>,
}
impl ToolRouter {
pub fn from_configs<'a, I>(cfgs: I) -> Result<Self>
where
I: IntoIterator<Item = &'a LspServerConfig>,
{
let mut by_language: HashMap<LanguageId, LanguageRoutes> = HashMap::new();
let mut order: Vec<ServerId> = Vec::new();
let mut seen_ids: HashMap<ServerId, &LspServerConfig> = HashMap::new();
for cfg in cfgs {
let id = cfg.id();
if let Some(previous) = seen_ids.get(&id) {
return Err(ConfigError::DuplicateServerId {
id,
first: Box::new(EntrySummary::of(previous)),
second: Box::new(EntrySummary::of(cfg)),
}
.into());
}
seen_ids.insert(id.clone(), cfg);
order.push(id.clone());
let routes = by_language.entry(cfg.language_id.clone()).or_default();
match &cfg.handles {
None => {
if let Some(existing) = &routes.default {
return Err(ConfigError::TwoCatchAllServers {
language: cfg.language_id.clone(),
existing: existing.clone(),
id,
}
.into());
}
routes.default = Some(id);
}
Some(tools) => {
for tool in tools {
if let Some(existing) = routes.explicit.get(tool) {
return Err(ConfigError::ToolClaimedTwice {
tool: *tool,
language: cfg.language_id.clone(),
existing: existing.clone(),
id,
}
.into());
}
routes.explicit.insert(*tool, id.clone());
}
}
}
}
for (language, routes) in &by_language {
if routes.default.is_none() {
let uncovered: Vec<&str> = ToolKind::ALL
.iter()
.filter(|t| !routes.explicit.contains_key(t))
.map(ToolKind::as_str)
.collect();
if !uncovered.is_empty() {
tracing::warn!(
"language '{language}' has no catch-all server and does not claim: {}",
uncovered.join(", ")
);
}
}
}
Ok(Self { by_language, order })
}
#[must_use]
pub fn catch_all<I>(entries: I) -> Self
where
I: IntoIterator<Item = (ServerId, LanguageId)>,
{
let mut by_language: HashMap<LanguageId, LanguageRoutes> = HashMap::new();
let mut order = Vec::new();
for (id, language) in entries {
order.push(id.clone());
by_language.entry(language).or_default().default = Some(id);
}
Self { by_language, order }
}
pub fn rebind(&mut self, settlement: impl Fn(&ServerId) -> ServerSettlement) {
for routes in self.by_language.values_mut() {
let catch_all = routes.default.clone().map(|id| (settlement(&id), id));
let dead: Vec<ToolKind> = routes
.explicit
.iter()
.filter(|(_, id)| settlement(id) == ServerSettlement::Failed)
.map(|(tool, _)| *tool)
.collect();
for tool in dead {
match &catch_all {
Some((ServerSettlement::Registered, id)) => {
routes.explicit.insert(tool, id.clone());
}
Some((ServerSettlement::Pending, _)) => {}
Some((ServerSettlement::Failed, _)) | None => {
routes.explicit.remove(&tool);
}
}
}
if matches!(catch_all, Some((ServerSettlement::Failed, _))) {
routes.default = None;
}
}
self.order
.retain(|id| settlement(id) != ServerSettlement::Failed);
}
pub fn rebind_to_registered(&mut self, registered: &HashSet<ServerId>) {
self.rebind(|id| {
if registered.contains(id) {
ServerSettlement::Registered
} else {
ServerSettlement::Failed
}
});
}
#[must_use]
pub fn languages_routed_to(&self, id: &ServerId) -> Vec<LanguageId> {
let mut languages: Vec<LanguageId> = self
.by_language
.iter()
.filter(|(_, routes)| {
routes.default.as_ref() == Some(id) || routes.explicit.values().any(|v| v == id)
})
.map(|(language, _)| language.clone())
.collect();
languages.sort_unstable();
languages
}
#[must_use]
pub fn catch_all_for(&self, language_id: &str) -> Option<&ServerId> {
self.by_language.get(language_id)?.default.as_ref()
}
#[must_use]
pub fn resolve(&self, language_id: &str, tool: ToolKind) -> Option<&ServerId> {
let routes = self.by_language.get(language_id)?;
routes.explicit.get(&tool).or(routes.default.as_ref())
}
pub fn resolve_any(&self, tool: ToolKind) -> std::result::Result<&ServerId, NoServerReason> {
let claims_explicitly = |id: &ServerId| {
self.by_language
.values()
.any(|r| r.explicit.get(&tool) == Some(id))
};
let is_catch_all = |id: &ServerId| {
self.by_language
.values()
.any(|r| r.default.as_ref() == Some(id))
};
self.order
.iter()
.find(|id| claims_explicitly(id))
.or_else(|| self.order.iter().find(|id| is_catch_all(id)))
.ok_or(if self.order.is_empty() {
NoServerReason::NothingRegistered
} else {
NoServerReason::NoClaimant
})
}
#[must_use]
pub fn has_language(&self, language_id: &str) -> bool {
self.by_language
.get(language_id)
.is_some_and(|r| r.default.is_some() || !r.explicit.is_empty())
}
#[must_use]
pub fn configured_languages(&self) -> Vec<LanguageId> {
let mut languages: Vec<LanguageId> = self.by_language.keys().cloned().collect();
languages.sort_unstable();
languages
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::*;
use crate::config::{ServerCommand, TimeoutSecs};
use crate::error::Error;
#[test]
fn test_server_id_new_rejects_blank_and_deserialization_agrees() {
assert_eq!(ServerId::new(""), Err(InvalidServerId));
assert_eq!(ServerId::new(" \t"), Err(InvalidServerId));
assert_eq!("pyright".parse::<ServerId>().unwrap().as_str(), "pyright");
assert!(serde_json::from_str::<ServerId>("\" \"").is_err());
assert_eq!(
serde_json::from_str::<ServerId>("\"pylsp\"").unwrap(),
ServerId::from_static("pylsp")
);
}
#[test]
#[should_panic(expected = "server id must be ASCII and not blank")]
fn test_server_id_from_static_panics_on_blank() {
let _ = ServerId::from_static(" ");
}
#[test]
fn test_server_id_static_and_owned_compare_and_serialize_alike() {
let owned = ServerId::new("rust".to_owned()).unwrap();
assert_eq!(owned, ServerId::from_static("rust"));
assert_eq!(owned, "rust");
assert_eq!(serde_json::to_string(&owned).unwrap(), "\"rust\"");
assert_eq!(String::from(owned), "rust");
}
#[test]
fn test_tool_set_rejects_empty_and_duplicates() {
assert_eq!(ToolSet::new(vec![]), Err(InvalidToolSet::Empty));
assert_eq!(
ToolSet::new(vec![ToolKind::Hover, ToolKind::Rename, ToolKind::Hover]),
Err(InvalidToolSet::Duplicate(ToolKind::Hover))
);
}
#[test]
fn test_tool_set_preserves_order_and_round_trips() {
let set = ToolSet::new(vec![ToolKind::Rename, ToolKind::Hover]).unwrap();
assert_eq!(
set.iter().copied().collect::<Vec<_>>(),
[ToolKind::Rename, ToolKind::Hover]
);
assert!(set.contains(ToolKind::Hover));
assert!(!set.contains(ToolKind::Definition));
let json = serde_json::to_string(&set).unwrap();
assert_eq!(json, r#"["rename","hover"]"#);
assert_eq!(serde_json::from_str::<ToolSet>(&json).unwrap(), set);
assert!(serde_json::from_str::<ToolSet>("[]").is_err());
}
fn cfg(
language_id: &str,
name: Option<&str>,
handles: Option<Vec<ToolKind>>,
) -> LspServerConfig {
LspServerConfig {
language_id: LanguageId::new(language_id).unwrap(),
command: ServerCommand::from_static("cmd").into(),
args: crate::config::ServerArgs::default(),
env: crate::config::ServerEnv::default(),
file_patterns: vec![],
initialization_options: None,
settings: None,
timeout_seconds: TimeoutSecs::new(30).unwrap(),
request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
heuristics: None,
name: name.map(|id| ServerId::new(id).unwrap()),
handles: handles.map(|tools| ToolSet::new(tools).unwrap()),
indexing: crate::bridge::IndexingPolicy::Auto,
}
}
#[test]
fn test_resolve_explicit_wins_over_catch_all() {
let configs = vec![
cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
cfg("python", Some("pylsp"), None),
];
let router = ToolRouter::from_configs(&configs).unwrap();
assert_eq!(
router.resolve("python", ToolKind::Hover),
Some(&ServerId::from_static("pyright"))
);
assert_eq!(
router.resolve("python", ToolKind::Diagnostics),
Some(&ServerId::from_static("pylsp"))
);
}
#[test]
fn test_resolve_no_catch_all_unclaimed_is_none() {
let configs = vec![cfg("python", Some("pyright"), Some(vec![ToolKind::Hover]))];
let router = ToolRouter::from_configs(&configs).unwrap();
assert_eq!(router.resolve("python", ToolKind::Diagnostics), None);
}
#[test]
fn test_resolve_any_explicit_claimer_beats_catch_all_declared_first() {
let configs = vec![
cfg("python", Some("python-narrow"), Some(vec![ToolKind::Hover])),
cfg("rust", Some("rust-catch-all"), None),
];
let router = ToolRouter::from_configs(&configs).unwrap();
assert_eq!(
router.resolve_any(ToolKind::WorkspaceSymbols),
Ok(&ServerId::from_static("rust-catch-all"))
);
}
#[test]
fn test_resolve_any_prefers_explicit_claimer_over_catch_all() {
let configs = vec![
cfg("rust", Some("rust-catch-all"), None),
cfg(
"python",
Some("python-explicit"),
Some(vec![ToolKind::WorkspaceSymbols]),
),
];
let router = ToolRouter::from_configs(&configs).unwrap();
assert_eq!(
router.resolve_any(ToolKind::WorkspaceSymbols),
Ok(&ServerId::from_static("python-explicit"))
);
}
#[test]
fn test_from_configs_rejects_duplicate_server_id_across_languages() {
let configs = vec![
cfg("python", None, None),
cfg("typescript", Some("python"), None),
];
let err = ToolRouter::from_configs(&configs).unwrap_err();
assert_matches!(err, Error::Config(ConfigError::DuplicateServerId { .. }));
}
#[test]
fn test_from_configs_duplicate_server_id_error_distinguishes_entries() {
let configs = vec![
LspServerConfig {
language_id: LanguageId::from_static("rust"),
command: ServerCommand::from_static("rust-analyzer").into(),
args: crate::config::ServerArgs::default(),
env: crate::config::ServerEnv::default(),
file_patterns: vec![],
initialization_options: None,
settings: None,
timeout_seconds: TimeoutSecs::new(30).unwrap(),
request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
heuristics: None,
name: None,
handles: None,
indexing: crate::bridge::IndexingPolicy::Auto,
},
LspServerConfig {
language_id: LanguageId::from_static("rust"),
command: ServerCommand::from_static("rust-analyzer").into(),
args: crate::config::ServerArgs::try_from(vec![
"--dummy-second-instance".to_string(),
])
.unwrap(),
env: crate::config::ServerEnv::default(),
file_patterns: vec![],
initialization_options: None,
settings: None,
timeout_seconds: TimeoutSecs::new(30).unwrap(),
request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
heuristics: None,
name: None,
handles: None,
indexing: crate::bridge::IndexingPolicy::Auto,
},
];
let err = ToolRouter::from_configs(&configs).unwrap_err();
let Error::Config(config_err) = err else {
panic!("expected Config, got {err:?}");
};
let msg = config_err.to_string();
assert!(!msg.contains("entry #"), "message was: {msg}");
assert!(msg.contains("rust-analyzer"), "message was: {msg}");
assert!(msg.contains(", 1 arg"), "message was: {msg}");
assert!(!msg.contains("1 args"), "message was: {msg}");
assert!(
!msg.contains("--dummy-second-instance"),
"message was: {msg}"
);
}
#[test]
fn test_entry_summary_pluralises_the_argument_count() {
let summary = |arg_count| EntrySummary {
language: LanguageId::from_static("rust"),
command: ServerCommand::from_static("ra").into(),
arg_count,
};
assert_eq!(summary(0).to_string(), "language 'rust', command 'ra'");
assert_eq!(
summary(1).to_string(),
"language 'rust', command 'ra', 1 arg"
);
assert_eq!(
summary(2).to_string(),
"language 'rust', command 'ra', 2 args"
);
}
#[test]
fn test_from_configs_duplicate_server_id_error_never_prints_arguments() {
let secret_args = |args: &[&str]| {
crate::config::ServerArgs::try_from(
args.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>(),
)
.unwrap()
};
let mut first = cfg("rust", None, None);
first.args = secret_args(&["--stdio"]);
let mut second = cfg("rust", None, None);
second.args = secret_args(&["--api-key=SECRET"]);
let err = ToolRouter::from_configs(&[first, second]).unwrap_err();
for rendered in [
format!("{err}"),
format!("{err:?}"),
crate::error::ErrorChain(&err).to_string(),
] {
assert!(!rendered.contains("SECRET"), "leaked: {rendered}");
}
}
#[test]
fn test_from_configs_duplicate_server_id_error_identical_entries_still_reports() {
let configs = vec![cfg("rust", None, None), cfg("rust", None, None)];
let err = ToolRouter::from_configs(&configs).unwrap_err();
let Error::Config(config_err) = err else {
panic!("expected Config, got {err:?}");
};
let msg = config_err.to_string();
assert!(!msg.contains("entry #"), "message was: {msg}");
assert!(
msg.contains("duplicate server id 'rust'"),
"message was: {msg}"
);
}
#[test]
fn test_from_configs_rejects_two_catch_alls() {
let configs = vec![
cfg("python", Some("a"), None),
cfg("python", Some("b"), None),
];
let err = ToolRouter::from_configs(&configs).unwrap_err();
assert_matches!(err, Error::Config(ConfigError::TwoCatchAllServers { .. }));
}
#[test]
fn test_from_configs_rejects_duplicate_tool_claim() {
let configs = vec![
cfg("python", Some("a"), Some(vec![ToolKind::Hover])),
cfg("python", Some("b"), Some(vec![ToolKind::Hover])),
];
let err = ToolRouter::from_configs(&configs).unwrap_err();
assert_matches!(err, Error::Config(ConfigError::ToolClaimedTwice { .. }));
}
#[test]
fn test_rebind_to_registered_dead_server_with_live_catch_all() {
let configs = vec![
cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
cfg("python", Some("pylsp"), None),
];
let mut router = ToolRouter::from_configs(&configs).unwrap();
let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("pylsp")]);
router.rebind_to_registered(®istered);
assert_eq!(
router.resolve("python", ToolKind::Hover),
Some(&ServerId::from_static("pylsp"))
);
}
#[test]
fn test_rebind_to_registered_dead_server_no_catch_all_drops_route() {
let configs = vec![
cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
cfg("python", Some("pylsp"), Some(vec![ToolKind::Diagnostics])),
];
let mut router = ToolRouter::from_configs(&configs).unwrap();
let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("pylsp")]);
router.rebind_to_registered(®istered);
assert_eq!(router.resolve("python", ToolKind::Hover), None);
assert_eq!(
router.resolve("python", ToolKind::Diagnostics),
Some(&ServerId::from_static("pylsp"))
);
}
#[test]
fn test_rebind_to_registered_all_failed_drops_everything() {
let configs = vec![cfg("rust", None, None)];
let mut router = ToolRouter::from_configs(&configs).unwrap();
router.rebind_to_registered(&HashSet::new());
assert_eq!(router.resolve("rust", ToolKind::Hover), None);
assert_eq!(
router.resolve_any(ToolKind::Hover),
Err(NoServerReason::NothingRegistered)
);
assert!(!router.has_language("rust"));
}
fn explicit_and_catch_all_router() -> ToolRouter {
ToolRouter::from_configs(&[
cfg("python", Some("pyright"), Some(vec![ToolKind::Hover])),
cfg("python", Some("pylsp"), None),
])
.unwrap()
}
#[test]
fn rebind_keeps_pending_routes_and_order() {
let mut router = ToolRouter::from_configs(&[
cfg("rust", Some("slow"), None),
cfg("python", Some("fast"), None),
])
.unwrap();
router.rebind(|id| {
if id.as_str() == "fast" {
ServerSettlement::Registered
} else {
ServerSettlement::Pending
}
});
assert_eq!(
router.resolve("rust", ToolKind::Hover),
Some(&ServerId::from_static("slow"))
);
assert_eq!(
router.resolve_any(ToolKind::Hover),
Ok(&ServerId::from_static("slow"))
);
}
#[test]
fn dead_explicit_route_waits_for_pending_catch_all_and_drops_when_it_fails() {
let settle = |pyright, pylsp| {
let mut router = explicit_and_catch_all_router();
router.rebind(|id| {
if id.as_str() == "pyright" {
pyright
} else {
pylsp
}
});
router.resolve("python", ToolKind::Hover).cloned()
};
let pending_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Pending);
assert_eq!(pending_catch_all, Some(ServerId::from_static("pyright")));
let registered_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Registered);
assert_eq!(registered_catch_all, Some(ServerId::from_static("pylsp")));
let failed_catch_all = settle(ServerSettlement::Failed, ServerSettlement::Failed);
assert_eq!(failed_catch_all, None);
}
#[test]
fn rebind_without_pending_servers_matches_the_batch_rule_for_every_outcome() {
for pyright_up in [false, true] {
for pylsp_up in [false, true] {
let registered: HashSet<ServerId> = [("pyright", pyright_up), ("pylsp", pylsp_up)]
.into_iter()
.filter(|(_, up)| *up)
.map(|(id, _)| ServerId::new(id).unwrap())
.collect();
let mut router = explicit_and_catch_all_router();
router.rebind_to_registered(®istered);
let hover = if pyright_up {
Some("pyright")
} else if pylsp_up {
Some("pylsp")
} else {
None
};
assert_eq!(
router
.resolve("python", ToolKind::Hover)
.map(ServerId::as_str),
hover,
"pyright up: {pyright_up}, pylsp up: {pylsp_up}"
);
assert_eq!(
router
.resolve("python", ToolKind::Diagnostics)
.map(ServerId::as_str),
pylsp_up.then_some("pylsp")
);
}
}
}
#[test]
fn test_rebind_prunes_order_for_resolve_any() {
let configs = vec![cfg("rust", Some("a"), None), cfg("python", Some("b"), None)];
let mut router = ToolRouter::from_configs(&configs).unwrap();
let registered: HashSet<ServerId> = HashSet::from([ServerId::from_static("b")]);
router.rebind_to_registered(®istered);
assert_eq!(
router.resolve_any(ToolKind::Hover),
Ok(&ServerId::from_static("b"))
);
}
#[test]
fn test_resolve_any_no_claimant_does_not_fall_back_to_arbitrary_server() {
let configs = vec![cfg("python", Some("pyright"), Some(vec![ToolKind::Hover]))];
let router = ToolRouter::from_configs(&configs).unwrap();
assert_eq!(
router.resolve_any(ToolKind::WorkspaceSymbols),
Err(NoServerReason::NoClaimant)
);
}
#[test]
fn test_has_language() {
let configs = vec![cfg("rust", None, None)];
let router = ToolRouter::from_configs(&configs).unwrap();
assert!(router.has_language("rust"));
assert!(!router.has_language("python"));
}
#[test]
fn test_catch_all_helper_registers_two_entries() {
let router = ToolRouter::catch_all([
(
ServerId::from_static("ts"),
LanguageId::from_static("typescript"),
),
(
ServerId::from_static("tsx"),
LanguageId::from_static("typescriptreact"),
),
]);
assert_eq!(
router.resolve("typescript", ToolKind::Hover),
Some(&ServerId::from_static("ts"))
);
assert_eq!(
router.resolve("typescriptreact", ToolKind::Hover),
Some(&ServerId::from_static("tsx"))
);
}
#[test]
fn test_tool_kind_as_str_and_all_len() {
assert_eq!(ToolKind::Hover.as_str(), "hover");
assert_eq!(ToolKind::CallHierarchy.as_str(), "call_hierarchy");
assert_eq!(ToolKind::ALL.len(), 21);
let unique_names: std::collections::HashSet<&str> =
ToolKind::ALL.iter().map(ToolKind::as_str).collect();
assert_eq!(unique_names.len(), ToolKind::ALL.len());
}
#[test]
fn test_server_id_display_and_as_str() {
let id = ServerId::from_static("pyright");
assert_eq!(id.as_str(), "pyright");
assert_eq!(id.to_string(), "pyright");
}
}