Child pages
  • Developing Custom Language Plugins for IntelliJ IDEA

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • The PsiElement.delete() method for the PsiElement subclasses for which Safe Delete is available. Deleting PSI elements is implemented by deleting the underlying AST nodes from the AST tree (which, in turn, causes the text ranges corresponding to the AST nodes to be deleted from the document).

Code Formatter

IDEA includes a powerful framework for implementing custom language formatters. In this framework, the plugin specifies the constraints on the spacing between different syntax elements, and the formatting engine, provided by IDEA, calculates the smallest number of whitespace modifications that need to be performed on the file to make it match the constraints.

The process of formatting a file or a file fragment consists of the following main steps:

  • The formatting model builder (FormattingModelBuilder), implemented by the plugin, provides a formatting model (FormattingModel) for the document to be formatted
  • The formatting model is requested to build the structure of the file as applies to formatting, as a tree of blocks (Block) with associated indent, wrap, alignment and spacing settings.
  • The formatting engine calculates the sequence of whitespace characters (spaces, tabs and/or line breaks) that needs to be placed at every block boundary, based on the formatting model provided by the plugin.
  • The formatting model is requested to insert the calculated whitespace characters at necessary positions in the file.

The structure of blocks is usually built in such a way that it mirrors the PSI structure of the file - for example, in Java code, the top-level formatting block covers the entire file, its children cover individual classes in the file, blocks on the next level cover methods inside classes, and so on. The formatter modifies only the characters between blocks, and the tree of blocks must be built in such a way that the bottom-level blocks cover all non-whitespace characters in the file: otherwise the characters between blocks may be deleted by the formatter.

If the formatting operation does not 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.

For every block, the plugin specifies the following properties:

  • The spacing (Spacing) specifies what spaces or line breaks are inserted between the specified children of the block. The spacing object specifies the minimum and maximum number of spaces that must be placed between the specified child blocks, the minimum number of line breaks to place there, and whether the existing line breaks and blank lines should be preserved. The formatting model can also specify that the spacing between the specified blocks may not be modified by the formatter.
  • The indent specifies how the block is indented relative to its parent block. There are different modes of indenting defined by factory methods in the Indent class. The most commonly used are the none indent (which means the child block is not indented), the regular indent (the child block is indented by the number of spaces specified in the "Project Code Style | General | Indent" setting) and the continuation indent (based on "Project Code Style | General | Continuation Indent" setting). If the formatting model does not specify an indent, the "continuation without first" mode is used, which means that the first block in a sequence of blocks with that type is not indented and the following blocks are indented with a continuation indent.
  • 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.
  • The alignment (Alignment) specifies which blocks should be aligned with each other. If two blocks with the alignment property set to the same object instance are placed in different lines, and if the second block is the first non-whitespace block in its line, the formatter inserts white spaces before the second block so that it starts from the same column as the first one.

For each of these properties, a number of special use settings exists, which are described in the JavaDoc comments for the respective classes.

An important special case in using the formatter is the smart indent performed when the user presses the Enter key in a source code file. To determine 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, depending 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.

Code Inspections and Intentions

The code inspections for custom languages use the same API as all other code inspections, based on the LocalInspectionTool class.

The functionality of LocalInspectionTool partially duplicates that of Annotator, and the main differences are that LocalInspectionTool supports batch analysis of code (through the Analyze | Inspect Code... action), the possibility to turn off the inspection and to configure the inspection options. If none of that is required and the analysis only needs to run in the active editor, Annotator provides better performance (because of its support for incremental analysis) and more flexibility for highlighting errors.

The code intentions for custom languages also use the regular API for intentions. The intention classes need to implement the IntentionAction interface and to be registered using the <intentionAction> bean in the plugin.xml.

Structure View

The Structure View implementation used for a specific file type in IDEA can be customized on many levels. If a custom language plugin provides an implementation of the StructureView interface, it can completely replace the standard structure view implementation with a custom user interface component. However, for most languages this is not necessary, and the standard Structure View implementation provided by IDEA can be reused.

