fix(oxfmt/lsp): return error on config resolving error (#26003)

Instead of silently ignoring config-resolve errors, and not formatting the document,
`oxfmt --lsp` now returns an `Err` instead, allowing users to see on their LSP client a meaningful message.

Hitting Format on a file, could produce a `toast` like in this following example:
<img width="463" height="173" alt="grafik" src="https://github.com/user-attachments/assets/7d549ab1-b65b-41bc-9fa0-f5f9d10989dc" />
This commit is contained in:
Sysix committed 2026-10-08 18:28:34 +00:00
1 parent 961f318571
commit 5dae56f577
7 files changed
+77 -31

No files matched your search

+14 -16
View File
@@ -226,7 +226,7 @@ impl Tool for ServerFormatter {
&file_content
};
let Some(result) = self.format_file(&path, source_text) else {
let Some(result) = self.format_file(&path, source_text)? else {
return Ok(vec![]); // No formatting for this file (unsupported or ignored)
};
@@ -238,7 +238,7 @@ impl Tool for ServerFormatter {
.ok_or_else(|| "In-memory formatting requires content".to_string())?;
let Some(result) =
self.format_in_memory(document.uri, source_text, &document.language_id)
self.format_in_memory(document.uri, source_text, &document.language_id)?
else {
return Ok(vec![]); // currently not supported
};
@@ -346,7 +346,7 @@ impl ServerFormatter {
scopes: &ConfigScopes,
path: &Path,
source_text: &str,
) -> Option<FormatResult> {
) -> Result<Option<FormatResult>, String> {
let resolver = match scopes.resolve(path) {
Ok(r) => r,
Err(err) => {
@@ -360,37 +360,35 @@ impl ServerFormatter {
if resolver.is_path_ignored(path, false) {
debug!("File is ignored by config ignorePatterns: {}", path.display());
return None;
return Ok(None);
}
let Some(strategy) = classify_file(path) else {
debug!("Unsupported file type for formatting: {}", path.display());
return None;
return Ok(None);
};
let plan = match resolver.resolve(path, strategy) {
Ok(ResolveOutcome::Format(plan)) => plan,
Ok(ResolveOutcome::MissingPlugin(plugin)) => {
warn!(
"Skipping `.{plugin}`: `{plugin}` plugin is not enabled in resolved config: {}",
return Err(format!(
"`{plugin}` plugin is not enabled in resolved config: {}",
path.display()
);
return None;
));
}
Err(err) => {
debug!("Config resolve error for {}: {err}", path.display());
return None;
return Err(format!("Config resolve error for {}: {err}", path.display()));
}
};
debug!("plan = {plan:?}");
Some(tokio::task::block_in_place(|| self.source_formatter.format(source_text, plan)))
Ok(Some(tokio::task::block_in_place(|| self.source_formatter.format(source_text, plan))))
}
fn format_file(&self, path: &Path, source_text: &str) -> Option<FormatResult> {
fn format_file(&self, path: &Path, source_text: &str) -> Result<Option<FormatResult>, String> {
let state = self.snapshot();
if is_ignored(&state.ignore_matchers, path, false, true) {
debug!("File is ignored by .prettierignore: {}", path.display());
return None;
return Ok(None);
}
self.resolve_and_format(&state.scopes, path, source_text)
}
@@ -400,11 +398,11 @@ impl ServerFormatter {
uri: &Uri,
source_text: &str,
language_id: &LanguageId,
) -> Option<FormatResult> {
) -> Result<Option<FormatResult>, String> {
let Some(path) = create_fake_file_path_from_language_id(language_id, &self.root_path, uri)
else {
debug!("Unsupported language id for in-memory formatting: {language_id:?}");
return None;
return Ok(None);
};
self.resolve_and_format(&self.snapshot().scopes, &path, source_text)
}
@@ -21,12 +21,8 @@ const count=1
---
<button onclick={()=>count}>{count}</button>
--- AFTER ----------
---
const count=1
---
<button onclick={()=>count}>{count}</button>
--- ERROR ----------
\`astro\` plugin is not enabled in resolved config: <cwd>/test/lsp/format/fixtures/format/test.astro
--------------------"
`;
@@ -49,10 +45,8 @@ file://<fixture>/format/test.svelte
<script>let count=$state(0);</script>
<button onclick={()=>count++}>{count}</button>
--- AFTER ----------
<script>let count=$state(0);</script>
<button onclick={()=>count++}>{count}</button>
--- ERROR ----------
\`svelte\` plugin is not enabled in resolved config: <cwd>/test/lsp/format/fixtures/format/test.svelte
--------------------"
`;
@@ -381,6 +375,18 @@ const x = "1"
--------------------"
`;
exports[`LSP formatting > error handling on format > should handle config-invalid-override/test.ts 1`] = `
"--- URI -----------
file://<fixture>/config-invalid-override/test.ts
--- BEFORE ---------
const value = 1;
--- ERROR ----------
Config resolve error for <cwd>/test/lsp/format/fixtures/config-invalid-override/test.ts: Invalid tabWidth: The indent width should be between 0 and 24
--------------------"
`;
exports[`LSP formatting > ignore patterns > should handle ignore-config/file.generated.ts 1`] = `
"--- URI -----------
file://<fixture>/ignore-config/file.generated.ts
@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": ["*.ts"],
"options": {
"tabWidth": 250
}
}
]
}
@@ -0,0 +1 @@
const value = 1;
@@ -166,6 +166,15 @@ describe("LSP formatting", () => {
});
});
describe("error handling on format", () => {
it.each([["config-invalid-override/test.ts", "typescript"]])(
"should handle %s",
async (path, languageId) => {
expect(await formatFixture(FIXTURES_DIR, path, languageId)).toMatchSnapshot();
},
);
});
describe("initializationOptions", () => {
it("should use custom config path from fmt.configPath", async () => {
expect(
+26 -5
View File
@@ -173,19 +173,36 @@ export async function formatFixtureContent(
}
await clientOrConfig.didOpen(fileUri, languageId, content);
const edits = await clientOrConfig.format(fileUri);
try {
const edits = await clientOrConfig.format(fileUri);
if (innerClient) {
await innerClient[Symbol.asyncDispose]();
}
if (innerClient) {
await innerClient[Symbol.asyncDispose]();
}
return `${uriSnapshotHeader(fileUri, fixturesDir)}
return `${uriSnapshotHeader(fileUri, fixturesDir)}
--- BEFORE ---------
${content}
--- AFTER ----------
${applyEdits(content, edits, languageId)}
--------------------
`.trim();
} catch (error) {
const msg =
error instanceof Error ? sanitizeMessage(error.message) : sanitizeMessage(error as string);
if (innerClient) {
await innerClient[Symbol.asyncDispose]();
}
return `${uriSnapshotHeader(fileUri, fixturesDir)}
--- BEFORE ---------
${content}
--- ERROR ----------
${msg}
--------------------
`.trim();
}
}
export async function formatMultipleFixtures(
@@ -302,3 +319,7 @@ function uriSnapshotHeader(fileUri: string, fixtureDir: string): string {
--- URI -----------
${safeUri}`;
}
function sanitizeMessage(message: string): string {
return message.replaceAll(process.cwd(), "<cwd>");
}
@@ -961,6 +961,7 @@ impl LanguageServer for Backend {
Ok(Some(edits))
}
Err(err) => {
error!(err);
Err(Error { code: ErrorCode::ServerError(1), message: Cow::Owned(err), data: None })
}
}