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)]
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}