use crate::lsp::ServerConfig; use serde_json::Value; pub fn resolve(value: Value) -> Result, 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() { "rust" => Some((&["rust-analyzer"], "rs")), "python" => Some((&["pylsp"], "py")), "ruby" => 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" ) { 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, _)) => command.iter().map(|argument| (*argument).into()).collect(), 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) } fn command(value: Value, language: &str) -> Result, 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, 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, language: &str, option: &str) -> Result { 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" )), } }