Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ vim.lsp.config("ts_bridge", {
global_plugins = {},
plugin_probe_dirs = {},
extra_args = {},
preferences = {},
format_options = {},
},
},
},
Expand All @@ -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
Expand Down Expand Up @@ -183,6 +188,8 @@ lspconfig.ts_bridge.setup({
global_plugins = {},
plugin_probe_dirs = {},
extra_args = {},
preferences = {},
format_options = {},
},
},
},
Expand Down Expand Up @@ -348,6 +355,8 @@ vim.lsp.config("ts_bridge", {
global_plugins = {},
plugin_probe_dirs = {},
extra_args = {},
preferences = {},
format_options = {},
},
},
},
Expand Down Expand Up @@ -377,6 +386,8 @@ if not configs.ts_bridge then
global_plugins = {},
plugin_probe_dirs = {},
extra_args = {},
preferences = {},
format_options = {},
},
},
},
Expand Down
39 changes: 37 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<String, Value>,
/// Formatting options forwarded to the tsserver `configure` command.
pub tsserver_format_options: Map<String, Value>,
/// Gate for tsserver-backed inlay hints; allows users to disable the feature entirely.
pub enable_inlay_hints: bool,
}
Expand All @@ -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,
}
}
Expand All @@ -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,
}
Expand Down Expand Up @@ -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()) {
Expand Down
88 changes: 66 additions & 22 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1008,7 +1008,7 @@ struct SessionState {
restart_progress: RestartProgress,
documents: DocumentStore,
inlay_cache: InlayHintCache,
inlay_preferences: InlayPreferenceState,
tsserver_configure: TsserverConfigureState,
}

impl SessionState {
Expand All @@ -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(),
}
}

Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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(&notif.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)
Expand Down Expand Up @@ -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())?;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1848,39 +1874,57 @@ struct InlayHintCache {
}

#[derive(Default)]
struct InlayPreferenceState {
configured_for: Option<bool>,
struct TsserverConfigureState {
last_args: Option<Map<String, Value>>,
}

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<String, Value> {
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<Vec<lsp_types::InlayHint>> {
let key = HintCacheKey::new(&params.text_document.uri, &params.range);
Expand Down