feat: add oxfmt/oxlint support for JS and TS projects (#717)

Below an oxfmt configuration (.oxfmtrc.json/.jsonc, oxfmt.config.ts/.mts) Reformat Code runs oxfmt instead of Prettier, with the same external-formatter settings; oxfmt formats whole files, so a selection formats the file with a notice. oxlint --lsp serves JS/TS/React/Vue/Svelte files below an oxlint config. Both are managed npm tools; a missing oxfmt gets the same Install prompt and error handling as Prettier, and formatter messages are translated for either tool.

Co-authored-by: d0whc3r <1378986+d0whc3r@users.noreply.github.com>
This commit is contained in:
d0whc3r
2026-11-02 11:22:49 +08:00
committed by GitHub
parent e2f8e8d5bc
commit 44c603f7db
14 changed files with 246 additions and 75 deletions
+39 -11
View File
@@ -7,6 +7,16 @@ use ide_core::{DocId, IdeError, IdeEvent, IdeResult, Range, TextEdit};
use ide_language::FormatterConfig;
use ide_lsp::types::FormatOptions;
use ide_tools::{Tools, formatter};
use std::path::Path;
const OXFMT_CONFIGS: [&str; 4] = [".oxfmtrc.json", ".oxfmtrc.jsonc", "oxfmt.config.ts", "oxfmt.config.mts"];
/// oxfmt below an oxfmt configuration file, else Prettier. The Prettier
/// settings (languages, preference, on save) apply to both.
fn external_tool(file: &Path) -> &'static str {
let oxfmt = OXFMT_CONFIGS.iter().any(|name| formatter::find_up(file, Path::new(name)).is_some());
if oxfmt { "oxfmt" } else { "prettier" }
}
fn external_selected(config: Option<&FormatterConfig>, language: &str, settings: &PrettierSettings, lsp: bool) -> bool {
config.is_some_and(|c| c.tool == "prettier")
@@ -44,16 +54,19 @@ pub async fn format(
return Err(IdeError::invalid("The file is read-only"));
}
let config = config.unwrap().clone();
let custom = ide_sdk::apply::setting_path(&project.root, &prettier.path);
let tool = external_tool(&snap.path);
let name = formatter::display_name(tool);
let custom = ide_sdk::apply::setting_path(&project.root, &prettier.path).filter(|_| tool == "prettier");
let tools = Tools::new(&host.local_data_dir);
let whole_selection = range.is_some() && !config.range;
let whole_selection = range.is_some() && !formatter::supports_range(tool, &config);
let edits: IdeResult<Vec<TextEdit>> = tokio::task::spawn_blocking(move || {
let text = ide_text::Document::new(&snap.text);
let offsets = range.map(|r| (text.pos_to_offset(r.start), text.pos_to_offset(r.end)));
let mut command = tools.formatter_command(&config.tool, &snap.path, custom.as_deref())?;
command.current_dir(snap.path.parent().ok_or_else(|| IdeError::invalid("Prettier: file has no directory"))?);
command.args(formatter::arguments(&config, &snap.path, options.tab_size, options.insert_spaces, offsets));
let formatted = formatter::run(command, &snap.text, &Default::default())?;
let mut command = tools.formatter_command(tool, &snap.path, custom.as_deref())?;
let dir = snap.path.parent().ok_or_else(|| IdeError::invalid(format!("{name}: file has no directory")))?;
command.current_dir(dir);
command.args(formatter::arguments(tool, &config, &snap.path, options.tab_size, options.insert_spaces, offsets));
let formatted = formatter::run(tool, command, &snap.text, &Default::default())?;
Ok(text
.diff(&formatted)
.changes
@@ -69,13 +82,13 @@ pub async fn format(
if edits.is_ok() && whole_selection {
project.sink.emit(IdeEvent::Notify {
level: NotifyLevel::Info,
title: "Prettier".into(),
body: Some(
"Prettier does not support selection formatting for this language; formatted the whole file".into(),
),
title: name.into(),
body: Some(format!(
"{name} does not support selection formatting for this language; formatted the whole file"
)),
});
}
edits.map_err(|e| e.with_tool("prettier"))
edits.map_err(|e| e.with_tool(tool))
}
#[cfg(test)]
@@ -104,4 +117,19 @@ mod tests {
assert!(!external_selected(ts.formatter.as_ref(), "typescript", &settings, false));
assert!(!external_selected(None, "rust", &settings, false));
}
#[test]
fn oxfmt_config_selects_oxfmt_over_prettier() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("web/src")).unwrap();
let (web, api) = (root.join("web/src/app.ts"), root.join("api/main.ts"));
assert_eq!(external_tool(&web), "prettier");
for config in OXFMT_CONFIGS {
std::fs::write(root.join("web").join(config), "{}").unwrap();
assert_eq!(external_tool(&web), "oxfmt", "{config}");
assert_eq!(external_tool(&api), "prettier", "{config}");
std::fs::remove_file(root.join("web").join(config)).unwrap();
}
}
}
+19 -1
View File
@@ -576,7 +576,7 @@ mod tests {
};
// After the TypeScript fallback (an available server before it would hide
// it); before the HTML server (hover, rename and symbols take the first).
assert_eq!(names("typescript"), ["vtsls", "typescript-language-server", "angular-language-server"]);
assert_eq!(names("typescript"), ["vtsls", "typescript-language-server", "angular-language-server", "oxlint"]);
assert_eq!(names("html"), ["angular-language-server", "vscode-html-language-server"]);
for lang in ["typescript", "html"] {
let cfg = reg.get(&LanguageId::new(lang)).unwrap();
@@ -586,6 +586,24 @@ mod tests {
}
}
#[test]
fn oxlint_serves_js_files_only_below_its_config() {
let reg = reg();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
std::fs::create_dir_all(root.join("apps/web/src")).unwrap();
std::fs::write(root.join("apps/web/.oxlintrc.json"), "{}").unwrap();
for lang in ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "svelte"] {
let cfg = reg.get(&LanguageId::new(lang)).unwrap();
let oxlint = cfg.language_servers.iter().find(|s| s.name == "oxlint").unwrap();
assert_eq!((oxlint.command.as_str(), oxlint.args.as_slice()), ("oxlint", ["--lsp".to_string()].as_slice()));
assert!(oxlint.require_root_marker && !oxlint.fallback, "{lang}");
let served = |file: &str| crate::marked_root(Some(root), &root.join(file), &oxlint.root_markers);
assert_eq!(served("apps/web/src/main.ts"), Some(root.join("apps/web")), "{lang}");
assert_eq!(served("apps/api/main.ts"), None, "{lang}");
}
}
#[test]
fn html_of_angular_projects_is_an_angular_template() {
let reg = reg();
+91 -38
View File
@@ -13,20 +13,30 @@ pub fn find_up(file: &Path, relative: &Path) -> Option<PathBuf> {
file.parent()?.ancestors().map(|dir| dir.join(relative)).find(|p| p.is_file())
}
fn package_bin(package: &Path) -> IdeResult<PathBuf> {
pub fn display_name(tool: &str) -> &str {
crate::registry::find(tool).map_or(tool, |t| t.display)
}
fn package_bin(package: &Path, tool: &str) -> IdeResult<PathBuf> {
let name = display_name(tool);
let json: serde_json::Value = serde_json::from_slice(&std::fs::read(package.join("package.json"))?)
.map_err(|e| IdeError::invalid(format!("Prettier: {e}")))?;
.map_err(|e| IdeError::invalid(format!("{name}: {e}")))?;
let bin = json["bin"]
.as_str()
.or_else(|| json["bin"]["prettier"].as_str())
.ok_or_else(|| IdeError::invalid("Prettier: package has no CLI entry point"))?;
.or_else(|| json["bin"][tool].as_str())
.ok_or_else(|| IdeError::invalid(format!("{name}: package has no CLI entry point")))?;
let path = package.join(bin);
if !path.is_file() {
return Err(IdeError::unavailable(format!("Prettier: CLI does not exist: {}", path.display())));
return Err(IdeError::unavailable(format!("{name}: CLI does not exist: {}", path.display())));
}
Ok(path)
}
/// Prettier formats a range for some languages (`config.range`); oxfmt formats whole files only.
pub fn supports_range(tool: &str, config: &FormatterConfig) -> bool {
tool == "prettier" && config.range
}
impl Tools {
/// Explicit executable/script, then the nearest project package, then managed tools and PATH.
pub fn formatter_command(&self, tool: &str, file: &Path, custom: Option<&Path>) -> IdeResult<Command> {
@@ -34,22 +44,26 @@ impl Tools {
}
fn resolve_formatter_command(&self, tool: &str, file: &Path, custom: Option<&Path>) -> IdeResult<Command> {
if tool != "prettier" {
if crate::registry::find(tool).is_none_or(|t| t.kind != crate::ToolKind::Formatter) {
return Err(IdeError::unavailable(format!("Unknown external formatter: {tool}")));
}
let package = Path::new("node_modules").join(tool);
let manifest = package.join("package.json");
let (path, node) = if let Some(custom) = custom.filter(|p| p.is_dir()) {
(package_bin(custom)?, true)
(package_bin(custom, tool)?, true)
} else if let Some(custom) = custom {
(custom.to_path_buf(), matches!(custom.extension().and_then(|s| s.to_str()), Some("js" | "cjs" | "mjs")))
} else if let Some(manifest) = find_up(file, Path::new("node_modules/prettier/package.json")) {
(package_bin(manifest.parent().unwrap())?, true)
} else if self.root.join("node_modules/prettier/package.json").is_file() {
(package_bin(&self.root.join("node_modules/prettier"))?, true)
} else if let Some(manifest) = find_up(file, &manifest) {
(package_bin(manifest.parent().unwrap(), tool)?, true)
} else if self.root.join(&manifest).is_file() {
(package_bin(&self.root.join(&package), tool)?, true)
} else {
(self.which("prettier").ok_or_else(|| IdeError::missing_tool("prettier"))?, false)
(self.which(tool).ok_or_else(|| IdeError::missing_tool(tool))?, false)
};
if node {
let node = self.which("node").ok_or_else(|| IdeError::unavailable("Prettier: Node.js is required"))?;
let name = display_name(tool);
let node =
self.which("node").ok_or_else(|| IdeError::unavailable(format!("{name}: Node.js is required")))?;
let mut cmd = self.command(node);
cmd.arg(path);
Ok(cmd)
@@ -59,34 +73,40 @@ impl Tools {
}
}
/// Project configuration overrides these defaults; Prettier resolves config and plugins itself.
/// Both tools resolve their configuration and plugins themselves. Prettier's
/// project configuration overrides the editor indentation passed here; oxfmt has
/// no such flags and reads its configuration and `.editorconfig` only.
pub fn arguments(
tool: &str,
config: &FormatterConfig,
file: &Path,
tab_size: u32,
insert_spaces: bool,
range: Option<(u32, u32)>,
) -> Vec<OsString> {
let parser = if config.parser == "markdown" && file.extension().is_some_and(|s| s == "mdx") {
"mdx"
} else {
&config.parser
};
let mut args: Vec<OsString> = [
"--parser",
parser,
"--config-precedence",
"file-override",
"--tab-width",
&tab_size.max(1).to_string(),
"--use-tabs",
if insert_spaces { "false" } else { "true" },
]
.into_iter()
.map(Into::into)
.collect();
let mut args: Vec<OsString> = Vec::new();
if tool == "prettier" {
let parser = if config.parser == "markdown" && file.extension().is_some_and(|s| s == "mdx") {
"mdx"
} else {
&config.parser
};
args.extend(
[
"--parser",
parser,
"--config-precedence",
"file-override",
"--tab-width",
&tab_size.max(1).to_string(),
"--use-tabs",
if insert_spaces { "false" } else { "true" },
]
.map(OsString::from),
);
}
args.extend(["--stdin-filepath".into(), file.as_os_str().to_owned()]);
if config.range
if supports_range(tool, config)
&& let Some((start, end)) = range
{
args.extend(["--range-start".into(), start.to_string().into(), "--range-end".into(), end.to_string().into()]);
@@ -100,8 +120,8 @@ pub fn arguments(
args
}
pub fn run(mut command: Command, text: &str, cancel: &CancellationToken) -> IdeResult<String> {
crate::process::run_input(&mut command, text, Duration::from_secs(15), cancel).map_err(|e| e.with_tool("prettier"))
pub fn run(tool: &str, mut command: Command, text: &str, cancel: &CancellationToken) -> IdeResult<String> {
crate::process::run_input(&mut command, text, Duration::from_secs(15), cancel).map_err(|e| e.with_tool(tool))
}
#[cfg(test)]
@@ -118,7 +138,7 @@ mod tests {
assert_eq!(missing.kind, ErrorKind::NotFound);
assert_eq!(missing.data, Some(ErrorData::Tool { tool: "prettier".into(), missing: true }));
let custom = tools.formatter_command("prettier", &file, Some(&dir.path().join("absent-prettier"))).unwrap();
let failure = run(custom, "text", &Default::default()).unwrap_err();
let failure = run("prettier", custom, "text", &Default::default()).unwrap_err();
assert_eq!(failure.data, Some(ErrorData::Tool { tool: "prettier".into(), missing: false }));
}
@@ -127,7 +147,7 @@ mod tests {
fn stderr_is_preserved_verbatim_even_when_longer_than_four_thousand_characters() {
let mut command = Command::new("sh");
command.args(["-c", "i=0; while [ $i -lt 1000 ]; do printf 'error\\n' >&2; i=$((i+1)); done; exit 1"]);
let failure = run(command, "", &Default::default()).unwrap_err();
let failure = run("prettier", command, "", &Default::default()).unwrap_err();
assert_eq!(failure.message, "error\n".repeat(1000));
assert_eq!(failure.data, Some(ide_core::error::ErrorData::Tool { tool: "prettier".into(), missing: false }));
}
@@ -161,7 +181,8 @@ mod tests {
("vue", true),
] {
let config = FormatterConfig { tool: "prettier".into(), parser: parser.into(), range: supports_range };
let args = arguments(&config, Path::new("/project with spaces/file.md"), 4, false, Some((3, 9)));
let args =
arguments("prettier", &config, Path::new("/project with spaces/file.md"), 4, false, Some((3, 9)));
let args: Vec<_> = args.iter().map(|a| a.to_string_lossy()).collect();
assert!(args.windows(2).any(|a| a == ["--config-precedence", "file-override"]));
assert!(args.windows(2).any(|a| a == ["--stdin-filepath", "/project with spaces/file.md"]));
@@ -171,4 +192,36 @@ mod tests {
assert!(!args.contains(&"--write".into()));
}
}
#[test]
fn oxfmt_runs_the_nearest_package_without_prettier_flags() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("node_modules/oxfmt/bin")).unwrap();
std::fs::write(root.join("node_modules/oxfmt/package.json"), r#"{"bin":{"oxfmt":"bin/oxfmt"}}"#).unwrap();
std::fs::write(root.join("node_modules/oxfmt/bin/oxfmt"), "").unwrap();
std::fs::write(root.join(".prettierignore"), "dist").unwrap();
let file = root.join("packages/web/src/app.ts");
let path = std::env::var_os("PATH").unwrap_or_default();
let tools = Tools::with_paths(root.join("tools"), path, crate::platform::Platform::current());
let command = tools.formatter_command("oxfmt", &file, None).unwrap();
let program = Path::new(command.get_program()).file_stem().unwrap().to_string_lossy().into_owned();
assert_eq!(program, "node");
let script: Vec<_> = command.get_args().collect();
assert_eq!(script, [root.join("node_modules/oxfmt/bin/oxfmt").as_os_str()]);
let config = FormatterConfig { tool: "prettier".into(), parser: "typescript".into(), range: true };
let args = arguments("oxfmt", &config, &file, 4, false, Some((3, 9)));
let ignore = root.join(".prettierignore").into_os_string();
assert_eq!(
args[..4],
["--stdin-filepath".into(), file.clone().into_os_string(), "--ignore-path".into(), ignore]
);
assert!(args[4..].chunks(2).all(|pair| pair[0] == "--ignore-path"), "{args:?}");
assert!(!supports_range("oxfmt", &config));
let unknown = tools.formatter_command("rust-analyzer", &file, None).unwrap_err();
assert!(unknown.message.contains("Unknown external formatter"), "{}", unknown.message);
}
}
+24 -6
View File
@@ -134,6 +134,28 @@ pub static TOOLS: &[ToolSpec] = &[
needs: NODE,
homepage: "https://prettier.io",
},
ToolSpec {
name: "oxfmt",
display: "oxfmt",
kind: Formatter,
commands: &["oxfmt"],
method: Method::Npm { packages: &["oxfmt"] },
fallback: None,
version: ver("oxfmt"),
needs: NODE,
homepage: "https://oxc.rs/docs/guide/usage/formatter",
},
ToolSpec {
name: "oxlint",
display: "oxlint",
kind: LanguageServer,
commands: &["oxlint"],
method: Method::Npm { packages: &["oxlint"] },
fallback: None,
version: ver("oxlint"),
needs: NODE,
homepage: "https://oxc.rs/docs/guide/usage/linter",
},
ToolSpec {
name: "rust-analyzer",
display: "rust-analyzer",
@@ -965,12 +987,8 @@ mod tests {
known.insert((d.adapter.clone(), d.command.clone()));
}
}
for l in &langs {
if let Some(f) = &l.formatter {
known.insert((f.tool.clone(), f.tool.clone()));
}
}
for t in TOOLS.iter().filter(|t| t.kind != Plugin) {
// The host selects the formatter of each file (Prettier or oxfmt).
for t in TOOLS.iter().filter(|t| !matches!(t.kind, Plugin | Formatter)) {
assert!(
known.contains(&(t.name.to_string(), t.commands[0].to_string())),
"{} / {} is not referenced by languages/*.toml",
+2 -2
View File
@@ -19,8 +19,8 @@ fn format(
};
let mut cmd = tools.formatter_command("prettier", path, None)?;
cmd.current_dir(path.parent().unwrap());
cmd.args(formatter::arguments(&config, path, 4, true, range));
formatter::run(cmd, text, &CancellationToken::new())
cmd.args(formatter::arguments("prettier", &config, path, 4, true, range));
formatter::run("prettier", cmd, text, &CancellationToken::new())
}
#[test]
+8
View File
@@ -27,6 +27,14 @@ args = ["--stdio"]
root-markers = ["jsconfig.json", "tsconfig.json", "package.json"]
install-hint = "npm i -g typescript@6 typescript-language-server"
[[language-servers]]
name = "oxlint"
command = "oxlint"
args = ["--lsp"]
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
require-root-marker = true
install-hint = "npm i -D oxlint"
[debugger]
adapter = "js-debug"
command = "js-debug-adapter"
+8
View File
@@ -26,6 +26,14 @@ args = ["--stdio"]
root-markers = ["jsconfig.json", "tsconfig.json", "package.json"]
install-hint = "npm i -g typescript@6 typescript-language-server"
[[language-servers]]
name = "oxlint"
command = "oxlint"
args = ["--lsp"]
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
require-root-marker = true
install-hint = "npm i -D oxlint"
[debugger]
adapter = "js-debug"
command = "js-debug-adapter"
+8
View File
@@ -13,3 +13,11 @@ command = "svelteserver"
args = ["--stdio"]
root-markers = ["svelte.config.js", "svelte.config.mjs", "svelte.config.ts", "package.json"]
install-hint = "npm i -g svelte-language-server"
[[language-servers]]
name = "oxlint"
command = "oxlint"
args = ["--lsp"]
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
require-root-marker = true
install-hint = "npm i -D oxlint"
+8
View File
@@ -37,6 +37,14 @@ root-markers = ["angular.json", "node_modules/@angular/core"]
require-root-marker = true
install-hint = "npm i -g @angular/language-server"
[[language-servers]]
name = "oxlint"
command = "oxlint"
args = ["--lsp"]
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
require-root-marker = true
install-hint = "npm i -D oxlint"
[debugger]
adapter = "js-debug"
command = "js-debug-adapter"
+8
View File
@@ -26,6 +26,14 @@ args = ["--stdio"]
root-markers = ["tsconfig.json", "jsconfig.json", "package.json"]
install-hint = "npm i -g typescript@6 typescript-language-server"
[[language-servers]]
name = "oxlint"
command = "oxlint"
args = ["--lsp"]
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
require-root-marker = true
install-hint = "npm i -D oxlint"
[debugger]
adapter = "js-debug"
command = "js-debug-adapter"
+8
View File
@@ -22,3 +22,11 @@ command = "vtsls"
args = ["--stdio"]
root-markers = ["tsconfig.json", "jsconfig.json", "package.json"]
install-hint = "npm i -g @vtsls/language-server"
[[language-servers]]
name = "oxlint"
command = "oxlint"
args = ["--lsp"]
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
require-root-marker = true
install-hint = "npm i -D oxlint"
+8 -5
View File
@@ -118,19 +118,22 @@ async function format(
try {
edits = await lspRequest(() => commands.lspFormat(doc.doc, range, { ...options, onSave: opts.onSave }));
} catch (e) {
if (e instanceof IpcError && e.data?.type === "tool" && e.data.tool === "prettier") {
if (e instanceof IpcError && e.data?.type === "tool" && (e.data.tool === "prettier" || e.data.tool === "oxfmt")) {
const tool = e.data.tool;
if (e.kind === "notFound" && e.data.missing) {
await executeAction("tools.promptInstall", { name: "prettier", sticky: !opts.onSave });
await executeAction("tools.promptInstall", { name: tool, sticky: !opts.onSave });
} else if (e.kind !== "cancelled") {
notifications().notify({
group: "tools",
level: "error",
title: tx("Prettier formatting failed"),
title: tool === "oxfmt" ? tx("oxfmt formatting failed") : tx("Prettier formatting failed"),
body:
e.kind === "timedOut"
? tx("Prettier formatting timed out")
? tool === "oxfmt"
? tx("oxfmt formatting timed out")
: tx("Prettier formatting timed out")
: createElement("span", { className: styles.diagnostics }, e.message),
key: `prettier-format-error:${doc.path}`,
key: `${tool}-format-error:${doc.path}`,
sticky: false,
});
}
+7 -6
View File
@@ -18,7 +18,6 @@ import {
import { formatDocument } from "./format";
import * as lsp from "./lsp";
const tool = { type: "tool", tool: "prettier", missing: false } as const;
const views: EditorView[] = [];
const viewFor = (path = "/project/file.css") => {
const view = new EditorView({
@@ -43,7 +42,9 @@ afterEach(() => {
_resetI18nForTests();
});
describe("external formatter errors", () => {
describe.each(["prettier", "oxfmt"])("%s formatter errors", (name) => {
const tool = { type: "tool", tool: name, missing: false } as const;
const display = name === "prettier" ? "Prettier" : name;
it("uses structured missing-tool information to request the shared install prompt", async () => {
const prompt = vi.fn();
const dispose = registerAction({
@@ -54,9 +55,9 @@ describe("external formatter errors", () => {
try {
failWith({ kind: "notFound", message: "unrelated diagnostic", data: { ...tool, missing: true } });
await formatDocument(viewFor());
expect(prompt).toHaveBeenCalledWith({ name: "prettier", sticky: true });
expect(prompt).toHaveBeenCalledWith({ name, sticky: true });
await formatDocument(viewFor(), { explicit: false, onSave: true });
expect(prompt).toHaveBeenLastCalledWith({ name: "prettier", sticky: false });
expect(prompt).toHaveBeenLastCalledWith({ name, sticky: false });
expect(useNotifications.getState().items).toHaveLength(0);
} finally {
dispose();
@@ -95,7 +96,7 @@ describe("external formatter errors", () => {
for (let i = 0; i < 3; i++) await formatDocument(view, { explicit: false, onSave: true });
const notes = useNotifications.getState().items;
expect(notes).toHaveLength(1);
expect(notes[0]).toMatchObject({ sticky: false, title: "Prettier 格式化失败" });
expect(notes[0]).toMatchObject({ sticky: false, title: `${display} 格式化失败` });
expect(balloonTimeout(notes[0]!)).toBe(10000);
render(<Balloons />);
expect(screen.getByRole("alert")).toHaveTextContent(stderr);
@@ -115,7 +116,7 @@ describe("external formatter errors", () => {
const request = failWith({ kind: "timedOut", message: "arbitrary backend text", data: tool });
const view = viewFor();
await formatDocument(view);
expect(useNotifications.getState().items[0]).toMatchObject({ body: "Prettier 格式化超时", sticky: false });
expect(useNotifications.getState().items[0]).toMatchObject({ body: `${display} 格式化超时`, sticky: false });
clearNotifications();
request.mockRejectedValue(new IpcError({ kind: "cancelled", message: "cancelled", data: tool }));
await formatDocument(view);
+8 -6
View File
@@ -1614,16 +1614,18 @@
"Install Missing Tool": "安装缺失的工具",
"Prettier formatting failed": "Prettier 格式化失败",
"Prettier formatting timed out": "Prettier 格式化超时",
"Prettier does not support selection formatting for this language; formatted the whole file": "Prettier 不支持此语言的选区格式化,已格式化整个文件",
"oxfmt formatting failed": "oxfmt 格式化失败",
"oxfmt formatting timed out": "oxfmt 格式化超时",
"{name} does not support selection formatting for this language; formatted the whole file": "{name} 不支持此语言的选区格式化,已格式化整个文件",
"Formatting timed out": "格式化超时",
"Prettier: Node.js is required": "Prettier:需要 Node.js",
"{name}: Node.js is required": "{name}:需要 Node.js",
"Formatter": "格式化工具",
"JSON with Comments": "带注释的 JSON",
"Unknown external formatter: {tool}": "未知的外部格式化工具:{tool}",
"Prettier: package has no CLI entry point": "Prettier:软件包未提供命令行入口",
"Prettier: CLI does not exist: {path}": "Prettier:命令行程序不存在:{path}",
"Prettier: file has no directory": "Prettier:文件路径不含目录",
"Prettier: {error}": "Prettier:{error}",
"{name}: package has no CLI entry point": "{name}:软件包未提供命令行入口",
"{name}: CLI does not exist: {path}": "{name}:命令行程序不存在:{path}",
"{name}: file has no directory": "{name}:文件路径不含目录",
"{name}: {error}": "{name}:{error}",
"Reformat File": "重新格式化文件",
"Reformat code": "重新格式化代码",
"Reformat code on save": "保存时重新格式化代码",