Child pages
  • Developing Custom Language Plugins for IntelliJ IDEA

Versions Compared

Key

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

...

In general, there is no single right way to implement a PSI for a custom language, and the plugin author can choose the PSI structure and set of methods which are the most convenient for the code which uses the PSI (error analysis, refactorings and so on). However, there is one base interface which needs to be used by a custom language PSI implementation in order to support features like rename and find usages. Every element which can be renamed or referenced (a class definition, a method definition and so on) needs to implement the PsiNamedElement interface, with methods getName() and setName().

...

The syntax and error highlighting is performed on multiple levels. The first level of syntax highlighting is based on the lexer output, and is provided through the SyntaxHighlighter interface. The syntax highligher highlighter returns the TextAttributeKey instances for each token type which needs special highlighting. For highlighting lexer errors, the standard TextAttributeKey for bad characters (HighligherColors.BAD_CHARACTER) can be used.

Example: SyntaxHighlighlighter implementation for Properties language

The second level of error highlighting happens during parsing. If a particular sequence of tokens is invalid according to the grammar of the language, the PsiBuilder.error() method can be used to highlight the invalid tokens and display an error message showing why they are not valid.

...

To highlight a region of text as a warning or error, the annotator calls createErrorAnnotation() or createWarningAnnotation() on the AnnotationHolder object passed to it, and optionally calls registerFix() on the returned Annotation object to add a quick fix for the error or warning. To apply additional syntax highlighting, the annotator can call AnnotationHolder.createInfoAnnotation() with an empty message and then call Annotation.setTextAttributes() to specify the text attributes key for the highlighting.

Example: Annotator for Properties language

Finally, if the custom language employs external tools for validating files in the language (for example, uses the Xerces library for XML schema validation), it can provide an implementation of the ExternalAnnotator interface and register it in com.intellij.externalAnnotator extension point. The ExternalAnnotator highlighting has the lowest priority and is invoked only after all other background processing has completed. It uses the same AnnotationHolder interface for converting the output of the external tool into editor highlighting.

The plugin can also provide a configuration interface to allow the user to configure the colors used for highlighting specific items. In order to do that, it should provide an implementation of ColorSettingPage and register it in the com.intellij.colorSettingsPage extension point.

Example: ColorSettingsPage for Properties language

The "Export to HTML" feature of IDEA uses the same syntax highlighting mechanism as the editor, so it will work automatically for custom languages which provide a syntax highlighter.

...

One of the most important and tricky parts in the implementation of a custom language PSI is resolving references. Resolving references means the ability to go from the usage of an element (access of a variable, call of a method and so on) to the declaration of the element (the variable definition, the method declaration and so on). This is obviously needed in order to support the IDEA "Go to Declaration" action (Ctrl-B and Ctrl-Click), and it is also a pre-requisite prerequisite for the Find Usages action, the Rename refactoring and the code completion.

All PSI elements which work as references (for which the Go to Declaration action applies) need to implement the PsiElement.getReference() method and to return a PsiReference implementation from that method. The PsiReference interface can be implemented by the same class as PsiElement, or by a different class. An element can also contain multiple references (for example, a string literal can contain multiple substrings which are valid full-qualified class names), in which case it can implement PsiElement.getReferences() and return the references as an array.

The main method of the PsiReference interface is resolve(), which returns the element to which the reference points, or null if it was not possible to resolve the reference to a valid element (for example, it points to an undefined class). A counterpart to this method is isReferenceTo(), which checks if the reference resolves to the specified element. The latter method can be implemented by calling resolve() and comparing the result with the passed PSI element, but additional optimizations (for example, performing the tree walk only if the text of the element is equal to the text of the reference) are possible.

IDEA provides a set of interfaces which can be used as a base for implementing resolve support, namely the PsiScopeProcessor interface and the PsiElement.processDeclarations() method. These interfaces have a number of extra complexities which are not necessary for most custom languages (like support for substituting Java generics types), but they are required if the custom language can have references to Java code. If Java interoperability is not required, the plugin can forgo the standard interfaces and provide its own, different implementation of resolve.

...

An extension of the PsiReference interface, which allows a reference to resolve to multiple targets, is the PsiPolyVariantReference interface. The targets to which the reference resolves are returned from the multiResolve() method. The Go to Declaration action for such references allows the user to choose the target to navigate to. The implementation of multiResolve can be also based on PsiScopeProcessor, and can collect all valid targets for the reference instead of stopping when the first valid target is found.

...