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