Skip to main content

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

FieldTypeRequiredDescription
idStringYesA unique identifier for the formatter within the extension.
labelStringYesA human-readable display name shown in the formatter picker (e.g., "Prettier", "Black").
commandIdStringYesThe ID of the command to execute when this formatter is invoked. The command must be registered by the same extension (or another loaded extension).
languagesList<String>NoA 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:

ParameterTypeRequiredDescription
idStringYesUnique formatter ID within the extension.
labelStringYesDisplay name for the formatter.
commandIdStringYesCommand to execute for formatting.
languagesList<String>NoLanguage 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:

ParameterTypeRequiredDescription
idStringYesThe 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

  1. The file extension (e.g., .ts) is mapped to a language identifier (e.g., typescript).
  2. Each formatter's languages list is checked against the resolved language identifier.
  3. If a formatter's languages list is empty, it matches all languages (acts as a universal formatter).
  4. 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 ExtensionsLanguage ID
.js, .jsx, .mjsjavascript
.ts, .tsxtypescript
.dartdart
.pypython
.rbruby
.gogo
.rsrust
.javajava
.ktkotlin
.swiftswift
.csscss
.scssscss
.lessless
.html, .htmhtml
.jsonjson
.mdmarkdown
.yaml, .ymlyaml
.xmlxml
.vuevue
.sveltesvelte
.phpphp
.c, .hc
.cpp, .hppcpp
.cscsharp
.lualua
.sh, .bashshell
.sqlsql
.graphql, .gqlgraphql

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

  1. Resolve language: The file extension of the active tab is mapped to a language identifier using the built-in mapping table.

  2. Check default formatter: The editor looks up the user's preferred formatter for the resolved language. This preference is persisted across sessions.

  3. Read editor content: If an extension formatter is selected, the current document content is read from the CodeMirror editor instance.

  4. Execute the command: The formatter's commandId is executed through the command registry with two arguments:

    • code — the full document text as a string
    • language — the resolved language identifier
  5. Apply the result: If the command returns a string, that string replaces the entire document content in the editor. If the command returns null or a non-string value, an error toast is shown.

  6. 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": []
}