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
10 changes: 7 additions & 3 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ These options can be passed with [initialize](#initialize), [workspace/didChange
| `typeAware` | `true` \| `false` | `false` | Enables type-aware linting |
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
| `fmt.experimental` | `true` \| `false` | `false` | Enables experimental formatting with `oxc_formatter` |
| `fmt.configPath` | `<string>` \| `null` | `null` | Path to a oxfmt configuration file, when `null` is passed, the server will use `.oxfmtrc.json` and the workspace root |

## Supported LSP Specifications from Server

Expand All @@ -47,7 +48,8 @@ The client can pass the workspace options like following:
"unusedDisableDirectives": "allow",
"typeAware": false,
"flags": {},
"fmt.experimental": false
"fmt.experimental": false,
"fmt.configPath": null
}
}]
}
Expand Down Expand Up @@ -84,7 +86,8 @@ The client can pass the workspace options like following:
"unusedDisableDirectives": "allow",
"typeAware": false,
"flags": {},
"fmt.experimental": false
"fmt.experimental": false,
"fmt.configPath": null
}
}]
}
Expand Down Expand Up @@ -178,6 +181,7 @@ The client can return a response like:
"unusedDisableDirectives": "allow",
"typeAware": false,
"flags": {},
"fmt.experimental": false
"fmt.experimental": false,
"fmt.configPath": null
}]
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"semicolons": "as-needed"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// semicolon is on character 8-9, which will be removed
debugger;
11 changes: 10 additions & 1 deletion crates/oxc_language_server/src/formatter/options.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use serde_json::Value;

#[derive(Debug, Default, Serialize, Clone)]
#[derive(Debug, Default, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct FormatOptions {
pub experimental: bool,
pub config_path: Option<String>,
}

impl<'de> Deserialize<'de> for FormatOptions {
Expand All @@ -29,6 +30,9 @@ impl TryFrom<Value> for FormatOptions {
experimental: object
.get("fmt.experimental")
.is_some_and(|run| serde_json::from_value::<bool>(run.clone()).unwrap_or_default()),
config_path: object
.get("fmt.configPath")
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
})
}
}
Expand All @@ -43,10 +47,12 @@ mod test {
fn test_valid_options_json() {
let json = json!({
"fmt.experimental": true,
"fmt.configPath": "./.oxfmtrc.json"
});

let options = FormatOptions::try_from(json).unwrap();
assert!(options.experimental);
assert_eq!(options.config_path.unwrap(), "./.oxfmtrc.json");
}

#[test]
Expand All @@ -55,15 +61,18 @@ mod test {

let options = FormatOptions::try_from(json).unwrap();
assert!(!options.experimental);
assert!(options.config_path.is_none());
}

#[test]
fn test_invalid_options_json() {
let json = json!({
"fmt.experimental": "what", // should be bool
"fmt.configPath": "./.oxfmtrc.json"
});

let options = FormatOptions::try_from(json).unwrap();
assert!(!options.experimental);
assert_eq!(options.config_path.unwrap(), "./.oxfmtrc.json");
}
}
40 changes: 29 additions & 11 deletions crates/oxc_language_server/src/formatter/server_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ use tower_lsp_server::{
lsp_types::{Position, Range, TextEdit, Uri},
};

use crate::FORMAT_CONFIG_FILE;

use crate::formatter::options::FormatOptions as LSPFormatOptions;
use crate::{FORMAT_CONFIG_FILE, utils::normalize_path};
pub struct ServerFormatter {
options: FormatOptions,
}

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

Self { options: Self::get_format_options(&root_path) }
Self { options: Self::get_format_options(&root_path, options.config_path.as_ref()) }
}

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

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

#[test]
fn test_formatter() {
Tester::new("fixtures/formatter/basic", Some(FormatOptions { experimental: true }))
.format_and_snapshot_single_file("basic.ts");
Tester::new(
"fixtures/formatter/basic",
Some(FormatOptions { experimental: true, ..Default::default() }),
)
.format_and_snapshot_single_file("basic.ts");
}

#[test]
fn test_root_config_detection() {
Tester::new("fixtures/formatter/root_config", Some(FormatOptions { experimental: true }))
.format_and_snapshot_single_file("semicolons-as-needed.ts");
Tester::new(
"fixtures/formatter/root_config",
Some(FormatOptions { experimental: true, ..Default::default() }),
)
.format_and_snapshot_single_file("semicolons-as-needed.ts");
}

