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