use crate::{LspWorkspaceFolderState, file_uri_to_path, normalize_path};
use serde::Serialize;
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceRuntimeRegistryBoundaryV0 {
pub product: &'static str,
pub owner: &'static str,
pub folder_state_owner: &'static str,
pub ownership_policy: Vec<&'static str>,
pub indexed_document_policy: Vec<&'static str>,
pub request_path_policy: Vec<&'static str>,
}
pub fn workspace_runtime_registry_contract() -> WorkspaceRuntimeRegistryBoundaryV0 {
WorkspaceRuntimeRegistryBoundaryV0 {
product: "omena-lsp-server.workspace-runtime-registry",
owner: "omena-lsp-server/runtime/workspaceRuntimeRegistry",
folder_state_owner: "omena-lsp-server",
ownership_policy: vec![
"longestWorkspaceRootOwnsDocument",
"filePathComponentBoundariesBeforeUriPrefix",
"workspaceFolderChangesRefreshDocumentOwnership",
],
indexed_document_policy: vec![
"indexStyleDocumentsPerWorkspaceRoot",
"evictIndexedDocumentsOnWorkspaceRemoval",
"retargetIndexedDocumentsToRemainingOwnerOnWorkspaceRemoval",
"openedDocumentsRemainAuthoritative",
],
request_path_policy: vec![
"noNodeWorkspaceRuntimeManagerOnRustLspPath",
"resolveWorkspaceOwnershipBeforeProviderExecution",
"keepWorkspaceOwnershipDeterministic",
],
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct WorkspaceRuntimeRegistry {
folders: BTreeMap<String, WorkspaceRuntimeFolderEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct WorkspaceRuntimeFolderEntry {
folder: LspWorkspaceFolderState,
root_path: Option<PathBuf>,
}
impl WorkspaceRuntimeRegistry {
pub(crate) fn clear(&mut self) {
self.folders.clear();
}
pub(crate) fn insert(&mut self, uri: impl Into<String>, name: impl Into<String>) {
let uri = uri.into();
let root_path = file_uri_to_path(uri.as_str()).map(normalize_path);
self.folders.insert(
uri.clone(),
WorkspaceRuntimeFolderEntry {
folder: LspWorkspaceFolderState {
uri,
name: name.into(),
},
root_path,
},
);
}
pub(crate) fn remove(&mut self, uri: &str) -> Option<LspWorkspaceFolderState> {
self.folders.remove(uri).map(|entry| entry.folder)
}
pub(crate) fn get(&self, uri: &str) -> Option<&LspWorkspaceFolderState> {
self.folders.get(uri).map(|entry| &entry.folder)
}
pub(crate) fn len(&self) -> usize {
self.folders.len()
}
pub(crate) fn folders(&self) -> impl Iterator<Item = &LspWorkspaceFolderState> {
self.folders.values().map(|entry| &entry.folder)
}
pub(crate) fn folder_snapshots(&self) -> Vec<LspWorkspaceFolderState> {
self.folders().cloned().collect()
}
pub(crate) fn resolve_owner_uri(&self, document_uri: &str) -> Option<String> {
let document_path = file_uri_to_path(document_uri).map(normalize_path);
self.folders
.values()
.filter_map(|entry| {
workspace_owner_score(entry, document_uri, document_path.as_deref())
.map(|score| (score, entry.folder.uri.clone()))
})
.max_by_key(|(score, _)| *score)
.map(|(_, uri)| uri)
}
}
fn workspace_owner_score(
entry: &WorkspaceRuntimeFolderEntry,
document_uri: &str,
document_path: Option<&Path>,
) -> Option<(u8, usize, usize)> {
if let (Some(root_path), Some(document_path)) = (entry.root_path.as_deref(), document_path)
&& path_is_equal_or_descendant(document_path, root_path)
{
return Some((1, path_depth(root_path), entry.folder.uri.len()));
}
if uri_is_equal_or_descendant(entry.folder.uri.as_str(), document_uri) {
return Some((0, 0, entry.folder.uri.len()));
}
None
}
fn path_is_equal_or_descendant(document_path: &Path, workspace_root: &Path) -> bool {
!workspace_root.as_os_str().is_empty()
&& (document_path == workspace_root || document_path.starts_with(workspace_root))
}
fn path_depth(path: &Path) -> usize {
path.components().count()
}
fn uri_is_equal_or_descendant(workspace_uri: &str, document_uri: &str) -> bool {
document_uri == workspace_uri
|| document_uri
.strip_prefix(workspace_uri)
.is_some_and(|suffix| suffix.starts_with('/'))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_owner_by_longest_workspace_root() {
let mut registry = WorkspaceRuntimeRegistry::default();
registry.insert("file:///repo", "repo");
registry.insert("file:///repo/packages/app", "app");
assert_eq!(
registry.resolve_owner_uri("file:///repo/packages/app/src/Button.module.scss"),
Some("file:///repo/packages/app".to_string()),
);
}
#[test]
fn path_boundaries_prevent_prefix_only_ownership() {
let mut registry = WorkspaceRuntimeRegistry::default();
registry.insert("file:///repo/app", "app");
assert_eq!(
registry.resolve_owner_uri("file:///repo/app2/src/Button.module.scss"),
None,
);
}
}