1use std::path::PathBuf;
10
11use serde_json::{Map, Value};
12
13#[derive(Debug, Clone, PartialEq)]
16pub struct PluginSettings {
17 pub separate_diagnostic_server: bool,
19 pub publish_diagnostic_on: DiagnosticPublishMode,
22 pub tsserver: TsserverLaunchOptions,
24 pub tsserver_preferences: Map<String, Value>,
26 pub tsserver_format_options: Map<String, Value>,
28 pub enable_inlay_hints: bool,
30}
31
32impl Default for PluginSettings {
33 fn default() -> Self {
34 Self {
35 separate_diagnostic_server: true,
36 publish_diagnostic_on: DiagnosticPublishMode::InsertLeave,
37 tsserver: TsserverLaunchOptions::default(),
38 tsserver_preferences: Map::new(),
39 tsserver_format_options: Map::new(),
40 enable_inlay_hints: true,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum DiagnosticPublishMode {
48 InsertLeave,
49 Change,
50}
51
52impl DiagnosticPublishMode {
53 pub fn from_str(value: &str) -> Self {
55 match value {
56 "change" => Self::Change,
57 _ => Self::InsertLeave,
58 }
59 }
60}
61
62#[derive(Debug, Default, Clone, PartialEq)]
64pub struct Config {
65 plugin: PluginSettings,
66}
67
68impl Config {
69 pub fn new(plugin: PluginSettings) -> Self {
70 Self { plugin }
71 }
72
73 pub fn plugin_mut(&mut self) -> &mut PluginSettings {
74 &mut self.plugin
75 }
76
77 pub fn plugin(&self) -> &PluginSettings {
78 &self.plugin
79 }
80
81 pub fn apply_workspace_settings(&mut self, settings: &Value) -> bool {
84 apply_settings_tree(settings, &mut self.plugin)
85 }
86}
87
88fn apply_settings_tree(value: &Value, plugin: &mut PluginSettings) -> bool {
89 let mut changed = false;
90 if let Some(map) = value.as_object() {
91 changed |= plugin.update_from_map(map);
92
93 for key in POSSIBLE_SETTING_ROOTS {
94 if let Some(candidate) = map.get(*key) {
95 changed |= apply_settings_tree(candidate, plugin);
96 }
97 }
98
99 if let Some(plugin_section) = map.get("plugin") {
100 changed |= apply_settings_tree(plugin_section, plugin);
101 }
102 }
103 changed
104}
105
106const POSSIBLE_SETTING_ROOTS: &[&str] = &["ts-bridge", "tsBridge", "tsbridge", "ts_bridge"];
107
108impl PluginSettings {
109 fn update_from_map(&mut self, map: &Map<String, Value>) -> bool {
110 let mut changed = false;
111
112 if let Some(value) = map
113 .get("separate_diagnostic_server")
114 .and_then(|v| v.as_bool())
115 {
116 if self.separate_diagnostic_server != value {
117 self.separate_diagnostic_server = value;
118 changed = true;
119 }
120 }
121
122 if let Some(value) = map.get("publish_diagnostic_on").and_then(|v| v.as_str()) {
123 let mode = DiagnosticPublishMode::from_str(value);
124 if self.publish_diagnostic_on != mode {
125 self.publish_diagnostic_on = mode;
126 changed = true;
127 }
128 }
129
130 if let Some(tsserver) = map.get("tsserver") {
131 changed |= self.tsserver.update_from_value(tsserver);
132 if let Some(tsserver_map) = tsserver.as_object() {
133 if tsserver_map.contains_key("preferences") {
134 let next = tsserver_map
135 .get("preferences")
136 .and_then(|v| v.as_object())
137 .cloned()
138 .unwrap_or_default();
139 if self.tsserver_preferences != next {
140 self.tsserver_preferences = next;
141 changed = true;
142 }
143 }
144
145 let format_value = if tsserver_map.contains_key("format_options") {
146 tsserver_map.get("format_options")
147 } else if tsserver_map.contains_key("formatOptions") {
148 tsserver_map.get("formatOptions")
149 } else {
150 None
151 };
152
153 if let Some(value) = format_value {
154 let next = value.as_object().cloned().unwrap_or_default();
155 if self.tsserver_format_options != next {
156 self.tsserver_format_options = next;
157 changed = true;
158 }
159 }
160 }
161 }
162
163 if let Some(value) = map.get("enable_inlay_hints").and_then(|v| v.as_bool()) {
164 if self.enable_inlay_hints != value {
165 self.enable_inlay_hints = value;
166 changed = true;
167 }
168 }
169
170 changed
171 }
172}
173
174#[derive(Debug, Clone, Default, PartialEq, Eq)]
176pub struct TsserverLaunchOptions {
177 pub locale: Option<String>,
178 pub log_directory: Option<PathBuf>,
179 pub log_verbosity: Option<TsserverLogVerbosity>,
180 pub max_old_space_size: Option<u32>,
181 pub global_plugins: Vec<String>,
182 pub plugin_probe_dirs: Vec<PathBuf>,
183 pub extra_args: Vec<String>,
184}
185
186impl TsserverLaunchOptions {
187 fn update_from_value(&mut self, value: &Value) -> bool {
188 let map = match value.as_object() {
189 Some(map) => map,
190 None => return false,
191 };
192 let mut changed = false;
193
194 if map.contains_key("locale") {
195 let next = map
196 .get("locale")
197 .and_then(|v| v.as_str())
198 .map(|s| s.to_string());
199 if self.locale != next {
200 self.locale = next;
201 changed = true;
202 }
203 }
204
205 if map.contains_key("log_directory") {
206 let next = map
207 .get("log_directory")
208 .and_then(|v| v.as_str())
209 .map(PathBuf::from);
210 if self.log_directory != next {
211 self.log_directory = next;
212 changed = true;
213 }
214 }
215
216 if map.contains_key("log_verbosity") {
217 let next = map
218 .get("log_verbosity")
219 .and_then(|v| v.as_str())
220 .and_then(TsserverLogVerbosity::from_str);
221 if self.log_verbosity != next {
222 self.log_verbosity = next;
223 changed = true;
224 }
225 }
226
227 if map.contains_key("max_old_space_size") {
228 let next = map
229 .get("max_old_space_size")
230 .and_then(|v| v.as_u64())
231 .and_then(|v| v.try_into().ok());
232 if self.max_old_space_size != next {
233 self.max_old_space_size = next;
234 changed = true;
235 }
236 }
237
238 if let Some(list) = map
239 .get("global_plugins")
240 .and_then(|value| string_list(value))
241 {
242 if self.global_plugins != list {
243 self.global_plugins = list;
244 changed = true;
245 }
246 }
247
248 if let Some(list) = map
249 .get("plugin_probe_dirs")
250 .and_then(|value| string_list(value))
251 .map(|entries| entries.into_iter().map(PathBuf::from).collect::<Vec<_>>())
252 {
253 if self.plugin_probe_dirs != list {
254 self.plugin_probe_dirs = list;
255 changed = true;
256 }
257 }
258
259 if let Some(list) = map.get("extra_args").and_then(|value| string_list(value)) {
260 if self.extra_args != list {
261 self.extra_args = list;
262 changed = true;
263 }
264 }
265
266 changed
267 }
268}
269
270fn string_list(value: &Value) -> Option<Vec<String>> {
271 let array = value.as_array()?;
272 let mut result = Vec::with_capacity(array.len());
273 for entry in array {
274 let Some(text) = entry.as_str() else {
275 continue;
276 };
277 result.push(text.to_string());
278 }
279 Some(result)
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum TsserverLogVerbosity {
284 Terse,
285 Normal,
286 RequestTime,
287 Verbose,
288}
289
290impl TsserverLogVerbosity {
291 pub fn from_str(value: &str) -> Option<Self> {
292 match value {
293 "terse" => Some(Self::Terse),
294 "normal" => Some(Self::Normal),
295 "requestTime" | "request_time" => Some(Self::RequestTime),
296 "verbose" => Some(Self::Verbose),
297 _ => None,
298 }
299 }
300
301 pub fn as_cli_flag(&self) -> &'static str {
302 match self {
303 Self::Terse => "terse",
304 Self::Normal => "normal",
305 Self::RequestTime => "requestTime",
306 Self::Verbose => "verbose",
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use serde_json::json;
315
316 #[test]
317 fn apply_workspace_settings_updates_tsserver_preferences_and_format_options() {
318 let mut config = Config::new(PluginSettings::default());
319 let settings = json!({
320 "ts-bridge": {
321 "tsserver": {
322 "preferences": {
323 "importModuleSpecifierPreference": "relative"
324 },
325 "format_options": {
326 "indentSize": 4
327 }
328 }
329 }
330 });
331
332 let changed = config.apply_workspace_settings(&settings);
333
334 assert!(changed);
335 assert_eq!(
336 config
337 .plugin()
338 .tsserver_preferences
339 .get("importModuleSpecifierPreference")
340 .and_then(|value| value.as_str()),
341 Some("relative")
342 );
343 assert_eq!(
344 config
345 .plugin()
346 .tsserver_format_options
347 .get("indentSize")
348 .and_then(|value| value.as_i64()),
349 Some(4)
350 );
351 }
352
353 #[test]
354 fn apply_workspace_settings_accepts_format_options_camel_case() {
355 let mut config = Config::new(PluginSettings::default());
356 let settings = json!({
357 "ts-bridge": {
358 "tsserver": {
359 "formatOptions": {
360 "tabSize": 2
361 }
362 }
363 }
364 });
365
366 let changed = config.apply_workspace_settings(&settings);
367
368 assert!(changed);
369 assert_eq!(
370 config
371 .plugin()
372 .tsserver_format_options
373 .get("tabSize")
374 .and_then(|value| value.as_i64()),
375 Some(2)
376 );
377 }
378}