use std::collections::HashMap;
use std::collections::HashSet;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub mod context_keys {
pub const HAS_BUFFER: &str = "has_buffer";
pub const LINE_NUMBERS: &str = "line_numbers";
pub const LINE_WRAP: &str = "line_wrap";
pub const PAGE_VIEW: &str = "page_view";
pub const COMPOSE_MODE: &str = "compose_mode";
pub const FILE_EXPLORER: &str = "file_explorer";
pub const MENU_BAR: &str = "menu_bar";
pub const FILE_EXPLORER_FOCUSED: &str = "file_explorer_focused";
pub const MOUSE_CAPTURE: &str = "mouse_capture";
pub const MOUSE_HOVER: &str = "mouse_hover";
pub const LSP_AVAILABLE: &str = "lsp_available";
pub const FILE_EXPLORER_SHOW_HIDDEN: &str = "file_explorer_show_hidden";
pub const FILE_EXPLORER_SHOW_GITIGNORED: &str = "file_explorer_show_gitignored";
pub const HAS_SELECTION: &str = "has_selection";
pub const CAN_COPY: &str = "can_copy";
pub const CAN_PASTE: &str = "can_paste";
pub const FORMATTER_AVAILABLE: &str = "formatter_available";
pub const INLAY_HINTS: &str = "inlay_hints";
pub const SESSION_MODE: &str = "session_mode";
pub const VERTICAL_SCROLLBAR: &str = "vertical_scrollbar";
pub const HORIZONTAL_SCROLLBAR: &str = "horizontal_scrollbar";
pub const SCROLL_SYNC: &str = "scroll_sync";
pub const HAS_SAME_BUFFER_SPLITS: &str = "has_same_buffer_splits";
pub const KEYMAP_DEFAULT: &str = "keymap_default";
pub const KEYMAP_EMACS: &str = "keymap_emacs";
pub const KEYMAP_VSCODE: &str = "keymap_vscode";
pub const KEYMAP_MACOS_GUI: &str = "keymap_macos_gui";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct ProcessLimits {
#[serde(default)]
pub max_memory_percent: Option<u32>,
#[serde(default)]
pub max_cpu_percent: Option<u32>,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_true() -> bool {
true
}
impl Default for ProcessLimits {
fn default() -> Self {
Self {
max_memory_percent: Some(50), max_cpu_percent: Some(90), enabled: cfg!(target_os = "linux"), }
}
}
impl ProcessLimits {
pub fn unlimited() -> Self {
Self {
max_memory_percent: None,
max_cpu_percent: None,
enabled: false,
}
}
pub fn default_cpu_limit_percent() -> u32 {
90
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum LspFeature {
Diagnostics,
Completion,
CodeAction,
DocumentSymbols,
WorkspaceSymbols,
Hover,
Definition,
References,
Format,
Rename,
SignatureHelp,
InlayHints,
FoldingRange,
SemanticTokens,
DocumentHighlight,
}
impl LspFeature {
pub fn is_merged(&self) -> bool {
matches!(
self,
LspFeature::Diagnostics
| LspFeature::Completion
| LspFeature::CodeAction
| LspFeature::DocumentSymbols
| LspFeature::WorkspaceSymbols
)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum FeatureFilter {
#[default]
All,
Only(HashSet<LspFeature>),
Except(HashSet<LspFeature>),
}
impl FeatureFilter {
pub fn allows(&self, feature: LspFeature) -> bool {
match self {
FeatureFilter::All => true,
FeatureFilter::Only(set) => set.contains(&feature),
FeatureFilter::Except(set) => !set.contains(&feature),
}
}
pub fn from_config(
only: &Option<Vec<LspFeature>>,
except: &Option<Vec<LspFeature>>,
) -> FeatureFilter {
match (only, except) {
(Some(only), _) => FeatureFilter::Only(only.iter().copied().collect()),
(_, Some(except)) => FeatureFilter::Except(except.iter().copied().collect()),
_ => FeatureFilter::All,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum LspLanguageConfig {
Multi(Vec<LspServerConfig>),
Single(Box<LspServerConfig>),
}
impl JsonSchema for LspLanguageConfig {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("LspLanguageConfig")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "One or more LSP server configs for this language.\nAccepts both a single object and an array for backwards compatibility.",
"type": "array",
"items": generator.subschema_for::<LspServerConfig>()
})
}
}
impl LspLanguageConfig {
pub fn into_vec(self) -> Vec<LspServerConfig> {
match self {
LspLanguageConfig::Single(c) => vec![*c],
LspLanguageConfig::Multi(v) => v,
}
}
pub fn as_slice(&self) -> &[LspServerConfig] {
match self {
LspLanguageConfig::Single(c) => std::slice::from_ref(c.as_ref()),
LspLanguageConfig::Multi(v) => v,
}
}
pub fn as_mut_slice(&mut self) -> &mut [LspServerConfig] {
match self {
LspLanguageConfig::Single(c) => std::slice::from_mut(c),
LspLanguageConfig::Multi(v) => v,
}
}
}
impl Default for LspLanguageConfig {
fn default() -> Self {
LspLanguageConfig::Single(Box::default())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-display-field" = "/command"))]
pub struct LspServerConfig {
#[serde(default)]
#[schemars(extend("x-order" = 1))]
pub command: String,
#[serde(default = "default_true")]
#[schemars(extend("x-order" = 2))]
pub enabled: bool,
#[serde(default)]
#[schemars(extend("x-order" = 3))]
pub name: Option<String>,
#[serde(default)]
#[schemars(extend("x-order" = 4))]
pub args: Vec<String>,
#[serde(default = "default_true")]
#[schemars(extend("x-order" = 5))]
pub auto_start: bool,
#[serde(default)]
#[schemars(extend("x-order" = 6))]
pub root_markers: Vec<String>,
#[serde(default)]
#[schemars(extend("x-section" = "Advanced", "x-order" = 10))]
pub env: HashMap<String, String>,
#[serde(default)]
#[schemars(extend("x-section" = "Advanced", "x-order" = 11))]
pub language_id_overrides: HashMap<String, String>,
#[serde(default)]
#[schemars(extend("x-section" = "Advanced", "x-order" = 12))]
pub initialization_options: Option<serde_json::Value>,
#[serde(default)]
#[schemars(extend("x-section" = "Advanced", "x-order" = 13))]
pub only_features: Option<Vec<LspFeature>>,
#[serde(default)]
#[schemars(extend("x-section" = "Advanced", "x-order" = 14))]
pub except_features: Option<Vec<LspFeature>>,
#[serde(default)]
#[schemars(extend("x-section" = "Advanced", "x-order" = 15))]
pub process_limits: ProcessLimits,
}
impl LspServerConfig {
pub fn display_name(&self) -> String {
if let Some(ref name) = self.name {
return name.clone();
}
std::path::Path::new(&self.command)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&self.command)
.to_string()
}
pub fn feature_filter(&self) -> FeatureFilter {
FeatureFilter::from_config(&self.only_features, &self.except_features)
}
pub fn merge_with_defaults(self, defaults: &LspServerConfig) -> LspServerConfig {
LspServerConfig {
name: self.name.or_else(|| defaults.name.clone()),
command: if self.command.is_empty() {
defaults.command.clone()
} else {
self.command
},
args: if self.args.is_empty() {
defaults.args.clone()
} else {
self.args
},
enabled: self.enabled,
auto_start: self.auto_start,
process_limits: self.process_limits,
only_features: self
.only_features
.or_else(|| defaults.only_features.clone()),
except_features: self
.except_features
.or_else(|| defaults.except_features.clone()),
initialization_options: self
.initialization_options
.or_else(|| defaults.initialization_options.clone()),
env: {
let mut merged = defaults.env.clone();
merged.extend(self.env);
merged
},
language_id_overrides: {
let mut merged = defaults.language_id_overrides.clone();
merged.extend(self.language_id_overrides);
merged
},
root_markers: if self.root_markers.is_empty() {
defaults.root_markers.clone()
} else {
self.root_markers
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_lsp_feature_is_merged() {
assert!(LspFeature::Diagnostics.is_merged());
assert!(LspFeature::Completion.is_merged());
assert!(LspFeature::CodeAction.is_merged());
assert!(LspFeature::DocumentSymbols.is_merged());
assert!(LspFeature::WorkspaceSymbols.is_merged());
assert!(!LspFeature::Hover.is_merged());
assert!(!LspFeature::Definition.is_merged());
assert!(!LspFeature::References.is_merged());
assert!(!LspFeature::Format.is_merged());
assert!(!LspFeature::Rename.is_merged());
assert!(!LspFeature::SignatureHelp.is_merged());
assert!(!LspFeature::InlayHints.is_merged());
assert!(!LspFeature::FoldingRange.is_merged());
assert!(!LspFeature::SemanticTokens.is_merged());
assert!(!LspFeature::DocumentHighlight.is_merged());
}
#[test]
fn test_feature_filter_all() {
let filter = FeatureFilter::All;
assert!(filter.allows(LspFeature::Hover));
assert!(filter.allows(LspFeature::Diagnostics));
assert!(filter.allows(LspFeature::Completion));
assert!(filter.allows(LspFeature::Rename));
}
#[test]
fn test_feature_filter_only() {
let mut set = HashSet::new();
set.insert(LspFeature::Diagnostics);
set.insert(LspFeature::Completion);
let filter = FeatureFilter::Only(set);
assert!(filter.allows(LspFeature::Diagnostics));
assert!(filter.allows(LspFeature::Completion));
assert!(!filter.allows(LspFeature::Hover));
assert!(!filter.allows(LspFeature::Definition));
}
#[test]
fn test_feature_filter_except() {
let mut set = HashSet::new();
set.insert(LspFeature::Format);
set.insert(LspFeature::Rename);
let filter = FeatureFilter::Except(set);
assert!(filter.allows(LspFeature::Hover));
assert!(filter.allows(LspFeature::Diagnostics));
assert!(!filter.allows(LspFeature::Format));
assert!(!filter.allows(LspFeature::Rename));
}
#[test]
fn test_feature_filter_from_config_none() {
let filter = FeatureFilter::from_config(&None, &None);
assert!(matches!(filter, FeatureFilter::All));
}
#[test]
fn test_feature_filter_from_config_only() {
let only = Some(vec![LspFeature::Diagnostics, LspFeature::Completion]);
let filter = FeatureFilter::from_config(&only, &None);
assert!(filter.allows(LspFeature::Diagnostics));
assert!(filter.allows(LspFeature::Completion));
assert!(!filter.allows(LspFeature::Hover));
}
#[test]
fn test_feature_filter_from_config_except() {
let except = Some(vec![LspFeature::Format]);
let filter = FeatureFilter::from_config(&None, &except);
assert!(filter.allows(LspFeature::Hover));
assert!(!filter.allows(LspFeature::Format));
}
#[test]
fn test_feature_filter_default() {
let filter = FeatureFilter::default();
assert!(matches!(filter, FeatureFilter::All));
}
}