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() { 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, 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, 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, context: &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!("{context} {option} must be a JSON object")), } }