Skip to main content

shuck_server/session/
index.rs

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/// Query handle for a document known to the server.
50#[derive(Clone)]
51pub enum DocumentQuery {
52    /// Open text document plus its resolved settings.
53    Text {
54        /// URL of the text document.
55        file_url: Url,
56        /// Current document contents and line index.
57        document: Arc<TextDocument>,
58        /// Resolved Shuck settings for the document.
59        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        // A global config change can affect any project without a local
198        // config, and cache keys carry no record of which fallback was used,
199        // so drop the whole cache rather than matching by config root.
200        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}