1#![allow(dead_code)]
2
3use std::path::Path;
4use std::sync::Arc;
5
6use lsp_types::{ClientCapabilities, FileEvent, Url};
7
8use crate::analysis::{DocumentAnalysis, DocumentAnalysisCache};
9use crate::edit::{DocumentKey, DocumentVersion};
10use crate::session::request_queue::RequestQueue;
11use crate::session::settings::GlobalClientSettings;
12use crate::workspace::Workspaces;
13use crate::{PositionEncoding, TextDocument};
14
15pub(crate) use self::capabilities::ResolvedClientCapabilities;
16pub use self::index::DocumentQuery;
17pub(crate) use self::index::WorkspaceSettingsSnapshot;
18pub(crate) use self::options::{AllOptions, WorkspaceOptionsMap};
19pub use self::options::{
20 ClientOptions, CompletionFeatureOptions, GlobalOptions, RenameFeatureOptions,
21 WorkspaceDiagnosticsFeatureOptions, WorkspaceSymbolFeatureOptions,
22};
23pub(crate) use self::request_queue::RequestCancellationToken;
24pub(crate) use self::settings::ClientSettings;
25pub(crate) use self::settings::ShuckSettings;
26pub use client::Client;
27
28mod capabilities;
29mod client;
30mod index;
31mod options;
32mod request_queue;
33mod settings;
34
35pub struct Session {
37 index: index::Index,
38 position_encoding: PositionEncoding,
39 global_settings: GlobalClientSettings,
40 resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
41 workspace_symbols: Arc<crate::symbols::WorkspaceSymbolIndex>,
42 workspace_diagnostics: Arc<crate::workspace_diagnostics::WorkspaceDiagnosticCache>,
43 analysis_cache: Arc<DocumentAnalysisCache>,
44 workspace_function_index: Arc<crate::workspace_functions::WorkspaceFunctionIndexCache>,
45 request_queue: RequestQueue,
46 shutdown_requested: bool,
47}
48
49#[derive(Clone)]
51pub struct DocumentSnapshot {
52 resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
53 client_settings: Arc<ClientSettings>,
54 document_ref: index::DocumentQuery,
55 position_encoding: PositionEncoding,
56 analysis_cache: Arc<DocumentAnalysisCache>,
57 analysis_settings_epoch: u64,
58}
59
60#[derive(Clone)]
61pub(crate) struct WorkspaceDocumentSnapshotFactory {
62 resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
63 position_encoding: PositionEncoding,
64 analysis_cache: Arc<DocumentAnalysisCache>,
65 analysis_settings_epoch: u64,
66}
67
68impl Session {
69 pub fn new(
71 client_capabilities: &ClientCapabilities,
72 position_encoding: PositionEncoding,
73 global: GlobalClientSettings,
74 workspaces: &Workspaces,
75 client: &Client,
76 ) -> crate::Result<Self> {
77 Ok(Self {
78 index: index::Index::new(workspaces, &global, client)?,
79 position_encoding,
80 global_settings: global,
81 resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
82 client_capabilities,
83 )),
84 workspace_symbols: Arc::new(crate::symbols::WorkspaceSymbolIndex::default()),
85 workspace_diagnostics: Arc::new(
86 crate::workspace_diagnostics::WorkspaceDiagnosticCache::default(),
87 ),
88 analysis_cache: Arc::new(DocumentAnalysisCache::new()),
89 workspace_function_index: Arc::new(
90 crate::workspace_functions::WorkspaceFunctionIndexCache::default(),
91 ),
92 request_queue: RequestQueue::new(),
93 shutdown_requested: false,
94 })
95 }
96
97 pub(crate) fn request_queue(&self) -> &RequestQueue {
98 &self.request_queue
99 }
100
101 pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue {
102 &mut self.request_queue
103 }
104
105 pub(crate) fn is_shutdown_requested(&self) -> bool {
106 self.shutdown_requested
107 }
108
109 pub(crate) fn set_shutdown_requested(&mut self, requested: bool) {
110 self.shutdown_requested = requested;
111 }
112
113 pub fn key_from_url(&self, url: Url) -> DocumentKey {
115 self.index.key_from_url(url)
116 }
117
118 pub fn take_snapshot(&self, url: Url) -> Option<DocumentSnapshot> {
120 let (settings, client_settings) = self
121 .index
122 .resolve_snapshot_settings(&url, self.global_settings.options());
123 let key = self.key_from_url(url);
124 Some(DocumentSnapshot {
125 resolved_client_capabilities: self.resolved_client_capabilities.clone(),
126 client_settings,
127 document_ref: self.index.make_document_ref(key, settings)?,
128 position_encoding: self.position_encoding,
129 analysis_cache: self.analysis_cache.clone(),
130 analysis_settings_epoch: self.analysis_cache.current_settings_epoch(),
131 })
132 }
133
134 pub(crate) fn update_text_document(
135 &mut self,
136 key: &DocumentKey,
137 content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
138 new_version: DocumentVersion,
139 ) -> crate::Result<()> {
140 let result =
141 self.index
142 .update_text_document(key, content_changes, new_version, self.encoding());
143 if result.is_ok() {
144 self.analysis_cache.invalidate_uri(&key.clone().into_url());
145 self.workspace_diagnostics
146 .invalidate_uri(&key.clone().into_url());
147 self.workspace_function_index.invalidate();
148 }
149 result
150 }
151
152 pub fn open_text_document(&mut self, url: Url, document: TextDocument) {
154 self.analysis_cache.invalidate_uri(&url);
155 self.workspace_diagnostics.invalidate_uri(&url);
156 self.workspace_function_index.invalidate();
157 self.index.open_text_document(url, document);
158 }
159
160 pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
161 self.index.close_document(key)?;
162 self.analysis_cache.invalidate_uri(&key.clone().into_url());
163 self.workspace_diagnostics
164 .invalidate_uri(&key.clone().into_url());
165 self.workspace_function_index.invalidate();
166 self.workspace_symbols
167 .invalidate_uri(&key.clone().into_url());
168 Ok(())
169 }
170
171 pub(crate) fn reload_settings(&mut self, changes: &[FileEvent], client: &Client) {
172 self.index.reload_settings(changes, client);
173 self.analysis_cache.clear();
174 self.workspace_diagnostics.invalidate_all();
175 self.workspace_function_index.invalidate();
176 self.workspace_symbols.invalidate_file_events(changes);
177 }
178
179 pub(crate) fn open_workspace_folder(&mut self, url: Url, client: &Client) -> crate::Result<()> {
180 self.index
181 .open_workspace_folder(url, &self.global_settings, client)?;
182 self.analysis_cache.clear();
183 self.workspace_diagnostics.invalidate_all();
184 self.workspace_function_index.invalidate();
185 self.workspace_symbols.invalidate_all();
186 Ok(())
187 }
188
189 pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> {
190 self.index.close_workspace_folder(url)?;
191 self.analysis_cache.clear();
192 self.workspace_diagnostics.invalidate_all();
193 self.workspace_function_index.invalidate();
194 self.workspace_symbols.invalidate_all();
195 Ok(())
196 }
197
198 pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
199 &self.resolved_client_capabilities
200 }
201
202 pub(crate) fn encoding(&self) -> PositionEncoding {
203 self.position_encoding
204 }
205
206 pub(crate) fn config_file_paths(&self) -> impl Iterator<Item = &Path> {
207 self.index.config_file_paths()
208 }
209
210 pub(crate) fn set_project_settings_cache_enabled(&mut self, enabled: bool) {
211 self.index.set_project_settings_cache_enabled(enabled);
212 }
213
214 pub(crate) fn update_client_options(&mut self, options: ClientOptions) {
215 self.analysis_cache.clear();
216 self.workspace_diagnostics.invalidate_all();
217 self.workspace_function_index.invalidate();
218 self.workspace_symbols.invalidate_all();
219 self.global_settings.update_options(options);
220 self.index.clear_project_settings_cache();
221 }
222
223 pub(crate) fn update_configuration(
224 &mut self,
225 options: ClientOptions,
226 workspace_options: Option<WorkspaceOptionsMap>,
227 ) {
228 self.analysis_cache.clear();
229 self.workspace_diagnostics.invalidate_all();
230 self.workspace_function_index.invalidate();
231 self.workspace_symbols.invalidate_all();
232 self.global_settings.update_options(options);
233 if let Some(workspace_options) = workspace_options {
234 self.index.update_workspace_options(workspace_options);
235 } else {
236 self.index.clear_project_settings_cache();
237 }
238 }
239
240 pub(crate) fn open_document_count(&self) -> usize {
241 self.index.open_document_count()
242 }
243
244 pub(crate) fn workspace_roots(&self) -> &[std::path::PathBuf] {
245 self.index.workspace_roots()
246 }
247
248 pub(crate) fn workspace_document_snapshot_factory(&self) -> WorkspaceDocumentSnapshotFactory {
249 WorkspaceDocumentSnapshotFactory {
250 resolved_client_capabilities: self.resolved_client_capabilities.clone(),
251 position_encoding: self.position_encoding,
252 analysis_cache: self.analysis_cache.clone(),
253 analysis_settings_epoch: self.analysis_cache.current_settings_epoch(),
254 }
255 }
256
257 pub(crate) fn workspace_diagnostic_context(
258 &self,
259 cancellation: RequestCancellationToken,
260 ) -> crate::workspace_diagnostics::WorkspaceDiagnosticContext {
261 let workspace_settings = self.index.workspace_settings_snapshot();
262 let workspace_roots = self.index.workspace_roots().to_vec();
263 let mut settings_workspace_roots = workspace_roots.clone();
264 for workspace in &workspace_settings {
265 let Some(canonical_root) = &workspace.canonical_root else {
266 continue;
267 };
268 if !settings_workspace_roots.contains(canonical_root) {
269 settings_workspace_roots.push(canonical_root.clone());
270 }
271 }
272 let open_documents = self
273 .index
274 .open_documents_snapshot()
275 .into_iter()
276 .filter_map(|document| {
277 let path = document.uri.to_file_path().ok()?;
278 let snapshot = self.take_snapshot(document.uri.clone())?;
279 Some(
280 crate::workspace_diagnostics::WorkspaceDiagnosticOpenDocument {
281 uri: document.uri,
282 path,
283 snapshot,
284 },
285 )
286 })
287 .collect();
288 crate::workspace_diagnostics::WorkspaceDiagnosticContext {
289 options: self.global_settings.options().server.workspace_diagnostics,
290 global_options: self.global_settings.options().clone(),
291 workspace_settings,
292 workspace_roots,
293 settings_workspace_roots,
294 open_documents,
295 snapshot_factory: self.workspace_document_snapshot_factory(),
296 cache: self.workspace_diagnostics.clone(),
297 cache_generation: self.workspace_diagnostics.generation(),
298 cancellation,
299 }
300 }
301
302 pub(crate) fn workspace_symbol_context(&self) -> crate::symbols::WorkspaceSymbolContext {
303 let workspace_settings = self.index.workspace_settings_snapshot();
304 let workspace_roots = self.index.workspace_roots().to_vec();
305 let mut settings_workspace_roots = workspace_roots.clone();
306 for workspace in &workspace_settings {
307 let Some(canonical_root) = &workspace.canonical_root else {
308 continue;
309 };
310 if !settings_workspace_roots
311 .iter()
312 .any(|root| root == canonical_root)
313 {
314 settings_workspace_roots.push(canonical_root.clone());
315 }
316 }
317
318 crate::symbols::WorkspaceSymbolContext {
319 index: self.workspace_symbols.clone(),
320 options: self.global_settings.workspace_symbol_options(),
321 global_options: self.global_settings.options().clone(),
322 workspace_settings,
323 workspace_roots,
324 settings_workspace_roots,
325 open_documents: self.index.open_documents_snapshot(),
326 encoding: self.position_encoding,
327 }
328 }
329
330 pub(crate) fn workspace_function_context(
332 &self,
333 cancellation: RequestCancellationToken,
334 ) -> crate::workspace_functions::WorkspaceFunctionContext {
335 let workspace_settings = self.index.workspace_settings_snapshot();
336 let workspace_roots = self.index.workspace_roots().to_vec();
337 let mut settings_workspace_roots = workspace_roots.clone();
338 for workspace in &workspace_settings {
339 let Some(canonical_root) = &workspace.canonical_root else {
340 continue;
341 };
342 if !settings_workspace_roots.contains(canonical_root) {
343 settings_workspace_roots.push(canonical_root.clone());
344 }
345 }
346 crate::workspace_functions::WorkspaceFunctionContext {
347 workspace_roots,
348 settings_workspace_roots,
349 workspace_settings,
350 global_options: self.global_settings.options().clone(),
351 open_documents: self.index.open_documents_snapshot(),
352 encoding: self.position_encoding,
353 max_files: self
354 .global_settings
355 .options()
356 .server
357 .call_hierarchy
358 .max_files,
359 epoch: self.workspace_function_index.current_epoch(),
360 cache: self.workspace_function_index.clone(),
361 cancellation,
362 }
363 }
364}
365
366impl DocumentSnapshot {
367 pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
368 &self.resolved_client_capabilities
369 }
370
371 pub(crate) fn client_settings(&self) -> &ClientSettings {
372 &self.client_settings
373 }
374
375 pub(crate) fn shuck_settings(&self) -> &ShuckSettings {
376 self.document_ref.settings()
377 }
378
379 pub fn query(&self) -> &index::DocumentQuery {
381 &self.document_ref
382 }
383
384 pub(crate) fn encoding(&self) -> PositionEncoding {
385 self.position_encoding
386 }
387
388 pub(crate) fn analysis(&self) -> Option<Arc<DocumentAnalysis>> {
389 self.analysis_cache.get_or_build(self)
390 }
391
392 pub(crate) fn analysis_settings_epoch(&self) -> u64 {
393 self.analysis_settings_epoch
394 }
395}
396
397impl WorkspaceDocumentSnapshotFactory {
398 pub(crate) fn snapshot(
399 &self,
400 uri: Url,
401 document: Arc<TextDocument>,
402 settings: Arc<ShuckSettings>,
403 client_settings: Arc<ClientSettings>,
404 ) -> DocumentSnapshot {
405 DocumentSnapshot {
406 resolved_client_capabilities: self.resolved_client_capabilities.clone(),
407 client_settings,
408 document_ref: DocumentQuery::Text {
409 file_url: uri,
410 document,
411 settings,
412 },
413 position_encoding: self.position_encoding,
414 analysis_cache: self.analysis_cache.clone(),
415 analysis_settings_epoch: self.analysis_settings_epoch,
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use crossbeam::channel;
423 use lsp_types::{
424 ClientCapabilities, DidChangeWatchedFilesClientCapabilities, FileChangeType, FileEvent,
425 TextDocumentContentChangeEvent, Url, WorkspaceClientCapabilities,
426 };
427
428 use super::*;
429 use crate::{ClientOptions, GlobalOptions, TextDocument, Workspace, Workspaces};
430
431 fn client_capabilities_with_dynamic_watched_files() -> ClientCapabilities {
432 ClientCapabilities {
433 workspace: Some(WorkspaceClientCapabilities {
434 did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
435 dynamic_registration: Some(true),
436 relative_pattern_support: None,
437 }),
438 ..WorkspaceClientCapabilities::default()
439 }),
440 ..ClientCapabilities::default()
441 }
442 }
443
444 fn make_test_session() -> (tempfile::TempDir, Session, Url) {
445 let workspace = tempfile::tempdir().expect("workspace should be created");
446 let workspace_uri =
447 Url::from_file_path(workspace.path()).expect("workspace path should convert");
448 let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
449 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
450 let (client_sender, _client_receiver) = channel::unbounded();
451 let client = Client::new(main_loop_sender, client_sender);
452 let global = GlobalOptions::default().into_settings(client.clone());
453 let mut session = Session::new(
454 &ClientCapabilities::default(),
455 PositionEncoding::UTF16,
456 global,
457 &workspaces,
458 &client,
459 )
460 .expect("test session should initialize");
461 let uri = Url::from_file_path(workspace.path().join("script.sh"))
462 .expect("script path should convert to a URL");
463 session.open_text_document(
464 uri.clone(),
465 TextDocument::new("#!/bin/bash\nname=value\necho \"$name\"\n".to_owned(), 1)
466 .with_language_id("shellscript"),
467 );
468 (workspace, session, uri)
469 }
470
471 #[test]
472 fn document_analysis_cache_reuses_same_document_version() {
473 let (_workspace, session, uri) = make_test_session();
474 let first = session
475 .take_snapshot(uri.clone())
476 .expect("test document should produce a snapshot")
477 .analysis()
478 .expect("shell document should have analysis");
479 let second = session
480 .take_snapshot(uri)
481 .expect("test document should produce a snapshot")
482 .analysis()
483 .expect("shell document should have analysis");
484
485 assert!(Arc::ptr_eq(&first, &second));
486 }
487
488 #[test]
489 fn document_analysis_cache_invalidates_after_document_change() {
490 let (_workspace, mut session, uri) = make_test_session();
491 let before = session
492 .take_snapshot(uri.clone())
493 .expect("test document should produce a snapshot")
494 .analysis()
495 .expect("shell document should have analysis");
496 let key = session.key_from_url(uri.clone());
497
498 session
499 .update_text_document(
500 &key,
501 vec![TextDocumentContentChangeEvent {
502 range: None,
503 range_length: None,
504 text: "#!/bin/bash\nother=value\necho \"$other\"\n".to_owned(),
505 }],
506 2,
507 )
508 .expect("document change should apply");
509
510 let after = session
511 .take_snapshot(uri)
512 .expect("test document should produce a snapshot")
513 .analysis()
514 .expect("shell document should have analysis");
515
516 assert!(!Arc::ptr_eq(&before, &after));
517 assert!(after.source().contains("other=value"));
518 }
519
520 #[test]
521 fn workspace_function_index_invalidates_after_closed_file_event() {
522 let (workspace, mut session, _uri) = make_test_session();
523 let before = session
524 .workspace_function_context(RequestCancellationToken::default())
525 .epoch;
526 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
527 let (client_sender, _client_receiver) = channel::unbounded();
528 let client = Client::new(main_loop_sender, client_sender);
529 let closed_uri = Url::from_file_path(workspace.path().join("closed.sh"))
530 .expect("closed file path should convert to a URL");
531
532 session.reload_settings(
533 &[FileEvent {
534 uri: closed_uri,
535 typ: FileChangeType::CHANGED,
536 }],
537 &client,
538 );
539
540 assert!(
541 session
542 .workspace_function_context(RequestCancellationToken::default())
543 .epoch
544 > before
545 );
546 }
547
548 #[test]
549 fn document_analysis_cache_invalidates_after_configuration_change() {
550 let (_workspace, mut session, uri) = make_test_session();
551 let stale_snapshot = session
552 .take_snapshot(uri.clone())
553 .expect("test document should produce a snapshot");
554 let before = stale_snapshot
555 .analysis()
556 .expect("shell document should have analysis");
557
558 session.update_client_options(ClientOptions {
559 lint: Some(shuck_config::LintConfig {
560 select: Some(vec!["C006".to_owned()]),
561 ..shuck_config::LintConfig::default()
562 }),
563 ..ClientOptions::default()
564 });
565
566 let stale_after_clear = stale_snapshot
567 .analysis()
568 .expect("stale snapshot can still analyze its own settings epoch");
569 let after = session
570 .take_snapshot(uri)
571 .expect("test document should produce a snapshot")
572 .analysis()
573 .expect("shell document should have analysis");
574
575 assert!(!Arc::ptr_eq(&before, &after));
576 assert!(!Arc::ptr_eq(&stale_after_clear, &after));
577 }
578
579 #[test]
580 fn take_snapshot_merges_global_and_workspace_options() {
581 let workspace_one = tempfile::tempdir().expect("workspace should be created");
582 let workspace_two = tempfile::tempdir().expect("workspace should be created");
583 let workspace_one_uri =
584 Url::from_file_path(workspace_one.path()).expect("workspace path should convert");
585 let workspace_two_uri =
586 Url::from_file_path(workspace_two.path()).expect("workspace path should convert");
587
588 let workspaces = Workspaces::new(vec![
589 Workspace::default(workspace_one_uri),
590 Workspace::new(workspace_two_uri.clone()).with_options(ClientOptions {
591 lint: Some(shuck_config::LintConfig {
592 select: Some(vec!["C006".to_owned()]),
593 ..shuck_config::LintConfig::default()
594 }),
595 format: Some(shuck_config::FormatConfig {
596 indent_width: Some(2),
597 ..shuck_config::FormatConfig::default()
598 }),
599 fix_all: Some(false),
600 ..ClientOptions::default()
601 }),
602 ]);
603 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
604 let (client_sender, _client_receiver) = channel::unbounded();
605 let client = Client::new(main_loop_sender, client_sender);
606 let global = GlobalOptions::default().into_settings(client.clone());
607 let mut session = Session::new(
608 &client_capabilities_with_dynamic_watched_files(),
609 PositionEncoding::UTF16,
610 global,
611 &workspaces,
612 &client,
613 )
614 .expect("test session should initialize");
615 session.set_project_settings_cache_enabled(true);
616 session.update_client_options(ClientOptions {
617 lint: Some(shuck_config::LintConfig {
618 select: Some(vec!["C001".to_owned()]),
619 ..shuck_config::LintConfig::default()
620 }),
621 format: Some(shuck_config::FormatConfig {
622 indent_style: Some("space".to_owned()),
623 ..shuck_config::FormatConfig::default()
624 }),
625 show_syntax_errors: Some(true),
626 ..ClientOptions::default()
627 });
628
629 let uri = Url::from_file_path(workspace_two.path().join("script.sh"))
630 .expect("test path should convert to a URL");
631 session.open_text_document(
632 uri.clone(),
633 TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
634 );
635
636 let snapshot = session
637 .take_snapshot(uri)
638 .expect("test document should produce a snapshot");
639
640 assert!(
641 snapshot
642 .shuck_settings()
643 .linter()
644 .rules
645 .contains(shuck_linter::Rule::UndefinedVariable)
646 );
647 assert_eq!(snapshot.shuck_settings().linter().rules.len(), 1);
648 assert_eq!(
649 snapshot.shuck_settings().formatter().indent_style(),
650 shuck_formatter::IndentStyle::Space
651 );
652 assert_eq!(snapshot.shuck_settings().formatter().indent_width(), 2);
653 assert!(!snapshot.client_settings().fix_all());
654 assert!(snapshot.client_settings().show_syntax_errors());
655 }
656
657 #[test]
658 fn update_configuration_updates_workspace_specific_options() {
659 let workspace_one = tempfile::tempdir().expect("workspace should be created");
660 let workspace_two = tempfile::tempdir().expect("workspace should be created");
661 let workspace_one_uri =
662 Url::from_file_path(workspace_one.path()).expect("workspace path should convert");
663 let workspace_two_uri =
664 Url::from_file_path(workspace_two.path()).expect("workspace path should convert");
665
666 let workspaces = Workspaces::new(vec![
667 Workspace::default(workspace_one_uri),
668 Workspace::new(workspace_two_uri.clone()).with_options(ClientOptions {
669 lint: Some(shuck_config::LintConfig {
670 select: Some(vec!["C006".to_owned()]),
671 ..shuck_config::LintConfig::default()
672 }),
673 ..ClientOptions::default()
674 }),
675 ]);
676 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
677 let (client_sender, _client_receiver) = channel::unbounded();
678 let client = Client::new(main_loop_sender, client_sender);
679 let global = GlobalOptions::default().into_settings(client.clone());
680 let mut session = Session::new(
681 &client_capabilities_with_dynamic_watched_files(),
682 PositionEncoding::UTF16,
683 global,
684 &workspaces,
685 &client,
686 )
687 .expect("test session should initialize");
688 session.set_project_settings_cache_enabled(true);
689
690 let uri = Url::from_file_path(workspace_two.path().join("script.sh"))
691 .expect("test path should convert to a URL");
692 session.open_text_document(
693 uri.clone(),
694 TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
695 );
696
697 let before = session
698 .take_snapshot(uri.clone())
699 .expect("test document should produce a snapshot");
700 assert!(
701 before
702 .shuck_settings()
703 .linter()
704 .rules
705 .contains(shuck_linter::Rule::UndefinedVariable)
706 );
707 assert_eq!(before.shuck_settings().linter().rules.len(), 1);
708
709 let mut workspace_options = WorkspaceOptionsMap::default();
710 workspace_options.insert(
711 workspace_two_uri,
712 ClientOptions {
713 lint: Some(shuck_config::LintConfig {
714 select: Some(vec!["C001".to_owned()]),
715 ..shuck_config::LintConfig::default()
716 }),
717 ..ClientOptions::default()
718 },
719 );
720 session.update_configuration(ClientOptions::default(), Some(workspace_options));
721
722 let after = session
723 .take_snapshot(uri)
724 .expect("test document should produce a snapshot");
725 assert!(
726 after
727 .shuck_settings()
728 .linter()
729 .rules
730 .contains(shuck_linter::Rule::UnusedAssignment)
731 );
732 assert_eq!(after.shuck_settings().linter().rules.len(), 1);
733 }
734
735 #[test]
736 fn update_client_options_invalidates_cached_project_settings() {
737 let workspace = tempfile::tempdir().expect("workspace should be created");
738 std::fs::write(
739 workspace.path().join(".shuck.toml"),
740 "[lint]\nselect = ['C001']\n",
741 )
742 .expect("config should be written");
743 let workspace_uri =
744 Url::from_file_path(workspace.path()).expect("workspace path should convert");
745 let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
746 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
747 let (client_sender, _client_receiver) = channel::unbounded();
748 let client = Client::new(main_loop_sender, client_sender);
749 let global = GlobalOptions::default().into_settings(client.clone());
750 let mut session = Session::new(
751 &client_capabilities_with_dynamic_watched_files(),
752 PositionEncoding::UTF16,
753 global,
754 &workspaces,
755 &client,
756 )
757 .expect("test session should initialize");
758 session.set_project_settings_cache_enabled(true);
759
760 let uri = Url::from_file_path(workspace.path().join("script.sh"))
761 .expect("test path should convert to a URL");
762 session.open_text_document(
763 uri.clone(),
764 TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
765 );
766
767 let before = session
768 .take_snapshot(uri.clone())
769 .expect("test document should produce a snapshot");
770 assert!(
771 before
772 .shuck_settings()
773 .linter()
774 .rules
775 .contains(shuck_linter::Rule::UnusedAssignment)
776 );
777 assert_eq!(before.shuck_settings().linter().rules.len(), 1);
778
779 session.update_client_options(ClientOptions {
780 lint: Some(shuck_config::LintConfig {
781 select: Some(vec!["C006".to_owned()]),
782 ..shuck_config::LintConfig::default()
783 }),
784 ..ClientOptions::default()
785 });
786
787 let after = session
788 .take_snapshot(uri)
789 .expect("test document should produce a snapshot");
790 assert!(
791 after
792 .shuck_settings()
793 .linter()
794 .rules
795 .contains(shuck_linter::Rule::UndefinedVariable)
796 );
797 assert_eq!(after.shuck_settings().linter().rules.len(), 1);
798 }
799
800 #[test]
801 fn nested_config_creation_switches_to_a_new_cache_key() {
802 let workspace = tempfile::tempdir().expect("workspace should be created");
803 std::fs::write(
804 workspace.path().join(".shuck.toml"),
805 "[lint]\nselect = ['C001']\n",
806 )
807 .expect("config should be written");
808 let nested = workspace.path().join("nested");
809 std::fs::create_dir_all(&nested).expect("nested dir should be created");
810 let workspace_uri =
811 Url::from_file_path(workspace.path()).expect("workspace path should convert");
812 let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
813 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
814 let (client_sender, _client_receiver) = channel::unbounded();
815 let client = Client::new(main_loop_sender, client_sender);
816 let global = GlobalOptions::default().into_settings(client.clone());
817 let mut session = Session::new(
818 &client_capabilities_with_dynamic_watched_files(),
819 PositionEncoding::UTF16,
820 global,
821 &workspaces,
822 &client,
823 )
824 .expect("test session should initialize");
825 session.set_project_settings_cache_enabled(true);
826
827 let uri = Url::from_file_path(nested.join("script.sh"))
828 .expect("test path should convert to a URL");
829 session.open_text_document(
830 uri.clone(),
831 TextDocument::new("foo=1\n".to_owned(), 1).with_language_id("shellscript"),
832 );
833
834 let before = session
835 .take_snapshot(uri.clone())
836 .expect("test document should produce a snapshot");
837 assert_eq!(
838 before.shuck_settings().project_root(),
839 Some(workspace.path())
840 );
841 assert!(
842 before
843 .shuck_settings()
844 .linter()
845 .rules
846 .contains(shuck_linter::Rule::UnusedAssignment)
847 );
848
849 std::fs::write(nested.join(".shuck.toml"), "[lint]\nselect = ['C006']\n")
850 .expect("nested config should be written");
851
852 let after = session
853 .take_snapshot(uri)
854 .expect("test document should produce a snapshot");
855 assert_eq!(
856 after.shuck_settings().project_root(),
857 Some(nested.as_path())
858 );
859 assert!(
860 after
861 .shuck_settings()
862 .linter()
863 .rules
864 .contains(shuck_linter::Rule::UndefinedVariable)
865 );
866 assert_eq!(after.shuck_settings().linter().rules.len(), 1);
867 }
868}