#[test]
fn test_custom_config_path() {
Tester::new(
"fixtures/formatter/custom_config_path",
Some(FormatOptions {
experimental: true,
config_path: Some("./format.json".to_string()),
}),
)
.format_and_snapshot_single_file("semicolons-as-needed.ts");
}
}
38 changes: 3 additions & 35 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::{Component, Path, PathBuf};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;

Expand All @@ -21,6 +21,7 @@ use crate::linter::{
options::{LintOptions as LSPLintOptions, Run},
tsgo_linter::TsgoLinter,
};
use crate::utils::normalize_path;
use crate::{ConcurrentHashMap, LINT_CONFIG_FILE};

use super::config_walker::ConfigWalker;
Expand Down Expand Up @@ -377,31 +378,6 @@ impl ServerLinter {
}
}

/// Normalize a path by removing `.` and resolving `..` components,
/// without touching the filesystem.
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
let mut result = PathBuf::new();

for component in path.as_ref().components() {
match component {
Component::ParentDir => {
result.pop();
}
Component::CurDir => {
// Skip current directory component
}
Component::Normal(c) => {
result.push(c);
}
Component::RootDir | Component::Prefix(_) => {
result.push(component.as_os_str());
}
}
}

result
}

#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
Expand All @@ -411,20 +387,12 @@ mod test {
linter::{
error_with_position::DiagnosticReport,
options::{LintOptions, Run, UnusedDisableDirectives},
server_linter::{ServerLinter, ServerLinterDiagnostics, normalize_path},
server_linter::{ServerLinter, ServerLinterDiagnostics},
},
tester::{Tester, get_file_path},
};
use rustc_hash::FxHashMap;

#[test]
fn test_normalize_path() {
assert_eq!(
normalize_path(Path::new("/root/directory/./.oxlintrc.json")),
Path::new("/root/directory/.oxlintrc.json")
);
}

#[test]
fn test_create_nested_configs_with_disabled_nested_configs() {
let mut flags = FxHashMap::default();
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod linter;
mod options;
#[cfg(test)]
mod tester;
mod utils;
mod worker;

use crate::backend::Backend;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/oxc_language_server/src/formatter/tester.rs
---
========================================
File: fixtures/formatter/custom_config_path/semicolons-as-needed.ts
========================================
Range: Range {
start: Position {
line: 1,
character: 8,
},
end: Position {
line: 1,
character: 9,
},
}
41 changes: 41 additions & 0 deletions crates/oxc_language_server/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::path::{Component, Path, PathBuf};

/// Normalize a path by removing `.` and resolving `..` components,
/// without touching the filesystem.
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
let mut result = PathBuf::new();

for component in path.as_ref().components() {
match component {
Component::ParentDir => {
result.pop();
}
Component::CurDir => {
// Skip current directory component
}
Component::Normal(c) => {
result.push(c);
}
Component::RootDir | Component::Prefix(_) => {
result.push(component.as_os_str());
}
}
}

result
}

#[cfg(test)]
mod test {
use std::path::Path;

use crate::utils::normalize_path;

#[test]
fn test_normalize_path() {
assert_eq!(
normalize_path(Path::new("/root/directory/./.oxlintrc.json")),
Path::new("/root/directory/.oxlintrc.json")
);
}
}
14 changes: 9 additions & 5 deletions crates/oxc_language_server/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ use crate::{
formatter::server_formatter::ServerFormatter,
linter::{
error_with_position::DiagnosticReport,
server_linter::{ServerLinter, ServerLinterRun, normalize_path},
server_linter::{ServerLinter, ServerLinterRun},
},
options::Options,
utils::normalize_path,
};

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

Expand Down Expand Up @@ -338,10 +340,12 @@ impl WorkspaceWorker {
}

let mut formatting = false;
if current_option.format.experimental != changed_options.format.experimental {
if current_option.format != changed_options.format {
if changed_options.format.experimental {
debug!("experimental formatter enabled");
*self.server_formatter.write().await = Some(ServerFormatter::new(&self.root_uri));
debug!("experimental formatter enabled/restarted");
// restart the formatter
*self.server_formatter.write().await =
Some(ServerFormatter::new(&self.root_uri, &changed_options.format));
formatting = true;
} else {
debug!("experimental formatter disabled");
Expand Down
Loading