Skip to content

Commit c1ed2c1

Browse files
committed
feat(language_server): support fmt.configPath configuration
1 parent d292fd3 commit c1ed2c1

File tree

10 files changed

+121
-55
lines changed

10 files changed

+121
-55
lines changed

crates/oxc_language_server/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ These options can be passed with [initialize](#initialize), [workspace/didChange
2828
| `typeAware` | `true` \| `false` | `false` | Enables type-aware linting |
2929
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
3030
| `fmt.experimental` | `true` \| `false` | `false` | Enables experimental formatting with `oxc_formatter` |
31+
| `fmt.configPath` | `<string>` \| `null` | `null` | Path to a oxfmt configuration file, when `null` is passed, the server will use `.oxftmrc.json` and the workspace root |
3132

3233
## Supported LSP Specifications from Server
3334

@@ -47,7 +48,8 @@ The client can pass the workspace options like following:
4748
"unusedDisableDirectives": "allow",
4849
"typeAware": false,
4950
"flags": {},
50-
"fmt.experimental": false
51+
"fmt.experimental": false,
52+
"fmt.configPath": null
5153
}
5254
}]
5355
}
@@ -84,7 +86,8 @@ The client can pass the workspace options like following:
8486
"unusedDisableDirectives": "allow",
8587
"typeAware": false,
8688
"flags": {},
87-
"fmt.experimental": false
89+
"fmt.experimental": false,
90+
"fmt.configPath": null
8891
}
8992
}]
9093
}
@@ -178,6 +181,7 @@ The client can return a response like:
178181
"unusedDisableDirectives": "allow",
179182
"typeAware": false,
180183
"flags": {},
181-
"fmt.experimental": false
184+
"fmt.experimental": false,
185+
"fmt.configPath": null
182186
}]
183187
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"semicolons": "as-needed"
3+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// semicolon is on character 8-9, which will be removed
2+
debugger;

crates/oxc_language_server/src/formatter/options.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use serde::{Deserialize, Deserializer, Serialize, de::Error};
22
use serde_json::Value;
33

4-
#[derive(Debug, Default, Serialize, Clone)]
4+
#[derive(Debug, Default, Serialize, Clone, PartialEq, Eq)]
55
#[serde(rename_all = "camelCase")]
66
pub struct FormatOptions {
77
pub experimental: bool,
8+
pub config_path: Option<String>,
89
}
910

1011
impl<'de> Deserialize<'de> for FormatOptions {
@@ -29,6 +30,9 @@ impl TryFrom<Value> for FormatOptions {
2930
experimental: object
3031
.get("fmt.experimental")
3132
.is_some_and(|run| serde_json::from_value::<bool>(run.clone()).unwrap_or_default()),
33+
config_path: object
34+
.get("fmt.configPath")
35+
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
3236
})
3337
}
3438
}
@@ -43,10 +47,12 @@ mod test {
4347
fn test_valid_options_json() {
4448
let json = json!({
4549
"fmt.experimental": true,
50+
"fmt.configPath": "./.oxfmtrc.json"
4651
});
4752

4853
let options = FormatOptions::try_from(json).unwrap();
4954
assert!(options.experimental);
55+
assert_eq!(options.config_path.unwrap(), "./.oxfmtrc.json");
5056
}
5157

5258
#[test]
@@ -55,15 +61,18 @@ mod test {
5561

5662
let options = FormatOptions::try_from(json).unwrap();
5763
assert!(!options.experimental);
64+
assert!(options.config_path.is_none());
5865
}
5966

6067
#[test]
6168
fn test_invalid_options_json() {
6269
let json = json!({
6370
"fmt.experimental": "what", // should be bool
71+
"fmt.configPath": "./.oxfmtrc.json"
6472
});
6573

6674
let options = FormatOptions::try_from(json).unwrap();
6775
assert!(!options.experimental);
76+
assert_eq!(options.config_path.unwrap(), "./.oxfmtrc.json");
6877
}
6978
}

crates/oxc_language_server/src/formatter/server_formatter.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ use tower_lsp_server::{
1010
lsp_types::{Position, Range, TextEdit, Uri},
1111
};
1212

