Skip to content
Closed
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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ oxc_traverse = { version = "0.105.0", path = "crates/oxc_traverse" } # AST trave

# publish = false
oxc_formatter = { path = "crates/oxc_formatter" } # Code formatting
oxc_format_support = { path = "crates/oxc_format_support" } # Format support helpers
oxc_language_server = { path = "crates/oxc_language_server", default-features = false } # Language server
oxc_linter = { path = "crates/oxc_linter" } # Linting engine
oxc_macros = { path = "crates/oxc_macros" } # Proc macros
Expand Down
5 changes: 4 additions & 1 deletion apps/oxfmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ doctest = false
oxc_allocator = { workspace = true, features = ["pool"] }
oxc_diagnostics = { workspace = true }
oxc_formatter = { workspace = true }
oxc_language_server = { workspace = true, default-features = false, features = ["formatter"] }
oxc_language_server = { workspace = true, default-features = false, features = [
"formatter",
"lsp-prettier",
] }
oxc_napi = { workspace = true }
oxc_parser = { workspace = true }
oxc_span = { workspace = true }
Expand Down
42 changes: 39 additions & 3 deletions apps/oxfmt/src/lsp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
use oxc_language_server::{ServerFormatterBuilder, run_server};
use std::sync::Arc;

use oxc_language_server::{ExternalFormatterBridge, ServerFormatterBuilder, run_server};
use serde_json::Value;
use tokio::task::block_in_place;

use crate::core::{
ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb, JsInitExternalFormatterCb,
};

struct NapiExternalFormatterBridge {
formatter: ExternalFormatter,
}

impl ExternalFormatterBridge for NapiExternalFormatterBridge {
fn init(&self, num_threads: usize) -> Result<(), String> {
block_in_place(|| self.formatter.init(num_threads).map(|_| ()))
}

fn format_file(
&self,
options: &Value,
parser: &str,
file: &str,
code: &str,
) -> Result<String, String> {
block_in_place(|| self.formatter.format_file(options, parser, file, code))
}
}

