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