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