1#![allow(dead_code)]
2
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, RwLock};
5
6use anyhow::anyhow;
7use lsp_types::{FileEvent, Url};
8use rustc_hash::{FxHashMap, FxHashSet};
9
10use crate::edit::{DocumentKey, DocumentVersion};
11use crate::session::settings::{
12 ClientSettings, GlobalClientSettings, ResolvedProjectSettings, SettingsResolveContext,
13 ShuckSettings,
14};
15use crate::session::{Client, ClientOptions, WorkspaceOptionsMap};
16use crate::workspace::Workspaces;
17use crate::{PositionEncoding, TextDocument};
18
19#[derive(Default)]
20pub(crate) struct Index {
21 documents: FxHashMap<Url, Arc<TextDocument>>,
22 workspace_roots: Vec<PathBuf>,
23 workspace_settings: Vec<WorkspaceSettings>,
24 cache_project_settings: bool,
25 project_settings_cache:
26 RwLock<FxHashMap<ProjectSettingsCacheKey, Arc<ResolvedProjectSettings>>>,
27}
28
29#[derive(Clone)]
30pub(crate) struct WorkspaceSettingsSnapshot {
31 pub(crate) root: PathBuf,
32 pub(crate) canonical_root: Option<PathBuf>,
33 pub(crate) options: Option<ClientOptions>,
34}
35
36#[derive(Clone)]
37struct WorkspaceSettings {
38 url: Url,
39 root: PathBuf,
40 options: Option<ClientOptions>,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44struct ProjectSettingsCacheKey {
45 config_root: PathBuf,
46 workspace_root: Option<PathBuf>,
47}
48
49#[derive(Clone)]
51pub enum DocumentQuery {
52 Text {
54 file_url: Url,
56 document: Arc<TextDocument>,
58 settings: Arc<ShuckSettings>,
60 },
61}
62
63impl Index {
64 pub(super) fn new(
65 workspaces: &Workspaces,
66 _global: &GlobalClientSettings,
67 _client: &Client,
68 ) -> crate::Result<Self> {
69 let workspace_settings = workspaces
70 .iter()
71 .filter_map(|workspace| {
72 workspace
73 .url()
74 .to_file_path()
75 .ok()
76 .map(|root| WorkspaceSettings {
77 url: workspace.url().clone(),
78 root,
79 options: workspace.options().cloned(),
80 })
81 })
82 .collect::<Vec<_>>();
83 let workspace_roots = workspace_settings
84 .iter()
85 .map(|workspace| workspace.root.clone())
86 .collect();
87 Ok(Self {
88 documents: FxHashMap::default(),
89 workspace_roots,
90 workspace_settings,
91 cache_project_settings: false,
92 project_settings_cache: RwLock::new(FxHashMap::default()),
93 })
94 }
95
96 pub(super) fn key_from_url(&self, url: Url) -> DocumentKey {
97 DocumentKey::Text(url)
98 }
99
100 pub(super) fn make_document_ref(
101 &self,
102 key: DocumentKey,
103 settings: Arc<crate::session::settings::ShuckSettings>,
104 ) -> Option<DocumentQuery> {
105 let DocumentKey::Text(url) = key;
106 let document = self.documents.get(&url)?.clone();
107 Some(DocumentQuery::Text {
108 file_url: url,
109 document,
110 settings,
111 })
112 }
113
114 pub(super) fn resolve_snapshot_settings(
115 &self,
116 url: &Url,
117 global_options: &ClientOptions,
118 ) -> (Arc<ShuckSettings>, Arc<ClientSettings>) {
119 let file_path = url.to_file_path().ok();
120 let workspace_settings = self.workspace_settings_for_url(url);
121
122 if let Some(workspace_options) =
123 workspace_settings.and_then(|workspace| workspace.options.as_ref())
124 {
125 let option_layers = [global_options, workspace_options];
126 return (
127 self.resolve_shuck_settings(
128 file_path.as_deref(),
129 workspace_settings.map(|workspace| workspace.root.as_path()),
130 &option_layers,
131 ),
132 Arc::new(ClientSettings::from_layered_options(&option_layers)),
133 );
134 }
135
136 let option_layers = [global_options];
137 (
138 self.resolve_shuck_settings(
139 file_path.as_deref(),
140 workspace_settings.map(|workspace| workspace.root.as_path()),
141 &option_layers,
142 ),
143 Arc::new(ClientSettings::from_layered_options(&option_layers)),
144 )
145 }
146
147 pub(super) fn has_open_document(&self, key: &DocumentKey) -> bool {
148 let DocumentKey::Text(url) = key;
149 self.documents.contains_key(url)
150 }
151
152 pub(super) fn update_text_document(
153 &mut self,
154 key: &DocumentKey,
155 content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
156 new_version: DocumentVersion,
157 encoding: PositionEncoding,
158 ) -> crate::Result<()> {
159 let DocumentKey::Text(url) = key;
160 let Some(document) = self.documents.get_mut(url) else {
161 return Err(anyhow!(
162 "text document URI does not point to an open document"
163 ));
164 };
165
166 std::sync::Arc::make_mut(document).apply_changes(content_changes, new_version, encoding);
167 Ok(())
168 }
169
170 pub(super) fn open_text_document(&mut self, url: Url, document: TextDocument) {
171 self.documents.insert(url, Arc::new(document));
172 }
173
174 pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
175 let DocumentKey::Text(url) = key;
176 self.documents
177 .remove(url)
178 .map(|_| ())
179 .ok_or_else(|| anyhow!("document is not open: {url}"))
180 }
181
182 pub(super) fn reload_settings(&mut self, changes: &[FileEvent], _client: &Client) {
183 let changed_paths = changes
184 .iter()
185 .filter_map(|change| change.uri.to_file_path().ok())
186 .filter(|path| {
187 matches!(
188 path.file_name().and_then(|name| name.to_str()),
189 Some(".shuck.toml" | "shuck.toml")
190 )
191 })
192 .collect::<Vec<_>>();
193 if changed_paths.is_empty() {
194 return;
195 }
196
197 let global_candidates = shuck_config::global_config_candidate_paths();
201 if changed_paths
202 .iter()
203 .any(|path| global_candidates.contains(path))
204 {
205 self.clear_project_settings_cache();
206 return;
207 }
208
209 let changed_roots = changed_paths
210 .iter()
211 .filter_map(|path| path.parent().map(Path::to_path_buf))
212 .collect::<FxHashSet<_>>();
213
214 self.project_settings_cache
215 .write()
216 .expect("settings cache lock should not be poisoned")
217 .retain(|cache_key, _| !changed_roots.contains(&cache_key.config_root));
218 }
219
220 pub(super) fn open_workspace_folder(
221 &mut self,
222 url: Url,
223 _global: &GlobalClientSettings,
224 _client: &Client,
225 ) -> crate::Result<()> {
226 let path = url
227 .to_file_path()
228 .map_err(|()| anyhow!("failed to convert workspace URL to file path: {url}"))?;
229 if !self.workspace_roots.contains(&path) {
230 self.workspace_roots.push(path.clone());
231 }
232 if !self
233 .workspace_settings
234 .iter()
235 .any(|workspace| workspace.root == path)
236 {
237 self.workspace_settings.push(WorkspaceSettings {
238 url,
239 root: path,
240 options: None,
241 });
242 }
243 self.clear_project_settings_cache();
244 Ok(())
245 }
246
247 pub(super) fn close_workspace_folder(&mut self, workspace_url: &Url) -> crate::Result<()> {
248 let path = workspace_url.to_file_path().map_err(|()| {
249 anyhow!("failed to convert workspace URL to file path: {workspace_url}")
250 })?;
251 self.workspace_roots.retain(|root| root != &path);
252 self.workspace_settings
253 .retain(|workspace| workspace.root != path);
254 self.documents.retain(|url, _| {
255 url.to_file_path()
256 .map(|file_path| !file_path.starts_with(&path))
257 .unwrap_or(true)
258 });
259 self.clear_project_settings_cache();
260 Ok(())
261 }
262
263 pub(super) fn config_file_paths(&self) -> impl Iterator<Item = &Path> {
264 std::iter::empty()
265 }
266
267 pub(super) fn workspace_roots(&self) -> &[PathBuf] {
268 &self.workspace_roots
269 }
270
271 pub(super) fn workspace_settings_snapshot(&self) -> Vec<WorkspaceSettingsSnapshot> {
272 self.workspace_settings
273 .iter()
274 .map(|workspace| WorkspaceSettingsSnapshot {
275 root: workspace.root.clone(),
276 canonical_root: workspace.root.canonicalize().ok(),
277 options: workspace.options.clone(),
278 })
279 .collect()
280 }
281
282 pub(super) fn open_documents_snapshot(&self) -> Vec<crate::symbols::WorkspaceOpenDocument> {
283 self.documents
284 .iter()
285 .map(|(url, document)| {
286 crate::symbols::WorkspaceOpenDocument::new(url.clone(), document.clone())
287 })
288 .collect()
289 }
290
291 pub(super) fn workspace_options_for_url(&self, url: &Url) -> Option<&ClientOptions> {
292 self.workspace_settings_for_url(url)
293 .and_then(|workspace| workspace.options.as_ref())
294 }
295
296 pub(super) fn clear_project_settings_cache(&self) {
297 self.project_settings_cache
298 .write()
299 .expect("settings cache lock should not be poisoned")
300 .clear();
301 }
302
303 pub(super) fn set_project_settings_cache_enabled(&mut self, enabled: bool) {
304 self.cache_project_settings = enabled;
305 if !enabled {
306 self.clear_project_settings_cache();
307 }
308 }
309
310 fn workspace_settings_for_url(&self, url: &Url) -> Option<&WorkspaceSettings> {
311 let path = url.to_file_path().ok()?;
312 self.workspace_settings
313 .iter()
314 .filter(|workspace| path.starts_with(&workspace.root))
315 .max_by_key(|workspace| workspace.root.components().count())
316 }
317
318 fn resolve_shuck_settings(
319 &self,
320 file_path: Option<&Path>,
321 workspace_root: Option<&Path>,
322 option_layers: &[&ClientOptions],
323 ) -> Arc<ShuckSettings> {
324 if !self.cache_project_settings {
325 return Arc::new(ShuckSettings::resolve(
326 file_path,
327 self.workspace_roots(),
328 option_layers,
329 ));
330 }
331
332 let Some(file_path) = file_path else {
333 return Arc::new(ShuckSettings::resolve(
334 None,
335 self.workspace_roots(),
336 option_layers,
337 ));
338 };
339
340 let context = SettingsResolveContext::for_file(file_path, self.workspace_roots());
341 let cache_key = ProjectSettingsCacheKey {
342 config_root: context.config_root().to_path_buf(),
343 workspace_root: workspace_root.map(Path::to_path_buf),
344 };
345
346 if let Some(cached) = self
347 .project_settings_cache
348 .read()
349 .expect("settings cache lock should not be poisoned")
350 .get(&cache_key)
351 .cloned()
352 {
353 return Arc::new(cached.for_file(Some(file_path)));
354 }
355
356 let resolved = Arc::new(ResolvedProjectSettings::resolve(&context, option_layers));
357 let cached = self
358 .project_settings_cache
359 .write()
360 .expect("settings cache lock should not be poisoned")
361 .entry(cache_key)
362 .or_insert_with(|| resolved.clone())
363 .clone();
364
365 Arc::new(cached.for_file(Some(file_path)))
366 }
367
368 pub(super) fn update_workspace_options(&mut self, mut workspace_options: WorkspaceOptionsMap) {
369 for workspace in &mut self.workspace_settings {
370 workspace.options = workspace_options.remove(&workspace.url);
371 }
372 self.clear_project_settings_cache();
373 }
374
375 pub(super) fn open_document_count(&self) -> usize {
376 self.documents.len()
377 }
378}
379
380impl DocumentQuery {
381 pub(crate) fn file_url(&self) -> &Url {
382 match self {
383 Self::Text { file_url, .. } => file_url,
384 }
385 }
386
387 pub(crate) fn file_path(&self) -> Option<PathBuf> {
388 self.file_url().to_file_path().ok()
389 }
390
391 pub(crate) fn document(&self) -> &Arc<TextDocument> {
392 match self {
393 Self::Text { document, .. } => document,
394 }
395 }
396
397 pub(crate) fn language_id(&self) -> Option<crate::edit::LanguageId> {
398 self.document().language_id()
399 }
400
401 pub(crate) fn settings(&self) -> &ShuckSettings {
402 match self {
403 Self::Text { settings, .. } => settings,
404 }
405 }
406}