The starting point for the structure view is the PsiStructureViewFactory interface, which is registered in the com.intellij.lang.psiStructureViewFactory extension point.

To reuse the IDEA implementation of the Structure View, the plugin returns a TreeBasedStructureViewBuilder from its PsiStructureViewFactory.getStructureViewBuilder() method. As the model for the builder, the plugin can specify a subclass of TextEditorBasedStructureViewModel, and by overriding methods of this subclass it customizes the structure view for a specific language.

The main method to override is getRoot(), which returns the instance of a class implementing the StructureViewTreeElement interface. IDEA doesn't provide a standard implementation of this interface, so a plugin will need to implement it completely.

The structure view tree is usually built as a partial mirror of the PSI tree. In the implementation of StructureViewTreeElement.getChildren(), the plugin can specify which of the child elements of a specific PSI tree node need to be represented as elements in the structure view. Another important method is getPresentation(), which can be used to customize the text, attributes and icon used to represent an element in the structure view.

The implementation of StructureViewTreeElement.getChildren() needs to be matched by TextEditorBasedStructureViewModel.getSuitableClasses(). The latter method returns an array of PsiElement-derived classes which can be shown as structure view elements, and is used to select the Structure View item matching the cursor position when the structure view is first opened or when the "Autoscroll from source" option is used.

Surround With

In order to support the "Code | Surround With..." action, the plugin needs to register one or more implementations of the SurroundDescriptor interface in the com.intellij.lang.surroundDescriptor extension point. Each of the surround descriptors defines a possible type of code fragment which can be surrounded - for example, one surround descriptor can handle surrounding expressions, and another can handle statements. Each surround descriptor, in turn, contains an array of Surrounder objects, defining specific templates which can be used for surrounding the selected code fragment (for example, "Surround With if", "Surround With for" and so on).

When the "Surround With..." action is invoked, IDEA queries all surround descriptors for the language until it finds one that returns a non-empty array from its getElementsToSurround() method. Then it calls the Surrounder.isApplicable() method for each surrounder in that descriptor to check if the specific template is applicable in the current context. Once the user selects a specific surrounder from the popup menu, the Surrounder.surroundElements() method is used to execute the surround action.

Go to Class and Go to Symbol

A custom language plugin can provide its own items to be included in the lists shown by IDEA when the user chooses the "Go to | Class..." or "Go to | Symbol..." action. In order to do so, the plugin must provide implementations for the ChooseByNameContributor interface (separate implementations need to be provided for "Go to Class" and "Go to Symbol"), and register them in the com.intellij.gotoClassContributor and com.intellij.gotoSymbolContributor extension points.

Each contributor needs to be able to return a complete list of names to show in the list for a specified project, which will then be filtered by IDEA according to the text typed by the user in the dialog. For each name in that list, the contributor needs to provide a list of NavigationItem instances (typically PsiElements), which specify the destinations to jump to when a specific name is selected from the list.

Additional Minor Features

In order to implement brace matching, once the syntax highlighting lexer has been implemented, all that is required is to implement the PairedBraceMatcher interface and to return an array of brace pairs (BracePair) for the language. Each brace pair specifies the characters for the opening and closing braces and the lexer token types for these characters. (In principle, it is possible to return multi-character tokens, like "begin" and "end", as the start and end tokens of a brace pair. IDEA will match such braces, but the highlighting for such braces will not be fully correct.)

Certain types of braces can be marked as structural. Structural braces have higher priority than regular braces: they are matched with each other even if there are unmatched braces of other types between them, and an opening non-structural braces is not matched with a closing one if one of them is inside a pair of matched structural braces and another is outside.

The code folding is controlled by the plugin through the FoldingBuilder interface. The interface returns the list of text ranges which are foldable (as an array of FoldingDescriptor objects), the replacement text which is shown for each range when it is folded, and the default state of each folding region (folded or unfolded).

The Comment Code feature is controlled through the Commenter interface. The interface can return the prefix for the line comment, and the prefix and suffix for the block comment, if such features are supported by the language.

The To Do view is supported automatically if the plugin provides a correct implementation of the ParserDefinition.getCommentTokens() method.

to be continued