Formatters
Overview
The Formatters contribution point lets extensions register code formatters for specific programming languages. When a user triggers the "Format Document" action, the editor checks whether an extension formatter is set as the default for the current file's language. If one is found, it executes the formatter's associated command; otherwise, it falls back to the built-in Prettier formatter.
Formatters can be declared statically in the extension manifest or registered dynamically at runtime via RPC. Users can choose their preferred formatter per language through the "Select Formatter" command in the command palette.
Manifest Declaration
Declare formatters in the contributes.formatters array of your extension's manifest.json:
{
"id": "my-formatter-extension",
"name": "My Formatter Extension",
"version": "1.0.0",
"contributes": {
"formatters": [
{
"id": "prettier-formatter",
"label": "Prettier",
"commandId": "prettier.format",
"languages": ["javascript", "typescript", "css"]
}
]
}
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | A unique identifier for the formatter within the extension. |
label | String | Yes | A human-readable display name shown in the formatter picker (e.g., "Prettier", "Black"). |
commandId | String | Yes | The ID of the command to execute when this formatter is invoked. The command must be registered by the same extension (or another loaded extension). |
languages | List<String> | No | A list of language identifiers this formatter supports (e.g., ["javascript", "typescript"]). When empty or omitted, the formatter matches all languages. |
Runtime Registration
Extensions can register and unregister formatters dynamically at runtime using the RPC API. This is useful when a formatter should only be available after certain conditions are met, such as detecting a configuration file in the project.
formatter.register
Registers a new formatter at runtime. The formatter is added to the extension's contributions and immediately becomes available in the formatter picker.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique formatter ID within the extension. |
label | String | Yes | Display name for the formatter. |
commandId | String | Yes | Command to execute for formatting. |
languages | List<String> | No | Language identifiers. Defaults to [] (matches all). |
Example RPC call:
{
"method": "formatter.register",
"params": {
"id": "black-formatter",
"label": "Black",
"commandId": "black.format",
"languages": ["python"]
}
}
Deferred command validation: When a formatter is registered, the editor checks whether commandId refers to a registered command. If the command is not found immediately, the editor waits up to 5 seconds (to account for asynchronous activation) before logging a warning. Formatting will silently fail if the command is never registered.
formatter.unregister
Removes a previously registered formatter.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
id | String | Yes | The formatter ID to remove. |
Example RPC call:
{
"method": "formatter.unregister",
"params": {
"id": "black-formatter"
}
}
Language Matching
Language matching determines which formatters are available for a given file. The system uses file extension-to-language mapping to resolve the current file's language identifier.
How matching works
- The file extension (e.g.,
.ts) is mapped to a language identifier (e.g.,typescript). - Each formatter's
languageslist is checked against the resolved language identifier. - If a formatter's
languageslist is empty, it matches all languages (acts as a universal formatter). - If the list is non-empty, the formatter matches only when the language identifier appears in the list.
Supported language mappings
The following file extensions are mapped to language identifiers automatically:
| File Extensions | Language ID |
|---|---|
.js, .jsx, .mjs | javascript |
.ts, .tsx | typescript |
.dart | dart |
.py | python |
.rb | ruby |
.go | go |
.rs | rust |
.java | java |
.kt | kotlin |
.swift | swift |
.css | css |
.scss | scss |
.less | less |
.html, .htm | html |
.json | json |
.md | markdown |
.yaml, .yml | yaml |
.xml | xml |
.vue | vue |
.svelte | svelte |
.php | php |
.c, .h | c |
.cpp, .hpp | cpp |
.cs | csharp |
.lua | lua |
.sh, .bash | shell |
.sql | sql |
.graphql, .gql | graphql |
For file extensions not in this table, the extension string (minus the leading dot) is used directly as the language identifier.
Command Execution Flow
When the user triggers "Format Document" (via the toolbar button or Ctrl+Shift+I), the following sequence occurs:
User triggers "Format Document"
|
v
Resolve language from file extension
|
v
Check for a user-selected default formatter for this language
|
+---> Default formatter is set?
| |
| Yes | No
| v |
| Look up the selected formatter |
| by its full ID |
| | |
| v |
| Read current editor content |
| via CodeMirror getValue() |
| | |
| v |
| Execute formatter's commandId |
| with { code, language } args |
| | |
| v |
| Command returns formatted |
| code as a String |
| | |
| v |
| Replace editor content |
| via CodeMirror setValue() |
| | |
v v v
| [Done] Fall back to built-in
| Prettier formatter
v
[Done]
Step-by-step details
-
Resolve language: The file extension of the active tab is mapped to a language identifier using the built-in mapping table.
-
Check default formatter: The editor looks up the user's preferred formatter for the resolved language. This preference is persisted across sessions.
-
Read editor content: If an extension formatter is selected, the current document content is read from the CodeMirror editor instance.
-
Execute the command: The formatter's
commandIdis executed through the command registry with two arguments:code— the full document text as a stringlanguage— the resolved language identifier
-
Apply the result: If the command returns a string, that string replaces the entire document content in the editor. If the command returns
nullor a non-string value, an error toast is shown. -
Fallback: If no extension formatter is configured as the default, the editor uses its built-in Prettier integration instead.
Selecting a Formatter
Users can choose their default formatter per language using the Select Formatter command (editor.selectFormatter), accessible from the command palette.
The formatter picker dialog shows:
- Built-in (Prettier) — always available as the default option
- Extension formatters — all formatters registered for the current file's language, listed with their label and source extension
When the user selects a formatter, the choice is persisted by language. For example, choosing "Black" for Python files does not affect the formatter used for JavaScript files.
If no extension formatters are available for the current language, the dialog shows a message suggesting the user install extensions.
Full Example
Below is a complete example of an extension that provides a code formatter for Python using an external tool.
manifest.json
{
"id": "python-black",
"name": "Black Formatter",
"version": "1.0.0",
"description": "Format Python code using the Black formatter",
"main": "main.js",
"activationEvents": ["onFileOpen:*.py"],
"contributes": {
"commands": [
{
"id": "black.format",
"label": "Format with Black",
"category": "Formatting"
}
],
"formatters": [
{
"id": "black",
"label": "Black",
"commandId": "black.format",
"languages": ["python"]
}
]
}
}
Extension activation (main.js)
import editor from "@srcnexus/ext-sdk";
editor.onLoad(() => {
editor.commands.registerCommand("black.format", async (args) => {
const { code, language } = args;
// Open a terminal and run the Black formatter
const { tabKey } = await editor.terminal.open({
name: "Black Formatter",
command: `echo ${JSON.stringify(code)} | python -m black -`,
});
// Wait for output and read it
await new Promise((resolve) => setTimeout(resolve, 2000));
const { output } = await editor.terminal.readOutput(tabKey);
await editor.terminal.close(tabKey);
// Return the formatted code
return output;
});
});
Universal formatter example
To create a formatter that applies to all languages, omit the languages field or pass an empty array:
{
"id": "my-universal-fmt",
"label": "Universal Formatter",
"commandId": "universal.format",
"languages": []
}