Skip to main content

shuck_server/
session.rs

1#![allow(dead_code)]
2
3use std::path::Path;
4use std::sync::Arc;
5
6use lsp_types::{ClientCapabilities, FileEvent, Url};
7
8use crate::edit::{DocumentKey, DocumentVersion};
9use crate::session::request_queue::RequestQueue;
10use crate::session::settings::{ClientSettings, GlobalClientSettings, ShuckSettings};
11use crate::workspace::Workspaces;
12use crate::{PositionEncoding, TextDocument};
13
14pub(crate) use self::capabilities::ResolvedClientCapabilities;
15pub use self::index::DocumentQuery;
16pub(crate) use self::options::{AllOptions, WorkspaceOptionsMap};
17pub use self::options::{ClientOptions, GlobalOptions};
18pub use client::Client;
19
20mod capabilities;
21mod client;
22mod index;
23mod options;
24mod request_queue;
25mod settings;
26
27pub struct Session {
28    index: index::Index,
29    position_encoding: PositionEncoding,
30    global_settings: GlobalClientSettings,
31    resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
32    request_queue: RequestQueue,
33    shutdown_requested: bool,
34}
35
36#[derive(Clone)]
37pub struct DocumentSnapshot {
38    resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
39    client_settings: Arc<ClientSettings>,
40    document_ref: index::DocumentQuery,
41    position_encoding: PositionEncoding,
42}
43
44impl Session {
45    pub fn new(
46        client_capabilities: &ClientCapabilities,
47        position_encoding: PositionEncoding,
48        global: GlobalClientSettings,
49        workspaces: &Workspaces,
50        client: &Client,
51    ) -> crate::Result<Self> {
52        Ok(Self {
53            index: index::Index::new(workspaces, &global, client)?,
54            position_encoding,
55            global_settings: global,
56            resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
57                client_capabilities,
58            )),
59            request_queue: RequestQueue::new(),
60            shutdown_requested: false,
61        })
62    }
63
64    pub(crate) fn request_queue(&self) -> &RequestQueue {
65        &self.request_queue
66    }
67
68    pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue {
69        &mut self.request_queue
70    }
71
72    pub(crate) fn is_shutdown_requested(&self) -> bool {
73        self.shutdown_requested
74    }
75
76    pub(crate) fn set_shutdown_requested(&mut self, requested: bool) {
77        self.shutdown_requested = requested;
78    }
79
80    pub fn key_from_url(&self, url: Url) -> DocumentKey {
81        self.index.key_from_url(url)
82    }
83
84    pub fn take_snapshot(&self, url: Url) -> Option<DocumentSnapshot> {
85        let (settings, client_settings) = self
86            .index
87            .resolve_snapshot_settings(&url, self.global_settings.options());
88        let key = self.key_from_url(url);
89        Some(DocumentSnapshot {
90            resolved_client_capabilities: self.resolved_client_capabilities.clone(),
91            client_settings,
92            document_ref: self.index.make_document_ref(key, settings)?,
93            position_encoding: self.position_encoding,
94        })
95    }
96
97    pub(crate) fn update_text_document(
98        &mut self,
99        key: &DocumentKey,
100        content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
101        new_version: DocumentVersion,
102    ) -> crate::Result<()> {
103        self.index
104            .update_text_document(key, content_changes, new_version, self.encoding())
105    }
106
107    pub fn open_text_document(&mut self, url: Url, document: TextDocument) {
108        self.index.open_text_document(url, document);
109    }
110
111    pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
112        self.index.close_document(key)
113    }
114
115    pub(crate) fn reload_settings(&mut self, changes: &[FileEvent], client: &Client) {
116        self.index.reload_settings(changes, client);
117    }
118
119    pub(crate) fn open_workspace_folder(&mut self, url: Url, client: &Client) -> crate::Result<()> {
120        self.index
121            .open_workspace_folder(url, &self.global_settings, client)
122    }
123
124    pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> {
125        self.index.close_workspace_folder(url)
126    }
127
128    pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
129        &self.resolved_client_capabilities
130    }
131
132    pub(crate) fn encoding(&self) -> PositionEncoding {
133        self.position_encoding
134    }
135
136    pub(crate) fn config_file_paths(&self) -> impl Iterator<Item = &Path> {
137        self.index.config_file_paths()
138    }
139
140    pub(crate) fn set_project_settings_cache_enabled(&mut self, enabled: bool) {
141        self.index.set_project_settings_cache_enabled(enabled);
142    }
143
144    pub(crate) fn update_client_options(&mut self, options: ClientOptions) {
145        self.global_settings.update_options(options);
146        self.index.clear_project_settings_cache();
147    }
148
149    pub(crate) fn update_configuration(
150        &mut self,
151        options: ClientOptions,
152        workspace_options: Option<WorkspaceOptionsMap>,
153    ) {
154        self.global_settings.update_options(options);
155        if let Some(workspace_options) = workspace_options {
156            self.index.update_workspace_options(workspace_options);
157        } else {
158            self.index.clear_project_settings_cache();
159        }
160    }
161
162    pub(crate) fn open_document_count(&self) -> usize {
163        self.index.open_document_count()
164    }
165
166    pub(crate) fn workspace_roots(&self) -> &[std::path::PathBuf] {
167        self.index.workspace_roots()
168    }
169}
170
171impl DocumentSnapshot {
172    pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
173        &self.resolved_client_capabilities
174    }
175
176    pub(crate) fn client_settings(&self) -> &ClientSettings {
177        &self.client_settings
178    }
179
180    pub(crate) fn shuck_settings(&self) -> &ShuckSettings {
181        self.document_ref.settings()
182    }
183
184    pub fn query(&self) -> &index::DocumentQuery {
185        &self.document_ref
186    }
187
188    pub(crate) fn encoding(&self) -> PositionEncoding {
189        self.position_encoding
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use crossbeam::channel;
196    use lsp_types::{
197        ClientCapabilities, DidChangeWatchedFilesClientCapabilities, Url,
198        WorkspaceClientCapabilities,
199    };
200
201    use super::*;
202    use crate::{ClientOptions, GlobalOptions, TextDocument, Workspace, Workspaces};
203
204    fn client_capabilities_with_dynamic_watched_files() -> ClientCapabilities {
205        ClientCapabilities {
206            workspace: Some(WorkspaceClientCapabilities {
207                did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
208                    dynamic_registration: Some(true),
209                    relative_pattern_support: None,
210                }),
211                ..WorkspaceClientCapabilities::default()
212            }),
213            ..ClientCapabilities::default()
214        }
215    }
216
217    #[test]
218    fn take_snapshot_merges_global_and_workspace_options() {
219        let workspace_one = tempfile::tempdir().expect("workspace should be created");
220        let workspace_two = tempfile::tempdir().expect("workspace should be created");
221        let workspace_one_uri =
222            Url::from_file_path(workspace_one.path()).expect("workspace path should convert");
223        let workspace_two_uri =
224            Url::from_file_path(workspace_two.path()).expect("workspace path should convert");
225
226        let workspaces = Workspaces::new(vec![
227            Workspace::default(workspace_one_uri),
228            Workspace::new(workspace_two_uri.clone()).with_options(ClientOptions {
229                lint: Some(shuck_config::LintConfig {
230                    select: Some(vec!["C006".to_owned()]),
231                    ..shuck_config::LintConfig::default()
232                }),
233                format: Some(shuck_config::FormatConfig {
234                    indent_width: Some(2),
235                    ..shuck_config::FormatConfig::default()
236                }),
237                fix_all: Some(false),
238                ..ClientOptions::default()
239            }),
240        ]);
241        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
242        let (client_sender, _client_receiver) = channel::unbounded();
243        let client = Client::new(main_loop_sender, client_sender);
244        let global = GlobalOptions::default().into_settings(client.clone());
245        let mut session = Session::new(
246            &client_capabilities_with_dynamic_watched_files(),
247            PositionEncoding::UTF16,
248            global,
249            &workspaces,
250            &client,
251        )
252        .expect("test session should initialize");
253        session.set_project_settings_cache_enabled(true);
254        session.update_client_options(ClientOptions {
255            lint: Some(shuck_config::LintConfig {
256                select: Some(vec!["C001".to_owned()]),
257                ..shuck_config::LintConfig::default()
258            }),
259            format: Some(shuck_config::FormatConfig {
260                indent_style: Some("space".to_owned()),
261                ..shuck_config::FormatConfig::default()
262            }),
263            show_syntax_errors: Some(true),
264            ..ClientOptions::default()
265        });
266
267        let uri = Url::from_file_path(workspace_two.path().join("script.sh"))
268            .expect("test path should convert to a URL");
269        session.open_text_document(
270            uri.clone(),
271            TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
272        );
273
274        let snapshot = session
275            .take_snapshot(uri)
276            .expect("test document should produce a snapshot");
277
278        assert!(
279            snapshot
280                .shuck_settings()
281                .linter()
282                .rules
283                .contains(shuck_linter::Rule::UndefinedVariable)
284        );
285        assert_eq!(snapshot.shuck_settings().linter().rules.len(), 1);
286        assert_eq!(
287            snapshot.shuck_settings().formatter().indent_style(),
288            shuck_formatter::IndentStyle::Space
289        );
290        assert_eq!(snapshot.shuck_settings().formatter().indent_width(), 2);
291        assert!(!snapshot.client_settings().fix_all());
292        assert!(snapshot.client_settings().show_syntax_errors());
293    }
294
295    #[test]
296    fn update_configuration_updates_workspace_specific_options() {
297        let workspace_one = tempfile::tempdir().expect("workspace should be created");
298        let workspace_two = tempfile::tempdir().expect("workspace should be created");
299        let workspace_one_uri =
300            Url::from_file_path(workspace_one.path()).expect("workspace path should convert");
301        let workspace_two_uri =
302            Url::from_file_path(workspace_two.path()).expect("workspace path should convert");
303
304        let workspaces = Workspaces::new(vec![
305            Workspace::default(workspace_one_uri),
306            Workspace::new(workspace_two_uri.clone()).with_options(ClientOptions {
307                lint: Some(shuck_config::LintConfig {
308                    select: Some(vec!["C006".to_owned()]),
309                    ..shuck_config::LintConfig::default()
310                }),
311                ..ClientOptions::default()
312            }),
313        ]);
314        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
315        let (client_sender, _client_receiver) = channel::unbounded();
316        let client = Client::new(main_loop_sender, client_sender);
317        let global = GlobalOptions::default().into_settings(client.clone());
318        let mut session = Session::new(
319            &client_capabilities_with_dynamic_watched_files(),
320            PositionEncoding::UTF16,
321            global,
322            &workspaces,
323            &client,
324        )
325        .expect("test session should initialize");
326        session.set_project_settings_cache_enabled(true);
327
328        let uri = Url::from_file_path(workspace_two.path().join("script.sh"))
329            .expect("test path should convert to a URL");
330        session.open_text_document(
331            uri.clone(),
332            TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
333        );
334
335        let before = session
336            .take_snapshot(uri.clone())
337            .expect("test document should produce a snapshot");
338        assert!(
339            before
340                .shuck_settings()
341                .linter()
342                .rules
343                .contains(shuck_linter::Rule::UndefinedVariable)
344        );
345        assert_eq!(before.shuck_settings().linter().rules.len(), 1);
346
347        let mut workspace_options = WorkspaceOptionsMap::default();
348        workspace_options.insert(
349            workspace_two_uri,
350            ClientOptions {
351                lint: Some(shuck_config::LintConfig {
352                    select: Some(vec!["C001".to_owned()]),
353                    ..shuck_config::LintConfig::default()
354                }),
355                ..ClientOptions::default()
356            },
357        );
358        session.update_configuration(ClientOptions::default(), Some(workspace_options));
359
360        let after = session
361            .take_snapshot(uri)
362            .expect("test document should produce a snapshot");
363        assert!(
364            after
365                .shuck_settings()
366                .linter()
367                .rules
368                .contains(shuck_linter::Rule::UnusedAssignment)
369        );
370        assert_eq!(after.shuck_settings().linter().rules.len(), 1);
371    }
372
373    #[test]
374    fn update_client_options_invalidates_cached_project_settings() {
375        let workspace = tempfile::tempdir().expect("workspace should be created");
376        std::fs::write(
377            workspace.path().join(".shuck.toml"),
378            "[lint]\nselect = ['C001']\n",
379        )
380        .expect("config should be written");
381        let workspace_uri =
382            Url::from_file_path(workspace.path()).expect("workspace path should convert");
383        let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
384        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
385        let (client_sender, _client_receiver) = channel::unbounded();
386        let client = Client::new(main_loop_sender, client_sender);
387        let global = GlobalOptions::default().into_settings(client.clone());
388        let mut session = Session::new(
389            &client_capabilities_with_dynamic_watched_files(),
390            PositionEncoding::UTF16,
391            global,
392            &workspaces,
393            &client,
394        )
395        .expect("test session should initialize");
396        session.set_project_settings_cache_enabled(true);
397
398        let uri = Url::from_file_path(workspace.path().join("script.sh"))
399            .expect("test path should convert to a URL");
400        session.open_text_document(
401            uri.clone(),
402            TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
403        );
404
405        let before = session
406            .take_snapshot(uri.clone())
407            .expect("test document should produce a snapshot");
408        assert!(
409            before
410                .shuck_settings()
411                .linter()
412                .rules
413                .contains(shuck_linter::Rule::UnusedAssignment)
414        );
415        assert_eq!(before.shuck_settings().linter().rules.len(), 1);
416
417        session.update_client_options(ClientOptions {
418            lint: Some(shuck_config::LintConfig {
419                select: Some(vec!["C006".to_owned()]),
420                ..shuck_config::LintConfig::default()
421            }),
422            ..ClientOptions::default()
423        });
424
425        let after = session
426            .take_snapshot(uri)
427            .expect("test document should produce a snapshot");
428        assert!(
429            after
430                .shuck_settings()
431                .linter()
432                .rules
433                .contains(shuck_linter::Rule::UndefinedVariable)
434        );
435        assert_eq!(after.shuck_settings().linter().rules.len(), 1);
436    }
437
438    #[test]
439    fn nested_config_creation_switches_to_a_new_cache_key() {
440        let workspace = tempfile::tempdir().expect("workspace should be created");
441        std::fs::write(
442            workspace.path().join(".shuck.toml"),
443            "[lint]\nselect = ['C001']\n",
444        )
445        .expect("config should be written");
446        let nested = workspace.path().join("nested");
447        std::fs::create_dir_all(&nested).expect("nested dir should be created");
448        let workspace_uri =
449            Url::from_file_path(workspace.path()).expect("workspace path should convert");
450        let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
451        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
452        let (client_sender, _client_receiver) = channel::unbounded();
453        let client = Client::new(main_loop_sender, client_sender);
454        let global = GlobalOptions::default().into_settings(client.clone());
455        let mut session = Session::new(
456            &client_capabilities_with_dynamic_watched_files(),
457            PositionEncoding::UTF16,
458            global,
459            &workspaces,
460            &client,
461        )
462        .expect("test session should initialize");
463        session.set_project_settings_cache_enabled(true);
464
465        let uri = Url::from_file_path(nested.join("script.sh"))
466            .expect("test path should convert to a URL");
467        session.open_text_document(
468            uri.clone(),
469            TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
470        );
471
472        let before = session
473            .take_snapshot(uri.clone())
474            .expect("test document should produce a snapshot");
475        assert_eq!(
476            before.shuck_settings().project_root(),
477            Some(workspace.path())
478        );
479        assert!(
480            before
481                .shuck_settings()
482                .linter()
483                .rules
484                .contains(shuck_linter::Rule::UnusedAssignment)
485        );
486
487        std::fs::write(nested.join(".shuck.toml"), "[lint]\nselect = ['C006']\n")
488            .expect("nested config should be written");
489
490        let after = session
491            .take_snapshot(uri)
492            .expect("test document should produce a snapshot");
493        assert_eq!(
494            after.shuck_settings().project_root(),
495            Some(nested.as_path())
496        );
497        assert!(
498            after
499                .shuck_settings()
500                .linter()
501                .rules
502                .contains(shuck_linter::Rule::UndefinedVariable)
503        );
504        assert_eq!(after.shuck_settings().linter().rules.len(), 1);
505    }
506}