IntelliJ Platform Plugin SDK Help

Code Formatter

The IntelliJ Platform includes a powerful framework for implementing custom language formatters. At its core, the framework represents formatting rules by nested blocks (Block) that cover the whole file. Each block specifies constraints on whitespaces, like indents, wraps, spacing, or alignments. This allows the formatting engine to calculate the smallest number of whitespace modifications necessary to properly format a file.

Introduction

The easiest way to understand how formatting works in practice is to use a small code example in an existing language and use PsiViewer to inspect how formatting blocks are built. To invoke PsiViewer with the possibility of inspecting Block Structure, use Tools | View PSI Structure... or Tools | View PSI Structure of Current File....

Formatting Blocks Structure

The image above shows a snippet of code at the top, the PSI structure at the bottom left and the block structure at the bottom right. Like in this example, the structure of blocks is usually built to reflect the PSI structure of the file. In other words, there is a root block that covers the entire file and its nested children cover smaller portions like classes, functions, etc. down to statements, identifiers, or braces. Comparing the PSI and block structure above reveals obvious similarities in the nesting.

In general, however, PSI structure and formatting model are two different things serving different purposes. While the structure of formatting blocks and PSI are usually similar, they do not have to match one-to-one. Additionally, it is vital to understand that the formatter modifies only characters between blocks. Therefore, the tree of blocks must cover all non-whitespace characters the bottom-level, or otherwise, the formatter may delete the characters between blocks. On the other hand, spacing elements should never be covered by blocks unless the space is intended to be left as it is.

Implementation

To format a file or a file fragment, the following steps are required:

  • Implement FormattingModelBuilder and register it as com.intellij.lang.formatter extension point in the plugin.xml.

  • The main purpose of the formatting model builder is its createModel() method that must provide a FormattingModel necessary for the framework to format the document. As an argument, it receives a FormattingContext which provides text range, PSI element, and other information required for formatting.

  • The formatting model builds the tree of blocks (Block) for the file where each block has associated indent, wrap, alignment, and spacing settings. Based on the provided tree of blocks, the formatting engine calculates the sequence of whitespace characters (spaces, tabs, and/or line breaks) that needs to be adjusted at necessary positions in the file.

FormattingModelBuilder and FormattingModel

When implementing a FormattingModelBuilder, it is common to use the createModel() method and its FormattingContext argument to set the stage for building the formatting blocks with the FormattingModel. Typically, the following steps are part of this preparation:

  • Retrieve both global and custom code style settings. Use the codeStyleSettings property of the FormattingContext to retrieve the settings which in turn provide access to custom settings through getCustomSettings(). These are commonly passed along and will be used to determine the correct amount of whitespace for indentations, wraps, etc. when building the tree of formatting blocks.

  • Create an instance of SpacingBuilder using the formatting settings.

  • Build the root Block that covers the whole file using the settings, the spacing builder, and the PSI node from the formatting context.

  • The root Block needs all required information for building its subblocks recursively and is used in the FormattingModel returned from createModel()

The IntelliJ Platform already provides concrete implementations of FormattingModel that should be used instead of implementing a custom one. Formatters for custom (programming) languages are usually built so that they mirror the PSI structure of the file. Although not required, this approach is reasonable when thinking about, for example, Java code where the top-level formatting block covers the entire file, its children cover individual classes, blocks on the next level cover methods inside classes, and so on. For these cases, plugins should create a PsiBasedFormattingModel by using FormattingModelProvider.createFormattingModelForPsiFile().

Another implementation of FormattingModel is DocumentBasedFormattingModel. However, in most cases, the PsiBasedFormattingModel should meet the requirements of custom language plugins.

Building the Block Tree

Instead of implementing the Block interface, AbstractBlock should be used as a base class which provides useful default implementations. Although the block implementation of a plugin is specific to the custom language, for the implementation of AbstractBlock some general remarks can be given. It is common to store an instance of the SpacingBuilder which can directly be used when implementing the getSpacing() method by calling the getSpacing() method of the SpacingBuilder.

Much of the work when implementing AbstractBlock goes into implementing buildChildren() that calculates blocks for the children of the current block's AST node. Use getNode().getChildren() to retrieve the AST node's children of the current block, and or each child that is not whitespace, build a subblock which is then added to the list of subblocks. While the exact method of building a subblock highly depends on the specific language, in general, the code for determining the correct Alignment, Indent and Wrap of a block inspects IElementType, checks if the node is in a specific TokenSet or asserts other properties.

The other two more intricate methods that need to be implemented are getChildAttributes() and isIncomplete(). Both are important when determining what indentation to use when Enter is pressed. To calculate the indent for the new line, the formatter engine calls the method getChildAttributes() on either the block immediately before the caret or the parent of that block. This depends on the return value of the isIncomplete() method for the block before the caret. If the block before the cursor is incomplete (contains elements that the user will probably type but has not yet typed, like a closing parenthesis of the parameter list or the trailing semicolon of a statement), getChildAttributes() is called on the block before the caret; otherwise, it's called on the parent block.

As an example, think of contexts (also called blocks) in languages like Java or C that are wrapped in curly braces. If the ASTNode of the current block is such a context or container element, the getChildAttributes() method could return Indent.getNormalIndent() to indent block elements in such a context. Similarly, the isComplete() method could check if for such context elements the first and last child are indeed the open and close curly braces and return false if not.

If the formatting operation doesn't affect the entire file (for example, if the formatter is called to format the pasted block of text), a complete tree of blocks is not built. Rather, only blocks for the text range covered by the formatting operation and their parents are built. Also note that code formatting can be suppressed per region via special comments.

