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
54 changes: 5 additions & 49 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use futures::future::join_all;
use log::{debug, info, warn};
use oxc_linter::FixKind;
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::{Deserialize, Serialize};
use std::{fmt::Debug, str::FromStr};
use options::{Options, Run, WorkspaceOption};
use rustc_hash::FxBuildHasher;
use std::str::FromStr;
use tokio::sync::{Mutex, OnceCell, SetError};
use tower_lsp_server::{
Client, LanguageServer, LspService, Server,
Expand All @@ -26,6 +25,7 @@ mod capabilities;
mod code_actions;
mod commands;
mod linter;
mod options;
#[cfg(test)]
mod tester;
mod worker;
Expand All @@ -44,49 +44,6 @@ struct Backend {
capabilities: OnceCell<Capabilities>,
}

#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum Run {
OnSave,
#[default]
OnType,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct Options {
run: Run,
config_path: Option<String>,
flags: FxHashMap<String, String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct WorkspaceOption {
workspace_uri: Uri,
options: Options,
}

impl Options {
fn use_nested_configs(&self) -> bool {
!self.flags.contains_key("disable_nested_config") || self.config_path.is_some()
}

fn fix_kind(&self) -> FixKind {
self.flags.get("fix_kind").map_or(FixKind::SafeFix, |kind| match kind.as_str() {
"safe_fix" => FixKind::SafeFix,
"safe_fix_or_suggestion" => FixKind::SafeFixOrSuggestion,
"dangerous_fix" => FixKind::DangerousFix,
"dangerous_fix_or_suggestion" => FixKind::DangerousFixOrSuggestion,
"none" => FixKind::None,
"all" => FixKind::All,
_ => {
info!("invalid fix_kind flag `{kind}`, fallback to `safe_fix`");
FixKind::SafeFix
}
})
}
}

impl LanguageServer for Backend {
#[expect(deprecated)] // `params.root_uri` is deprecated, we are only falling back to it if no workspace folder is provided
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
Expand All @@ -100,8 +57,7 @@ impl LanguageServer for Backend {
return Some(new_settings);
}

let deprecated_settings =
serde_json::from_value::<Options>(value.get_mut("settings")?.take()).ok();
let deprecated_settings = Options::try_from(value.get_mut("settings")?.take()).ok();

// the client has deprecated settings and has a deprecated root uri.
// handle all things like the old way
Expand Down
181 changes: 181 additions & 0 deletions crates/oxc_language_server/src/options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use log::info;
use oxc_linter::FixKind;
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use serde_json::Value;
use tower_lsp_server::lsp_types::Uri;

#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum Run {
OnSave,
#[default]
OnType,
}

#[derive(Debug, Default, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Options {
pub run: Run,
pub config_path: Option<String>,
pub flags: FxHashMap<String, String>,
}

impl Options {
pub fn use_nested_configs(&self) -> bool {
!self.flags.contains_key("disable_nested_config") || self.config_path.is_some()
}

pub fn fix_kind(&self) -> FixKind {
self.flags.get("fix_kind").map_or(FixKind::SafeFix, |kind| match kind.as_str() {
"safe_fix" => FixKind::SafeFix,
"safe_fix_or_suggestion" => FixKind::SafeFixOrSuggestion,
"dangerous_fix" => FixKind::DangerousFix,
"dangerous_fix_or_suggestion" => FixKind::DangerousFixOrSuggestion,
"none" => FixKind::None,
"all" => FixKind::All,
_ => {
info!("invalid fix_kind flag `{kind}`, fallback to `safe_fix`");
FixKind::SafeFix
}
})
}
}

impl<'de> Deserialize<'de> for Options {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
Options::try_from(value).map_err(Error::custom)
}
}

impl TryFrom<Value> for Options {
type Error = String;

fn try_from(value: Value) -> Result<Self, Self::Error> {
let Some(object) = value.as_object() else {
return Err("no object passed".to_string());
};

let mut flags = FxHashMap::with_capacity_and_hasher(2, FxBuildHasher);
if let Some(json_flags) = object.get("flags").and_then(|value| value.as_object()) {
if let Some(disable_nested_config) =
json_flags.get("disable_nested_config").and_then(|value| value.as_str())
{
flags
.insert("disable_nested_config".to_string(), disable_nested_config.to_string());
}

if let Some(fix_kind) = json_flags.get("fix_kind").and_then(|value| value.as_str()) {
flags.insert("fix_kind".to_string(), fix_kind.to_string());
}
}

Ok(Self {
run: object
.get("run")
.map(|run| serde_json::from_value::<Run>(run.clone()).unwrap_or_default())
.unwrap_or_default(),
config_path: object
.get("configPath")
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
flags,
})
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceOption {
pub workspace_uri: Uri,
pub options: Options,
}

#[cfg(test)]
mod test {
use serde_json::json;

use super::{Options, Run, WorkspaceOption};

#[test]
fn test_valid_options_json() {
let json = json!({
"run": "onSave",
"configPath": "./custom.json",
"flags": {
"disable_nested_config": "true",
"fix_kind": "dangerous_fix"
}
});

let options = Options::try_from(json).unwrap();
assert_eq!(options.run, Run::OnSave);
assert_eq!(options.config_path, Some("./custom.json".into()));
assert_eq!(options.flags.get("disable_nested_config"), Some(&"true".to_string()));
assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string()));
}

#[test]
fn test_empty_options_json() {
let json = json!({});

let options = Options::try_from(json).unwrap();
assert_eq!(options.run, Run::OnType);
assert_eq!(options.config_path, None);
assert!(options.flags.is_empty());
}

#[test]
fn test_invalid_options_json() {
let json = json!({
"run": true,
"configPath": "./custom.json"
});

let options = Options::try_from(json).unwrap();
assert_eq!(options.run, Run::OnType); // fallback
assert_eq!(options.config_path, Some("./custom.json".into()));
assert!(options.flags.is_empty());
}

#[test]
fn test_invalid_flags_options_json() {
let json = json!({
"configPath": "./custom.json",
"flags": {
"disable_nested_config": true, // should be string
"fix_kind": "dangerous_fix"
}
});

let options = Options::try_from(json).unwrap();
assert_eq!(options.run, Run::OnType); // fallback
assert_eq!(options.config_path, Some("./custom.json".into()));
assert_eq!(options.flags.get("disable_nested_config"), None);
assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string()));
}

#[test]
fn test_invalid_workspace_options_json() {
let json = json!([{
"workspaceUri": "file:///root/",
"options": {
"run": true,
"configPath": "./custom.json"
}
}]);

let workspace = serde_json::from_value::<Vec<WorkspaceOption>>(json).unwrap();

assert_eq!(workspace.len(), 1);
assert_eq!(workspace[0].workspace_uri.path().as_str(), "/root/");

let options = &workspace[0].options;
assert_eq!(options.run, Run::OnType); // fallback
assert_eq!(options.config_path, Some("./custom.json".into()));
assert!(options.flags.is_empty());
}
}
2 changes: 1 addition & 1 deletion editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ Following configuration are supported via `settings.json` and can be changed for
## Testing

Run `pnpm server:build:debug` to build the language server.
After that, you can test the vscode plugin + E2E Tests with `pnm test`.
After that, you can test the vscode plugin + E2E Tests with `pnpm test`.
Loading