Skip to main content

shuck_server/session/
options.rs

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/// Global initialization options accepted by the Shuck LSP server.
12#[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    /// Resolve client-provided options into runtime global settings.
23    pub fn into_settings(self, client: Client) -> GlobalClientSettings {
24        GlobalClientSettings::new(self.client, client)
25    }
26}
27
28/// Per-client or per-workspace Shuck options supplied through LSP settings.
29#[derive(Clone, Debug, Default, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ClientOptions {
32    #[serde(default)]
33    /// Lint configuration overrides.
34    pub lint: Option<LintConfig>,
35    #[serde(default)]
36    /// Format configuration overrides.
37    pub format: Option<FormatConfig>,
38    #[serde(default)]
39    /// Whether source-level fix-all actions are enabled.
40    pub fix_all: Option<bool>,
41    #[serde(default)]
42    /// Whether unsafe fixes may be offered.
43    pub unsafe_fixes: Option<bool>,
44    #[serde(default)]
45    /// Whether parser diagnostics should be shown.
46    pub show_syntax_errors: Option<bool>,
47    #[serde(default)]
48    /// Server-only editor feature options.
49    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/// Options for server-only editor features.
63#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct ServerOptions {
65    /// Workspace-wide symbol search configuration.
66    pub workspace_symbols: WorkspaceSymbolFeatureOptions,
67    /// Completion configuration.
68    pub completion: CompletionFeatureOptions,
69    /// Rename configuration.
70    pub rename: RenameFeatureOptions,
71    /// Cross-file call hierarchy configuration.
72    pub call_hierarchy: CallHierarchyFeatureOptions,
73    workspace_symbols_overrides: WorkspaceSymbolFeatureOptionsOverrides,
74    completion_overrides: CompletionFeatureOptionsOverrides,
75    rename_overrides: RenameFeatureOptionsOverrides,
76    call_hierarchy_overrides: CallHierarchyFeatureOptionsOverrides,
77}
78
79impl ServerOptions {
80    pub(crate) fn workspace_symbols_layered_over(
81        &self,
82        base: WorkspaceSymbolFeatureOptions,
83    ) -> WorkspaceSymbolFeatureOptions {
84        if self.workspace_symbols_overrides.has_overrides() {
85            self.workspace_symbols_overrides.apply_to(base)
86        } else if self.workspace_symbols != WorkspaceSymbolFeatureOptions::default() {
87            self.workspace_symbols
88        } else {
89            base
90        }
91    }
92
93    pub(crate) fn completion_layered_over(
94        &self,
95        base: CompletionFeatureOptions,
96    ) -> CompletionFeatureOptions {
97        if self.completion_overrides.has_overrides() {
98            self.completion_overrides.apply_to(base)
99        } else if self.completion != CompletionFeatureOptions::default() {
100            self.completion
101        } else {
102            base
103        }
104    }
105
106    pub(crate) fn rename_layered_over(&self, base: RenameFeatureOptions) -> RenameFeatureOptions {
107        if self.rename_overrides.has_overrides() {
108            self.rename_overrides.apply_to(base)
109        } else if self.rename != RenameFeatureOptions::default() {
110            self.rename
111        } else {
112            base
113        }
114    }
115
116    pub(crate) fn call_hierarchy_layered_over(
117        &self,
118        base: CallHierarchyFeatureOptions,
119    ) -> CallHierarchyFeatureOptions {
120        if self.call_hierarchy_overrides.has_overrides() {
121            self.call_hierarchy_overrides.apply_to(base)
122        } else if self.call_hierarchy != CallHierarchyFeatureOptions::default() {
123            self.call_hierarchy
124        } else {
125            base
126        }
127    }
128}
129
130impl<'de> Deserialize<'de> for ServerOptions {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: Deserializer<'de>,
134    {
135        #[derive(Deserialize, Default)]
136        #[serde(rename_all = "camelCase")]
137        struct RawServerOptions {
138            #[serde(default)]
139            workspace_symbols: WorkspaceSymbolFeatureOptionsOverrides,
140            #[serde(default)]
141            completion: CompletionFeatureOptionsOverrides,
142            #[serde(default)]
143            rename: RenameFeatureOptionsOverrides,
144            #[serde(default)]
145            call_hierarchy: CallHierarchyFeatureOptionsOverrides,
146        }
147
148        let raw = RawServerOptions::deserialize(deserializer)?;
149        Ok(Self {
150            workspace_symbols: raw
151                .workspace_symbols
152                .apply_to(WorkspaceSymbolFeatureOptions::default()),
153            completion: raw.completion.apply_to(CompletionFeatureOptions::default()),
154            rename: raw.rename.apply_to(RenameFeatureOptions::default()),
155            call_hierarchy: raw
156                .call_hierarchy
157                .apply_to(CallHierarchyFeatureOptions::default()),
158            workspace_symbols_overrides: raw.workspace_symbols,
159            completion_overrides: raw.completion,
160            rename_overrides: raw.rename,
161            call_hierarchy_overrides: raw.call_hierarchy,
162        })
163    }
164}
165
166#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
167#[serde(rename_all = "camelCase")]
168struct WorkspaceSymbolFeatureOptionsOverrides {
169    #[serde(default)]
170    enabled: Option<bool>,
171    #[serde(default)]
172    max_files: Option<usize>,
173}
174
175impl WorkspaceSymbolFeatureOptionsOverrides {
176    fn has_overrides(self) -> bool {
177        self.enabled.is_some() || self.max_files.is_some()
178    }
179
180    fn apply_to(self, base: WorkspaceSymbolFeatureOptions) -> WorkspaceSymbolFeatureOptions {
181        WorkspaceSymbolFeatureOptions {
182            enabled: self.enabled.unwrap_or(base.enabled),
183            max_files: self.max_files.unwrap_or(base.max_files),
184        }
185    }
186}
187
188/// Configuration for `workspace/symbol`.
189#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
190#[serde(rename_all = "camelCase")]
191pub struct WorkspaceSymbolFeatureOptions {
192    /// Whether the workspace symbol index should serve requests.
193    #[serde(default = "default_workspace_symbols_enabled")]
194    pub enabled: bool,
195    /// Maximum number of closed workspace files to index.
196    #[serde(default = "default_workspace_symbols_max_files")]
197    pub max_files: usize,
198}
199
200#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
201#[serde(rename_all = "camelCase")]
202struct CompletionFeatureOptionsOverrides {
203    #[serde(default)]
204    include_runtime_names: Option<bool>,
205    #[serde(default)]
206    include_keywords: Option<bool>,
207}
208
209impl CompletionFeatureOptionsOverrides {
210    fn has_overrides(self) -> bool {
211        self.include_runtime_names.is_some() || self.include_keywords.is_some()
212    }
213
214    fn apply_to(self, base: CompletionFeatureOptions) -> CompletionFeatureOptions {
215        CompletionFeatureOptions {
216            include_runtime_names: self
217                .include_runtime_names
218                .unwrap_or(base.include_runtime_names),
219            include_keywords: self.include_keywords.unwrap_or(base.include_keywords),
220        }
221    }
222}
223
224/// Configuration for `textDocument/completion`.
225#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
226#[serde(rename_all = "camelCase")]
227pub struct CompletionFeatureOptions {
228    /// Include runtime-provided parameter names.
229    #[serde(default = "default_completion_include_runtime_names")]
230    pub include_runtime_names: bool,
231    /// Include shell keywords in command-position completion.
232    #[serde(default = "default_completion_include_keywords")]
233    pub include_keywords: bool,
234}
235
236impl Default for CompletionFeatureOptions {
237    fn default() -> Self {
238        Self {
239            include_runtime_names: true,
240            include_keywords: true,
241        }
242    }
243}
244
245#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
246#[serde(rename_all = "camelCase")]
247struct RenameFeatureOptionsOverrides {
248    #[serde(default)]
249    allow_cross_file: Option<bool>,
250}
251
252impl RenameFeatureOptionsOverrides {
253    fn has_overrides(self) -> bool {
254        self.allow_cross_file.is_some()
255    }
256
257    fn apply_to(self, base: RenameFeatureOptions) -> RenameFeatureOptions {
258        RenameFeatureOptions {
259            allow_cross_file: self.allow_cross_file.unwrap_or(base.allow_cross_file),
260        }
261    }
262}
263
264/// Configuration for rename requests.
265#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
266#[serde(rename_all = "camelCase")]
267pub struct RenameFeatureOptions {
268    /// Allow rename edits outside the current document.
269    #[serde(default)]
270    pub allow_cross_file: bool,
271}
272
273impl Default for WorkspaceSymbolFeatureOptions {
274    fn default() -> Self {
275        Self {
276            enabled: true,
277            max_files: 5000,
278        }
279    }
280}
281
282fn default_workspace_symbols_enabled() -> bool {
283    true
284}
285
286#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
287#[serde(rename_all = "camelCase")]
288struct CallHierarchyFeatureOptionsOverrides {
289    #[serde(default)]
290    max_files: Option<usize>,
291}
292
293impl CallHierarchyFeatureOptionsOverrides {
294    fn has_overrides(self) -> bool {
295        self.max_files.is_some()
296    }
297
298    fn apply_to(self, base: CallHierarchyFeatureOptions) -> CallHierarchyFeatureOptions {
299        CallHierarchyFeatureOptions {
300            max_files: self.max_files.unwrap_or(base.max_files),
301        }
302    }
303}
304
305/// Configuration for cross-file call hierarchy.
306#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
307#[serde(rename_all = "camelCase")]
308pub struct CallHierarchyFeatureOptions {
309    /// Maximum number of workspace files to index for the call graph.
310    #[serde(default = "default_call_hierarchy_max_files")]
311    pub max_files: usize,
312}
313
314impl Default for CallHierarchyFeatureOptions {
315    fn default() -> Self {
316        Self {
317            max_files: default_call_hierarchy_max_files(),
318        }
319    }
320}
321
322fn default_call_hierarchy_max_files() -> usize {
323    10_000
324}
325
326fn default_workspace_symbols_max_files() -> usize {
327    5000
328}
329
330fn default_completion_include_runtime_names() -> bool {
331    true
332}
333
334fn default_completion_include_keywords() -> bool {
335    true
336}
337
338#[derive(Debug, Deserialize, Default)]
339#[serde(rename_all = "camelCase")]
340pub(crate) struct TracingOptions {
341    pub(crate) log_file: Option<std::path::PathBuf>,
342    pub(crate) log_level: Option<logging::LogLevel>,
343}
344
345#[derive(Debug, Default)]
346pub(crate) struct AllOptions {
347    pub(crate) global: GlobalOptions,
348    pub(crate) workspace: Option<WorkspaceOptionsMap>,
349}
350
351#[derive(Debug, Deserialize, Default)]
352#[serde(rename_all = "camelCase")]
353struct InitializationOptions {
354    #[serde(default)]
355    shuck: GlobalOptions,
356    #[serde(default)]
357    workspace: Option<WorkspaceOptionsMap>,
358}
359
360impl AllOptions {
361    pub(crate) fn from_value(value: serde_json::Value, _client: &Client) -> Self {
362        if value
363            .as_object()
364            .is_some_and(|object| object.contains_key("shuck"))
365        {
366            let options =
367                serde_json::from_value::<InitializationOptions>(value).unwrap_or_default();
368            return Self {
369                global: options.shuck,
370                workspace: options.workspace,
371            };
372        }
373
374        let global = serde_json::from_value::<GlobalOptions>(value).unwrap_or_default();
375        Self {
376            global,
377            workspace: None,
378        }
379    }
380}