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