1use lsp_types::Url;
2use rustc_hash::FxHashMap;
3use serde::{Deserialize, Deserializer};
4use shuck_config::{FormatConfig, LintConfig, ShuckConfig};
5
6use crate::session::settings::GlobalClientSettings;
7use crate::{Client, logging};
8
9pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>;
10
11#[derive(Debug, Deserialize, Default)]
13#[serde(rename_all = "camelCase")]
14pub struct GlobalOptions {
15 #[serde(flatten)]
16 client: ClientOptions,
17 #[serde(default)]
18 pub(crate) tracing: TracingOptions,
19}
20
21impl GlobalOptions {
22 pub fn into_settings(self, client: Client) -> GlobalClientSettings {
24 GlobalClientSettings::new(self.client, client)
25 }
26}
27
28#[derive(Clone, Debug, Default, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ClientOptions {
32 #[serde(default)]
33 pub lint: Option<LintConfig>,
35 #[serde(default)]
36 pub format: Option<FormatConfig>,
38 #[serde(default)]
39 pub fix_all: Option<bool>,
41 #[serde(default)]
42 pub unsafe_fixes: Option<bool>,
44 #[serde(default)]
45 pub show_syntax_errors: Option<bool>,
47 #[serde(default)]
48 pub server: ServerOptions,
50}
51
52impl ClientOptions {
53 pub(crate) fn to_config_overrides(&self) -> ShuckConfig {
54 ShuckConfig {
55 lint: self.lint.clone().unwrap_or_default(),
56 format: self.format.clone().unwrap_or_default(),
57 ..ShuckConfig::default()
58 }
59 }
60}
61
62#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct ServerOptions {
65 pub workspace_symbols: WorkspaceSymbolFeatureOptions,
67 pub completion: CompletionFeatureOptions,
69 pub rename: RenameFeatureOptions,
71 pub call_hierarchy: CallHierarchyFeatureOptions,
73 pub workspace_diagnostics: WorkspaceDiagnosticsFeatureOptions,
75 workspace_symbols_overrides: WorkspaceSymbolFeatureOptionsOverrides,
76 completion_overrides: CompletionFeatureOptionsOverrides,
77 rename_overrides: RenameFeatureOptionsOverrides,
78 call_hierarchy_overrides: CallHierarchyFeatureOptionsOverrides,
79 workspace_diagnostics_overrides: WorkspaceDiagnosticsFeatureOptionsOverrides,
80}
81
82impl ServerOptions {
83 pub(crate) fn workspace_symbols_layered_over(
84 &self,
85 base: WorkspaceSymbolFeatureOptions,
86 ) -> WorkspaceSymbolFeatureOptions {
87 if self.workspace_symbols_overrides.has_overrides() {
88 self.workspace_symbols_overrides.apply_to(base)
89 } else if self.workspace_symbols != WorkspaceSymbolFeatureOptions::default() {
90 self.workspace_symbols
91 } else {
92 base
93 }
94 }
95
96 pub(crate) fn completion_layered_over(
97 &self,
98 base: CompletionFeatureOptions,
99 ) -> CompletionFeatureOptions {
100 if self.completion_overrides.has_overrides() {
101 self.completion_overrides.apply_to(base)
102 } else if self.completion != CompletionFeatureOptions::default() {
103 self.completion
104 } else {
105 base
106 }
107 }
108
109 pub(crate) fn rename_layered_over(&self, base: RenameFeatureOptions) -> RenameFeatureOptions {
110 if self.rename_overrides.has_overrides() {
111 self.rename_overrides.apply_to(base)
112 } else if self.rename != RenameFeatureOptions::default() {
113 self.rename
114 } else {
115 base
116 }
117 }
118
119 pub(crate) fn call_hierarchy_layered_over(
120 &self,
121 base: CallHierarchyFeatureOptions,
122 ) -> CallHierarchyFeatureOptions {
123 if self.call_hierarchy_overrides.has_overrides() {
124 self.call_hierarchy_overrides.apply_to(base)
125 } else if self.call_hierarchy != CallHierarchyFeatureOptions::default() {
126 self.call_hierarchy
127 } else {
128 base
129 }
130 }
131
132 pub(crate) fn workspace_diagnostics_layered_over(
133 &self,
134 base: WorkspaceDiagnosticsFeatureOptions,
135 ) -> WorkspaceDiagnosticsFeatureOptions {
136 if self.workspace_diagnostics_overrides.has_overrides() {
137 self.workspace_diagnostics_overrides.apply_to(base)
138 } else if self.workspace_diagnostics != WorkspaceDiagnosticsFeatureOptions::default() {
139 self.workspace_diagnostics
140 } else {
141 base
142 }
143 }
144}
145
146impl<'de> Deserialize<'de> for ServerOptions {
147 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148 where
149 D: Deserializer<'de>,
150 {
151 #[derive(Deserialize, Default)]
152 #[serde(rename_all = "camelCase")]
153 struct RawServerOptions {
154 #[serde(default)]
155 workspace_symbols: WorkspaceSymbolFeatureOptionsOverrides,
156 #[serde(default)]
157 completion: CompletionFeatureOptionsOverrides,
158 #[serde(default)]
159 rename: RenameFeatureOptionsOverrides,
160 #[serde(default)]
161 call_hierarchy: CallHierarchyFeatureOptionsOverrides,
162 #[serde(default)]
163 workspace_diagnostics: WorkspaceDiagnosticsFeatureOptionsOverrides,
164 }
165
166 let raw = RawServerOptions::deserialize(deserializer)?;
167 Ok(Self {
168 workspace_symbols: raw
169 .workspace_symbols
170 .apply_to(WorkspaceSymbolFeatureOptions::default()),
171 completion: raw.completion.apply_to(CompletionFeatureOptions::default()),
172 rename: raw.rename.apply_to(RenameFeatureOptions::default()),
173 call_hierarchy: raw
174 .call_hierarchy
175 .apply_to(CallHierarchyFeatureOptions::default()),
176 workspace_diagnostics: raw
177 .workspace_diagnostics
178 .apply_to(WorkspaceDiagnosticsFeatureOptions::default()),
179 workspace_symbols_overrides: raw.workspace_symbols,
180 completion_overrides: raw.completion,
181 rename_overrides: raw.rename,
182 call_hierarchy_overrides: raw.call_hierarchy,
183 workspace_diagnostics_overrides: raw.workspace_diagnostics,
184 })
185 }
186}
187
188#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
189#[serde(rename_all = "camelCase")]
190struct WorkspaceSymbolFeatureOptionsOverrides {
191 #[serde(default)]
192 enabled: Option<bool>,
193 #[serde(default)]
194 max_files: Option<usize>,
195}
196
197impl WorkspaceSymbolFeatureOptionsOverrides {
198 fn has_overrides(self) -> bool {
199 self.enabled.is_some() || self.max_files.is_some()
200 }
201
202 fn apply_to(self, base: WorkspaceSymbolFeatureOptions) -> WorkspaceSymbolFeatureOptions {
203 WorkspaceSymbolFeatureOptions {
204 enabled: self.enabled.unwrap_or(base.enabled),
205 max_files: self.max_files.unwrap_or(base.max_files),
206 }
207 }
208}
209
210#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
212#[serde(rename_all = "camelCase")]
213pub struct WorkspaceSymbolFeatureOptions {
214 #[serde(default = "default_workspace_symbols_enabled")]
216 pub enabled: bool,
217 #[serde(default = "default_workspace_symbols_max_files")]
219 pub max_files: usize,
220}
221
222#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
223#[serde(rename_all = "camelCase")]
224struct CompletionFeatureOptionsOverrides {
225 #[serde(default)]
226 include_runtime_names: Option<bool>,
227 #[serde(default)]
228 include_keywords: Option<bool>,
229}
230
231impl CompletionFeatureOptionsOverrides {
232 fn has_overrides(self) -> bool {
233 self.include_runtime_names.is_some() || self.include_keywords.is_some()
234 }
235
236 fn apply_to(self, base: CompletionFeatureOptions) -> CompletionFeatureOptions {
237 CompletionFeatureOptions {
238 include_runtime_names: self
239 .include_runtime_names
240 .unwrap_or(base.include_runtime_names),
241 include_keywords: self.include_keywords.unwrap_or(base.include_keywords),
242 }
243 }
244}
245
246#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "camelCase")]
249pub struct CompletionFeatureOptions {
250 #[serde(default = "default_completion_include_runtime_names")]
252 pub include_runtime_names: bool,
253 #[serde(default = "default_completion_include_keywords")]
255 pub include_keywords: bool,
256}
257
258impl Default for CompletionFeatureOptions {
259 fn default() -> Self {
260 Self {
261 include_runtime_names: true,
262 include_keywords: true,
263 }
264 }
265}
266
267#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "camelCase")]
269struct RenameFeatureOptionsOverrides {
270 #[serde(default)]
271 allow_cross_file: Option<bool>,
272}
273
274impl RenameFeatureOptionsOverrides {
275 fn has_overrides(self) -> bool {
276 self.allow_cross_file.is_some()
277 }
278
279 fn apply_to(self, base: RenameFeatureOptions) -> RenameFeatureOptions {
280 RenameFeatureOptions {
281 allow_cross_file: self.allow_cross_file.unwrap_or(base.allow_cross_file),
282 }
283 }
284}
285
286#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
288#[serde(rename_all = "camelCase")]
289pub struct RenameFeatureOptions {
290 #[serde(default = "default_cross_file_rename_enabled")]
292 pub allow_cross_file: bool,
293}
294
295impl Default for RenameFeatureOptions {
296 fn default() -> Self {
297 Self {
298 allow_cross_file: default_cross_file_rename_enabled(),
299 }
300 }
301}
302
303fn default_cross_file_rename_enabled() -> bool {
304 true
305}
306
307impl Default for WorkspaceSymbolFeatureOptions {
308 fn default() -> Self {
309 Self {
310 enabled: true,
311 max_files: 5000,
312 }
313 }
314}
315
316fn default_workspace_symbols_enabled() -> bool {
317 true
318}
319
320#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
321#[serde(rename_all = "camelCase")]
322struct CallHierarchyFeatureOptionsOverrides {
323 #[serde(default)]
324 max_files: Option<usize>,
325}
326
327impl CallHierarchyFeatureOptionsOverrides {
328 fn has_overrides(self) -> bool {
329 self.max_files.is_some()
330 }
331
332 fn apply_to(self, base: CallHierarchyFeatureOptions) -> CallHierarchyFeatureOptions {
333 CallHierarchyFeatureOptions {
334 max_files: self.max_files.unwrap_or(base.max_files),
335 }
336 }
337}
338
339#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
341#[serde(rename_all = "camelCase")]
342pub struct CallHierarchyFeatureOptions {
343 #[serde(default = "default_call_hierarchy_max_files")]
345 pub max_files: usize,
346}
347
348impl Default for CallHierarchyFeatureOptions {
349 fn default() -> Self {
350 Self {
351 max_files: default_call_hierarchy_max_files(),
352 }
353 }
354}
355
356fn default_call_hierarchy_max_files() -> usize {
357 10_000
358}
359
360#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
361#[serde(rename_all = "camelCase")]
362struct WorkspaceDiagnosticsFeatureOptionsOverrides {
363 #[serde(default)]
364 enabled: Option<bool>,
365 #[serde(default)]
366 max_files: Option<usize>,
367 #[serde(default)]
368 max_entries: Option<usize>,
369 #[serde(default)]
370 max_source_bytes: Option<usize>,
371}
372
373impl WorkspaceDiagnosticsFeatureOptionsOverrides {
374 fn has_overrides(self) -> bool {
375 self.enabled.is_some()
376 || self.max_files.is_some()
377 || self.max_entries.is_some()
378 || self.max_source_bytes.is_some()
379 }
380
381 fn apply_to(
382 self,
383 base: WorkspaceDiagnosticsFeatureOptions,
384 ) -> WorkspaceDiagnosticsFeatureOptions {
385 WorkspaceDiagnosticsFeatureOptions {
386 enabled: self.enabled.unwrap_or(base.enabled),
387 max_files: self.max_files.unwrap_or(base.max_files),
388 max_entries: self.max_entries.unwrap_or(base.max_entries),
389 max_source_bytes: self.max_source_bytes.unwrap_or(base.max_source_bytes),
390 }
391 }
392}
393
394#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
396#[serde(rename_all = "camelCase")]
397pub struct WorkspaceDiagnosticsFeatureOptions {
398 #[serde(default = "default_workspace_diagnostics_enabled")]
400 pub enabled: bool,
401 #[serde(default = "default_workspace_diagnostics_max_files")]
403 pub max_files: usize,
404 #[serde(default = "default_workspace_diagnostics_max_entries")]
406 pub max_entries: usize,
407 #[serde(default = "default_workspace_diagnostics_max_source_bytes")]
409 pub max_source_bytes: usize,
410}
411
412impl Default for WorkspaceDiagnosticsFeatureOptions {
413 fn default() -> Self {
414 Self {
415 enabled: default_workspace_diagnostics_enabled(),
416 max_files: default_workspace_diagnostics_max_files(),
417 max_entries: default_workspace_diagnostics_max_entries(),
418 max_source_bytes: default_workspace_diagnostics_max_source_bytes(),
419 }
420 }
421}
422
423fn default_workspace_diagnostics_enabled() -> bool {
424 true
425}
426
427fn default_workspace_diagnostics_max_files() -> usize {
428 1_000
429}
430
431fn default_workspace_diagnostics_max_entries() -> usize {
432 10_000
433}
434
435fn default_workspace_diagnostics_max_source_bytes() -> usize {
436 32 * 1024 * 1024
437}
438
439fn default_workspace_symbols_max_files() -> usize {
440 5000
441}
442
443fn default_completion_include_runtime_names() -> bool {
444 true
445}
446
447fn default_completion_include_keywords() -> bool {
448 true
449}
450
451#[derive(Debug, Deserialize, Default)]
452#[serde(rename_all = "camelCase")]
453pub(crate) struct TracingOptions {
454 pub(crate) log_file: Option<std::path::PathBuf>,
455 pub(crate) log_level: Option<logging::LogLevel>,
456}
457
458#[derive(Debug, Default)]
459pub(crate) struct AllOptions {
460 pub(crate) global: GlobalOptions,
461 pub(crate) workspace: Option<WorkspaceOptionsMap>,
462}
463
464#[derive(Debug, Deserialize, Default)]
465#[serde(rename_all = "camelCase")]
466struct InitializationOptions {
467 #[serde(default)]
468 shuck: GlobalOptions,
469 #[serde(default)]
470 workspace: Option<WorkspaceOptionsMap>,
471}
472
473impl AllOptions {
474 pub(crate) fn from_value(value: serde_json::Value) -> Self {
475 if value
476 .as_object()
477 .is_some_and(|object| object.contains_key("shuck"))
478 {
479 let options =
480 serde_json::from_value::<InitializationOptions>(value).unwrap_or_default();
481 return Self {
482 global: options.shuck,
483 workspace: options.workspace,
484 };
485 }
486
487 let global = serde_json::from_value::<GlobalOptions>(value).unwrap_or_default();
488 Self {
489 global,
490 workspace: None,
491 }
492 }
493
494 pub(crate) fn workspace_diagnostics_enabled(&self) -> bool {
495 let global = self.global.client.server.workspace_diagnostics;
496 global.enabled
497 || self.workspace.as_ref().is_some_and(|workspaces| {
498 workspaces.values().any(|options| {
499 options
500 .server
501 .workspace_diagnostics_layered_over(global)
502 .enabled
503 })
504 })
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::AllOptions;
511
512 #[test]
513 fn workspace_diagnostics_are_enabled_by_default_and_can_be_disabled() {
514 let defaults = AllOptions::from_value(serde_json::json!({}));
515 assert!(defaults.workspace_diagnostics_enabled());
516
517 let disabled = AllOptions::from_value(serde_json::json!({
518 "shuck": {
519 "server": {
520 "workspaceDiagnostics": {
521 "enabled": false
522 }
523 }
524 }
525 }));
526 assert!(!disabled.workspace_diagnostics_enabled());
527 }
528
529 #[test]
530 fn unrelated_workspace_options_preserve_the_global_diagnostic_opt_out() {
531 let disabled = AllOptions::from_value(serde_json::json!({
532 "shuck": {
533 "server": {
534 "workspaceDiagnostics": {
535 "enabled": false
536 }
537 }
538 },
539 "workspace": {
540 "file:///workspace": {
541 "showSyntaxErrors": true
542 }
543 }
544 }));
545 assert!(!disabled.workspace_diagnostics_enabled());
546
547 let enabled_for_workspace = AllOptions::from_value(serde_json::json!({
548 "shuck": {
549 "server": {
550 "workspaceDiagnostics": {
551 "enabled": false
552 }
553 }
554 },
555 "workspace": {
556 "file:///workspace": {
557 "server": {
558 "workspaceDiagnostics": {
559 "enabled": true
560 }
561 }
562 }
563 }
564 }));
565 assert!(enabled_for_workspace.workspace_diagnostics_enabled());
566 }
567}