1use std::collections::HashMap;
7use std::collections::HashSet;
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12pub mod context_keys {
15 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
52pub struct ProcessLimits {
53 #[serde(default)]
56 pub max_memory_percent: Option<u32>,
57
58 #[serde(default)]
61 pub max_cpu_percent: Option<u32>,
62
63 #[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), max_cpu_percent: Some(90), enabled: cfg!(target_os = "linux"), }
79 }
80}
81
82impl ProcessLimits {
83 pub fn unlimited() -> Self {
85 Self {
86 max_memory_percent: None,
87 max_cpu_percent: None,
88 enabled: false,
89 }
90 }
91
92 pub fn default_cpu_limit_percent() -> u32 {
94 90
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "snake_case")]
105pub enum LspFeature {
106 Diagnostics,
108 Completion,
110 CodeAction,
112 DocumentSymbols,
114 WorkspaceSymbols,
116 Hover,
118 Definition,
120 References,
122 Format,
124 Rename,
126 SignatureHelp,
128 InlayHints,
130 FoldingRange,
132 SemanticTokens,
134 DocumentHighlight,
136}
137
138impl LspFeature {
139 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#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(untagged)]
199pub enum LspLanguageConfig {
200 Multi(Vec<LspServerConfig>),
202 Single(Box<LspServerConfig>),
204}
205
206impl 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 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 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
257#[schemars(extend("x-display-field" = "/command"))]
258pub struct LspServerConfig {
259 #[serde(default)]
262 #[schemars(extend("x-order" = 1))]
263 pub command: String,
264
265 #[serde(default = "default_true")]
267 #[schemars(extend("x-order" = 2))]
268 pub enabled: bool,
269
270 #[serde(default)]
273 #[schemars(extend("x-order" = 3))]
274 pub name: Option<String>,
275
276 #[serde(default)]
278 #[schemars(extend("x-order" = 4))]
279 pub args: Vec<String>,
280
281 #[serde(default = "default_true")]
286 #[schemars(extend("x-order" = 5))]
287 pub auto_start: bool,
288
289 #[serde(default)]
298 #[schemars(extend("x-order" = 6))]
299 pub root_markers: Vec<String>,
300
301 #[serde(default)]
304 #[schemars(extend("x-section" = "Advanced", "x-order" = 10))]
305 pub env: HashMap<String, String>,
306
307 #[serde(default)]
311 #[schemars(extend("x-section" = "Advanced", "x-order" = 11))]
312 pub language_id_overrides: HashMap<String, String>,
313
314 #[serde(default)]
317 #[schemars(extend("x-section" = "Advanced", "x-order" = 12))]
318 pub initialization_options: Option<serde_json::Value>,
319
320 #[serde(default)]
323 #[schemars(extend("x-section" = "Advanced", "x-order" = 13))]
324 pub only_features: Option<Vec<LspFeature>>,
325
326 #[serde(default)]
329 #[schemars(extend("x-section" = "Advanced", "x-order" = 14))]
330 pub except_features: Option<Vec<LspFeature>>,
331
332 #[serde(default)]
334 #[schemars(extend("x-section" = "Advanced", "x-order" = 15))]
335 pub process_limits: ProcessLimits,
336}
337
338impl LspServerConfig {
339 pub fn display_name(&self) -> String {
346 if let Some(ref name) = self.name {
347 return name.clone();
348 }
349 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 pub fn feature_filter(&self) -> FeatureFilter {
359 FeatureFilter::from_config(&self.only_features, &self.except_features)
360 }
361
362 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}