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    workspace_symbols_overrides: WorkspaceSymbolFeatureOptionsOverrides,
72    completion_overrides: CompletionFeatureOptionsOverrides,
73    rename_overrides: RenameFeatureOptionsOverrides,
74}
75
76impl ServerOptions {
77    pub(crate) fn workspace_symbols_layered_over(
78        &self,
79        base: WorkspaceSymbolFeatureOptions,
80    ) -> WorkspaceSymbolFeatureOptions {
81        if self.workspace_symbols_overrides.has_overrides() {
82            self.workspace_symbols_overrides.apply_to(base)
83        } else if self.workspace_symbols != WorkspaceSymbolFeatureOptions::default() {
84            self.workspace_symbols
85        } else {
86            base
87        }
88    }
89
90    pub(crate) fn completion_layered_over(
91        &self,
92        base: CompletionFeatureOptions,
93    ) -> CompletionFeatureOptions {
94        if self.completion_overrides.has_overrides() {
95            self.completion_overrides.apply_to(base)
96        } else if self.completion != CompletionFeatureOptions::default() {
97            self.completion
98        } else {
99            base
100        }
101    }
102
103    pub(crate) fn rename_layered_over(&self, base: RenameFeatureOptions) -> RenameFeatureOptions {
104        if self.rename_overrides.has_overrides() {
105            self.rename_overrides.apply_to(base)
106        } else if self.rename != RenameFeatureOptions::default() {
107            self.rename
108        } else {
109            base
110        }
111    }
112}
113
114impl<'de> Deserialize<'de> for ServerOptions {
115    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116    where
117        D: Deserializer<'de>,
118    {
119        #[derive(Deserialize, Default)]
120        #[serde(rename_all = "camelCase")]
121        struct RawServerOptions {
122            #[serde(default)]
123            workspace_symbols: WorkspaceSymbolFeatureOptionsOverrides,
124            #[serde(default)]
125            completion: CompletionFeatureOptionsOverrides,
126            #[serde(default)]
127            rename: RenameFeatureOptionsOverrides,
128        }
129
130        let raw = RawServerOptions::deserialize(deserializer)?;
131        Ok(Self {
132            workspace_symbols: raw
133                .workspace_symbols
134                .apply_to(WorkspaceSymbolFeatureOptions::default()),
135            completion: raw.completion.apply_to(CompletionFeatureOptions::default()),
136            rename: raw.rename.apply_to(RenameFeatureOptions::default()),
137            workspace_symbols_overrides: raw.workspace_symbols,
138            completion_overrides: raw.completion,
139            rename_overrides: raw.rename,
140        })
141    }
142}
143
144#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
145#[serde(rename_all = "camelCase")]
146struct WorkspaceSymbolFeatureOptionsOverrides {
147    #[serde(default)]
148    enabled: Option<bool>,
149    #[serde(default)]
150    max_files: Option<usize>,
151}
152
153impl WorkspaceSymbolFeatureOptionsOverrides {
154    fn has_overrides(self) -> bool {
155        self.enabled.is_some() || self.max_files.is_some()
156    }
157
158    fn apply_to(self, base: WorkspaceSymbolFeatureOptions) -> WorkspaceSymbolFeatureOptions {
159        WorkspaceSymbolFeatureOptions {
160            enabled: self.enabled.unwrap_or(base.enabled),
161            max_files: self.max_files.unwrap_or(base.max_files),
162        }
163    }
164}
165
166/// Configuration for `workspace/symbol`.
167#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
168#[serde(rename_all = "camelCase")]
169pub struct WorkspaceSymbolFeatureOptions {
170    /// Whether the workspace symbol index should serve requests.
171    #[serde(default = "default_workspace_symbols_enabled")]
172    pub enabled: bool,
173    /// Maximum number of closed workspace files to index.
174    #[serde(default = "default_workspace_symbols_max_files")]
175    pub max_files: usize,
176}
177
178#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
179#[serde(rename_all = "camelCase")]
180struct CompletionFeatureOptionsOverrides {
181    #[serde(default)]
182    include_runtime_names: Option<bool>,
183    #[serde(default)]
184    include_keywords: Option<bool>,
185}
186
187impl CompletionFeatureOptionsOverrides {
188    fn has_overrides(self) -> bool {
189        self.include_runtime_names.is_some() || self.include_keywords.is_some()
190    }
191
192    fn apply_to(self, base: CompletionFeatureOptions) -> CompletionFeatureOptions {
193        CompletionFeatureOptions {
194            include_runtime_names: self
195                .include_runtime_names
196                .unwrap_or(base.include_runtime_names),
197            include_keywords: self.include_keywords.unwrap_or(base.include_keywords),
198        }
199    }
200}
201
202/// Configuration for `textDocument/completion`.
203#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "camelCase")]
205pub struct CompletionFeatureOptions {
206    /// Include runtime-provided parameter names.
207    #[serde(default = "default_completion_include_runtime_names")]
208    pub include_runtime_names: bool,
209    /// Include shell keywords in command-position completion.
210    #[serde(default = "default_completion_include_keywords")]
211    pub include_keywords: bool,
212}
213
214impl Default for CompletionFeatureOptions {
215    fn default() -> Self {
216        Self {
217            include_runtime_names: true,
218            include_keywords: true,
219        }
220    }
221}
222
223#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
224#[serde(rename_all = "camelCase")]
225struct RenameFeatureOptionsOverrides {
226    #[serde(default)]
227    allow_cross_file: Option<bool>,
228}
229
230impl RenameFeatureOptionsOverrides {
231    fn has_overrides(self) -> bool {
232        self.allow_cross_file.is_some()
233    }
234
235    fn apply_to(self, base: RenameFeatureOptions) -> RenameFeatureOptions {
236        RenameFeatureOptions {
237            allow_cross_file: self.allow_cross_file.unwrap_or(base.allow_cross_file),
238        }
239    }
240}
241
242/// Configuration for rename requests.
243#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
244#[serde(rename_all = "camelCase")]
245pub struct RenameFeatureOptions {
246    /// Allow rename edits outside the current document.
247    #[serde(default)]
248    pub allow_cross_file: bool,
249}
250
251impl Default for WorkspaceSymbolFeatureOptions {
252    fn default() -> Self {
253        Self {
254            enabled: true,
255            max_files: 5000,
256        }
257    }
258}
259
260fn default_workspace_symbols_enabled() -> bool {
261    true
262}
263
264fn default_workspace_symbols_max_files() -> usize {
265    5000
266}
267
268fn default_completion_include_runtime_names() -> bool {
269    true
270}
271
272fn default_completion_include_keywords() -> bool {
273    true
274}
275
276#[derive(Debug, Deserialize, Default)]
277#[serde(rename_all = "camelCase")]
278pub(crate) struct TracingOptions {
279    pub(crate) log_file: Option<std::path::PathBuf>,
280    pub(crate) log_level: Option<logging::LogLevel>,
281}
282
283#[derive(Debug, Default)]
284pub(crate) struct AllOptions {
285    pub(crate) global: GlobalOptions,
286    pub(crate) workspace: Option<WorkspaceOptionsMap>,
287}
288
289#[derive(Debug, Deserialize, Default)]
290#[serde(rename_all = "camelCase")]
291struct InitializationOptions {
292    #[serde(default)]
293    shuck: GlobalOptions,
294    #[serde(default)]
295    workspace: Option<WorkspaceOptionsMap>,
296}
297
298impl AllOptions {
299    pub(crate) fn from_value(value: serde_json::Value, _client: &Client) -> Self {
300        if value
301            .as_object()
302            .is_some_and(|object| object.contains_key("shuck"))
303        {
304            let options =
305                serde_json::from_value::<InitializationOptions>(value).unwrap_or_default();
306            return Self {
307                global: options.shuck,
308                workspace: options.workspace,
309            };
310        }
311
312        let global = serde_json::from_value::<GlobalOptions>(value).unwrap_or_default();
313        Self {
314            global,
315            workspace: None,
316        }
317    }
318}