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