Skip to main content

fresh/
types.rs

1//! Shared configuration types used by both schema generation and runtime.
2//!
3//! These types are kept in a separate module so that the schema generator
4//! can import them without pulling in heavy runtime dependencies.
5
6use std::collections::HashMap;
7use std::collections::HashSet;
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12/// Constants for menu context state keys
13/// These are used both in menu item `when` conditions and `checkbox` states
14pub mod context_keys {
15    pub const LINE_NUMBERS: &str = "line_numbers";
16    pub const LINE_WRAP: &str = "line_wrap";
17    pub const PAGE_VIEW: &str = "page_view";
18    /// Backward-compatible alias for PAGE_VIEW
19    pub const COMPOSE_MODE: &str = "compose_mode";
20    pub const FILE_EXPLORER: &str = "file_explorer";
21    pub const MENU_BAR: &str = "menu_bar";
22    pub const FILE_EXPLORER_FOCUSED: &str = "file_explorer_focused";
23    pub const MOUSE_CAPTURE: &str = "mouse_capture";
24    pub const MOUSE_HOVER: &str = "mouse_hover";
25    pub const LSP_AVAILABLE: &str = "lsp_available";
26    pub const FILE_EXPLORER_SHOW_HIDDEN: &str = "file_explorer_show_hidden";
27    pub const FILE_EXPLORER_SHOW_GITIGNORED: &str = "file_explorer_show_gitignored";
28    pub const HAS_SELECTION: &str = "has_selection";
29    pub const FORMATTER_AVAILABLE: &str = "formatter_available";
30    pub const INLAY_HINTS: &str = "inlay_hints";
31    pub const SESSION_MODE: &str = "session_mode";
32    pub const VERTICAL_SCROLLBAR: &str = "vertical_scrollbar";
33    pub const HORIZONTAL_SCROLLBAR: &str = "horizontal_scrollbar";
34    pub const SCROLL_SYNC: &str = "scroll_sync";
35    pub const HAS_SAME_BUFFER_SPLITS: &str = "has_same_buffer_splits";
36    pub const KEYMAP_DEFAULT: &str = "keymap_default";
37    pub const KEYMAP_EMACS: &str = "keymap_emacs";
38    pub const KEYMAP_VSCODE: &str = "keymap_vscode";
39    pub const KEYMAP_MACOS_GUI: &str = "keymap_macos_gui";
40}
41
42/// Configuration for process resource limits
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
44pub struct ProcessLimits {
45    /// Maximum memory usage as percentage of total system memory (None = no limit)
46    /// Default is 50% of total system memory
47    #[serde(default)]
48    pub max_memory_percent: Option<u32>,
49
50    /// Maximum CPU usage as percentage of total CPU (None = no limit)
51    /// For multi-core systems, 100% = 1 core, 200% = 2 cores, etc.
52    #[serde(default)]
53    pub max_cpu_percent: Option<u32>,
54
55    /// Enable resource limiting (can be disabled per-platform)
56    #[serde(default = "default_true")]
57    pub enabled: bool,
58}
59
60fn default_true() -> bool {
61    true
62}
63
64impl Default for ProcessLimits {
65    fn default() -> Self {
66        Self {
67            max_memory_percent: Some(50),       // 50% of total memory
68            max_cpu_percent: Some(90),          // 90% of total CPU
69            enabled: cfg!(target_os = "linux"), // Only enabled on Linux by default
70        }
71    }
72}
73
74impl ProcessLimits {
75    /// Create a new ProcessLimits with no restrictions
76    pub fn unlimited() -> Self {
77        Self {
78            max_memory_percent: None,
79            max_cpu_percent: None,
80            enabled: false,
81        }
82    }
83
84    /// Get the default CPU limit (90% of total CPU)
85    pub fn default_cpu_limit_percent() -> u32 {
86        90
87    }
88}
89
90/// LSP features that can be routed to specific servers in a multi-server setup.
91///
92/// Features are classified as either "merged" (results from all servers are combined)
93/// or "exclusive" (first eligible server wins). This classification is used by the
94/// dispatch layer, not by this enum itself.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
96#[serde(rename_all = "snake_case")]
97pub enum LspFeature {
98    /// Diagnostics (merged: combined from all servers)
99    Diagnostics,
100    /// Code completion (merged: combined from all servers)
101    Completion,
102    /// Code actions / quick fixes (merged: combined from all servers)
103    CodeAction,
104    /// Document symbols (merged: combined from all servers)
105    DocumentSymbols,
106    /// Workspace symbols (merged: combined from all servers)
107    WorkspaceSymbols,
108    /// Hover information (exclusive: first eligible server wins)
109    Hover,
110    /// Go to definition, declaration, type definition, implementation (exclusive)
111    Definition,
112    /// Find references (exclusive)
113    References,
114    /// Document formatting and range formatting (exclusive)
115    Format,
116    /// Rename and prepare rename (exclusive)
117    Rename,
118    /// Signature help (exclusive)
119    SignatureHelp,
120    /// Inlay hints (exclusive)
121    InlayHints,
122    /// Folding ranges (exclusive)
123    FoldingRange,
124    /// Semantic tokens (exclusive)
125    SemanticTokens,
126    /// Document highlight (exclusive)
127    DocumentHighlight,
128}
129
130impl LspFeature {
131    /// Whether this feature produces merged results from all eligible servers.
132    /// Merged features send requests to all servers and combine the results.
133    /// Non-merged (exclusive) features use only the first eligible server.
134    pub fn is_merged(&self) -> bool {
135        matches!(
136            self,
137            LspFeature::Diagnostics
138                | LspFeature::Completion
139                | LspFeature::CodeAction
140                | LspFeature::DocumentSymbols
141                | LspFeature::WorkspaceSymbols
142        )
143    }
144}
145
146/// Feature filter for an LSP server, controlling which features it handles.
147///
148/// - `All`: The server handles all features (default).
149/// - `Only(set)`: The server handles only the listed features.
150/// - `Except(set)`: The server handles all features except the listed ones.
151#[derive(Debug, Clone, Default, PartialEq, Eq)]
152pub enum FeatureFilter {
153    #[default]
154    All,
155    Only(HashSet<LspFeature>),
156    Except(HashSet<LspFeature>),
157}
158
159impl FeatureFilter {
160    /// Check if this filter allows a given feature.
161    pub fn allows(&self, feature: LspFeature) -> bool {
162        match self {
163            FeatureFilter::All => true,
164            FeatureFilter::Only(set) => set.contains(&feature),
165            FeatureFilter::Except(set) => !set.contains(&feature),
166        }
167    }
168
169    /// Build a FeatureFilter from the only_features/except_features config fields.
170    pub fn from_config(
171        only: &Option<Vec<LspFeature>>,
172        except: &Option<Vec<LspFeature>>,
173    ) -> FeatureFilter {
174        match (only, except) {
175            (Some(only), _) => FeatureFilter::Only(only.iter().copied().collect()),
176            (_, Some(except)) => FeatureFilter::Except(except.iter().copied().collect()),
177            _ => FeatureFilter::All,
178        }
179    }
180}
181
182/// Wrapper for deserializing a per-language LSP config that can be either
183/// a single server object or an array of server objects.
184///
185/// ```json
186/// { "lsp": { "rust": { "command": "rust-analyzer" } } }          // single
187/// { "lsp": { "python": [{ "command": "pyright" }, { "command": "ruff" }] } }  // multi
188/// ```
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(untagged)]
191pub enum LspLanguageConfig {
192    /// Multiple servers for this language (array form)
193    Multi(Vec<LspServerConfig>),
194    /// A single server for this language (object form)
195    Single(Box<LspServerConfig>),
196}
197
198/// Custom JsonSchema: always advertise the canonical array form so the settings
199/// UI renders a structured array editor. The `#[serde(untagged)]` on the enum
200/// still accepts both single-object and array forms during deserialization.
201impl JsonSchema for LspLanguageConfig {
202    fn schema_name() -> std::borrow::Cow<'static, str> {
203        std::borrow::Cow::Borrowed("LspLanguageConfig")
204    }
205
206    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
207        schemars::json_schema!({
208            "description": "One or more LSP server configs for this language.\nAccepts both a single object and an array for backwards compatibility.",
209            "type": "array",
210            "items": generator.subschema_for::<LspServerConfig>()
211        })
212    }
213}
214
215impl LspLanguageConfig {
216    /// Convert to a Vec of server configs.
217    pub fn into_vec(self) -> Vec<LspServerConfig> {
218        match self {
219            LspLanguageConfig::Single(c) => vec![*c],
220            LspLanguageConfig::Multi(v) => v,
221        }
222    }
223
224    /// Get a reference as a slice of server configs.
225    pub fn as_slice(&self) -> &[LspServerConfig] {
226        match self {
227            LspLanguageConfig::Single(c) => std::slice::from_ref(c.as_ref()),
228            LspLanguageConfig::Multi(v) => v,
229        }
230    }
231
232    /// Get a mutable reference as a slice of server configs.
233    pub fn as_mut_slice(&mut self) -> &mut [LspServerConfig] {
234        match self {
235            LspLanguageConfig::Single(c) => std::slice::from_mut(c),
236            LspLanguageConfig::Multi(v) => v,
237        }
238    }
239}
240
241impl Default for LspLanguageConfig {
242    fn default() -> Self {
243        LspLanguageConfig::Single(Box::default())
244    }
245}
246
247/// LSP server configuration
248#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
249#[schemars(extend("x-display-field" = "/command"))]
250pub struct LspServerConfig {
251    /// Command to spawn the server.
252    /// Required when enabled=true, ignored when enabled=false.
253    #[serde(default)]
254    #[schemars(extend("x-order" = 1))]
255    pub command: String,
256
257    /// Whether the server is enabled
258    #[serde(default = "default_true")]
259    #[schemars(extend("x-order" = 2))]
260    pub enabled: bool,
261
262    /// Display name for this server (e.g., "tsserver", "eslint").
263    /// Defaults to the command basename if not specified.
264    #[serde(default)]
265    #[schemars(extend("x-order" = 3))]
266    pub name: Option<String>,
267
268    /// Arguments to pass to the server
269    #[serde(default)]
270    #[schemars(extend("x-order" = 4))]
271    pub args: Vec<String>,
272
273    /// Whether to auto-start this LSP server when opening matching files
274    /// If false (default), the server must be started manually via command palette
275    #[serde(default)]
276    #[schemars(extend("x-order" = 5))]
277    pub auto_start: bool,
278
279    /// File/directory names to search for when detecting the workspace root.
280    /// The editor walks upward from the opened file's directory looking for
281    /// any of these markers. The first directory containing a match becomes
282    /// the workspace root sent to the LSP server.
283    ///
284    /// If empty, falls back to `[".git"]` as a universal marker.
285    /// If the walk reaches a filesystem boundary without a match, uses the
286    /// file's parent directory (never cwd or $HOME).
287    #[serde(default)]
288    #[schemars(extend("x-order" = 6))]
289    pub root_markers: Vec<String>,
290
291    /// Environment variables to set for the LSP server process.
292    /// These are added to (or override) the inherited parent environment.
293    #[serde(default)]
294    #[schemars(extend("x-section" = "Advanced", "x-order" = 10))]
295    pub env: HashMap<String, String>,
296
297    /// Override the LSP languageId sent in textDocument/didOpen based on file extension.
298    /// Maps file extension (without dot) to LSP language ID string.
299    /// For example: `{"tsx": "typescriptreact", "jsx": "javascriptreact"}`
300    #[serde(default)]
301    #[schemars(extend("x-section" = "Advanced", "x-order" = 11))]
302    pub language_id_overrides: HashMap<String, String>,
303
304    /// Custom initialization options to send to the server
305    /// These are passed in the `initializationOptions` field of the LSP Initialize request
306    #[serde(default)]
307    #[schemars(extend("x-section" = "Advanced", "x-order" = 12))]
308    pub initialization_options: Option<serde_json::Value>,
309
310    /// Restrict this server to only handle the listed features.
311    /// Mutually exclusive with `except_features`. If neither is set, all features are handled.
312    #[serde(default)]
313    #[schemars(extend("x-section" = "Advanced", "x-order" = 13))]
314    pub only_features: Option<Vec<LspFeature>>,
315
316    /// Exclude the listed features from this server.
317    /// Mutually exclusive with `only_features`. If neither is set, all features are handled.
318    #[serde(default)]
319    #[schemars(extend("x-section" = "Advanced", "x-order" = 14))]
320    pub except_features: Option<Vec<LspFeature>>,
321
322    /// Process resource limits (memory and CPU)
323    #[serde(default)]
324    #[schemars(extend("x-section" = "Advanced", "x-order" = 15))]
325    pub process_limits: ProcessLimits,
326}
327
328impl LspServerConfig {
329    /// Merge this config with defaults, using default values for empty/unset fields.
330    ///
331    /// This is used when loading configs where fields like `command` may be empty
332    /// (serde's default) because they weren't specified in the user's config file.
333    /// Resolve the display name for this server.
334    /// Returns the explicit name if set, otherwise the basename of the command.
335    pub fn display_name(&self) -> String {
336        if let Some(ref name) = self.name {
337            return name.clone();
338        }
339        // Use command basename
340        std::path::Path::new(&self.command)
341            .file_name()
342            .and_then(|n| n.to_str())
343            .unwrap_or(&self.command)
344            .to_string()
345    }
346
347    /// Build the FeatureFilter for this server config.
348    pub fn feature_filter(&self) -> FeatureFilter {
349        FeatureFilter::from_config(&self.only_features, &self.except_features)
350    }
351
352    /// Merge this config with defaults, using default values for empty/unset fields.
353    ///
354    /// This is used when loading configs where fields like `command` may be empty
355    /// (serde's default) because they weren't specified in the user's config file.
356    pub fn merge_with_defaults(self, defaults: &LspServerConfig) -> LspServerConfig {
357        LspServerConfig {
358            name: self.name.or_else(|| defaults.name.clone()),
359            command: if self.command.is_empty() {
360                defaults.command.clone()
361            } else {
362                self.command
363            },
364            args: if self.args.is_empty() {
365                defaults.args.clone()
366            } else {
367                self.args
368            },
369            enabled: self.enabled,
370            auto_start: self.auto_start,
371            process_limits: self.process_limits,
372            only_features: self
373                .only_features
374                .or_else(|| defaults.only_features.clone()),
375            except_features: self
376                .except_features
377                .or_else(|| defaults.except_features.clone()),
378            initialization_options: self
379                .initialization_options
380                .or_else(|| defaults.initialization_options.clone()),
381            env: {
382                let mut merged = defaults.env.clone();
383                merged.extend(self.env);
384                merged
385            },
386            language_id_overrides: {
387                let mut merged = defaults.language_id_overrides.clone();
388                merged.extend(self.language_id_overrides);
389                merged
390            },
391            root_markers: if self.root_markers.is_empty() {
392                defaults.root_markers.clone()
393            } else {
394                self.root_markers
395            },
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use std::collections::HashSet;
404
405    #[test]
406    fn test_lsp_feature_is_merged() {
407        assert!(LspFeature::Diagnostics.is_merged());
408        assert!(LspFeature::Completion.is_merged());
409        assert!(LspFeature::CodeAction.is_merged());
410        assert!(LspFeature::DocumentSymbols.is_merged());
411        assert!(LspFeature::WorkspaceSymbols.is_merged());
412
413        assert!(!LspFeature::Hover.is_merged());
414        assert!(!LspFeature::Definition.is_merged());
415        assert!(!LspFeature::References.is_merged());
416        assert!(!LspFeature::Format.is_merged());
417        assert!(!LspFeature::Rename.is_merged());
418        assert!(!LspFeature::SignatureHelp.is_merged());
419        assert!(!LspFeature::InlayHints.is_merged());
420        assert!(!LspFeature::FoldingRange.is_merged());
421        assert!(!LspFeature::SemanticTokens.is_merged());
422        assert!(!LspFeature::DocumentHighlight.is_merged());
423    }
424
425    #[test]
426    fn test_feature_filter_all() {
427        let filter = FeatureFilter::All;
428        assert!(filter.allows(LspFeature::Hover));
429        assert!(filter.allows(LspFeature::Diagnostics));
430        assert!(filter.allows(LspFeature::Completion));
431        assert!(filter.allows(LspFeature::Rename));
432    }
433
434    #[test]
435    fn test_feature_filter_only() {
436        let mut set = HashSet::new();
437        set.insert(LspFeature::Diagnostics);
438        set.insert(LspFeature::Completion);
439        let filter = FeatureFilter::Only(set);
440
441        assert!(filter.allows(LspFeature::Diagnostics));
442        assert!(filter.allows(LspFeature::Completion));
443        assert!(!filter.allows(LspFeature::Hover));
444        assert!(!filter.allows(LspFeature::Definition));
445    }
446
447    #[test]
448    fn test_feature_filter_except() {
449        let mut set = HashSet::new();
450        set.insert(LspFeature::Format);
451        set.insert(LspFeature::Rename);
452        let filter = FeatureFilter::Except(set);
453
454        assert!(filter.allows(LspFeature::Hover));
455        assert!(filter.allows(LspFeature::Diagnostics));
456        assert!(!filter.allows(LspFeature::Format));
457        assert!(!filter.allows(LspFeature::Rename));
458    }
459
460    #[test]
461    fn test_feature_filter_from_config_none() {
462        let filter = FeatureFilter::from_config(&None, &None);
463        assert!(matches!(filter, FeatureFilter::All));
464    }
465
466    #[test]
467    fn test_feature_filter_from_config_only() {
468        let only = Some(vec![LspFeature::Diagnostics, LspFeature::Completion]);
469        let filter = FeatureFilter::from_config(&only, &None);
470        assert!(filter.allows(LspFeature::Diagnostics));
471        assert!(filter.allows(LspFeature::Completion));
472        assert!(!filter.allows(LspFeature::Hover));
473    }
474
475    #[test]
476    fn test_feature_filter_from_config_except() {
477        let except = Some(vec![LspFeature::Format]);
478        let filter = FeatureFilter::from_config(&None, &except);
479        assert!(filter.allows(LspFeature::Hover));
480        assert!(!filter.allows(LspFeature::Format));
481    }
482
483    #[test]
484    fn test_feature_filter_default() {
485        let filter = FeatureFilter::default();
486        assert!(matches!(filter, FeatureFilter::All));
487    }
488}