#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use anyhow::anyhow;
use lsp_types::{FileEvent, Url};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::edit::{DocumentKey, DocumentVersion};
use crate::session::settings::{
ClientSettings, GlobalClientSettings, ResolvedProjectSettings, SettingsResolveContext,
ShuckSettings,
};
use crate::session::{Client, ClientOptions, WorkspaceOptionsMap};
use crate::workspace::Workspaces;
use crate::{PositionEncoding, TextDocument};
#[derive(Default)]
pub(crate) struct Index {
documents: FxHashMap<Url, Arc<TextDocument>>,
workspace_roots: Vec<PathBuf>,
workspace_settings: Vec<WorkspaceSettings>,
cache_project_settings: bool,
project_settings_cache:
RwLock<FxHashMap<ProjectSettingsCacheKey, Arc<ResolvedProjectSettings>>>,
}
#[derive(Clone)]
pub(crate) struct WorkspaceSettingsSnapshot {
pub(crate) root: PathBuf,
pub(crate) canonical_root: Option<PathBuf>,
pub(crate) options: Option<ClientOptions>,
}
#[derive(Clone)]
struct WorkspaceSettings {
url: Url,
root: PathBuf,
options: Option<ClientOptions>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct ProjectSettingsCacheKey {
config_root: PathBuf,
workspace_root: Option<PathBuf>,
}
#[derive(Clone)]
pub enum DocumentQuery {
Text {
file_url: Url,
document: Arc<TextDocument>,
settings: Arc<ShuckSettings>,
},
}
impl Index {
pub(super) fn new(
workspaces: &Workspaces,
_global: &GlobalClientSettings,
_client: &Client,
) -> crate::Result<Self> {
let workspace_settings = workspaces
.iter()
.filter_map(|workspace| {
workspace
.url()
.to_file_path()
.ok()
.map(|root| WorkspaceSettings {
url: workspace.url().clone(),
root,
options: workspace.options().cloned(),
})
})
.collect::<Vec<_>>();
let workspace_roots = workspace_settings
.iter()
.map(|workspace| workspace.root.clone())
.collect();
Ok(Self {
documents: FxHashMap::default(),
workspace_roots,
workspace_settings,
cache_project_settings: false,
project_settings_cache: RwLock::new(FxHashMap::default()),
})
}
pub(super) fn key_from_url(&self, url: Url) -> DocumentKey {
DocumentKey::Text(url)
}
pub(super) fn make_document_ref(
&self,
key: DocumentKey,
settings: Arc<crate::session::settings::ShuckSettings>,
) -> Option<DocumentQuery> {
let DocumentKey::Text(url) = key;
let document = self.documents.get(&url)?.clone();
Some(DocumentQuery::Text {
file_url: url,
document,
settings,
})
}
pub(super) fn resolve_snapshot_settings(
&self,
url: &Url,
global_options: &ClientOptions,
) -> (Arc<ShuckSettings>, Arc<ClientSettings>) {
let file_path = url.to_file_path().ok();
let workspace_settings = self.workspace_settings_for_url(url);
if let Some(workspace_options) =
workspace_settings.and_then(|workspace| workspace.options.as_ref())
{
let option_layers = [global_options, workspace_options];
return (
self.resolve_shuck_settings(
file_path.as_deref(),
workspace_settings.map(|workspace| workspace.root.as_path()),
&option_layers,
),
Arc::new(ClientSettings::from_layered_options(&option_layers)),
);
}
let option_layers = [global_options];
(
self.resolve_shuck_settings(
file_path.as_deref(),
workspace_settings.map(|workspace| workspace.root.as_path()),
&option_layers,
),
Arc::new(ClientSettings::from_layered_options(&option_layers)),
)
}
pub(super) fn has_open_document(&self, key: &DocumentKey) -> bool {
let DocumentKey::Text(url) = key;
self.documents.contains_key(url)
}
pub(super) fn update_text_document(
&mut self,
key: &DocumentKey,
content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
new_version: DocumentVersion,
encoding: PositionEncoding,
) -> crate::Result<()> {
let DocumentKey::Text(url) = key;
let Some(document) = self.documents.get_mut(url) else {
return Err(anyhow!(
"text document URI does not point to an open document"
));
};
std::sync::Arc::make_mut(document).apply_changes(content_changes, new_version, encoding);
Ok(())
}
pub(super) fn open_text_document(&mut self, url: Url, document: TextDocument) {
self.documents.insert(url, Arc::new(document));
}
pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
let DocumentKey::Text(url) = key;
self.documents
.remove(url)
.map(|_| ())
.ok_or_else(|| anyhow!("document is not open: {url}"))
}
pub(super) fn reload_settings(&mut self, changes: &[FileEvent], _client: &Client) {
let changed_paths = changes
.iter()
.filter_map(|change| change.uri.to_file_path().ok())
.filter(|path| {
matches!(
path.file_name().and_then(|name| name.to_str()),
Some(".shuck.toml" | "shuck.toml")
)
})
.collect::<Vec<_>>();
if changed_paths.is_empty() {
return;
}
let global_candidates = shuck_config::global_config_candidate_paths();
if changed_paths
.iter()
.any(|path| global_candidates.contains(path))
{
self.clear_project_settings_cache();
return;
}
let changed_roots = changed_paths
.iter()
.filter_map(|path| path.parent().map(Path::to_path_buf))
.collect::<FxHashSet<_>>();
self.project_settings_cache
.write()
.expect("settings cache lock should not be poisoned")
.retain(|cache_key, _| !changed_roots.contains(&cache_key.config_root));
}
pub(super) fn open_workspace_folder(
&mut self,
url: Url,
_global: &GlobalClientSettings,
_client: &Client,
) -> crate::Result<()> {
let path = url
.to_file_path()
.map_err(|()| anyhow!("failed to convert workspace URL to file path: {url}"))?;
if !self.workspace_roots.contains(&path) {
self.workspace_roots.push(path.clone());
}
if !self
.workspace_settings
.iter()
.any(|workspace| workspace.root == path)
{
self.workspace_settings.push(WorkspaceSettings {
url,
root: path,
options: None,
});
}
self.clear_project_settings_cache();
Ok(())
}
pub(super) fn close_workspace_folder(&mut self, workspace_url: &Url) -> crate::Result<()> {
let path = workspace_url.to_file_path().map_err(|()| {
anyhow!("failed to convert workspace URL to file path: {workspace_url}")
})?;
self.workspace_roots.retain(|root| root != &path);
self.workspace_settings
.retain(|workspace| workspace.root != path);
self.documents.retain(|url, _| {
url.to_file_path()
.map(|file_path| !file_path.starts_with(&path))
.unwrap_or(true)
});
self.clear_project_settings_cache();
Ok(())
}
pub(super) fn config_file_paths(&self) -> impl Iterator<Item = &Path> {
std::iter::empty()
}
pub(super) fn workspace_roots(&self) -> &[PathBuf] {
&self.workspace_roots
}
pub(super) fn workspace_settings_snapshot(&self) -> Vec<WorkspaceSettingsSnapshot> {
self.workspace_settings
.iter()
.map(|workspace| WorkspaceSettingsSnapshot {
root: workspace.root.clone(),
canonical_root: workspace.root.canonicalize().ok(),
options: workspace.options.clone(),
})
.collect()
}
pub(super) fn open_documents_snapshot(&self) -> Vec<crate::symbols::WorkspaceOpenDocument> {
self.documents
.iter()
.map(|(url, document)| {
crate::symbols::WorkspaceOpenDocument::new(url.clone(), document.clone())
})
.collect()
}
pub(super) fn workspace_options_for_url(&self, url: &Url) -> Option<&ClientOptions> {
self.workspace_settings_for_url(url)
.and_then(|workspace| workspace.options.as_ref())
}
pub(super) fn clear_project_settings_cache(&self) {
self.project_settings_cache
.write()
.expect("settings cache lock should not be poisoned")
.clear();
}
pub(super) fn set_project_settings_cache_enabled(&mut self, enabled: bool) {
self.cache_project_settings = enabled;
if !enabled {
self.clear_project_settings_cache();
}
}
fn workspace_settings_for_url(&self, url: &Url) -> Option<&WorkspaceSettings> {
let path = url.to_file_path().ok()?;
self.workspace_settings
.iter()
.filter(|workspace| path.starts_with(&workspace.root))
.max_by_key(|workspace| workspace.root.components().count())
}
fn resolve_shuck_settings(
&self,
file_path: Option<&Path>,
workspace_root: Option<&Path>,
option_layers: &[&ClientOptions],
) -> Arc<ShuckSettings> {
if !self.cache_project_settings {
return Arc::new(ShuckSettings::resolve(
file_path,
self.workspace_roots(),
option_layers,
));
}
let Some(file_path) = file_path else {
return Arc::new(ShuckSettings::resolve(
None,
self.workspace_roots(),
option_layers,
));
};
let context = SettingsResolveContext::for_file(file_path, self.workspace_roots());
let cache_key = ProjectSettingsCacheKey {
config_root: context.config_root().to_path_buf(),
workspace_root: workspace_root.map(Path::to_path_buf),
};
if let Some(cached) = self
.project_settings_cache
.read()
.expect("settings cache lock should not be poisoned")
.get(&cache_key)
.cloned()
{
return Arc::new(cached.for_file(Some(file_path)));
}
let resolved = Arc::new(ResolvedProjectSettings::resolve(&context, option_layers));
let cached = self
.project_settings_cache
.write()
.expect("settings cache lock should not be poisoned")
.entry(cache_key)
.or_insert_with(|| resolved.clone())
.clone();
Arc::new(cached.for_file(Some(file_path)))
}
pub(super) fn update_workspace_options(&mut self, mut workspace_options: WorkspaceOptionsMap) {
for workspace in &mut self.workspace_settings {
workspace.options = workspace_options.remove(&workspace.url);
}
self.clear_project_settings_cache();
}
pub(super) fn open_document_count(&self) -> usize {
self.documents.len()
}
}
impl DocumentQuery {
pub(crate) fn file_url(&self) -> &Url {
match self {
Self::Text { file_url, .. } => file_url,
}
}
pub(crate) fn file_path(&self) -> Option<PathBuf> {
self.file_url().to_file_path().ok()
}
pub(crate) fn document(&self) -> &Arc<TextDocument> {
match self {
Self::Text { document, .. } => document,
}
}
pub(crate) fn language_id(&self) -> Option<crate::edit::LanguageId> {
self.document().language_id()
}
pub(crate) fn settings(&self) -> &ShuckSettings {
match self {
Self::Text { settings, .. } => settings,
}
}
}