/// Run the language server
pub async fn run_lsp() {
pub async fn run_lsp(
init_external_formatter_cb: JsInitExternalFormatterCb,
format_embedded_cb: JsFormatEmbeddedCb,
format_file_cb: JsFormatFileCb,
) {
let external_formatter =
ExternalFormatter::new(init_external_formatter_cb, format_embedded_cb, format_file_cb);
let bridge = Arc::new(NapiExternalFormatterBridge { formatter: external_formatter });

run_server(
"oxfmt".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
vec![Box::new(ServerFormatterBuilder)],
vec![Box::new(ServerFormatterBuilder::new(Some(bridge)))],
)
.await;
}
7 changes: 6 additions & 1 deletion apps/oxfmt/src/main_napi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ pub async fn run_cli(
Mode::Init => ("init".to_string(), None),
Mode::Migrate(_) => ("migrate:prettier".to_string(), None),
Mode::Lsp => {
run_lsp().await;
run_lsp(
init_external_formatter_cb,
format_embedded_cb,
format_file_cb,
)
.await;
("lsp".to_string(), Some(0))
}
Mode::Stdin(_) => {
Expand Down
26 changes: 26 additions & 0 deletions crates/oxc_format_support/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "oxc_format_support"
version = "0.1.0"
authors.workspace = true
categories.workspace = true
edition.workspace = true
homepage.workspace = true
include = ["/src"]
keywords.workspace = true
license.workspace = true
publish = false
repository.workspace = true
rust-version.workspace = true
description.workspace = true

[lints]
workspace = true

[dependencies]
oxc_formatter = { workspace = true }
serde_json = { workspace = true }
json-strip-comments = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
serde_json = { workspace = true }
125 changes: 125 additions & 0 deletions crates/oxc_format_support/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::path::{Path, PathBuf};

use oxc_formatter::{FormatOptions, oxfmtrc::Oxfmtrc};
use serde_json::Value;

pub enum PrettierFileStrategy {
External { parser_name: &'static str },
}

pub fn detect_prettier_file(path: &Path) -> Option<PrettierFileStrategy> {
let extension = path.extension()?.to_str()?;

let parser_name = match extension {
"json" => "json",
"jsonc" => "jsonc",
"css" => "css",
"md" => "markdown",
_ => return None,
};

Some(PrettierFileStrategy::External { parser_name })
}

pub fn load_oxfmtrc(root: &Path) -> Result<(FormatOptions, Value), String> {
let config_path = find_oxfmtrc(root);

let json_string = match &config_path {
Some(path) => {
let mut json_string = std::fs::read_to_string(path)
// Do not include OS error, it differs between platforms
.map_err(|_| format!("Failed to read config {}: File not found", path.display()))?;
json_strip_comments::strip(&mut json_string)
.map_err(|err| format!("Failed to strip comments from {}: {err}", path.display()))?;
json_string
}
None => "{}".to_string(),
};

let raw_config: Value = serde_json::from_str(&json_string)
.map_err(|err| format!("Failed to parse config: {err}"))?;

let oxfmtrc: Oxfmtrc = serde_json::from_value(raw_config.clone())
.map_err(|err| format!("Failed to deserialize Oxfmtrc: {err}"))?;

let (format_options, _) = oxfmtrc
.into_options()
.map_err(|err| format!("Failed to parse configuration.\n{err}"))?;

let mut external_options = raw_config;
Oxfmtrc::populate_prettier_config(&format_options, &mut external_options);

Ok((format_options, external_options))
}

fn find_oxfmtrc(root: &Path) -> Option<PathBuf> {
root.ancestors().find_map(|dir| {
let json_path = dir.join(".oxfmtrc.json");
if json_path.exists() {
return Some(json_path);
}
let jsonc_path = dir.join(".oxfmtrc.jsonc");
if jsonc_path.exists() {
return Some(jsonc_path);
}
None
})
}

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

use serde_json::Value;
use tempfile::tempdir;

use super::{detect_prettier_file, load_oxfmtrc};

#[test]
fn detect_prettier_file_extensions() {
let cases = [
("file.json", "json"),
("file.jsonc", "jsonc"),
("file.css", "css"),
("file.md", "markdown"),
];

for (path, parser_name) in cases {
let strategy = detect_prettier_file(Path::new(path)).expect("expected strategy");
match strategy {
super::PrettierFileStrategy::External { parser_name: name } => {
assert_eq!(name, parser_name);
}
}
}

assert!(detect_prettier_file(Path::new("file.ts")).is_none());
}

#[test]
fn load_oxfmtrc_defaults_when_missing() {
let dir = tempdir().expect("tempdir");
let result = load_oxfmtrc(dir.path());
assert!(result.is_ok());
let (_, external_options) = result.unwrap();
assert!(external_options.is_object());
}

#[test]
fn load_oxfmtrc_jsonc_with_comments() {
let dir = tempdir().expect("tempdir");
let config_path = dir.path().join(".oxfmtrc.jsonc");
fs::write(
&config_path,
"{\n// comment\n\"printWidth\": 120\n}\n",
)
.expect("write config");

let (_, external_options) = load_oxfmtrc(dir.path()).expect("load config");
assert_eq!(
external_options.get("printWidth").and_then(Value::as_u64),
Some(120)
);
}
}
2 changes: 2 additions & 0 deletions crates/oxc_language_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ oxc_allocator = { workspace = true, optional = true }
oxc_data_structures = { workspace = true, features = ["rope"], optional = true }
oxc_diagnostics = { workspace = true, optional = true }
oxc_formatter = { workspace = true, optional = true }
oxc_format_support = { workspace = true, optional = true }
oxc_linter = { workspace = true, optional = true }
oxc_parser = { workspace = true, optional = true }

Expand Down Expand Up @@ -67,3 +68,4 @@ formatter = [
#
"dep:ignore",
]
lsp-prettier = ["dep:oxc_format_support"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"b":1,"a":2}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use serde_json::Value;

pub trait ExternalFormatterBridge: Send + Sync {
fn init(&self, num_threads: usize) -> Result<(), String>;
fn format_file(
&self,
options: &Value,
parser: &str,
file: &str,
code: &str,
) -> Result<String, String>;
}

#[derive(Debug, Default)]
pub struct NoopBridge;

impl ExternalFormatterBridge for NoopBridge {
fn init(&self, _num_threads: usize) -> Result<(), String> {
Ok(())
}

fn format_file(
&self,
_options: &Value,
_parser: &str,
_file: &str,
_code: &str,
) -> Result<String, String> {
Err("External formatter bridge not configured".to_string())
}
}
2 changes: 2 additions & 0 deletions crates/oxc_language_server/src/formatter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod options;
mod external_formatter_bridge;
mod server_formatter;
#[cfg(test)]
mod tester;

pub use external_formatter_bridge::{ExternalFormatterBridge, NoopBridge};
pub use server_formatter::ServerFormatterBuilder;

const FORMAT_CONFIG_FILES: &[&str; 2] = &[".oxfmtrc.json", ".oxfmtrc.jsonc"];
Loading
Loading