Skip to content

Commit 9faf871

Browse files
authored
Merge branch 'main' into copilot/add-problem-matcher-vscode
2 parents 4bc8ecd + 3bc3579 commit 9faf871

16 files changed

Lines changed: 362 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ All notable changes to the "prettier-vscode" extension will be documented in thi
1414
- Fixed parser detection fallback when using plugins with Prettier v3
1515
- Added new Prettier v3 options: `objectWrap`, `experimentalOperatorPosition`
1616
- Added support for TypeScript config files (`.prettierrc.ts`, `.prettierrc.cts`, `.prettierrc.mts`, `prettier.config.ts`, etc.) introduced in Prettier 3.5.0 - Thanks to [@dr2009](https://github.com/dr2009)
17+
- Added `source.fixAll.prettier` code action for use with `editor.codeActionsOnSave` to run Prettier before other formatters like ESLint (#1277)
18+
- Fixed issue where unnecessary TextEdits were applied when document was already formatted, which could cause spurious changes or cursor positioning issues (#3232)
1719

1820
## [11.0.0]
1921

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,31 @@ The problem matchers will parse Prettier's output and display:
256256
- Syntax errors with file, line, column, and error message
257257
- Warnings for files that need formatting
258258

259+
### Using Code Actions on Save
260+
261+
You can use VS Code's `editor.codeActionsOnSave` to run Prettier before other formatters like ESLint. This is useful when you want to format with Prettier first and then apply ESLint fixes.
262+
263+
```jsonc
264+
// .vscode/settings.json
265+
{
266+
"editor.codeActionsOnSave": {
267+
"source.fixAll.prettier": "explicit",
268+
},
269+
}
270+
```
271+
272+
You can also combine Prettier with ESLint:
273+
274+
```jsonc
275+
// .vscode/settings.json
276+
{
277+
"editor.codeActionsOnSave": {
278+
"source.fixAll.prettier": "explicit",
279+
"source.fixAll.eslint": "explicit",
280+
},
281+
}
282+
```
283+
259284
## Workspace Trust
260285

261286
This extension utilizes VS Code [Workspace Trust](https://code.visualstudio.com/docs/editor/workspace-trust) features. When this extension is run on an untrusted workspace, it will only use the built in version of prettier. No plugins, local, or global modules will be supported. Additionally, certain settings are also restricted - see each setting for details.

src/ModuleResolverNode.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,33 @@ export class ModuleResolver implements ModuleResolverInterface {
402402
return "error";
403403
}
404404

405+
// Log what config file was found (if any)
406+
if (configPath) {
407+
this.loggingService.logInfo(`Using config file at ${configPath}`);
408+
}
409+
410+
// Log if editorconfig will be considered
411+
if (vscodeConfig.useEditorConfig) {
412+
this.loggingService.logInfo(
413+
"EditorConfig support is enabled, checking for .editorconfig files",
414+
);
415+
}
416+
405417
let resolvedConfig: PrettierOptions | null;
406418
try {
419+
const customConfigPath = vscodeConfig.configPath
420+
? getWorkspaceRelativePath(fileName, vscodeConfig.configPath)
421+
: undefined;
422+
423+
// Log if a custom config path is specified in VS Code settings
424+
if (customConfigPath) {
425+
this.loggingService.logInfo(
426+
`Using custom config path from settings: ${customConfigPath}`,
427+
);
428+
}
429+
407430
const resolveConfigOptions: PrettierResolveConfigOptions = {
408-
config: vscodeConfig.configPath
409-
? getWorkspaceRelativePath(fileName, vscodeConfig.configPath)
410-
: configPath,
431+
config: customConfigPath ?? configPath,
411432
editorconfig: vscodeConfig.useEditorConfig,
412433
};
413434
resolvedConfig = await prettierInstance.resolveConfig(
@@ -419,6 +440,24 @@ export class ModuleResolver implements ModuleResolverInterface {
419440
return "error";
420441
}
421442

443+
// Log what configuration was resolved
444+
if (resolvedConfig) {
445+
this.loggingService.logInfo("Resolved config:", resolvedConfig);
446+
}
447+
448+
// Determine config source for better user feedback
449+
if (!configPath && resolvedConfig && vscodeConfig.useEditorConfig) {
450+
// Config was resolved but no Prettier config file was found
451+
// This means settings came from .editorconfig
452+
this.loggingService.logInfo(
453+
"No Prettier config file found, but settings were loaded from .editorconfig",
454+
);
455+
} else if (!configPath && !resolvedConfig) {
456+
this.loggingService.logInfo(
457+
"No local configuration (i.e. .prettierrc or .editorconfig) detected, will fall back to VS Code configuration",
458+
);
459+
}
460+
422461
if (!vscodeConfig.requireConfig) {
423462
return resolvedConfig;
424463
}

src/PrettierCodeActionProvider.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
CancellationToken,
3+
CodeAction,
4+
CodeActionContext,
5+
CodeActionKind,
6+
CodeActionProvider,
7+
Range,
8+
TextDocument,
9+
TextEdit,
10+
WorkspaceEdit,
11+
} from "vscode";
12+
import { ExtensionFormattingOptions } from "./types.js";
13+
14+
/**
15+
* Provides code actions for formatting with Prettier.
16+
* This enables using Prettier with codeActionsOnSave.
17+
*/
18+
export class PrettierCodeActionProvider implements CodeActionProvider {
19+
public static readonly providedCodeActionKinds = [
20+
CodeActionKind.SourceFixAll.append("prettier"),
21+
];
22+
23+
constructor(
24+
private provideEdits: (
25+
document: TextDocument,
26+
options: ExtensionFormattingOptions,
27+
) => Promise<TextEdit[]>,
28+
) {}
29+
30+
public async provideCodeActions(
31+
document: TextDocument,
32+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
33+
range: Range,
34+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
35+
context: CodeActionContext,
36+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
37+
token: CancellationToken,
38+
): Promise<CodeAction[]> {
39+
const edits = await this.provideEdits(document, {
40+
force: false,
41+
});
42+
43+
if (edits.length === 0) {
44+
return [];
45+
}
46+
47+
const action = new CodeAction(
48+
"Format with Prettier",
49+
CodeActionKind.SourceFixAll.append("prettier"),
50+
);
51+
const workspaceEdit = new WorkspaceEdit();
52+
workspaceEdit.set(document.uri, edits);
53+
action.edit = workspaceEdit;
54+
55+
return [action];
56+
}
57+
}

src/PrettierEditService.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getParserFromLanguageId } from "./utils/get-parser-from-language.js";
1616
import { LoggingService } from "./LoggingService.js";
1717
import { RESTART_TO_ENABLE } from "./message.js";
1818
import { PrettierEditProvider } from "./PrettierEditProvider.js";
19+
import { PrettierCodeActionProvider } from "./PrettierCodeActionProvider.js";
1920
import { FormatterStatus, StatusBar } from "./StatusBar.js";
2021
import {
2122
ExtensionFormattingOptions,
@@ -132,6 +133,7 @@ const PRETTIER_CONFIG_FILES = [
132133
export default class PrettierEditService implements Disposable {
133134
private formatterHandler: undefined | Disposable;
134135
private rangeFormatterHandler: undefined | Disposable;
136+
private codeActionHandler: undefined | Disposable;
135137
private registeredWorkspaces = new Set<string>();
136138

137139
private allLanguages: string[] = [];
@@ -202,7 +204,14 @@ export default class PrettierEditService implements Disposable {
202204
);
203205

204206
const edits = await this.provideEdits(editor.document, { force: true });
205-
if (edits.length !== 1) {
207+
if (edits.length === 0) {
208+
this.loggingService.logInfo("Document is already formatted.");
209+
return;
210+
}
211+
if (edits.length > 1) {
212+
this.loggingService.logWarning(
213+
`Unexpected multiple edits (${edits.length}), expected 0 or 1`,
214+
);
206215
return;
207216
}
208217

@@ -311,8 +320,10 @@ export default class PrettierEditService implements Disposable {
311320
this.moduleResolver.dispose();
312321
this.formatterHandler?.dispose();
313322
this.rangeFormatterHandler?.dispose();
323+
this.codeActionHandler?.dispose();
314324
this.formatterHandler = undefined;
315325
this.rangeFormatterHandler = undefined;
326+
this.codeActionHandler = undefined;
316327
};
317328

318329
private registerDocumentFormatEditorProviders({
@@ -321,6 +332,9 @@ export default class PrettierEditService implements Disposable {
321332
}: ISelectors) {
322333
this.dispose();
323334
const editProvider = new PrettierEditProvider(this.provideEdits);
335+
const codeActionProvider = new PrettierCodeActionProvider(
336+
this.provideEdits,
337+
);
324338
this.rangeFormatterHandler =
325339
languages.registerDocumentRangeFormattingEditProvider(
326340
rangeLanguageSelector,
@@ -330,6 +344,14 @@ export default class PrettierEditService implements Disposable {
330344
languageSelector,
331345
editProvider,
332346
);
347+
this.codeActionHandler = languages.registerCodeActionsProvider(
348+
languageSelector,
349+
codeActionProvider,
350+
{
351+
providedCodeActionKinds:
352+
PrettierCodeActionProvider.providedCodeActionKinds,
353+
},
354+
);
333355
}
334356

335357
/**
@@ -446,11 +468,27 @@ export default class PrettierEditService implements Disposable {
446468
const duration = new Date().getTime() - startTime;
447469
this.loggingService.logInfo(`Formatting completed in ${duration}ms.`);
448470
const edit = this.minimalEdit(document, result);
471+
if (!edit) {
472+
// Document is already formatted, no changes needed
473+
this.loggingService.logDebug(
474+
"Document is already formatted, no changes needed.",
475+
);
476+
return [];
477+
}
449478
return [edit];
450479
};
451480

452-
private minimalEdit(document: TextDocument, string1: string) {
481+
private minimalEdit(
482+
document: TextDocument,
483+
string1: string,
484+
): TextEdit | null {
453485
const string0 = document.getText();
486+
487+
// Quick check: if strings are identical, no edit needed
488+
if (string0 === string1) {
489+
return null;
490+
}
491+
454492
// length of common prefix
455493
let i = 0;
456494
while (
@@ -661,8 +699,8 @@ export default class PrettierEditService implements Disposable {
661699

662700
this.loggingService.logInfo(
663701
fallbackToVSCodeConfig
664-
? "No local configuration (i.e. .prettierrc or .editorconfig) detected, falling back to VS Code configuration"
665-
: "Detected local configuration (i.e. .prettierrc or .editorconfig), VS Code configuration will not be used",
702+
? "Using VS Code configuration"
703+
: "Using local configuration (VS Code configuration will not be used)",
666704
);
667705

668706
let rangeFormattingOptions: RangeFormattingOptions | undefined;

src/test/suite/codeAction.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as assert from "assert";
2+
import * as vscode from "vscode";
3+
import { ensureExtensionActivated } from "./testUtils.js";
4+
5+
describe("Test Prettier Code Actions", () => {
6+
const unformattedCode = `const x = { a: 1, b: 2 }`;
7+
8+
before(async () => {
9+
await ensureExtensionActivated();
10+
});
11+
12+
it("provides source.fixAll.prettier code action", async () => {
13+
const doc = await vscode.workspace.openTextDocument({
14+
content: unformattedCode,
15+
language: "javascript",
16+
});
17+
await vscode.window.showTextDocument(doc);
18+
19+
// Get code actions for the document
20+
const codeActions = await vscode.commands.executeCommand<
21+
vscode.CodeAction[]
22+
>(
23+
"vscode.executeCodeActionProvider",
24+
doc.uri,
25+
new vscode.Range(0, 0, doc.lineCount, 0),
26+
);
27+
28+
// Find the Prettier code action
29+
const prettierAction = codeActions?.find(
30+
(action) => action.kind?.value === "source.fixAll.prettier",
31+
);
32+
33+
assert.ok(prettierAction, "Prettier code action should be available");
34+
assert.equal(
35+
prettierAction?.title,
36+
"Format with Prettier",
37+
"Code action should have correct title",
38+
);
39+
});
40+
41+
it("formats document using code action", async () => {
42+
const doc = await vscode.workspace.openTextDocument({
43+
content: unformattedCode,
44+
language: "javascript",
45+
});
46+
await vscode.window.showTextDocument(doc);
47+
48+
// Get code actions for the document
49+
const codeActions = await vscode.commands.executeCommand<
50+
vscode.CodeAction[]
51+
>(
52+
"vscode.executeCodeActionProvider",
53+
doc.uri,
54+
new vscode.Range(0, 0, doc.lineCount, 0),
55+
);
56+
57+
// Find and apply the Prettier code action
58+
const prettierAction = codeActions?.find(
59+
(action) => action.kind?.value === "source.fixAll.prettier",
60+
);
61+
62+
assert.ok(prettierAction, "Prettier code action should be available");
63+
64+
// Apply the code action
65+
if (prettierAction?.edit) {
66+
await vscode.workspace.applyEdit(prettierAction.edit);
67+
}
68+
69+
const formattedText = doc.getText();
70+
71+
// Verify the document was formatted correctly
72+
// Normalize line endings for cross-platform compatibility
73+
const normalizedFormatted = formattedText.replace(/\r\n/g, "\n");
74+
const expectedNormalized = `const x = { a: 1, b: 2 };\n`;
75+
76+
assert.equal(
77+
normalizedFormatted,
78+
expectedNormalized,
79+
"Document should be formatted correctly after applying code action",
80+
);
81+
});
82+
});

0 commit comments

Comments
 (0)