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