Skip to content
Draft
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
63 changes: 63 additions & 0 deletions docs/on-type-formatting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# On-Type Formatting

This extension now supports on-type formatting, which automatically formats your code as you type specific trigger characters.

## Trigger Characters

The following trigger characters will automatically format your code:

- `;` (semicolon) - Formats the document when you complete a statement
- `}` (closing brace) - Formats the document when you complete a block

## Enabling On-Type Formatting

On-type formatting is controlled by VS Code's built-in `editor.formatOnType` setting. To enable it:

1. Open VS Code settings (File > Preferences > Settings or `Ctrl+,`)
2. Search for "format on type"
3. Enable the `Editor: Format On Type` setting

Alternatively, add this to your `settings.json`:

```json
{
"editor.formatOnType": true
}
```

## How It Works

When you type a trigger character (`;` or `}`), the extension will:

1. Check if the trigger character was typed at the end of a line
2. Format the entire document using Prettier
3. Apply minimal edits to update the document

The formatting respects all your Prettier configuration settings and `.prettierrc` files.

## Behavior

- **Smart triggering**: The formatter only triggers when you type the character at the end of meaningful content, not in the middle of editing
- **Respects ignore files**: Files listed in `.prettierignore` won't be formatted
- **Configuration aware**: Uses your project's Prettier configuration
- **Cancellable**: Long-running formatting operations can be cancelled by VS Code

## Performance

On-type formatting formats the entire document to ensure consistency. For very large files, you may want to:

- Use format-on-save instead (`editor.formatOnSave`)
- Disable on-type formatting for specific languages
- Use range formatting (select code and use Format Selection)

## Disabling On-Type Formatting

To disable on-type formatting while keeping other Prettier features:

```json
{
"editor.formatOnType": false
}
```

This will keep format-on-save and manual formatting working while disabling automatic formatting as you type.
19 changes: 13 additions & 6 deletions src/PrettierCodeActionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,28 @@ export class PrettierCodeActionProvider implements CodeActionProvider {
private provideEdits: (
document: TextDocument,
options: ExtensionFormattingOptions,
token?: CancellationToken,
) => Promise<TextEdit[]>,
) {}

