diff --git a/README.md b/README.md index a30e41c..cdef8c2 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,8 @@ vim.lsp.config("ts_bridge", { global_plugins = {}, plugin_probe_dirs = {}, extra_args = {}, + preferences = {}, + format_options = {}, }, }, }, @@ -150,6 +152,9 @@ vim.lsp.config("ts_bridge", { vim.lsp.enable("ts_bridge") ``` +`tsserver.preferences` and `tsserver.format_options` are forwarded to +tsserver’s `configure` request (keys are passed through as-is). + If you're using `nvim-lspconfig`, the equivalent registration is: ```lua @@ -183,6 +188,8 @@ lspconfig.ts_bridge.setup({ global_plugins = {}, plugin_probe_dirs = {}, extra_args = {}, + preferences = {}, + format_options = {}, }, }, }, @@ -348,6 +355,8 @@ vim.lsp.config("ts_bridge", { global_plugins = {}, plugin_probe_dirs = {}, extra_args = {}, + preferences = {}, + format_options = {}, }, }, }, @@ -377,6 +386,8 @@ if not configs.ts_bridge then global_plugins = {}, plugin_probe_dirs = {}, extra_args = {}, + preferences = {}, + format_options = {}, }, }, }, diff --git a/src/config/mod.rs b/src/config/mod.rs index d09e491..6a3bd6c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -12,7 +12,7 @@ use serde_json::{Map, Value}; /// Settings that are evaluated once during plugin setup (analogous to the Lua /// `settings` table). Additional fields will be introduced as we port features. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct PluginSettings { /// Whether we spin up a paired semantic tsserver dedicated to diagnostics. pub separate_diagnostic_server: bool, @@ -21,6 +21,10 @@ pub struct PluginSettings { pub publish_diagnostic_on: DiagnosticPublishMode, /// Launch arguments and logging preferences forwarded to tsserver. pub tsserver: TsserverLaunchOptions, + /// User preferences forwarded to the tsserver `configure` command. + pub tsserver_preferences: Map, + /// Formatting options forwarded to the tsserver `configure` command. + pub tsserver_format_options: Map, /// Gate for tsserver-backed inlay hints; allows users to disable the feature entirely. pub enable_inlay_hints: bool, } @@ -31,6 +35,8 @@ impl Default for PluginSettings { separate_diagnostic_server: true, publish_diagnostic_on: DiagnosticPublishMode::InsertLeave, tsserver: TsserverLaunchOptions::default(), + tsserver_preferences: Map::new(), + tsserver_format_options: Map::new(), enable_inlay_hints: true, } } @@ -54,7 +60,7 @@ impl DiagnosticPublishMode { } /// Global configuration facade that exposes read-only handles to each settings struct. -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Config { plugin: PluginSettings, } @@ -123,6 +129,35 @@ impl PluginSettings { if let Some(tsserver) = map.get("tsserver") { changed |= self.tsserver.update_from_value(tsserver); + if let Some(tsserver_map) = tsserver.as_object() { + if tsserver_map.contains_key("preferences") { + let next = tsserver_map + .get("preferences") + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default(); + if self.tsserver_preferences != next { + self.tsserver_preferences = next; + changed = true; + } + } + + let format_value = if tsserver_map.contains_key("format_options") { + tsserver_map.get("format_options") + } else if tsserver_map.contains_key("formatOptions") { + tsserver_map.get("formatOptions") + } else { + None + }; + + if let Some(value) = format_value { + let next = value.as_object().cloned().unwrap_or_default(); + if self.tsserver_format_options != next { + self.tsserver_format_options = next; + changed = true; + } + } + } } if let Some(value) = map.get("enable_inlay_hints").and_then(|v| v.as_bool()) { diff --git a/src/server.rs b/src/server.rs index 5fd6454..86ca5c4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -31,7 +31,7 @@ use lsp_types::{ InlayHintRefreshRequest, InlayHintRequest, Request as LspRequest, WorkDoneProgressCreate, }, }; -use serde_json::{self, Value, json}; +use serde_json::{self, Map, Value, json}; use crate::config::{Config, PluginSettings}; use crate::documents::{DocumentStore, OpenDocumentSnapshot, TextSpan}; @@ -1008,7 +1008,7 @@ struct SessionState { restart_progress: RestartProgress, documents: DocumentStore, inlay_cache: InlayHintCache, - inlay_preferences: InlayPreferenceState, + tsserver_configure: TsserverConfigureState, } impl SessionState { @@ -1028,7 +1028,7 @@ impl SessionState { restart_progress: RestartProgress::new(init.session_id), documents: DocumentStore::default(), inlay_cache: InlayHintCache::default(), - inlay_preferences: InlayPreferenceState::default(), + tsserver_configure: TsserverConfigureState::default(), } } @@ -1088,7 +1088,7 @@ impl SessionState { ProjectEvent::Server(event) => self.handle_server_event(event), ProjectEvent::ConfigUpdated(config) => { self.config = config; - self.inlay_preferences.invalidate(); + self.tsserver_configure.invalidate(); self.inlay_cache.clear(); Ok(()) } @@ -1183,6 +1183,9 @@ impl SessionState { .unwrap_or_else(|| params.text_document.uri.to_string()); let spec = crate::protocol::text_document::did_open::handle(params, &self.workspace_root); + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + log::warn!("failed to configure tsserver: {err}"); + } if let Err(err) = self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1214,6 +1217,9 @@ impl SessionState { .unwrap_or_else(|| params.text_document.uri.to_string()); let spec = crate::protocol::text_document::did_change::handle(params, &self.workspace_root); + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + log::warn!("failed to configure tsserver: {err}"); + } if let Err(err) = self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1241,6 +1247,9 @@ impl SessionState { } let spec = crate::protocol::text_document::did_close::handle(params, &self.workspace_root); + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + log::warn!("failed to configure tsserver: {err}"); + } if let Err(err) = self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1257,12 +1266,15 @@ impl SessionState { self.config = update.config; if update.changed { log::info!("workspace settings reloaded from didChangeConfiguration"); - self.inlay_preferences.invalidate(); + self.tsserver_configure.invalidate(); // TODO: restart auxiliary tsserver processes when toggles require it. } return Ok(false); } if let Some(spec) = protocol::route_notification(¬if.method, notif.params.clone()) { + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + log::warn!("failed to configure tsserver: {err}"); + } if let Err(err) = self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1349,7 +1361,6 @@ impl SessionState { if method == InlayHintRequest::METHOD { let enabled = self.config.plugin().enable_inlay_hints; - self.inlay_preferences.ensure(&self.config, &self.project)?; if !enabled { let response = Response::new_ok(id, Value::Array(Vec::new())); self.connection.sender.send(response.into())?; @@ -1383,6 +1394,15 @@ impl SessionState { } if let Some(spec) = spec { + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + let response = Response::new_err( + id, + ErrorCode::InternalError as i32, + format!("failed to configure tsserver: {err}"), + ); + self.connection.sender.send(response.into())?; + return Ok(false); + } match self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1493,7 +1513,7 @@ impl SessionState { self.diag_state.clear(); self.inlay_cache.clear(); - self.inlay_preferences.invalidate(); + self.tsserver_configure.invalidate(); if let Err(err) = self.restart_progress .begin(&self.connection, "Restarting TypeScript server", kind) @@ -1531,6 +1551,9 @@ impl SessionState { fn request_file_diagnostics(&mut self, file: &str) { let spec = protocol::diagnostics::request_for_file(file); + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + log::warn!("failed to configure tsserver: {err}"); + } match self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1565,6 +1588,9 @@ impl SessionState { }, }; let spec = crate::protocol::text_document::did_open::handle(params, &self.workspace_root); + if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) { + log::warn!("failed to configure tsserver: {err}"); + } if let Err(err) = self .project .dispatch_request(spec.route, spec.payload, spec.priority) @@ -1848,39 +1874,57 @@ struct InlayHintCache { } #[derive(Default)] -struct InlayPreferenceState { - configured_for: Option, +struct TsserverConfigureState { + last_args: Option>, } -impl InlayPreferenceState { +impl TsserverConfigureState { fn ensure(&mut self, config: &Config, project: &ProjectHandle) -> anyhow::Result<()> { - let desired = config.plugin().enable_inlay_hints; - if self.configured_for == Some(desired) { + let args = tsserver_configure_args(config); + if self.last_args.as_ref() == Some(&args) { return Ok(()); } - self.dispatch(project, desired)?; - self.configured_for = Some(desired); - Ok(()) - } - - fn dispatch(&self, project: &ProjectHandle, enabled: bool) -> anyhow::Result<()> { let request = json!({ "command": "configure", - "arguments": { - "preferences": crate::protocol::text_document::inlay_hint::preferences(enabled), - } + "arguments": args, }); let _ = project .dispatch_request(Route::Both, request, Priority::Const) .context("failed to dispatch tsserver configure request")?; + self.last_args = Some(args); Ok(()) } fn invalidate(&mut self) { - self.configured_for = None; + self.last_args = None; } } +fn tsserver_configure_args(config: &Config) -> Map { + let mut args = Map::new(); + + // Merge user preferences with the inlay hint gate so `enable_inlay_hints` + // always wins for inlay-specific keys. + let mut preferences = config.plugin().tsserver_preferences.clone(); + let inlay_preferences = + crate::protocol::text_document::inlay_hint::preferences(config.plugin().enable_inlay_hints); + if let Some(map) = inlay_preferences.as_object() { + for (key, value) in map { + preferences.insert(key.clone(), value.clone()); + } + } + args.insert("preferences".to_string(), Value::Object(preferences)); + + if !config.plugin().tsserver_format_options.is_empty() { + args.insert( + "formatOptions".to_string(), + Value::Object(config.plugin().tsserver_format_options.clone()), + ); + } + + args +} + impl InlayHintCache { fn lookup(&self, params: &lsp_types::InlayHintParams) -> Option> { let key = HintCacheKey::new(¶ms.text_document.uri, ¶ms.range);