mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 01:16:18 +04:00
migrate language server config to new config system
This commit is contained in:
parent
7ba8674466
commit
fb13130701
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -62,9 +62,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca87830a3e3fb156dc96cfbd31cb620265dd053be734723f22b760d6cc3c3051"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@ -1069,6 +1069,7 @@ name = "helix-core"
|
||||
version = "23.10.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"bitflags 2.4.1",
|
||||
"chrono",
|
||||
@ -1079,6 +1080,7 @@ dependencies = [
|
||||
"helix-config",
|
||||
"helix-loader",
|
||||
"imara-diff",
|
||||
"indexmap",
|
||||
"indoc",
|
||||
"log",
|
||||
"nucleo",
|
||||
@ -1147,6 +1149,7 @@ dependencies = [
|
||||
name = "helix-lsp"
|
||||
version = "23.10.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
@ -1155,6 +1158,7 @@ dependencies = [
|
||||
"helix-core",
|
||||
"helix-loader",
|
||||
"helix-parsec",
|
||||
"indexmap",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"parking_lot",
|
||||
@ -1358,12 +1362,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -14,6 +14,7 @@ homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-config = { path = "../helix-config" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
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"
|
||||
which = "5.0.0"
|
||||
parking_lot = "0.12.1"
|
||||
ahash = "0.8.6"
|
||||
indexmap = { version = "2.1.0", features = ["serde"] }
|
||||
|
@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
config::LanguageServerConfig,
|
||||
find_lsp_workspace, jsonrpc,
|
||||
transport::{Payload, Transport},
|
||||
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_loader::{self, VERSION_AND_GIT_HASH};
|
||||
use lsp::{
|
||||
@ -13,15 +16,14 @@
|
||||
};
|
||||
use lsp_types as lsp;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tokio::{
|
||||
io::{BufReader, BufWriter},
|
||||
process::{Child, Command},
|
||||
@ -50,13 +52,11 @@ pub struct Client {
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
|
||||
config: Option<Value>,
|
||||
root_path: std::path::PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
|
||||
initialize_notify: Arc<Notify>,
|
||||
/// workspace folders added while the server is still initializing
|
||||
req_timeout: u64,
|
||||
config: Arc<OptionManager>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@ -170,23 +170,20 @@ fn add_workspace_folder(
|
||||
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
pub fn start(
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
config: Option<Value>,
|
||||
server_environment: HashMap<String, String>,
|
||||
config: Arc<OptionManager>,
|
||||
root_markers: &[String],
|
||||
manual_roots: &[PathBuf],
|
||||
id: usize,
|
||||
name: String,
|
||||
req_timeout: u64,
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||
// 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)
|
||||
.envs(server_environment)
|
||||
.args(args)
|
||||
.envs(config.enviorment().iter().map(|(k, v)| (&**k, &**v)))
|
||||
.args(config.args().iter().map(|v| &**v))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@ -233,7 +230,6 @@ pub fn start(
|
||||
request_counter: AtomicU64::new(0),
|
||||
capabilities: OnceCell::new(),
|
||||
config,
|
||||
req_timeout,
|
||||
root_path,
|
||||
root_uri,
|
||||
workspace_folders: Mutex::new(workspace_folders),
|
||||
@ -374,8 +370,8 @@ pub fn offset_encoding(&self) -> OffsetEncoding {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Option<&Value> {
|
||||
self.config.as_ref()
|
||||
pub fn config(&self) -> config::Guard<Option<Box<Value>>> {
|
||||
self.config.server_config()
|
||||
}
|
||||
|
||||
pub async fn workspace_folders(
|
||||
@ -404,7 +400,7 @@ fn call<R: lsp::request::Request>(
|
||||
where
|
||||
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>(
|
||||
@ -512,7 +508,7 @@ pub fn reply(
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -524,7 +520,7 @@ pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result<lsp::Init
|
||||
// clients will prefer _uri if possible
|
||||
root_path: self.root_path.to_str().map(|path| path.to_owned()),
|
||||
root_uri: self.root_uri.clone(),
|
||||
initialization_options: self.config.clone(),
|
||||
initialization_options: self.config().as_deref().cloned(),
|
||||
capabilities: lsp::ClientCapabilities {
|
||||
workspace: Some(lsp::WorkspaceClientCapabilities {
|
||||
configuration: Some(true),
|
||||
@ -1152,17 +1148,12 @@ pub fn text_document_formatting(
|
||||
};
|
||||
|
||||
// merge FormattingOptions with 'config.format'
|
||||
let config_format = self
|
||||
.config
|
||||
.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 {
|
||||
let mut config_format = self.config.format();
|
||||
let options = if !config_format.is_empty() {
|
||||
// passed in options take precedence over 'config.format'
|
||||
properties.extend(options.properties);
|
||||
config_format.extend(options.properties);
|
||||
lsp::FormattingOptions {
|
||||
properties,
|
||||
properties: config_format,
|
||||
..options
|
||||
}
|
||||
} else {
|
||||
|
67
helix-lsp/src/config.rs
Normal file
67
helix-lsp/src/config.rs
Normal file
@ -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 config;
|
||||
pub mod file_event;
|
||||
pub mod jsonrpc;
|
||||
pub mod snippet;
|
||||
@ -11,6 +12,7 @@
|
||||
pub use lsp_types as lsp;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use helix_config::OptionRegistry;
|
||||
use helix_core::{
|
||||
path,
|
||||
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
|
||||
@ -26,6 +28,8 @@
|
||||
use thiserror::Error;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use crate::config::init_config;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
pub type LanguageServerName = String;
|
||||
|
||||
@ -636,17 +640,25 @@ pub struct Registry {
|
||||
counter: usize,
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
|
||||
pub file_event_handler: file_event::Handler,
|
||||
pub config: OptionRegistry,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
|
||||
Self {
|
||||
let mut res = Self {
|
||||
inner: HashMap::new(),
|
||||
syn_loader,
|
||||
counter: 0,
|
||||
incoming: SelectAll::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> {
|
||||
@ -882,15 +894,11 @@ fn start_client(
|
||||
enable_snippets: bool,
|
||||
) -> Result<NewClient> {
|
||||
let (client, incoming, initialize_notify) = Client::start(
|
||||
&ls_config.command,
|
||||
&ls_config.args,
|
||||
ls_config.config.clone(),
|
||||
ls_config.environment.clone(),
|
||||
todo!(),
|
||||
&config.roots,
|
||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
||||
id,
|
||||
name,
|
||||
ls_config.timeout,
|
||||
doc_path,
|
||||
)?;
|
||||
|
||||
|
@ -699,7 +699,7 @@ macro_rules! language_server {
|
||||
// Trigger a workspace/didChangeConfiguration notification after initialization.
|
||||
// This might not be required by the spec but Neovim does this as well, so it's
|
||||
// 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()));
|
||||
}
|
||||
|
||||
@ -1023,7 +1023,8 @@ macro_rules! language_server {
|
||||
.items
|
||||
.iter()
|
||||
.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() {
|
||||
// for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
|
||||
if !section.is_empty() {
|
||||
@ -1032,7 +1033,7 @@ macro_rules! language_server {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(config)
|
||||
Some(config.to_owned())
|
||||
})
|
||||
.collect();
|
||||
Ok(json!(result))
|
||||
|
Loading…
Reference in New Issue
Block a user