migrate language server config to new config system

pull/9318/head
Pascal Kuthe 11 months ago
parent 7ba8674466
commit fb13130701
No known key found for this signature in database
GPG Key ID: D715E8655AE166A6

13
Cargo.lock generated

@ -62,9 +62,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.78" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca87830a3e3fb156dc96cfbd31cb620265dd053be734723f22b760d6cc3c3051" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
@ -1069,6 +1069,7 @@ name = "helix-core"
version = "23.10.0" version = "23.10.0"
dependencies = [ dependencies = [
"ahash", "ahash",
"anyhow",
"arc-swap", "arc-swap",
"bitflags 2.4.1", "bitflags 2.4.1",
"chrono", "chrono",
@ -1079,6 +1080,7 @@ dependencies = [
"helix-config", "helix-config",
"helix-loader", "helix-loader",
"imara-diff", "imara-diff",
"indexmap",
"indoc", "indoc",
"log", "log",
"nucleo", "nucleo",
@ -1147,6 +1149,7 @@ dependencies = [
name = "helix-lsp" name = "helix-lsp"
version = "23.10.0" version = "23.10.0"
dependencies = [ dependencies = [
"ahash",
"anyhow", "anyhow",
"futures-executor", "futures-executor",
"futures-util", "futures-util",
@ -1155,6 +1158,7 @@ dependencies = [
"helix-core", "helix-core",
"helix-loader", "helix-loader",
"helix-parsec", "helix-parsec",
"indexmap",
"log", "log",
"lsp-types", "lsp-types",
"parking_lot", "parking_lot",
@ -1358,12 +1362,13 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.0.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown 0.14.3",
"serde",
] ]
[[package]] [[package]]

@ -14,6 +14,7 @@ homepage.workspace = true
[dependencies] [dependencies]
helix-core = { path = "../helix-core" } helix-core = { path = "../helix-core" }
helix-config = { path = "../helix-config" }
helix-loader = { path = "../helix-loader" } helix-loader = { path = "../helix-loader" }
helix-parsec = { path = "../helix-parsec" } helix-parsec = { path = "../helix-parsec" }
@ -30,3 +31,5 @@ tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
which = "5.0.0" which = "5.0.0"
parking_lot = "0.12.1" parking_lot = "0.12.1"
ahash = "0.8.6"
indexmap = { version = "2.1.0", features = ["serde"] }

@ -1,9 +1,12 @@
use crate::{ use crate::{
config::LanguageServerConfig,
find_lsp_workspace, jsonrpc, find_lsp_workspace, jsonrpc,
transport::{Payload, Transport}, transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use anyhow::Context;
use helix_config::{self as config, OptionManager};
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope}; use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{ use lsp::{
@ -13,15 +16,14 @@ use lsp::{
}; };
use lsp_types as lsp; use lsp_types as lsp;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::future::Future; use std::future::Future;
use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use std::sync::{ use std::sync::{
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
Arc, Arc,
}; };
use std::{collections::HashMap, path::PathBuf};
use tokio::{ use tokio::{
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
process::{Child, Command}, process::{Child, Command},
@ -50,13 +52,11 @@ pub struct Client {
server_tx: UnboundedSender<Payload>, server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64, request_counter: AtomicU64,
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>, pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
config: Option<Value>,
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>, root_uri: Option<lsp::Url>,
workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>, workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
initialize_notify: Arc<Notify>, initialize_notify: Arc<Notify>,
/// workspace folders added while the server is still initializing config: Arc<OptionManager>,
req_timeout: u64,
} }
impl Client { impl Client {
@ -170,23 +170,20 @@ impl Client {
#[allow(clippy::type_complexity, clippy::too_many_arguments)] #[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn start( pub fn start(
cmd: &str, config: Arc<OptionManager>,
args: &[String],
config: Option<Value>,
server_environment: HashMap<String, String>,
root_markers: &[String], root_markers: &[String],
manual_roots: &[PathBuf], manual_roots: &[PathBuf],
id: usize, id: usize,
name: String, name: String,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> { ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
// Resolve path to the binary // Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; let cmd = which::which(config.command().as_deref().context("no command defined")?)
.map_err(|err| anyhow::anyhow!(err))?;
let process = Command::new(cmd) let process = Command::new(cmd)
.envs(server_environment) .envs(config.enviorment().iter().map(|(k, v)| (&**k, &**v)))
.args(args) .args(config.args().iter().map(|v| &**v))
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@ -233,7 +230,6 @@ impl Client {
request_counter: AtomicU64::new(0), request_counter: AtomicU64::new(0),
capabilities: OnceCell::new(), capabilities: OnceCell::new(),
config, config,
req_timeout,
root_path, root_path,
root_uri, root_uri,
workspace_folders: Mutex::new(workspace_folders), workspace_folders: Mutex::new(workspace_folders),
@ -374,8 +370,8 @@ impl Client {
.unwrap_or_default() .unwrap_or_default()
} }
pub fn config(&self) -> Option<&Value> { pub fn config(&self) -> config::Guard<Option<Box<Value>>> {
self.config.as_ref() self.config.server_config()
} }
pub async fn workspace_folders( pub async fn workspace_folders(
@ -404,7 +400,7 @@ impl Client {
where where
R::Params: serde::Serialize, R::Params: serde::Serialize,
{ {
self.call_with_timeout::<R>(params, self.req_timeout) self.call_with_timeout::<R>(params, self.config.timeout())
} }
fn call_with_timeout<R: lsp::request::Request>( fn call_with_timeout<R: lsp::request::Request>(
@ -512,7 +508,7 @@ impl Client {
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------
pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result<lsp::InitializeResult> { pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result<lsp::InitializeResult> {
if let Some(config) = &self.config { if let Some(config) = &*self.config() {
log::info!("Using custom LSP config: {}", config); log::info!("Using custom LSP config: {}", config);
} }
@ -524,7 +520,7 @@ impl Client {
// clients will prefer _uri if possible // clients will prefer _uri if possible
root_path: self.root_path.to_str().map(|path| path.to_owned()), root_path: self.root_path.to_str().map(|path| path.to_owned()),
root_uri: self.root_uri.clone(), root_uri: self.root_uri.clone(),
initialization_options: self.config.clone(), initialization_options: self.config().as_deref().cloned(),
capabilities: lsp::ClientCapabilities { capabilities: lsp::ClientCapabilities {
workspace: Some(lsp::WorkspaceClientCapabilities { workspace: Some(lsp::WorkspaceClientCapabilities {
configuration: Some(true), configuration: Some(true),
@ -1152,17 +1148,12 @@ impl Client {
}; };
// merge FormattingOptions with 'config.format' // merge FormattingOptions with 'config.format'
let config_format = self let mut config_format = self.config.format();
.config let options = if !config_format.is_empty() {
.as_ref()
.and_then(|cfg| cfg.get("format"))
.and_then(|fmt| HashMap::<String, lsp::FormattingProperty>::deserialize(fmt).ok());
let options = if let Some(mut properties) = config_format {
// passed in options take precedence over 'config.format' // passed in options take precedence over 'config.format'
properties.extend(options.properties); config_format.extend(options.properties);
lsp::FormattingOptions { lsp::FormattingOptions {
properties, properties: config_format,
..options ..options
} }
} else { } else {

@ -0,0 +1,67 @@
use std::collections::HashMap;
use anyhow::bail;
use helix_config::{options, List, Map, String, Ty, Value};
use crate::lsp;
// TODO: differentiating between Some(null) and None is not really practical
// since the distinction is lost on a roundtrip trough config::Value.
// Porbably better to change our code to treat null the way we currently
// treat None
options! {
struct LanguageServerConfig {
/// The name or path of the language server binary to execute. Binaries must be in `$PATH`
command: Option<String> = None,
/// A list of arguments to pass to the language server binary
#[read = deref]
args: List<String> = List::default(),
/// Any environment variables that will be used when starting the language server
enviorment: Map<String> = Map::default(),
/// LSP initialization options
#[name = "config"]
server_config: Option<Box<serde_json::Value>> = None,
/// LSP initialization options
#[read = copy]
timeout: u64 = 20,
// TODO: merge
/// LSP formatting options
#[name = "config.format"]
#[read = fold(HashMap::new(), fold_format_config, FormatConfig)]
format: Map<FormattingProperty> = Map::default()
}
}
type FormatConfig = HashMap<std::string::String, lsp::FormattingProperty>;
fn fold_format_config(config: &Map<FormattingProperty>, mut res: FormatConfig) -> FormatConfig {
for (k, v) in config.iter() {
res.entry(k.to_string()).or_insert_with(|| v.0.clone());
}
res
}
// damm orphan rules :/
#[derive(Debug, PartialEq, Clone)]
struct FormattingProperty(lsp::FormattingProperty);
impl Ty for FormattingProperty {
fn from_value(val: Value) -> anyhow::Result<Self> {
match val {
Value::Int(_) => Ok(FormattingProperty(lsp::FormattingProperty::Number(
i32::from_value(val)?,
))),
Value::Bool(val) => Ok(FormattingProperty(lsp::FormattingProperty::Bool(val))),
Value::String(val) => Ok(FormattingProperty(lsp::FormattingProperty::String(val))),
_ => bail!("expected a string, boolean or integer"),
}
}
fn to_value(&self) -> Value {
match self.0 {
lsp::FormattingProperty::Bool(val) => Value::Bool(val),
lsp::FormattingProperty::Number(val) => Value::Int(val as _),
lsp::FormattingProperty::String(ref val) => Value::String(val.clone()),
}
}
}

@ -1,4 +1,5 @@
mod client; mod client;
mod config;
pub mod file_event; pub mod file_event;
pub mod jsonrpc; pub mod jsonrpc;
pub mod snippet; pub mod snippet;
@ -11,6 +12,7 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp; pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use helix_config::OptionRegistry;
use helix_core::{ use helix_core::{
path, path,
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures}, syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
@ -26,6 +28,8 @@ use std::{
use thiserror::Error; use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::config::init_config;
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
pub type LanguageServerName = String; pub type LanguageServerName = String;
@ -636,17 +640,25 @@ pub struct Registry {
counter: usize, counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>, pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
pub file_event_handler: file_event::Handler, pub file_event_handler: file_event::Handler,
pub config: OptionRegistry,
} }
impl Registry { impl Registry {
pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self { pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
Self { let mut res = Self {
inner: HashMap::new(), inner: HashMap::new(),
syn_loader, syn_loader,
counter: 0, counter: 0,
incoming: SelectAll::new(), incoming: SelectAll::new(),
file_event_handler: file_event::Handler::new(), file_event_handler: file_event::Handler::new(),
} config: OptionRegistry::new(),
};
res.reset_config();
res
}
pub fn reset_config(&mut self) {
init_config(&mut self.config);
} }
pub fn get_by_id(&self, id: usize) -> Option<&Client> { pub fn get_by_id(&self, id: usize) -> Option<&Client> {
@ -882,15 +894,11 @@ fn start_client(
enable_snippets: bool, enable_snippets: bool,
) -> Result<NewClient> { ) -> Result<NewClient> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&ls_config.command, todo!(),
&ls_config.args,
ls_config.config.clone(),
ls_config.environment.clone(),
&config.roots, &config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id, id,
name, name,
ls_config.timeout,
doc_path, doc_path,
)?; )?;

@ -699,7 +699,7 @@ impl Application {
// Trigger a workspace/didChangeConfiguration notification after initialization. // Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's // This might not be required by the spec but Neovim does this as well, so it's
// probably a good idea for compatibility. // probably a good idea for compatibility.
if let Some(config) = language_server.config() { if let Some(config) = language_server.config().as_deref() {
tokio::spawn(language_server.did_change_configuration(config.clone())); tokio::spawn(language_server.did_change_configuration(config.clone()));
} }
@ -1023,7 +1023,8 @@ impl Application {
.items .items
.iter() .iter()
.map(|item| { .map(|item| {
let mut config = language_server.config()?; let config = language_server.config();
let mut config = config.as_deref()?;
if let Some(section) = item.section.as_ref() { if let Some(section) = item.section.as_ref() {
// for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server') // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
if !section.is_empty() { if !section.is_empty() {
@ -1032,7 +1033,7 @@ impl Application {
} }
} }
} }
Some(config) Some(config.to_owned())
}) })
.collect(); .collect();
Ok(json!(result)) Ok(json!(result))

Loading…
Cancel
Save