use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::sync::RwLock;
use url::Url;
use super::state::{encode_rfc3986_path_chars, file_url, uri_to_path};
use super::{ClientPath, InvalidClientPath, WorkspaceRoots};
use crate::util::{MAX_ECHOED_PATH_BYTES, escape_bounded};
const SCHEME: &str = "lsp-diagnostics";
const PREFIX: &str = "lsp-diagnostics://";
pub const MAX_SUBSCRIPTIONS: usize = 1_000;
pub(crate) const MAX_LISTEN_STREAMS: usize = 100;
#[derive(Debug, Error)]
pub enum ResourceUriError {
#[error(
"path must be absolute and valid UTF-8: {}",
escape_bounded(.0, MAX_ECHOED_PATH_BYTES)
)]
InvalidPath(String),
#[error(
"expected '{SCHEME}:///' prefix in URI: {}",
escape_bounded(.0, MAX_ECHOED_PATH_BYTES)
)]
InvalidScheme(String),
#[error("malformed resource URI")]
UrlParse(#[source] url::ParseError),
#[error(
"URI does not name a filesystem path: {}",
escape_bounded(.0, MAX_ECHOED_PATH_BYTES)
)]
NotAFilePath(String),
#[error("decoded URI path is not acceptable")]
ClientPath(#[source] InvalidClientPath),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub(crate) enum SubscriptionError {
#[error("subscription limit of {MAX_SUBSCRIPTIONS} reached")]
LimitReached,
#[error("listen stream limit of {MAX_LISTEN_STREAMS} reached")]
ListenLimitReached,
}
impl From<SubscriptionError> for crate::error::Error {
fn from(err: SubscriptionError) -> Self {
match err {
SubscriptionError::LimitReached => Self::SubscriptionLimitReached {
max: MAX_SUBSCRIPTIONS,
},
SubscriptionError::ListenLimitReached => Self::ListenStreamsExhausted {
max: MAX_LISTEN_STREAMS,
},
}
}
}
pub fn make_uri(path: &Path) -> Result<String, ResourceUriError> {
let file_url =
file_url(path).ok_or_else(|| ResourceUriError::InvalidPath(path.display().to_string()))?;
let encoded = encode_rfc3986_path_chars(&file_url);
let after_scheme = encoded.strip_prefix(file_url.scheme()).unwrap_or(&encoded);
let uri = format!("{SCHEME}{after_scheme}");
Ok(uri)
}
pub fn parse_uri(uri: &str) -> Result<ClientPath, ResourceUriError> {
if !uri.starts_with(PREFIX) {
return Err(ResourceUriError::InvalidScheme(uri.to_string()));
}
let after_prefix = &uri[PREFIX.len()..];
if !after_prefix.starts_with('/') {
return Err(ResourceUriError::InvalidScheme(format!(
"non-empty authority in URI: {uri}"
)));
}
let file_uri = format!("file://{after_prefix}");
let url = Url::parse(&file_uri).map_err(ResourceUriError::UrlParse)?;
let path = url
.to_file_path()
.map_err(|()| ResourceUriError::NotAFilePath(file_uri))?;
ClientPath::try_from(path).map_err(ResourceUriError::ClientPath)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct DiagnosticsResourceUri(String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PublishedDiagnosticsUri {
source: lsp_types::Uri,
canonical: lsp_types::Uri,
is_canonical: bool,
form: CanonicalForm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CanonicalForm {
Resolved,
Fallback,
}
impl PublishedDiagnosticsUri {
pub(super) fn from_canonical_path(
published: &lsp_types::Uri,
published_path: &Path,
canonical_path: &Path,
form: CanonicalForm,
roots: &WorkspaceRoots,
) -> Option<Self> {
let canonical = super::try_path_to_uri(canonical_path)?;
roots.contains_canonical(canonical_path).then(|| Self {
source: published.clone(),
canonical,
is_canonical: form == CanonicalForm::Resolved
&& same_path(published_path, canonical_path),
form,
})
}
#[cfg(test)]
pub(crate) fn for_test(source: lsp_types::Uri, canonical: lsp_types::Uri) -> Self {
Self::for_test_with_form(source, canonical, CanonicalForm::Resolved)
}
#[cfg(test)]
pub(crate) fn for_test_with_form(
source: lsp_types::Uri,
canonical: lsp_types::Uri,
form: CanonicalForm,
) -> Self {
let is_canonical = source == canonical && form == CanonicalForm::Resolved;
Self {
source,
canonical,
is_canonical,
form,
}
}
pub(crate) const fn form(&self) -> CanonicalForm {
self.form
}
pub(crate) const fn is_canonical(&self) -> bool {
self.is_canonical
}
pub(crate) const fn source(&self) -> &lsp_types::Uri {
&self.source
}
pub(crate) const fn canonical(&self) -> &lsp_types::Uri {
&self.canonical
}
}
fn same_path(a: &Path, b: &Path) -> bool {
if cfg!(windows) {
a.as_os_str()
.to_string_lossy()
.eq_ignore_ascii_case(&b.as_os_str().to_string_lossy())
} else {
a == b
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResolvedResource {
pub(crate) path: PathBuf,
pub(crate) uri: DiagnosticsResourceUri,
}
impl DiagnosticsResourceUri {
pub(crate) fn resolve(
raw: &str,
roots: &WorkspaceRoots,
) -> crate::error::Result<ResolvedResource> {
let parsed = parse_uri(raw)?;
let validated = roots.validate_blocking(&parsed)?;
let path = dunce::simplified(validated.as_path()).to_path_buf();
let uri = Self(make_uri(&path).map_err(|_| crate::Error::PathToUri(path.clone()))?);
Ok(ResolvedResource { path, uri })
}
pub(crate) fn for_published(published: &PublishedDiagnosticsUri) -> Option<Self> {
Self::for_canonical_file(&published.canonical)
}
pub(crate) fn for_canonical_file(file: &lsp_types::Uri) -> Option<Self> {
make_uri(&uri_to_path(file)?).ok().map(Self)
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
#[cfg(test)]
pub(crate) fn for_test(uri: &str) -> Self {
Self(uri.to_owned())
}
}
impl fmt::Display for DiagnosticsResourceUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<DiagnosticsResourceUri> for String {
fn from(uri: DiagnosticsResourceUri) -> Self {
uri.0
}
}
#[derive(Debug, Default)]
struct SubscriptionState {
canonical: HashSet<DiagnosticsResourceUri>,
aliases: HashMap<String, DiagnosticsResourceUri>,
}
#[derive(Debug)]
pub(crate) struct ResourceSubscriptions(RwLock<SubscriptionState>);
impl Default for ResourceSubscriptions {
fn default() -> Self {
Self::new()
}
}
impl ResourceSubscriptions {
#[must_use]
pub fn new() -> Self {
Self(RwLock::new(SubscriptionState::default()))
}
pub(crate) fn from_canonical(uris: impl IntoIterator<Item = DiagnosticsResourceUri>) -> Self {
Self(RwLock::new(SubscriptionState {
canonical: uris.into_iter().collect(),
aliases: HashMap::new(),
}))
}
pub(crate) async fn subscribe(
&self,
uri: DiagnosticsResourceUri,
) -> Result<bool, SubscriptionError> {
let mut state = self.0.write().await;
if !state.canonical.contains(&uri) && state.canonical.len() >= MAX_SUBSCRIPTIONS {
return Err(SubscriptionError::LimitReached);
}
Ok(state.canonical.insert(uri))
}
pub(crate) async fn record_alias(
&self,
raw_uri: String,
canonical_uri: &DiagnosticsResourceUri,
) {
if raw_uri == canonical_uri.as_str() {
return;
}
let mut state = self.0.write().await;
if !state.canonical.contains(canonical_uri) {
return;
}
state.aliases.retain(|_, c| c != canonical_uri);
state.aliases.insert(raw_uri, canonical_uri.clone());
}
pub async fn is_empty(&self) -> bool {
self.0.read().await.canonical.is_empty()
}
pub(crate) async fn unsubscribe(
&self,
canonical: Option<&DiagnosticsResourceUri>,
raw: &str,
) -> Option<DiagnosticsResourceUri> {
let mut state = self.0.write().await;
let removed = state
.aliases
.remove(raw)
.and_then(|aliased| state.canonical.take(&aliased))
.or_else(|| canonical.and_then(|c| state.canonical.take(c)))
.or_else(|| {
state
.canonical
.take(&DiagnosticsResourceUri(raw.to_owned()))
});
if let Some(removed) = &removed {
state.aliases.retain(|_, c| c != removed);
}
drop(state);
removed
}
pub(crate) async fn snapshot(&self) -> Vec<DiagnosticsResourceUri> {
self.0.read().await.canonical.iter().cloned().collect()
}
pub(crate) async fn contains(&self, uri: &DiagnosticsResourceUri) -> bool {
self.0.read().await.canonical.contains(uri)
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::*;
#[cfg(unix)]
use crate::test_lsp::client_path;
fn roots_of(root: &Path) -> WorkspaceRoots {
WorkspaceRoots::from_paths(&[root.to_path_buf()]).unwrap()
}
#[cfg(windows)]
#[test]
fn test_make_uri_encodes_a_windows_rooted_path_without_a_drive() {
let uri = make_uri(Path::new(r"\ws\main.rs")).unwrap();
assert_eq!(uri, "lsp-diagnostics:///ws/main.rs");
}
#[test]
fn test_make_uri_rejects_relative_path() {
let result = make_uri(Path::new("relative/path.rs"));
assert!(result.is_err());
}
#[test]
fn test_parse_uri_rejects_wrong_scheme() {
let result = parse_uri("file:///home/user/main.rs");
assert!(result.is_err());
}
#[test]
fn test_parse_uri_rejects_decoded_nul() {
let drive = if cfg!(windows) { "C:/" } else { "" };
let result = parse_uri(&format!("lsp-diagnostics:///{drive}ws/a%00b.rs"));
assert_matches!(result, Err(ResourceUriError::ClientPath(_)));
}
#[test]
fn test_parse_uri_rejects_http_scheme() {
let result = parse_uri("https://example.com/file.rs");
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn test_make_uri_simple_path() {
let uri = make_uri(Path::new("/home/user/main.rs")).unwrap();
assert_eq!(uri, "lsp-diagnostics:///home/user/main.rs");
}
#[cfg(unix)]
#[test]
fn test_make_uri_scheme_prefix() {
let uri = make_uri(Path::new("/tmp/file.rs")).unwrap();
assert!(uri.starts_with("lsp-diagnostics:///"));
}
#[cfg(unix)]
#[test]
fn test_parse_uri_simple() {
let path = PathBuf::from("/home/user/main.rs");
let uri = make_uri(&path).unwrap();
let recovered = parse_uri(&uri).unwrap();
assert_eq!(recovered.as_path(), path);
}
#[cfg(unix)]
#[test]
fn test_round_trip_special_chars() {
let paths = [
"/home/user/my file.rs",
"/tmp/café/main.rs",
"/data/100%/test.rs",
"/workspace/query?param/file.rs",
"/repo/branch#fragment/src.rs",
"/путь/к/файлу.rs",
];
for raw in &paths {
let path = PathBuf::from(raw);
let uri = make_uri(&path).expect(raw);
assert!(
uri.starts_with("lsp-diagnostics:///"),
"URI should start with correct scheme: {uri}"
);
let recovered = parse_uri(&uri).expect(&uri);
assert_eq!(recovered.as_path(), path, "Round-trip failed for: {raw}");
}
}
#[cfg(unix)]
#[test]
fn test_wire_format_percent_encoded() {
let path = Path::new("/home/user/my file.rs");
let uri = make_uri(path).unwrap();
assert!(uri.contains("%20"), "Expected %20 in: {uri}");
assert!(uri.starts_with("lsp-diagnostics:///"));
}
#[cfg(unix)]
#[test]
fn test_make_uri_percent_encodes_reserved_chars() {
let path = Path::new("/home/user/test[]^|{}`.ts");
let uri = make_uri(path).unwrap();
for (raw, encoded) in [
('[', "%5B"),
(']', "%5D"),
('^', "%5E"),
('|', "%7C"),
('{', "%7B"),
('}', "%7D"),
('`', "%60"),
] {
assert!(
uri.contains(encoded),
"expected {raw:?} to be percent-encoded as {encoded} in {uri}"
);
}
assert!(
!uri.contains(['[', ']', '^', '|', '{', '}', '`']),
"no raw reserved characters should remain in {uri}"
);
assert_eq!(parse_uri(&uri).unwrap().as_path(), path);
}
use crate::bridge::resolve_one;
use crate::test_lsp::{diagnostics_uri as u, workspace_with_main_rs as workspace};
#[test]
fn test_resolve_rejects_foreign_scheme() {
let (_dir, root, _file) = workspace();
let err = DiagnosticsResourceUri::resolve(
"file:///tmp/main.rs",
&WorkspaceRoots::from_paths(&[root]).unwrap(),
)
.unwrap_err();
assert_matches!(err, crate::Error::ResourceUri(_), "got {err:?}");
}
#[test]
fn test_resolve_rejects_non_empty_authority() {
let (_dir, root, _file) = workspace();
let err = DiagnosticsResourceUri::resolve(
"lsp-diagnostics://host/main.rs",
&WorkspaceRoots::from_paths(&[root]).unwrap(),
)
.unwrap_err();
assert_matches!(err, crate::Error::ResourceUri(_), "got {err:?}");
}
#[cfg(unix)]
#[test]
fn test_resolve_admits_configured_symlink_spelling() {
let (_dir, root, file) = workspace();
let link = root.join("link_to_root");
std::os::unix::fs::symlink(&root, &link).unwrap();
let roots = roots_of(&link);
let raw = make_uri(&link.join(file.strip_prefix(&root).unwrap())).unwrap();
let resolved = DiagnosticsResourceUri::resolve(&raw, &roots).unwrap();
assert_eq!(resolved.path, file);
}
#[test]
fn test_resolve_nul_uri_is_invalid_params() {
let (_dir, root, _file) = workspace();
let err =
DiagnosticsResourceUri::resolve("lsp-diagnostics:///ws/a%00b.rs", &roots_of(&root))
.unwrap_err();
assert_eq!(
err.mcp_error_kind(),
crate::error::McpErrorKind::InvalidParams
);
}
#[test]
fn test_resolve_rejects_path_outside_workspace() {
let (_dir, root, _file) = workspace();
let (_other_dir, _other_root, other_file) = workspace();
let raw = make_uri(&other_file).unwrap();
assert!(
DiagnosticsResourceUri::resolve(&raw, &WorkspaceRoots::from_paths(&[root]).unwrap())
.is_err()
);
}
#[test]
fn test_resolve_rejects_missing_file() {
let (_dir, root, _file) = workspace();
let raw = make_uri(&root.join("missing.rs")).unwrap();
assert!(
DiagnosticsResourceUri::resolve(&raw, &WorkspaceRoots::from_paths(&[root]).unwrap())
.is_err()
);
}
#[test]
fn test_resolve_yields_canonical_path_and_uri() {
let (_dir, root, file) = workspace();
let raw = make_uri(&file).unwrap();
let resolved =
DiagnosticsResourceUri::resolve(&raw, &WorkspaceRoots::from_paths(&[root]).unwrap())
.unwrap();
assert_eq!(resolved.path, file);
assert_eq!(resolved.uri.as_str(), raw);
}
#[cfg(unix)]
#[test]
fn test_resolve_canonicalizes_symlinked_path() {
let (_dir, root, file) = workspace();
let link = root.join("link.rs");
std::os::unix::fs::symlink(&file, &link).unwrap();
let raw = make_uri(&link).unwrap();
let resolved =
DiagnosticsResourceUri::resolve(&raw, &WorkspaceRoots::from_paths(&[root]).unwrap())
.unwrap();
assert_eq!(resolved.uri.as_str(), make_uri(&file).unwrap());
}
#[tokio::test]
async fn test_for_published_matches_resolve_for_same_file() {
let (_dir, root, file) = workspace();
let roots = WorkspaceRoots::from_paths(&[root]).unwrap();
let resolved = DiagnosticsResourceUri::resolve(&make_uri(&file).unwrap(), &roots).unwrap();
let published = crate::bridge::path_to_uri(&file).unwrap();
let published = resolve_one(&published, &roots).await.unwrap();
assert_eq!(
DiagnosticsResourceUri::for_published(&published),
Some(resolved.uri)
);
}
#[tokio::test]
async fn test_published_resolve_rejects_non_file_uri() {
let (_dir, root, _file) = workspace();
let uri = lsp_types::Uri::from("untitled:Untitled-1");
assert_eq!(resolve_one(&uri, &roots_of(&root)).await, None);
}
#[tokio::test]
async fn test_published_resolve_percent_encoded_spelling_is_canonical() {
let (_dir, root, file) = workspace();
let canonical = crate::bridge::path_to_uri(&file).unwrap();
let encoded = lsp_types::Uri::from(canonical.as_ref().replace("main.rs", "main%2Ers"));
assert_ne!(encoded, canonical);
let published = resolve_one(&encoded, &roots_of(&root)).await.unwrap();
assert!(published.is_canonical());
assert_eq!(published.canonical(), &canonical);
}
#[tokio::test]
async fn test_published_resolve_deleted_file_keeps_canonical_form() {
let (_dir, root, file) = workspace();
std::fs::remove_file(&file).unwrap();
let uri = crate::bridge::path_to_uri(&file).unwrap();
let published = resolve_one(&uri, &roots_of(&root)).await.unwrap();
assert_eq!(published.canonical(), &uri);
}
#[cfg(unix)]
#[tokio::test]
async fn test_published_resolve_maps_symlink_to_canonical_key() {
let (_dir, root, file) = workspace();
let link = root.join("link.rs");
std::os::unix::fs::symlink(&file, &link).unwrap();
let link_uri = crate::bridge::path_to_uri(&link).unwrap();
let published = resolve_one(&link_uri, &roots_of(&root)).await.unwrap();
assert_eq!(published.source(), &link_uri);
assert_eq!(
published.canonical(),
&crate::bridge::path_to_uri(&file).unwrap()
);
let client_side = crate::bridge::Translator::cached_diagnostics_uri(
&roots_of(&root),
&client_path(&link),
)
.await
.unwrap();
assert_eq!(published.canonical(), &client_side);
}
#[cfg(unix)]
#[tokio::test]
async fn test_published_resolve_drops_symlink_pointing_outside() {
let (_dir, root, _file) = workspace();
let outside = tempfile::TempDir::new().unwrap();
std::fs::write(outside.path().join("x.rs"), "").unwrap();
let link = root.join("out.rs");
std::os::unix::fs::symlink(outside.path().join("x.rs"), &link).unwrap();
let link_uri = crate::bridge::path_to_uri(&link).unwrap();
assert_eq!(resolve_one(&link_uri, &roots_of(&root)).await, None);
}
#[tokio::test]
async fn test_subscribe_and_contains() {
let subs = ResourceSubscriptions::new();
let uri = u("lsp-diagnostics:///home/user/main.rs");
assert!(!subs.contains(&uri).await);
assert!(subs.subscribe(uri.clone()).await.unwrap());
assert!(subs.contains(&uri).await);
}
#[tokio::test]
async fn test_from_canonical_prepopulates() {
let uri = u("lsp-diagnostics:///tmp/file.rs");
let subs = ResourceSubscriptions::from_canonical([uri.clone()]);
assert!(subs.contains(&uri).await);
assert!(!subs.is_empty().await);
}
#[tokio::test]
async fn test_subscribe_duplicate_returns_false() {
let subs = ResourceSubscriptions::new();
let uri = u("lsp-diagnostics:///tmp/file.rs");
assert!(subs.subscribe(uri.clone()).await.unwrap());
assert!(!subs.subscribe(uri).await.unwrap());
}
#[tokio::test]
async fn test_unsubscribe() {
let subs = ResourceSubscriptions::new();
let uri = u("lsp-diagnostics:///tmp/file.rs");
subs.subscribe(uri.clone()).await.unwrap();
assert_eq!(
subs.unsubscribe(Some(&uri), uri.as_str()).await,
Some(uri.clone())
);
assert!(!subs.contains(&uri).await);
}
#[tokio::test]
async fn test_unsubscribe_nonexistent_returns_none() {
let subs = ResourceSubscriptions::new();
let missing = u("lsp-diagnostics:///nonexistent.rs");
assert!(
subs.unsubscribe(Some(&missing), missing.as_str())
.await
.is_none()
);
}
#[tokio::test]
async fn test_unsubscribe_without_canonical_matches_raw_verbatim() {
let subs = ResourceSubscriptions::new();
let uri = u("lsp-diagnostics:///tmp/file.rs");
subs.subscribe(uri.clone()).await.unwrap();
assert_eq!(subs.unsubscribe(None, uri.as_str()).await, Some(uri));
}
#[tokio::test]
async fn test_unsubscribe_resolves_recorded_alias() {
let subs = ResourceSubscriptions::new();
let raw = "lsp-diagnostics:///var/tmp/file.rs";
let canonical = u("lsp-diagnostics:///private/var/tmp/file.rs");
subs.subscribe(canonical.clone()).await.unwrap();
subs.record_alias(raw.to_owned(), &canonical).await;
assert!(subs.contains(&canonical).await);
assert_eq!(subs.unsubscribe(None, raw).await, Some(canonical.clone()));
assert!(!subs.contains(&canonical).await);
}
#[tokio::test]
async fn test_unsubscribe_alias_wins_over_retargeted_canonical() {
let subs = ResourceSubscriptions::new();
let raw = "lsp-diagnostics:///link.rs";
let (a, b) = (u("lsp-diagnostics:///a.rs"), u("lsp-diagnostics:///b.rs"));
subs.subscribe(a.clone()).await.unwrap();
subs.subscribe(b.clone()).await.unwrap();
subs.record_alias(raw.to_owned(), &a).await;
assert_eq!(subs.unsubscribe(Some(&b), raw).await, Some(a.clone()));
assert!(!subs.contains(&a).await);
assert!(subs.contains(&b).await, "the retargeted entry must survive");
}
#[tokio::test]
async fn test_unsubscribe_falls_back_to_alias_when_canonical_changed() {
let subs = ResourceSubscriptions::new();
let raw = "lsp-diagnostics:///link.rs";
let original = u("lsp-diagnostics:///old.rs");
let retargeted = u("lsp-diagnostics:///new.rs");
subs.subscribe(original.clone()).await.unwrap();
subs.record_alias(raw.to_owned(), &original).await;
assert_eq!(
subs.unsubscribe(Some(&retargeted), raw).await,
Some(original.clone())
);
assert!(!subs.contains(&original).await);
}
#[tokio::test]
async fn test_record_alias_noop_when_raw_equals_canonical() {
let subs = ResourceSubscriptions::new();
let uri = u("lsp-diagnostics:///tmp/file.rs");
subs.subscribe(uri.clone()).await.unwrap();
subs.record_alias(uri.as_str().to_owned(), &uri).await;
assert_eq!(subs.unsubscribe(Some(&uri), uri.as_str()).await, Some(uri));
}
#[tokio::test]
async fn test_unsubscribe_by_canonical_clears_stale_aliases() {
let subs = ResourceSubscriptions::new();
let raw = "lsp-diagnostics:///var/tmp/file.rs";
let canonical = u("lsp-diagnostics:///private/var/tmp/file.rs");
subs.subscribe(canonical.clone()).await.unwrap();
subs.record_alias(raw.to_owned(), &canonical).await;
assert!(subs.unsubscribe(Some(&canonical), "other").await.is_some());
assert!(subs.unsubscribe(None, raw).await.is_none());
}
#[tokio::test]
async fn test_record_alias_keeps_only_latest_alias_per_canonical() {
let subs = ResourceSubscriptions::new();
let canonical = u("lsp-diagnostics:///file.rs");
subs.subscribe(canonical.clone()).await.unwrap();
let raws = [
"lsp-diagnostics:///%66ile.rs",
"lsp-diagnostics:///fil%65.rs",
"lsp-diagnostics:///file%2Ers",
];
for raw in raws {
subs.record_alias(raw.to_owned(), &canonical).await;
}
for raw in &raws[..raws.len() - 1] {
assert!(subs.unsubscribe(None, raw).await.is_none());
}
let latest = raws.last().unwrap();
assert!(subs.unsubscribe(None, latest).await.is_some());
assert!(!subs.contains(&canonical).await);
}
#[tokio::test]
async fn test_record_alias_noop_for_unsubscribed_canonical() {
let subs = ResourceSubscriptions::new();
let raw = "lsp-diagnostics:///var/tmp/file.rs";
let canonical = u("lsp-diagnostics:///private/var/tmp/file.rs");
subs.record_alias(raw.to_owned(), &canonical).await;
subs.subscribe(canonical.clone()).await.unwrap();
assert!(subs.unsubscribe(None, raw).await.is_none());
assert!(subs.unsubscribe(Some(&canonical), raw).await.is_some());
}
#[tokio::test]
async fn test_subscribe_cap_exceeded() {
let subs = ResourceSubscriptions::new();
for i in 0..MAX_SUBSCRIPTIONS {
subs.subscribe(u(&format!("lsp-diagnostics:///file{i}.rs")))
.await
.unwrap();
}
let result = subs.subscribe(u("lsp-diagnostics:///overflow.rs")).await;
assert_eq!(result, Err(SubscriptionError::LimitReached));
}
}