feat: migrate LSP to opt-in native language servers
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
use crate::lsp::ServerConfig;
|
||||
use serde::Deserialize;
|
||||
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 = match language_id.as_str() {
|
||||
"rust" => Some(("rust-analyzer", "rs")),
|
||||
"python" => Some(("pylsp", "py")),
|
||||
_ => 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"
|
||||
) {
|
||||
return Err(format!(
|
||||
"LSP language {language_id:?} has unknown option {key:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let command = match options.remove("command") {
|
||||
Some(value) => command(value, &language_id)?,
|
||||
None => match preset {
|
||||
Some((command, _)) => vec![command.into()],
|
||||
None => {
|
||||
return Err(format!(
|
||||
"LSP language {language_id:?} requires command argv"
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
let 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 initialization_options = object(
|
||||
options.remove("initialization_options"),
|
||||
&language_id,
|
||||
"initialization_options",
|
||||
)?;
|
||||
let settings = object(options.remove("settings"), &language_id, "settings")?;
|
||||
servers.push(ServerConfig {
|
||||
language_id,
|
||||
command,
|
||||
extensions,
|
||||
initialization_options,
|
||||
settings,
|
||||
});
|
||||
if servers.len() > 16 {
|
||||
return Err("LSP configuration supports at most 16 enabled languages".into());
|
||||
}
|
||||
}
|
||||
Ok(servers)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<ServerConfig>, D::Error> {
|
||||
resolve(Value::deserialize(deserializer)?).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn command(value: Value, language: &str) -> Result<Vec<String>, String> {
|
||||
let error = || {
|
||||
format!(
|
||||
"LSP language {language:?} 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>, language: &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!(
|
||||
"LSP language {language:?} {option} must be a JSON object"
|
||||
)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user