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)]
284 #[schemars(extend("x-order" = 5))]
285 pub auto_start: bool,
286
287 #[serde(default)]
296 #[schemars(extend("x-order" = 6))]
297 pub root_markers: Vec<String>,
298
299 #[serde(default)]
302 #[schemars(extend("x-section" = "Advanced", "x-order" = 10))]
303 pub env: HashMap<String, String>,
304
305 #[serde(default)]
309 #[schemars(extend("x-section" = "Advanced", "x-order" = 11))]
310 pub language_id_overrides: HashMap<String, String>,
311
312 #[serde(default)]
315 #[schemars(extend("x-section" = "Advanced", "x-order" = 12))]
316 pub initialization_options: Option<serde_json::Value>,
317
318 #[serde(default)]
321 #[schemars(extend("x-section" = "Advanced", "x-order" = 13))]
322 pub only_features: Option<Vec<LspFeature>>,
323
324 #[serde(default)]
327 #[schemars(extend("x-section" = "Advanced", "x-order" = 14))]
328 pub except_features: Option<Vec<LspFeature>>,
329
330 #[serde(default)]
332 #[schemars(extend("x-section" = "Advanced", "x-order" = 15))]
333 pub process_limits: ProcessLimits,
334}
335
336impl LspServerConfig {
337 pub fn display_name(&self) -> String {
344 if let Some(ref name) = self.name {
345 return name.clone();
346 }
347 std::path::Path::new(&self.command)
349 .file_name()
350 .and_then(|n| n.to_str())
351 .unwrap_or(&self.command)
352 .to_string()
353 }
354
355 pub fn feature_filter(&self) -> FeatureFilter {
357 FeatureFilter::from_config(&self.only_features, &self.except_features)
358 }
359
360 pub fn merge_with_defaults(self, defaults: &LspServerConfig) -> LspServerConfig {
365 LspServerConfig {
366 name: self.name.or_else(|| defaults.name.clone()),
367 command: if self.command.is_empty() {
368 defaults.command.clone()
369 } else {
370 self.command
371 },
372 args: if self.args.is_empty() {
373 defaults.args.clone()
374 } else {
375 self.args
376 },
377 enabled: self.enabled,
378 auto_start: self.auto_start,
379 process_limits: self.process_limits,
380 only_features: self
381 .only_features
382 .or_else(|| defaults.only_features.clone()),
383 except_features: self
384 .except_features
385 .or_else(|| defaults.except_features.clone()),
386 initialization_options: self
387 .initialization_options
388 .or_else(|| defaults.initialization_options.clone()),
389 env: {
390 let mut merged = defaults.env.clone();
391 merged.extend(self.env);
392 merged
393 },
394 language_id_overrides: {
395 let mut merged = defaults.language_id_overrides.clone();
396 merged.extend(self.language_id_overrides);
397 merged
398 },
399 root_markers: if self.root_markers.is_empty() {
400 defaults.root_markers.clone()
401 } else {
402 self.root_markers
403 },
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use std::collections::HashSet;
412
413 #[test]
414 fn test_lsp_feature_is_merged() {
415 assert!(LspFeature::Diagnostics.is_merged());
416 assert!(LspFeature::Completion.is_merged());
417 assert!(LspFeature::CodeAction.is_merged());
418 assert!(LspFeature::DocumentSymbols.is_merged());
419 assert!(LspFeature::WorkspaceSymbols.is_merged());
420
421 assert!(!LspFeature::Hover.is_merged());
422 assert!(!LspFeature::Definition.is_merged());
423 assert!(!LspFeature::References.is_merged());
424 assert!(!LspFeature::Format.is_merged());
425 assert!(!LspFeature::Rename.is_merged());
426 assert!(!LspFeature::SignatureHelp.is_merged());
427 assert!(!LspFeature::InlayHints.is_merged());
428 assert!(!LspFeature::FoldingRange.is_merged());
429 assert!(!LspFeature::SemanticTokens.is_merged());
430 assert!(!LspFeature::DocumentHighlight.is_merged());
431 }
432
433 #[test]
434 fn test_feature_filter_all() {
435 let filter = FeatureFilter::All;
436 assert!(filter.allows(LspFeature::Hover));
437 assert!(filter.allows(LspFeature::Diagnostics));
438 assert!(filter.allows(LspFeature::Completion));
439 assert!(filter.allows(LspFeature::Rename));
440 }
441
442 #[test]
443 fn test_feature_filter_only() {
444 let mut set = HashSet::new();
445 set.insert(LspFeature::Diagnostics);
446 set.insert(LspFeature::Completion);
447 let filter = FeatureFilter::Only(set);
448
449 assert!(filter.allows(LspFeature::Diagnostics));
450 assert!(filter.allows(LspFeature::Completion));
451 assert!(!filter.allows(LspFeature::Hover));
452 assert!(!filter.allows(LspFeature::Definition));
453 }
454
455 #[test]
456 fn test_feature_filter_except() {
457 let mut set = HashSet::new();
458 set.insert(LspFeature::Format);
459 set.insert(LspFeature::Rename);
460 let filter = FeatureFilter::Except(set);
461
462 assert!(filter.allows(LspFeature::Hover));
463 assert!(filter.allows(LspFeature::Diagnostics));
464 assert!(!filter.allows(LspFeature::Format));
465 assert!(!filter.allows(LspFeature::Rename));
466 }
467
468 #[test]
469 fn test_feature_filter_from_config_none() {
470 let filter = FeatureFilter::from_config(&None, &None);
471 assert!(matches!(filter, FeatureFilter::All));
472 }
473
474 #[test]
475 fn test_feature_filter_from_config_only() {
476 let only = Some(vec![LspFeature::Diagnostics, LspFeature::Completion]);
477 let filter = FeatureFilter::from_config(&only, &None);
478 assert!(filter.allows(LspFeature::Diagnostics));
479 assert!(filter.allows(LspFeature::Completion));
480 assert!(!filter.allows(LspFeature::Hover));
481 }
482
483 #[test]
484 fn test_feature_filter_from_config_except() {
485 let except = Some(vec![LspFeature::Format]);
486 let filter = FeatureFilter::from_config(&None, &except);
487 assert!(filter.allows(LspFeature::Hover));
488 assert!(!filter.allows(LspFeature::Format));
489 }
490
491 #[test]
492 fn test_feature_filter_default() {
493 let filter = FeatureFilter::default();
494 assert!(matches!(filter, FeatureFilter::All));
495 }
496}