Skip to content

Commit 58926e6

Browse files
committed
refactor(language_server): introduce dummy ServerFormatter
1 parent ad14a41 commit 58926e6

File tree

13 files changed

+309
-76
lines changed

13 files changed

+309
-76
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_language_server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ doctest = false
2424
[dependencies]
2525
oxc_allocator = { workspace = true }
2626
oxc_diagnostics = { workspace = true }
27+
oxc_formatter = { workspace = true }
2728
oxc_linter = { workspace = true, features = ["language_server"] }
29+
oxc_parser = { workspace = true }
2830

2931
#
3032
env_logger = { workspace = true, features = ["humantime"] }

crates/oxc_language_server/README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ These options can be passed with [initialize](#initialize), [workspace/didChange
2727
| `unusedDisableDirectives` | `"allow" \| "warn"` \| "deny"` | `"allow"` | Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway |
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` |
30+
| `fmt.experimental` | `true` \| `false` | `false` | Enables experimental formatting with `oxc_formatter` |
3031

3132
## Supported LSP Specifications from Server
3233

@@ -45,7 +46,8 @@ The client can pass the workspace options like following:
4546
"tsConfigPath": null,
4647
"unusedDisableDirectives": "allow",
4748
"typeAware": false,
48-
"flags": {}
49+
"flags": {},
50+
"fmt.experimental": false
4951
}
5052
}]
5153
}
@@ -81,7 +83,8 @@ The client can pass the workspace options like following:
8183
"tsConfigPath": null,
8284
"unusedDisableDirectives": "allow",
8385
"typeAware": false,
84-
"flags": {}
86+
"flags": {},
87+
"fmt.experimental": false
8588
}
8689
}]
8790
}
@@ -142,6 +145,10 @@ Returns a list of [CodeAction](https://microsoft.github.io/language-server-proto
142145

143146
Returns a [PublishDiagnostic object](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#publishDiagnosticsParams)
144147

148+
#### [textDocument/formatting](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting)
149+
150+
Returns a list of [TextEdit](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit)
151+
145152
## Optional LSP Specifications from Client
146153

147154
### Client
@@ -170,6 +177,7 @@ The client can return a response like:
170177
"tsConfigPath": null,
171178
"unusedDisableDirectives": "allow",
172179
"typeAware": false,
173-
"flags": {}
180+
"flags": {},
181+
"fmt.experimental": false
174182
}]
175183
```

crates/oxc_language_server/src/capabilities.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,19 @@ impl From<ClientCapabilities> for Capabilities {
4040
watched_files.dynamic_registration.is_some_and(|dynamic| dynamic)
4141
})
4242
});
43-
// TODO: enable it when we support formatting
44-
// let formatting = value.text_document.as_ref().is_some_and(|text_document| {
45-
// text_document.formatting.is_some_and(|formatting| {
46-
// formatting.dynamic_registration.is_some_and(|dynamic| dynamic)
47-
// })
48-
// });
43+
let dynamic_formatting = value.text_document.as_ref().is_some_and(|text_document| {
44+
text_document.formatting.is_some_and(|formatting| {
45+
formatting.dynamic_registration.is_some_and(|dynamic| dynamic)
46+
})
47+
});
4948

5049
Self {
5150
code_action_provider,
5251
workspace_apply_edit,
5352
workspace_execute_command,
5453
workspace_configuration,
5554
dynamic_watchers,
56-
dynamic_formatting: false,
55+
dynamic_formatting,
5756
}
5857
}
5958
}
@@ -100,6 +99,8 @@ impl From<Capabilities> for ServerCapabilities {
10099
} else {
101100
None
102101
},
102+
// the server supports formatting, but it will tell the client if he enabled the setting
103+
document_formatting_provider: None,
103104
..ServerCapabilities::default()
104105
}
105106
}

crates/oxc_language_server/src/file_system.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ impl LSPFileSystem {
1616
self.files.pin().insert(uri.clone(), content);
1717
}
1818

19-
#[expect(dead_code)] // used for the oxc_formatter in the future
2019
pub fn get(&self, uri: &Uri) -> Option<String> {
2120
self.files.pin().get(uri).cloned()
2221
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod options;
2+
pub mod server_formatter;
Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,69 @@
1-
use serde::{Deserialize, Serialize};
1+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
2+
use serde_json::Value;
23

3-
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
4+
#[derive(Debug, Default, Serialize, Clone)]
45
#[serde(rename_all = "camelCase")]
5-
pub struct FormatOptions;
6+
pub struct FormatOptions {
7+
pub experimental: bool,
8+
}
9+
10+
impl<'de> Deserialize<'de> for FormatOptions {
11+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
12+
where
13+
D: Deserializer<'de>,
14+
{
15+
let value = Value::deserialize(deserializer)?;
16+
FormatOptions::try_from(value).map_err(Error::custom)
17+
}
18+
}
19+
20+
impl TryFrom<Value> for FormatOptions {
21+
type Error = String;
22+
23+
fn try_from(value: Value) -> Result<Self, Self::Error> {
24+
let Some(object) = value.as_object() else {
25+
return Err("no object passed".to_string());
26+
};
27+
28+
Ok(Self {
29+
experimental: object
30+
.get("fmt.experimental")
31+
.is_some_and(|run| serde_json::from_value::<bool>(run.clone()).unwrap_or_default()),
32+
})
33+
}
34+
}
35+
36+
#[cfg(test)]
37+
mod test {
38+
use serde_json::json;
39+
40+
use super::FormatOptions;
41+
42+
#[test]
43+
fn test_valid_options_json() {
44+
let json = json!({
45+
"fmt.experimental": true,
46+
});
47+
48+
let options = FormatOptions::try_from(json).unwrap();
49+
assert!(options.experimental);
50+
}
51+
52+
#[test]
53+
fn test_empty_options_json() {
54+
let json = json!({});
55+
56+
let options = FormatOptions::try_from(json).unwrap();
57+
assert!(!options.experimental);
58+
}
59+
60+
#[test]
61+
fn test_invalid_options_json() {
62+
let json = json!({
63+
"fmt.experimental": "what", // should be bool
64+
});
65+
66+
let options = FormatOptions::try_from(json).unwrap();
67+
assert!(!options.experimental);
68+
}
69+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use oxc_allocator::Allocator;
2+
use oxc_formatter::{FormatOptions, Formatter, get_supported_source_type};
3+
use oxc_parser::{ParseOptions, Parser};
4+
use tower_lsp_server::{
5+
UriExt,
6+
lsp_types::{Position, Range, TextEdit, Uri},
7+
};
8+
9+
use crate::LSP_MAX_INT;
10+
11+
pub struct ServerFormatter;
12+
13+
impl ServerFormatter {
14+
pub fn new() -> Self {
15+
Self {}
16+
}
17+
18+
#[expect(clippy::unused_self)]
19+
pub fn run_single(&self, uri: &Uri, content: Option<String>) -> Option<Vec<TextEdit>> {
20+
let path = uri.to_file_path()?;
21+
let source_type = get_supported_source_type(&path)?;
22+
let source_text = if let Some(content) = content {
23+
content
24+
} else {
25+
std::fs::read_to_string(&path).ok()?
26+
};
27+
28+
let allocator = Allocator::new();
29+
let ret = Parser::new(&allocator, &source_text, source_type)
30+
.with_options(ParseOptions {
31+
parse_regular_expression: false,
32+
// Enable all syntax features
33+
allow_v8_intrinsics: true,
34+
allow_return_outside_function: true,
35+
// `oxc_formatter` expects this to be false
36+
preserve_parens: false,
37+
})
38+
.parse();
39+
40+
if !ret.errors.is_empty() {
41+
return None;
42+
}
43+
44+
let options = FormatOptions::default();
45+
let code = Formatter::new(&allocator, options).build(&ret.program);
46+
47+
// nothing has changed
48+
if code == source_text {
49+
return Some(vec![]);
50+
}
51+
52+
Some(vec![TextEdit::new(
53+
Range::new(Position::new(0, 0), Position::new(LSP_MAX_INT, 0)),
54+
code,
55+
)])
56+
}
57+
}

crates/oxc_language_server/src/linter/error_with_position.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ use tower_lsp_server::lsp_types::{
77

88
use oxc_diagnostics::Severity;
99

10-
// max range for LSP integer is 2^31 - 1
11-
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseTypes
12-
const LSP_MAX_INT: u32 = 2u32.pow(31) - 1;
10+
use crate::LSP_MAX_INT;
1311

1412
#[derive(Debug, Clone)]
1513
pub struct DiagnosticReport {

0 commit comments

Comments
 (0)