13-
use crate::FORMAT_CONFIG_FILE;
14-
13+
use crate::formatter::options::FormatOptions as LSPFormatOptions;
14+
use crate::{FORMAT_CONFIG_FILE, utils::normalize_path};
1515
pub struct ServerFormatter {
1616
options: FormatOptions,
1717
}
1818

1919
impl ServerFormatter {
20-
pub fn new(root_uri: &Uri) -> Self {
20+
pub fn new(root_uri: &Uri, options: &LSPFormatOptions) -> Self {
2121
let root_path = root_uri.to_file_path().unwrap();
2222

23-
Self { options: Self::get_format_options(&root_path) }
23+
Self { options: Self::get_format_options(&root_path, options.config_path.as_ref()) }
2424
}
2525

2626
pub fn run_single(&self, uri: &Uri, content: Option<String>) -> Option<Vec<TextEdit>> {
@@ -75,9 +75,9 @@ impl ServerFormatter {
7575
)])
7676
}
7777

78-
fn get_format_options(root_path: &Path) -> FormatOptions {
79-
let config_path = FORMAT_CONFIG_FILE;
80-
let config = root_path.join(config_path); // normalize_path when supporting `oxc.fmt.configPath`
78+
fn get_format_options(root_path: &Path, config_path: Option<&String>) -> FormatOptions {
79+
let config_path = config_path.map_or(FORMAT_CONFIG_FILE, |v| v);
80+
let config = normalize_path(root_path.join(config_path));
8181
let oxfmtrc = if config.try_exists().is_ok_and(|exists| exists) {
8282
if let Ok(oxfmtrc) = Oxfmtrc::from_file(&config) {
8383
oxfmtrc
@@ -232,13 +232,31 @@ mod tests {
232232

233233
#[test]
234234
fn test_formatter() {
235-
Tester::new("fixtures/formatter/basic", Some(FormatOptions { experimental: true }))
236-
.format_and_snapshot_single_file("basic.ts");
235+
Tester::new(
236+
"fixtures/formatter/basic",
237+
Some(FormatOptions { experimental: true, ..Default::default() }),
238+
)
239+
.format_and_snapshot_single_file("basic.ts");
237240
}
238241

239242
#[test]
240243
fn test_root_config_detection() {
241-
Tester::new("fixtures/formatter/root_config", Some(FormatOptions { experimental: true }))
242-
.format_and_snapshot_single_file("semicolons-as-needed.ts");
244+
Tester::new(
245+
"fixtures/formatter/root_config",
246+
Some(FormatOptions { experimental: true, ..Default::default() }),
247+
)
248+
.format_and_snapshot_single_file("semicolons-as-needed.ts");
249+
}
250+
251+
#[test]
252+
fn test_custom_config_path() {
253+
Tester::new(
254+
"fixtures/formatter/custom_config_path",
255+
Some(FormatOptions {
256+
experimental: true,
257+
config_path: Some("./format.json".to_string()),
258+
}),
259+
)
260+
.format_and_snapshot_single_file("semicolons-as-needed.ts");
243261
}
244262
}

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::path::{Component, Path, PathBuf};
1+
use std::path::{Path, PathBuf};
22
use std::str::FromStr;
33
use std::sync::Arc;
44

@@ -21,6 +21,7 @@ use crate::linter::{
2121
options::{LintOptions as LSPLintOptions, Run},
2222
tsgo_linter::TsgoLinter,
2323
};
24+
use crate::utils::normalize_path;
2425
use crate::{ConcurrentHashMap, LINT_CONFIG_FILE};
2526

2627
use super::config_walker::ConfigWalker;
@@ -377,31 +378,6 @@ impl ServerLinter {
377378
}
378379
}
379380

380-
/// Normalize a path by removing `.` and resolving `..` components,
381-
/// without touching the filesystem.
382-
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
383-
let mut result = PathBuf::new();
384-
385-
for component in path.as_ref().components() {
386-
match component {
387-
Component::ParentDir => {
388-
result.pop();
389-
}
390-
Component::CurDir => {
391-
// Skip current directory component
392-
}
393-
Component::Normal(c) => {
394-
result.push(c);
395-
}
396-
Component::RootDir | Component::Prefix(_) => {
397-
result.push(component.as_os_str());
398-
}
399-
}
400-
}
401-
402-
result
403-
}
404-
405381
#[cfg(test)]
406382
mod test {
407383
use std::path::{Path, PathBuf};
@@ -411,20 +387,12 @@ mod test {
411387
linter::{
412388
error_with_position::DiagnosticReport,
413389
options::{LintOptions, Run, UnusedDisableDirectives},
414-
server_linter::{ServerLinter, ServerLinterDiagnostics, normalize_path},
390+
server_linter::{ServerLinter, ServerLinterDiagnostics},
415391
},
416392
tester::{Tester, get_file_path},
417393
};
418394
use rustc_hash::FxHashMap;
419395

420-
#[test]
421-
fn test_normalize_path() {
422-
assert_eq!(
423-
normalize_path(Path::new("/root/directory/./.oxlintrc.json")),
424-
Path::new("/root/directory/.oxlintrc.json")
425-
);
426-
}
427-
428396
#[test]
429397
fn test_create_nested_configs_with_disabled_nested_configs() {
430398
let mut flags = FxHashMap::default();

crates/oxc_language_server/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod linter;
1111
mod options;
1212
#[cfg(test)]
1313
mod tester;
14+
mod utils;
1415
mod worker;
1516

1617
use crate::backend::Backend;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: crates/oxc_language_server/src/formatter/tester.rs
3+
---
4+
========================================
5+
File: fixtures/formatter/custom_config_path/semicolons-as-needed.ts
6+
========================================
7+
Range: Range {
8+
start: Position {
9+
line: 1,
10+
character: 8,
11+
},
12+
end: Position {
13+
line: 1,
14+
character: 9,
15+
},
16+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::path::{Component, Path, PathBuf};
2+
3+
/// Normalize a path by removing `.` and resolving `..` components,
4+
/// without touching the filesystem.
5+
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
6+
let mut result = PathBuf::new();
7+
8+
for component in path.as_ref().components() {
9+
match component {
10+
Component::ParentDir => {
11+
result.pop();
12+
}
13+
Component::CurDir => {
14+
// Skip current directory component
15+
}
16+
Component::Normal(c) => {
17+
result.push(c);
18+
}
19+
Component::RootDir | Component::Prefix(_) => {
20+
result.push(component.as_os_str());
21+
}
22+
}
23+
}
24+
25+
result
26+
}
27+
28+
#[cfg(test)]
29+
mod test {
30+
use std::path::Path;
31+
32+
use crate::utils::normalize_path;
33+
34+
#[test]
35+
fn test_normalize_path() {
36+
assert_eq!(
37+
normalize_path(Path::new("/root/directory/./.oxlintrc.json")),
38+
Path::new("/root/directory/.oxlintrc.json")
39+
);
40+
}
41+
}

crates/oxc_language_server/src/worker.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ use crate::{
1616
formatter::server_formatter::ServerFormatter,
1717
linter::{
1818
error_with_position::DiagnosticReport,
19-
server_linter::{ServerLinter, ServerLinterRun, normalize_path},
19+
server_linter::{ServerLinter, ServerLinterRun},
2020
},
2121
options::Options,
22+
utils::normalize_path,
2223
};
2324

2425
/// A worker that manages the individual tools for a specific workspace
@@ -71,7 +72,8 @@ impl WorkspaceWorker {
7172
*self.server_linter.write().await = Some(ServerLinter::new(&self.root_uri, &options.lint));
7273
if options.format.experimental {
7374
debug!("experimental formatter enabled");
74-
*self.server_formatter.write().await = Some(ServerFormatter::new(&self.root_uri));
75+
*self.server_formatter.write().await =
76+
Some(ServerFormatter::new(&self.root_uri, &options.format));
7577
}
7678
}
7779

@@ -338,10 +340,12 @@ impl WorkspaceWorker {
338340
}
339341

340342
let mut formatting = false;
341-
if current_option.format.experimental != changed_options.format.experimental {
343+
if current_option.format != changed_options.format {
342344
if changed_options.format.experimental {
343-
debug!("experimental formatter enabled");
344-
*self.server_formatter.write().await = Some(ServerFormatter::new(&self.root_uri));
345+
debug!("experimental formatter enabled/restarted");
346+
// restart the formatter
347+
*self.server_formatter.write().await =
348+
Some(ServerFormatter::new(&self.root_uri, &changed_options.format));
345349
formatting = true;
346350
} else {
347351
debug!("experimental formatter disabled");

0 commit comments

Comments
 (0)