For every block, the plugin specifies the following properties for which several particular use settings exist. These settings have a detailed description in the Javadoc comments for the respective classes.

Indent

Indent specifies how the block is indented relative to its parent block. It provides various factory methods to create different types of indents based on specific formatting settings. There are different modes of indenting defined by factory methods, and the most commonly used are:

  • The none indent (getNoneIndent()) means the child block is not indented.

  • The regular indent (getNormalIndent()) indents the child block by the number of spaces specified in the Tabs and Indents | Indent code style settings.

  • The continuation indent (getContinuationIndent()) is based on Tabs and Indents | Continuation indent code style settings, typically used for multi-line statements.

If not specified, the default "continuation without first" indent mode is used, meaning the first block is not indented, but later blocks are. The class also allows for configuring indents to be relative to their direct parent block or to enforce parent indents on children starting on a new line. This is useful in complex formatting scenarios, such as aligning blocks within a method call or ensuring consistent indentation in nested structures. Indent also includes methods to create indents with a specific number of spaces and options to control their behavior relative to parent blocks.

Spacing

Spacing indicates what spaces or line breaks are inserted between the specified children of a block. The spacing object specifies the minimum and maximum number of spaces that must be placed between the child blocks. It also specifies the minimum number of line breaks to include and whether the existing line breaks and blank lines should be preserved. The formatting model can also specify that the formatter may not modify the spacing between the specified blocks.

SpacingBuilder helps to build a rule-based configuration for spacing and is an easy way to specify when to put spaces before, after, between, or inside certain elements. The rules typically reflect all possible spacing settings for a language. An example on how to implement such a spacing builder can be found in the Custom Language Support Tutorial: Formatter.

Wrap

The wrap (Wrap) specifies whether the content of the block is wrapped to the next line. Wrapping is performed by inserting a line break before the block content. The plugin can specify that a particular block is never wrapped, always wrapped, or wrapped only if it exceeds the right margin.

Alignment

Alignment is designed to specify which blocks should be aligned with each other in a formatting model. Blocks that return the same Alignment object instance from the getAlignment() method will be aligned together. If two blocks with the same alignment are on different lines and the second block is the first non-whitespace block on its line, the formatter inserts whitespaces before the second block to align it with the first block.

The Alignment includes the Anchor enum with two values, LEFT and RIGHT, determining how the code blocks are aligned. The default alignment is created with the left anchor. To create an alignment instance, the class provides several static methods:

  • createAlignment(): Creates an alignment with default settings (backward shift not allowed, left anchor).

  • createAlignment(boolean allowBackwardShift): Creates an alignment specifying whether backward shift is allowed, with the left anchor.

  • createAlignment(boolean allowBackwardShift, Anchor anchor): Creates an alignment with the specified backward shift setting and anchor.

Backward shift allows a former-aligned element to be shifted right to align with a later element. For example, in the code:

int start = 1; int finish = 2;

the = in int start = 1 can be shifted right to align with the = in int finish = 2 if backward shift is allowed.

Additionally, the createChildAlignment() method allows creating an alignment where blocks are aligned based on a base alignment. This is useful for cases where nested alignments are needed, such as aligning a ternary operator with its condition.

Examples

Further Tips

  • To change the default "block name" taken from class name, return a custom Block.getDebugName().

  • The formatter in the IntelliJ Platform SDK only controls spacing, including indentation, between adjacent leaf blocks. Therefore, the first leaf block won't get indentation. If this is needed, use a PostFormatProcessor (see also below) to add the required indentation at the beginning (see forum post with example).

  • An indent is ignored if a corresponding block doesn't start with a new line. To enforce the indent in this case using enforceIndentToChildren parameter in the factory method Indent#getIndent(Intent.Type.NORMAL, false, true) (see forum post with example).

Non-Whitespace Modifications

Sometimes a plugin requires performing non-whitespace character modifications like reordering methods, changing letter cases, or adding missing braces. The formatting framework provides extension points allowing to achieve these goals.

Pre-Processor

Allows executing additional processing before the actual formatting is performed. For example, it can be used to adjust the formatting range or modify the code by adding, removing, or converting elements like braces, semicolons, quotes, etc. All the introduced changes will be handled by the main formatting step.

To register a formatting pre-processor, a plugin has to provide an implementation of PreFormatProcessor and register it in the com.intellij.preFormatProcessor extension point.

Example: JsonTrailingCommaRemover removing trailing commas in JSON files

Post-Processor

It is similar to the pre-processor but is run after the actual formatting is performed. It can be used for adding, removing, or converting elements like braces, semicolons, quotes, changing letter-cases, etc.

To register a formatting post-processor, a plugin has to provide an implementation of PostFormatProcessor and register it in the com.intellij.postFormatProcessor extension point.

Example: TrailingCommaPostFormatProcessor inserting trailing commas in Kotlin files

Rearranger

Allows custom languages to provide user-configurable arrangement/grouping rules for element types supported by language plugin. Rules can be refined via modifiers and name; ordering can be applied additionally. See Rearranger and related for Javadoc.

Code Style Settings

To set the default indent size for a plugin's language and allow user configuration of tab and indent sizes, implement the FileTypeIndentOptionsProvider interface and register it at the com.intellij.fileTypeIndentOptionsProvider extension point. The return value of createIndentOptions() sets the default indent size.

Example: Custom Language Support Tutorial: Code Style Settings

Restricting Formatting

Use LanguageFormattingRestriction to restrict (automatic) code formatting for given contexts.

External Code Formatter

2021.3

Register AsyncDocumentFormattingService implementation in the com.intellij.formattingService extension point to invoke external formatter instead of IDE's builtin formatter.

Example: ShExternalFormatter from Shell Script plugin

Last modified: 23 July 2024