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