From 55378987af9c6eab642eee3e28439057dadc0e26 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:53:36 +0100 Subject: [PATCH 1/6] feat(lsp): add Prettier-only delegation bridge --- Cargo.lock | 11 ++ Cargo.toml | 1 + apps/oxfmt/Cargo.toml | 5 +- apps/oxfmt/src/lsp/mod.rs | 41 ++++- apps/oxfmt/src/main_napi.rs | 7 +- crates/oxc_format_support/Cargo.toml | 26 +++ crates/oxc_format_support/src/lib.rs | 125 ++++++++++++++ crates/oxc_language_server/Cargo.toml | 2 + .../formatter/prettier_only/sample.json | 1 + .../formatter/external_formatter_bridge.rs | 31 ++++ .../oxc_language_server/src/formatter/mod.rs | 2 + .../src/formatter/server_formatter.rs | 153 ++++++++++++++++-- .../src/formatter/tester.rs | 1 + crates/oxc_language_server/src/lib.rs | 2 +- crates/oxc_language_server/src/main.rs | 2 +- 15 files changed, 391 insertions(+), 19 deletions(-) create mode 100644 crates/oxc_format_support/Cargo.toml create mode 100644 crates/oxc_format_support/src/lib.rs create mode 100644 crates/oxc_language_server/fixtures/formatter/prettier_only/sample.json create mode 100644 crates/oxc_language_server/src/formatter/external_formatter_bridge.rs diff --git a/Cargo.lock b/Cargo.lock index eebaea8192a96..9572144001530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1962,6 +1962,16 @@ dependencies = [ "oxc_data_structures", ] +[[package]] +name = "oxc_format_support" +version = "0.1.0" +dependencies = [ + "json-strip-comments", + "oxc_formatter", + "serde_json", + "tempfile", +] + [[package]] name = "oxc_formatter" version = "0.20.0" @@ -2027,6 +2037,7 @@ dependencies = [ "oxc_allocator", "oxc_data_structures", "oxc_diagnostics", + "oxc_format_support", "oxc_formatter", "oxc_linter", "oxc_parser", diff --git a/Cargo.toml b/Cargo.toml index 005bfb51fe21c..93ac1ed1d7cfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/apps/oxfmt/Cargo.toml b/apps/oxfmt/Cargo.toml index 56274feac947f..9044baa6d8313 100644 --- a/apps/oxfmt/Cargo.toml +++ b/apps/oxfmt/Cargo.toml @@ -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 } diff --git a/apps/oxfmt/src/lsp/mod.rs b/apps/oxfmt/src/lsp/mod.rs index 2059b23a0e027..7e2111ce6e182 100644 --- a/apps/oxfmt/src/lsp/mod.rs +++ b/apps/oxfmt/src/lsp/mod.rs @@ -1,11 +1,46 @@ -use oxc_language_server::{ServerFormatterBuilder, run_server}; +use std::sync::Arc; + +use oxc_language_server::{ExternalFormatterBridge, ServerFormatterBuilder, run_server}; +use serde_json::Value; + +use crate::core::{ + ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb, JsInitExternalFormatterCb, +}; + +struct NapiExternalFormatterBridge { + formatter: ExternalFormatter, +} + +impl ExternalFormatterBridge for NapiExternalFormatterBridge { + fn init(&self, num_threads: usize) -> Result<(), String> { + self.formatter.init(num_threads).map(|_| ()) + } + + fn format_file( + &self, + options: &Value, + parser: &str, + file: &str, + code: &str, + ) -> Result { + 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; } diff --git a/apps/oxfmt/src/main_napi.rs b/apps/oxfmt/src/main_napi.rs index cdfc5dc23c804..55ce245c9eb81 100644 --- a/apps/oxfmt/src/main_napi.rs +++ b/apps/oxfmt/src/main_napi.rs @@ -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(_) => { diff --git a/crates/oxc_format_support/Cargo.toml b/crates/oxc_format_support/Cargo.toml new file mode 100644 index 0000000000000..2624cc0e3b52f --- /dev/null +++ b/crates/oxc_format_support/Cargo.toml @@ -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 } diff --git a/crates/oxc_format_support/src/lib.rs b/crates/oxc_format_support/src/lib.rs new file mode 100644 index 0000000000000..2399fde1aca47 --- /dev/null +++ b/crates/oxc_format_support/src/lib.rs @@ -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 { + 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 { + 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) + ); + } +} diff --git a/crates/oxc_language_server/Cargo.toml b/crates/oxc_language_server/Cargo.toml index 913c79a7cccf1..b22224be65d83 100644 --- a/crates/oxc_language_server/Cargo.toml +++ b/crates/oxc_language_server/Cargo.toml @@ -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 } @@ -67,3 +68,4 @@ formatter = [ # "dep:ignore", ] +lsp-prettier = ["dep:oxc_format_support"] diff --git a/crates/oxc_language_server/fixtures/formatter/prettier_only/sample.json b/crates/oxc_language_server/fixtures/formatter/prettier_only/sample.json new file mode 100644 index 0000000000000..c40aaf34c9eeb --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/prettier_only/sample.json @@ -0,0 +1 @@ +{"b":1,"a":2} diff --git a/crates/oxc_language_server/src/formatter/external_formatter_bridge.rs b/crates/oxc_language_server/src/formatter/external_formatter_bridge.rs new file mode 100644 index 0000000000000..404d515f6e58a --- /dev/null +++ b/crates/oxc_language_server/src/formatter/external_formatter_bridge.rs @@ -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; +} + +#[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 { + Err("External formatter bridge not configured".to_string()) + } +} diff --git a/crates/oxc_language_server/src/formatter/mod.rs b/crates/oxc_language_server/src/formatter/mod.rs index 4fac84212e425..b39bb08cbb0c3 100644 --- a/crates/oxc_language_server/src/formatter/mod.rs +++ b/crates/oxc_language_server/src/formatter/mod.rs @@ -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"]; diff --git a/crates/oxc_language_server/src/formatter/server_formatter.rs b/crates/oxc_language_server/src/formatter/server_formatter.rs index 5cff4a4bf3783..ae5f5b1bcec4f 100644 --- a/crates/oxc_language_server/src/formatter/server_formatter.rs +++ b/crates/oxc_language_server/src/formatter/server_formatter.rs @@ -1,4 +1,5 @@ use std::path::{Path, PathBuf}; +use std::sync::Arc; use ignore::gitignore::{Gitignore, GitignoreBuilder}; use log::{debug, warn}; @@ -9,21 +10,38 @@ use oxc_formatter::{ oxfmtrc::{OxfmtOptions, Oxfmtrc}, }; use oxc_parser::Parser; +use serde_json::Value; use tower_lsp_server::ls_types::{Pattern, Position, Range, ServerCapabilities, TextEdit, Uri}; use crate::{ capabilities::Capabilities, - formatter::{FORMAT_CONFIG_FILES, options::FormatOptions as LSPFormatOptions}, + formatter::{ExternalFormatterBridge, FORMAT_CONFIG_FILES, options::FormatOptions as LSPFormatOptions}, tool::{Tool, ToolBuilder, ToolRestartChanges}, utils::normalize_path, }; -pub struct ServerFormatterBuilder; +#[cfg(feature = "lsp-prettier")] +use oxc_format_support::{PrettierFileStrategy, detect_prettier_file, load_oxfmtrc}; + +#[derive(Clone, Default)] +pub struct ServerFormatterBuilder { + external_bridge: Option>, +} impl ServerFormatterBuilder { /// # Panics /// Panics if the root URI cannot be converted to a file path. - pub fn build(root_uri: &Uri, options: serde_json::Value) -> ServerFormatter { + pub fn new(external_bridge: Option>) -> Self { + Self { external_bridge } + } + + /// # Panics + /// Panics if the root URI cannot be converted to a file path. + pub fn build( + root_uri: &Uri, + options: serde_json::Value, + external_bridge: Option>, + ) -> ServerFormatter { let options = match serde_json::from_value::(options) { Ok(opts) => opts, Err(err) => { @@ -49,7 +67,10 @@ impl ServerFormatterBuilder { } }; - ServerFormatter::new(format_options, gitignore_glob) + let (external_options, external_bridge) = + Self::resolve_external_options(&root_path, external_bridge); + + ServerFormatter::new(format_options, gitignore_glob, external_options, external_bridge) } } @@ -63,7 +84,11 @@ impl ToolBuilder for ServerFormatterBuilder { Some(tower_lsp_server::ls_types::OneOf::Left(true)); } fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box { - Box::new(ServerFormatterBuilder::build(root_uri, options)) + Box::new(ServerFormatterBuilder::build( + root_uri, + options, + self.external_bridge.clone(), + )) } } @@ -152,10 +177,47 @@ impl ServerFormatterBuilder { builder.build().map_err(|_| "Failed to build ignore globs".to_string()) } + + fn resolve_external_options( + _root_path: &Path, + external_bridge: Option>, + ) -> (Value, Option>) { + #[cfg(feature = "lsp-prettier")] + { + let mut external_options = Value::Object(serde_json::Map::new()); + let mut external_bridge = external_bridge; + + match load_oxfmtrc(_root_path) { + Ok((_, options)) => { + external_options = options; + } + Err(err) => { + debug!("Failed to load .oxfmtrc for external formatter: {err}"); + } + } + + if let Some(bridge) = external_bridge.as_ref() { + if let Err(err) = bridge.init(1) { + debug!("Failed to initialize external formatter bridge: {err}"); + external_bridge = None; + } + } + + return (external_options, external_bridge); + } + + #[cfg(not(feature = "lsp-prettier"))] + { + (Value::Object(serde_json::Map::new()), external_bridge) + } + } } pub struct ServerFormatter { options: FormatOptions, gitignore_glob: Option, + #[cfg_attr(not(feature = "lsp-prettier"), allow(dead_code))] + external_options: Value, + external_bridge: Option>, } impl Tool for ServerFormatter { @@ -196,7 +258,11 @@ impl Tool for ServerFormatter { return ToolRestartChanges { tool: None, watch_patterns: None }; } - let new_formatter = ServerFormatterBuilder::build(root_uri, new_options_json.clone()); + let new_formatter = ServerFormatterBuilder::build( + root_uri, + new_options_json.clone(), + self.external_bridge.clone(), + ); let watch_patterns = new_formatter.get_watcher_patterns(new_options_json); ToolRestartChanges { tool: Some(Box::new(new_formatter)), @@ -230,7 +296,8 @@ impl Tool for ServerFormatter { ) -> ToolRestartChanges { // TODO: Check if the changed file is actually a config file - let new_formatter = ServerFormatterBuilder::build(root_uri, options); + let new_formatter = + ServerFormatterBuilder::build(root_uri, options, self.external_bridge.clone()); ToolRestartChanges { tool: Some(Box::new(new_formatter)), @@ -249,7 +316,6 @@ impl Tool for ServerFormatter { return None; } - let source_type = get_supported_source_type(&path).map(enable_jsx_source_type)?; // Declaring Variable to satisfy borrow checker let file_content; let source_text = if let Some(content) = content { @@ -268,6 +334,53 @@ impl Tool for ServerFormatter { &file_content }; + #[cfg(feature = "lsp-prettier")] + if let Some(strategy) = detect_prettier_file(&path) { + let PrettierFileStrategy::External { parser_name } = strategy; + let Some(bridge) = &self.external_bridge else { + debug!( + "External formatter bridge not available for {}", + path.display() + ); + return None; + }; + + let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); + let code = match bridge.format_file( + &self.external_options, + parser_name, + file_name, + source_text, + ) { + Ok(code) => code, + Err(err) => { + debug!( + "External formatter failed for {}: {err}", + path.display() + ); + return None; + } + }; + + if code == *source_text { + return Some(vec![]); + } + + let (start, end, replacement) = compute_minimal_text_edit(source_text, &code); + let rope = Rope::from(source_text); + let (start_line, start_character) = get_line_column(&rope, start, source_text); + let (end_line, end_character) = get_line_column(&rope, end, source_text); + + return Some(vec![TextEdit::new( + Range::new( + Position::new(start_line, start_character), + Position::new(end_line, end_character), + ), + replacement.to_string(), + )]); + } + + let source_type = get_supported_source_type(&path).map(enable_jsx_source_type)?; let allocator = Allocator::new(); let ret = Parser::new(&allocator, source_text, source_type) .with_options(get_parse_options()) @@ -300,8 +413,13 @@ impl Tool for ServerFormatter { } impl ServerFormatter { - pub fn new(options: FormatOptions, gitignore_glob: Option) -> Self { - Self { options, gitignore_glob } + pub fn new( + options: FormatOptions, + gitignore_glob: Option, + external_options: Value, + external_bridge: Option>, + ) -> Self { + Self { options, gitignore_glob, external_options, external_bridge } } fn is_ignored(&self, path: &Path) -> bool { @@ -377,7 +495,7 @@ mod tests_builder { fn test_server_capabilities() { use tower_lsp_server::ls_types::{OneOf, ServerCapabilities}; - let builder = ServerFormatterBuilder; + let builder = ServerFormatterBuilder::default(); let mut capabilities = ServerCapabilities::default(); builder.server_capabilities(&mut capabilities, &Capabilities::default()); @@ -466,7 +584,9 @@ mod tests { use serde_json::json; use super::compute_minimal_text_edit; - use crate::formatter::tester::Tester; + use crate::formatter::tester::{Tester, get_file_uri}; + use crate::tool::Tool; + use crate::ServerFormatterBuilder; #[test] #[should_panic(expected = "assertion failed")] @@ -604,4 +724,13 @@ mod tests { ) .format_and_snapshot_multiple_file(&["ignored.ts", "not-ignored.js"]); } + + #[test] + fn test_prettier_only_without_bridge() { + let root_uri = Tester::get_root_uri("fixtures/formatter/prettier_only"); + let formatter = ServerFormatterBuilder::build(&root_uri, json!({}), None); + let uri = get_file_uri("fixtures/formatter/prettier_only/sample.json"); + let formatted = formatter.run_format(&uri, None); + assert!(formatted.is_none()); + } } diff --git a/crates/oxc_language_server/src/formatter/tester.rs b/crates/oxc_language_server/src/formatter/tester.rs index 585d250ea5b1f..85eaedef20529 100644 --- a/crates/oxc_language_server/src/formatter/tester.rs +++ b/crates/oxc_language_server/src/formatter/tester.rs @@ -58,6 +58,7 @@ impl Tester<'_> { ServerFormatterBuilder::build( &Self::get_root_uri(self.relative_root_dir), self.options.clone(), + None, ) } diff --git a/crates/oxc_language_server/src/lib.rs b/crates/oxc_language_server/src/lib.rs index aee925815d18a..2be8999d039e1 100644 --- a/crates/oxc_language_server/src/lib.rs +++ b/crates/oxc_language_server/src/lib.rs @@ -17,7 +17,7 @@ mod worker; use crate::backend::Backend; #[cfg(feature = "formatter")] -pub use crate::formatter::ServerFormatterBuilder; +pub use crate::formatter::{ExternalFormatterBridge, NoopBridge, ServerFormatterBuilder}; #[cfg(feature = "linter")] pub use crate::linter::ServerLinterBuilder; pub use crate::tool::{Tool, ToolBuilder, ToolRestartChanges, ToolShutdownChanges}; diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index 29452b86828cf..1b6fa4e93e7f5 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -4,7 +4,7 @@ async fn main() { let tools: Vec> = { let mut v: Vec> = Vec::new(); #[cfg(feature = "formatter")] - v.push(Box::new(oxc_language_server::ServerFormatterBuilder)); + v.push(Box::new(oxc_language_server::ServerFormatterBuilder::default())); #[cfg(feature = "linter")] v.push(Box::new(oxc_language_server::ServerLinterBuilder)); v From 23185d86f069b6ed55a1dc4fd37585e343fd480c Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:53:43 +0100 Subject: [PATCH 2/6] test(vscode): scope prettier-only formatter cases --- .../prettier.component.html | 1 + .../fixtures/formatting_prettier/prettier.css | 1 + .../fixtures/formatting_prettier/prettier.gql | 1 + .../formatting_prettier/prettier.graphql | 1 + .../formatting_prettier/prettier.handlebars | 1 + .../fixtures/formatting_prettier/prettier.hbs | 1 + .../fixtures/formatting_prettier/prettier.htm | 1 + .../formatting_prettier/prettier.html | 1 + .../formatting_prettier/prettier.json | 1 + .../formatting_prettier/prettier.json5 | 1 + .../formatting_prettier/prettier.jsonc | 1 + .../formatting_prettier/prettier.less | 1 + .../formatting_prettier/prettier.markdown | 3 ++ .../fixtures/formatting_prettier/prettier.md | 3 ++ .../fixtures/formatting_prettier/prettier.mdx | 3 ++ .../formatting_prettier/prettier.pcss | 1 + .../formatting_prettier/prettier.postcss | 1 + .../formatting_prettier/prettier.scss | 1 + .../fixtures/formatting_prettier/prettier.vue | 1 + .../formatting_prettier/prettier.xhtml | 1 + .../formatting_prettier/prettier.yaml | 4 +++ .../fixtures/formatting_prettier/prettier.yml | 4 +++ .../vscode/tests/e2e_server_formatter.spec.ts | 34 +++++++++++++++++++ 23 files changed, 68 insertions(+) create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.component.html create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.css create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.gql create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.graphql create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.handlebars create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.hbs create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.htm create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.html create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.json create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.json5 create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.jsonc create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.less create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.markdown create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.md create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.mdx create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.pcss create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.postcss create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.scss create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.vue create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.xhtml create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.yaml create mode 100644 editors/vscode/fixtures/formatting_prettier/prettier.yml diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.component.html b/editors/vscode/fixtures/formatting_prettier/prettier.component.html new file mode 100644 index 0000000000000..c6b64e7f95091 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.component.html @@ -0,0 +1 @@ +
Hi
diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.css b/editors/vscode/fixtures/formatting_prettier/prettier.css new file mode 100644 index 0000000000000..a800e6579d1b6 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.css @@ -0,0 +1 @@ +.foo{color:red} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.gql b/editors/vscode/fixtures/formatting_prettier/prettier.gql new file mode 100644 index 0000000000000..09d9703f331f9 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.gql @@ -0,0 +1 @@ +query{user{id name}} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.graphql b/editors/vscode/fixtures/formatting_prettier/prettier.graphql new file mode 100644 index 0000000000000..09d9703f331f9 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.graphql @@ -0,0 +1 @@ +query{user{id name}} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.handlebars b/editors/vscode/fixtures/formatting_prettier/prettier.handlebars new file mode 100644 index 0000000000000..5078dd9ed5f23 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.handlebars @@ -0,0 +1 @@ +{{#if foo}}
Hi
{{/if}} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.hbs b/editors/vscode/fixtures/formatting_prettier/prettier.hbs new file mode 100644 index 0000000000000..5078dd9ed5f23 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.hbs @@ -0,0 +1 @@ +{{#if foo}}
Hi
{{/if}} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.htm b/editors/vscode/fixtures/formatting_prettier/prettier.htm new file mode 100644 index 0000000000000..c6b64e7f95091 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.htm @@ -0,0 +1 @@ +
Hi
diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.html b/editors/vscode/fixtures/formatting_prettier/prettier.html new file mode 100644 index 0000000000000..c6b64e7f95091 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.html @@ -0,0 +1 @@ +
Hi
diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.json b/editors/vscode/fixtures/formatting_prettier/prettier.json new file mode 100644 index 0000000000000..99b9b83258f1b --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.json @@ -0,0 +1 @@ +{"a":1,"b":[1,2]} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.json5 b/editors/vscode/fixtures/formatting_prettier/prettier.json5 new file mode 100644 index 0000000000000..99b9b83258f1b --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.json5 @@ -0,0 +1 @@ +{"a":1,"b":[1,2]} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.jsonc b/editors/vscode/fixtures/formatting_prettier/prettier.jsonc new file mode 100644 index 0000000000000..99b9b83258f1b --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.jsonc @@ -0,0 +1 @@ +{"a":1,"b":[1,2]} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.less b/editors/vscode/fixtures/formatting_prettier/prettier.less new file mode 100644 index 0000000000000..a800e6579d1b6 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.less @@ -0,0 +1 @@ +.foo{color:red} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.markdown b/editors/vscode/fixtures/formatting_prettier/prettier.markdown new file mode 100644 index 0000000000000..1ba2836fdecc8 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.markdown @@ -0,0 +1,3 @@ +# Title +- a +- b diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.md b/editors/vscode/fixtures/formatting_prettier/prettier.md new file mode 100644 index 0000000000000..1ba2836fdecc8 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.md @@ -0,0 +1,3 @@ +# Title +- a +- b diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.mdx b/editors/vscode/fixtures/formatting_prettier/prettier.mdx new file mode 100644 index 0000000000000..1ba2836fdecc8 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.mdx @@ -0,0 +1,3 @@ +# Title +- a +- b diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.pcss b/editors/vscode/fixtures/formatting_prettier/prettier.pcss new file mode 100644 index 0000000000000..a800e6579d1b6 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.pcss @@ -0,0 +1 @@ +.foo{color:red} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.postcss b/editors/vscode/fixtures/formatting_prettier/prettier.postcss new file mode 100644 index 0000000000000..a800e6579d1b6 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.postcss @@ -0,0 +1 @@ +.foo{color:red} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.scss b/editors/vscode/fixtures/formatting_prettier/prettier.scss new file mode 100644 index 0000000000000..a800e6579d1b6 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.scss @@ -0,0 +1 @@ +.foo{color:red} diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.vue b/editors/vscode/fixtures/formatting_prettier/prettier.vue new file mode 100644 index 0000000000000..148842321d187 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.vue @@ -0,0 +1 @@ + diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.xhtml b/editors/vscode/fixtures/formatting_prettier/prettier.xhtml new file mode 100644 index 0000000000000..c6b64e7f95091 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.xhtml @@ -0,0 +1 @@ +
Hi
diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.yaml b/editors/vscode/fixtures/formatting_prettier/prettier.yaml new file mode 100644 index 0000000000000..f00c19dced745 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.yaml @@ -0,0 +1,4 @@ +a:1 +b: +- 1 +- 2 diff --git a/editors/vscode/fixtures/formatting_prettier/prettier.yml b/editors/vscode/fixtures/formatting_prettier/prettier.yml new file mode 100644 index 0000000000000..f00c19dced745 --- /dev/null +++ b/editors/vscode/fixtures/formatting_prettier/prettier.yml @@ -0,0 +1,4 @@ +a:1 +b: +- 1 +- 2 diff --git a/editors/vscode/tests/e2e_server_formatter.spec.ts b/editors/vscode/tests/e2e_server_formatter.spec.ts index 915c6e37ea5e9..06b63e88475b9 100644 --- a/editors/vscode/tests/e2e_server_formatter.spec.ts +++ b/editors/vscode/tests/e2e_server_formatter.spec.ts @@ -66,4 +66,38 @@ suite('E2E Server Formatter', () => { strictEqual(content.toString(), "class X {\n foo() {\n return 42\n }\n}\n"); }); + test('formats prettier-only file types', async () => { + await loadFixture('formatting_prettier'); + await workspace.getConfiguration('editor').update('defaultFormatter', 'oxc.oxc-vscode'); + await workspace.saveAll(); + + await sleep(500); + + const expectedJson = "{\n \"a\": 1,\n \"b\": [1, 2]\n}\n"; + const expectedCss = ".foo {\n color: red;\n}\n"; + const expectedMarkdown = "# Title\n\n- a\n- b\n"; + + const cases: Array<[string, string]> = [ + ["prettier.json", expectedJson], + ["prettier.jsonc", expectedJson], + ["prettier.css", expectedCss], + ["prettier.md", expectedMarkdown], + ]; + + for (const [file, expected] of cases) { + const fileUri = Uri.joinPath(fixturesWorkspaceUri(), 'fixtures', file); + const original = (await workspace.fs.readFile(fileUri)).toString(); + const document = await workspace.openTextDocument(fileUri); + await window.showTextDocument(document); + await commands.executeCommand('editor.action.formatDocument'); + await workspace.saveAll(); + const content = await workspace.fs.readFile(fileUri); + + const actual = content.toString(); + const matchesExpected = actual === expected; + const matchesOriginal = actual === original; + strictEqual(matchesExpected || matchesOriginal, true, `${file} should be formatted or unchanged`); + } + }); + }); From a76169d9b0d189e2bbddfb095de3655c8fcca5c9 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Mon, 29 Dec 2025 00:31:36 +0100 Subject: [PATCH 3/6] fix(oxfmt): avoid nested tokio runtime in LSP --- apps/oxfmt/src/lsp/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/oxfmt/src/lsp/mod.rs b/apps/oxfmt/src/lsp/mod.rs index 7e2111ce6e182..17b6a71aace74 100644 --- a/apps/oxfmt/src/lsp/mod.rs +++ b/apps/oxfmt/src/lsp/mod.rs @@ -2,6 +2,7 @@ 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, @@ -13,7 +14,7 @@ struct NapiExternalFormatterBridge { impl ExternalFormatterBridge for NapiExternalFormatterBridge { fn init(&self, num_threads: usize) -> Result<(), String> { - self.formatter.init(num_threads).map(|_| ()) + block_in_place(|| self.formatter.init(num_threads).map(|_| ())) } fn format_file( @@ -23,7 +24,7 @@ impl ExternalFormatterBridge for NapiExternalFormatterBridge { file: &str, code: &str, ) -> Result { - self.formatter.format_file(options, parser, file, code) + block_in_place(|| self.formatter.format_file(options, parser, file, code)) } } From 7f650f0ffccbfb3eab2338bffa0570d192288163 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Mon, 29 Dec 2025 00:31:42 +0100 Subject: [PATCH 4/6] fix(vscode): activate formatter on json/css/markdown --- editors/vscode/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 4a8677302aaad..eaeca53bd0ff5 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -284,9 +284,13 @@ ] }, "activationEvents": [ + "onLanguage:css", "onLanguage:astro", "onLanguage:javascript", "onLanguage:javascriptreact", + "onLanguage:json", + "onLanguage:jsonc", + "onLanguage:markdown", "onLanguage:svelte", "onLanguage:typescript", "onLanguage:typescriptreact", From 756efa8d488c36cbbd3bc2e7bcac37d77af5016c Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:09:16 +0100 Subject: [PATCH 5/6] fix(vscode): scope formatter selector to supported types --- editors/vscode/client/tools/formatter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/editors/vscode/client/tools/formatter.ts b/editors/vscode/client/tools/formatter.ts index c7937b2df40ec..31ea34089b432 100644 --- a/editors/vscode/client/tools/formatter.ts +++ b/editors/vscode/client/tools/formatter.ts @@ -72,9 +72,14 @@ export default class FormatterTool implements ToolInterface { const supportedExtensions = [ "cjs", "cts", + "css", "js", "jsx", + "json", + "jsonc", "mjs", + "md", + "markdown", "mts", "ts", "tsx", From 599acc3d18661bbafe630098b3110bca32c71668 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:09:23 +0100 Subject: [PATCH 6/6] test(vscode): require prettier-only formatting --- editors/vscode/tests/e2e_server_formatter.spec.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/editors/vscode/tests/e2e_server_formatter.spec.ts b/editors/vscode/tests/e2e_server_formatter.spec.ts index 06b63e88475b9..876770499a38f 100644 --- a/editors/vscode/tests/e2e_server_formatter.spec.ts +++ b/editors/vscode/tests/e2e_server_formatter.spec.ts @@ -73,7 +73,7 @@ suite('E2E Server Formatter', () => { await sleep(500); - const expectedJson = "{\n \"a\": 1,\n \"b\": [1, 2]\n}\n"; + const expectedJson = "{ \"a\": 1, \"b\": [1, 2] }\n"; const expectedCss = ".foo {\n color: red;\n}\n"; const expectedMarkdown = "# Title\n\n- a\n- b\n"; @@ -84,9 +84,9 @@ suite('E2E Server Formatter', () => { ["prettier.md", expectedMarkdown], ]; + // oxlint-disable eslint/no-await-in-loop -- VS Code formatting must be run sequentially per file. for (const [file, expected] of cases) { const fileUri = Uri.joinPath(fixturesWorkspaceUri(), 'fixtures', file); - const original = (await workspace.fs.readFile(fileUri)).toString(); const document = await workspace.openTextDocument(fileUri); await window.showTextDocument(document); await commands.executeCommand('editor.action.formatDocument'); @@ -94,10 +94,9 @@ suite('E2E Server Formatter', () => { const content = await workspace.fs.readFile(fileUri); const actual = content.toString(); - const matchesExpected = actual === expected; - const matchesOriginal = actual === original; - strictEqual(matchesExpected || matchesOriginal, true, `${file} should be formatted or unchanged`); + strictEqual(actual, expected, `${file} should be formatted`); } + // oxlint-enable eslint/no-await-in-loop }); });