Files
vamp/src/lsp_config.rs
T
2026-10-14 13:12:50 +02:00

226 lines
8.8 KiB
Rust

use crate::lsp::ServerConfig;
use serde_json::Value;
pub fn resolve(value: Value) -> Result<Vec<ServerConfig>, String> {
let Value::Object(languages) = value else {
return Err("LSP configuration must be a language-keyed object".into());
};
let mut servers = Vec::new();
for (language_id, value) in languages {
if language_id.is_empty() || language_id.chars().any(char::is_control) {
return Err("LSP language keys must not be empty or contain control characters".into());
}
let preset: Option<(&[&[&str]], &str)> = match language_id.as_str() {
crate::syntax::RUST_ID => Some((&[&["rust-analyzer"]], "rs")),
crate::syntax::PYTHON_ID => Some((&[&["ty", "server"], &["ruff", "server"]], "py")),
crate::syntax::RUBY_ID => Some((&[&["solargraph", "stdio"]], "rb")),
_ => None,
};
let mut options = match value {
Value::Bool(false) => continue,
Value::Bool(true) => {
if preset.is_none() {
return Err(format!(
"LSP language {language_id:?} has no built-in preset; provide command and extensions"
));
}
serde_json::Map::new()
}
Value::Object(options) => options,
_ => {
return Err(format!(
"LSP language {language_id:?} must be true, false, or a configuration object"
));
}
};
for key in options.keys() {
if !matches!(
key.as_str(),
"command" | "extensions" | "initialization_options" | "settings" | "servers"
) {
return Err(format!(
"LSP language {language_id:?} has unknown option {key:?}"
));
}
}
let mut extensions = match options.remove("extensions") {
Some(value) => extensions(value, &language_id)?,
None => match preset {
Some((_, extension)) => vec![extension.into()],
None => return Err(format!("LSP language {language_id:?} requires extensions")),
},
};
let explicit_servers = options.contains_key("servers");
let configurations = match options.remove("servers") {
Some(value) => {
if ["command", "initialization_options", "settings"]
.iter()
.any(|key| options.contains_key(*key))
{
return Err(format!(
"LSP language {language_id:?} servers cannot be combined with command, initialization_options, or settings"
));
}
let Value::Array(configurations) = value else {
return Err(format!(
"LSP language {language_id:?} servers must be a nonempty array of configuration objects"
));
};
if configurations.is_empty() {
return Err(format!(
"LSP language {language_id:?} servers must be a nonempty array of configuration objects"
));
}
configurations
}
None if options.contains_key("command") => vec![Value::Object(options)],
None => {
let Some((commands, _)) = preset else {
return Err(format!(
"LSP language {language_id:?} requires command argv or servers"
));
};
let mut configurations = Vec::with_capacity(commands.len());
for (index, command) in commands.iter().enumerate() {
let mut configuration = if index + 1 == commands.len() {
std::mem::take(&mut options)
} else {
options.clone()
};
configuration.insert(
"command".into(),
Value::Array(
command
.iter()
.map(|argument| Value::from(*argument))
.collect(),
),
);
if language_id == "python" && index == 0 {
configuration
.entry("initialization_options")
.or_insert_with(
|| serde_json::json!({"experimental": {"useUv": "off"}}),
);
}
configurations.push(Value::Object(configuration));
}
configurations
}
};
if servers.len() + configurations.len() > 16 {
return Err("LSP configuration supports at most 16 configured servers".into());
}
let configuration_count = configurations.len();
let mut language_id = language_id;
for (index, configuration) in configurations.into_iter().enumerate() {
let context = if explicit_servers {
format!("LSP language {language_id:?} servers[{index}]")
} else {
format!("LSP language {language_id:?}")
};
let Value::Object(mut configuration) = configuration else {
return Err(format!("{context} must be a configuration object"));
};
for key in configuration.keys() {
if !matches!(
key.as_str(),
"command" | "initialization_options" | "settings"
) {
return Err(format!("{context} has unknown option {key:?}"));
}
}
let command = match configuration.remove("command") {
Some(value) => command(value, &context)?,
None => return Err(format!("{context} requires command argv")),
};
let initialization_options = object(
configuration.remove("initialization_options"),
&context,
"initialization_options",
)?;
let settings = object(configuration.remove("settings"), &context, "settings")?;
servers.push(ServerConfig {
language_id: if index + 1 == configuration_count {
std::mem::take(&mut language_id)
} else {
language_id.clone()
},
command,
extensions: if index + 1 == configuration_count {
std::mem::take(&mut extensions)
} else {
extensions.clone()
},
initialization_options,
settings,
});
}
}
Ok(servers)
}
fn command(value: Value, context: &str) -> Result<Vec<String>, String> {
let error = || {
format!(
"{context} command must be nonempty argv with a nonempty executable and no NUL bytes"
)
};
let Value::Array(values) = value else {
return Err(error());
};
if values.is_empty() {
return Err(error());
}
let mut command = Vec::with_capacity(values.len());
for value in values {
let Value::String(argument) = value else {
return Err(error());
};
if argument.contains('\0') || (command.is_empty() && argument.is_empty()) {
return Err(error());
}
command.push(argument);
}
Ok(command)
}
fn extensions(value: Value, language: &str) -> Result<Vec<String>, String> {
let error = || {
format!(
"LSP language {language:?} extensions must be a nonempty array of suffixes without dots, separators, whitespace, or control characters"
)
};
let Value::Array(values) = value else {
return Err(error());
};
if values.is_empty() {
return Err(error());
}
let mut extensions = Vec::with_capacity(values.len());
for value in values {
let Value::String(extension) = value else {
return Err(error());
};
if extension.is_empty()
|| extension.chars().any(|character| {
matches!(character, '.' | '/' | '\\')
|| character.is_whitespace()
|| character.is_control()
})
{
return Err(error());
}
extensions.push(extension);
}
Ok(extensions)
}
fn object(value: Option<Value>, context: &str, option: &str) -> Result<Value, String> {
match value {
None => Ok(Value::Object(serde_json::Map::new())),
Some(value) if value.is_object() => Ok(value),
Some(_) => Err(format!("{context} {option} must be a JSON object")),
}
}