public async provideCodeActions(
document: TextDocument,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
range: Range,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
context: CodeActionContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
token: CancellationToken,
): Promise<CodeAction[]> {
const edits = await this.provideEdits(document, {
force: false,
});
// Check if cancellation was requested before starting
if (token.isCancellationRequested) {
return [];
}

const edits = await this.provideEdits(
document,
{
force: false,
},
token,
);

if (edits.length === 0) {
return [];
Expand Down
39 changes: 27 additions & 12 deletions src/PrettierEditProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,48 @@ export class PrettierEditProvider
private provideEdits: (
document: TextDocument,
options: ExtensionFormattingOptions,
token?: CancellationToken,
) => Promise<TextEdit[]>,
) {}

public async provideDocumentRangeFormattingEdits(
document: TextDocument,
range: Range,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options: FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
token: CancellationToken,
): Promise<TextEdit[]> {
return this.provideEdits(document, {
rangeEnd: document.offsetAt(range.end),
rangeStart: document.offsetAt(range.start),
force: false,
});
// Check if cancellation was requested before starting
if (token.isCancellationRequested) {
return [];
}

return this.provideEdits(
document,
{
rangeEnd: document.offsetAt(range.end),
rangeStart: document.offsetAt(range.start),
force: false,
},
token,
);
}

public async provideDocumentFormattingEdits(
document: TextDocument,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options: FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
token: CancellationToken,
): Promise<TextEdit[]> {
return this.provideEdits(document, {
force: false,
});
// Check if cancellation was requested before starting
if (token.isCancellationRequested) {
return [];
}

return this.provideEdits(
document,
{
force: false,
},
token,
);
}
}
27 changes: 27 additions & 0 deletions src/PrettierEditService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from "path";
import { pathToFileURL } from "url";
import {
CancellationToken,
Disposable,
DocumentFilter,
languages,
Expand All @@ -16,6 +17,7 @@ import { getParserFromLanguageId } from "./utils/get-parser-from-language.js";
import { LoggingService } from "./LoggingService.js";
import { RESTART_TO_ENABLE } from "./message.js";
import { PrettierEditProvider } from "./PrettierEditProvider.js";
import { PrettierOnTypeFormattingEditProvider } from "./PrettierOnTypeFormattingEditProvider.js";
import { PrettierCodeActionProvider } from "./PrettierCodeActionProvider.js";
import { FormatterStatus, StatusBar } from "./StatusBar.js";
import {
Expand Down Expand Up @@ -133,6 +135,7 @@ const PRETTIER_CONFIG_FILES = [
export default class PrettierEditService implements Disposable {
private formatterHandler: undefined | Disposable;
private rangeFormatterHandler: undefined | Disposable;
private onTypeFormatterHandler: undefined | Disposable;
private codeActionHandler: undefined | Disposable;
private registeredWorkspaces = new Set<string>();

Expand Down Expand Up @@ -320,9 +323,11 @@ export default class PrettierEditService implements Disposable {
this.moduleResolver.dispose();
this.formatterHandler?.dispose();
this.rangeFormatterHandler?.dispose();
this.onTypeFormatterHandler?.dispose();
this.codeActionHandler?.dispose();
this.formatterHandler = undefined;
this.rangeFormatterHandler = undefined;
this.onTypeFormatterHandler = undefined;
this.codeActionHandler = undefined;
};

Expand All @@ -332,6 +337,9 @@ export default class PrettierEditService implements Disposable {
}: ISelectors) {
this.dispose();
const editProvider = new PrettierEditProvider(this.provideEdits);
const onTypeEditProvider = new PrettierOnTypeFormattingEditProvider(
this.provideEdits,
);
const codeActionProvider = new PrettierCodeActionProvider(
this.provideEdits,
);
Expand All @@ -344,6 +352,13 @@ export default class PrettierEditService implements Disposable {
languageSelector,
editProvider,
);
this.onTypeFormatterHandler =
languages.registerOnTypeFormattingEditProvider(
languageSelector,
onTypeEditProvider,
";",
"}",
);
this.codeActionHandler = languages.registerCodeActionsProvider(
languageSelector,
codeActionProvider,
Expand Down Expand Up @@ -458,9 +473,21 @@ export default class PrettierEditService implements Disposable {
private provideEdits = async (
document: TextDocument,
options: ExtensionFormattingOptions,
token?: CancellationToken,
): Promise<TextEdit[]> => {
// Check for cancellation before starting
if (token?.isCancellationRequested) {
return [];
}

const startTime = new Date().getTime();
const result = await this.format(document.getText(), document, options);

// Check for cancellation after formatting
if (token?.isCancellationRequested) {
return [];
}

if (!result) {
// No edits happened, return never so VS Code can try other formatters
return [];
Expand Down
68 changes: 68 additions & 0 deletions src/PrettierOnTypeFormattingEditProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
CancellationToken,
FormattingOptions,
OnTypeFormattingEditProvider,
Position,
TextDocument,
TextEdit,
} from "vscode";
import { ExtensionFormattingOptions } from "./types.js";

export class PrettierOnTypeFormattingEditProvider
implements OnTypeFormattingEditProvider
{
constructor(
private provideEdits: (
document: TextDocument,
options: ExtensionFormattingOptions,
token?: CancellationToken,
) => Promise<TextEdit[]>,
) {}

public async provideOnTypeFormattingEdits(
document: TextDocument,
position: Position,
ch: string,
options: FormattingOptions,
token: CancellationToken,
): Promise<TextEdit[]> {
// Check if cancellation was requested before starting
if (token.isCancellationRequested) {
return [];
}
// Get the line where the trigger character was typed
const line = document.lineAt(position.line);
const lineText = line.text;

// Only format if the trigger character was typed at the end of meaningful content
// (not in the middle of a line being edited)
const textAfterCursor = lineText.substring(position.character);

// If there's non-whitespace content after the cursor, don't format
// This prevents formatting while the user is still typing in the middle of a line
if (textAfterCursor.trim().length > 0) {
return [];
}

// Verify the trigger character is at the cursor position
// The character is already inserted, so check position.character - 1
if (position.character === 0) {
return [];
}
const charBeforeCursor = lineText.charAt(position.character - 1);
if (charBeforeCursor !== ch) {
return [];
}

// For on-type formatting, format the entire document
// This ensures consistent formatting and matches user expectations
// Prettier doesn't support formatting single lines in isolation
return this.provideEdits(
document,
{
force: false,
},
token,
);
}
}
Loading
Loading