In this part: backlight, reference system, autocompletion, code navigation. The previous part is here .
Syntax and error highlighting
The class used in IDEA to determine how the corresponding text range should be highlighted is called
TextAttributesKey . An instance of this class is created for each different type of element that needs to be highlighted (keywords, numbers, strings, comments, etc.), it defines the default attributes that apply to elements of the corresponding type (for example, keywords are bold , numbers - in blue, lines - in italics and green background). The mapping of the TextAttributesKey to specific attributes used in the editor is determined by the
EditorColorsScheme class and can be customized by the user if the plugin provides the appropriate configuration interface. In the backlight can be used overlay several TextAttributeKey: for example, one key can define the style, and the other - the color of the element.
Basic syntax highlighting
Syntax and error highlighting is performed at different levels. On the first, syntax highlighting, based on the results of lexical analysis, is performed via the
SyntaxHighlighter interface. This interface returns TextAttributeKey instances for each type of token that requires special highlighting. To highlight lexer errors, use the standard object of the TextAttributeKey class for invalid characters (
HighligherColors.BAD_CHARACTER
).
The color management scheme has changed a bit in Intelli IDEA 12.1 to facilitate the work of circuit designers and make the same mapping for different programming languages, even if the scheme was not originally intended for them. Previously, language plug-ins used fixed color schemes are not always compatible, for example, with dark themes. The new implementation allows you to define dependencies on a set of standard text attributes associated with a schema, rather than a specific language. Attributes for specific languages ​​can still be set by the schema designer, but now they are optional. New color schemes have the .icls extension to avoid compatibility issues.
Now we use the
DefaultLanguageHighlighterColors class to define text attributes and dependencies on standard keys.
')
Example:
SyntaxHighlighlighter implementation for the Properties plugin.
At the second level of illumination - the selection of errors that occurred during the syntactic analysis. If a specific chain of tokens does not match the grammar of the language, the PsiBuilder.error () method can be used to highlight invalid tokens and display an error message.
Annotations
The third level of illumination is performed using the
Annotator interface. The plugin can register one or several annotations at the
com.intellij.annotator
extension
com.intellij.annotator
, after which they will be called in the background process while highlighting elements of the PSI tree of the user language. Annotations can analyze not only syntax, but also semantics and thus provide a more subtle logic for handling and highlighting errors. The abstract may contain functionality for solving detected problems (so-called quick fix).
When a file is modified, the annotation is called incrementally to process only the changed elements of the PSI tree.
To highlight a specific text range as an error or warning, the annotation calls createErrorAnnotation () or createWarningAnnotation () on an object of type
AnnotationHolder , and optionally registerFix () on the returned Annotator object to add error correction logic. To apply additional highlighting, the annotation may call AnnotationHolder.createInfoAnnotation () with a blank message and then, by calling Annotation.setTextAttributes (), set text attributes.
Example:
Annotator for the language Properties .
External annotations
Finally, if the user language uses external tools to validate files, their results can be provided by implementing the
ExternalAnnotator interface and registering it at the expansion point
com.intellij.externalAnnotator
. Illumination using ExternalAnnotator has the lowest priority and is performed only after all background processes are completed. External annotations use the same AnnotationHolder interface to adapt the output of external tools and display the backlight.
Color Settings Page
The plugin can also provide a configuration interface that allows the user to customize the colors of certain elements. To do this, you must create an instance of the
ColorSettingPage class and register it in the expansion point
com.intellij.colorSettingsPage
.
Example:
ColorSettingsPage for Properties .
The “Export to HTML” function uses the same highlighting mechanism as the editor, so it becomes available for the user language immediately after the implementation of SyntaxHighlighter.
IntelliJ IDEA Reference System
One of the most important and confusing parts in the implementation of the program structure of the user language is the resolution of links, i.e. the ability to proceed from the place of use of the element (variable in the expression, method call, etc.) to the place of definition (variable declaration, method, etc.). This is required to support many IDEA functions, such as “Go to Declaration” (Ctrl-B and Ctrl-Click), as well as when searching for usages, renaming, autocompletion.
Each PSI element that is supposed to work with a link must override the PsiElement.getReference () method so that it returns the corresponding implementation of the
PsiReference interface. This interface can be implemented both by the PsiElement class itself and as a separate one. If an element can contain several links (for example, a string with a list of classes), then it must implement the PsiElement.getReferences () method, which returns an array of links.
The main method of the PsiReference interface is resolve (), which returns either the element pointed to by the link or null if it is impossible to resolve the link (for example, if it indicates an undefined class). The opposite method is isReferenceTo (), which checks whether the link points to a specific element. The latter method can be implemented by calling resolve () and comparing the result with the passed PSI element, but allows the developer to apply additional optimizations.
Example:
Link to ResourceBundle in Properties plugin.
IDEA provides a number of interfaces that can be used as a base for implementing reference support, namely the
PsiScopeProcessor interface and the PsiElement.processDeclarations () method. These interfaces have many complexities that are not necessary for most user languages ​​(for example, support for substitution of generic types), but they are necessary if the user language can refer to Java code. If interoperability with Java is not required, or there are other reasons, the plugin can override the standard implementation of link resolution.
The standard IDEA helper classes used to resolve links consist of the following components:
- a class that implements the PsiScopeProcessor interface, which collects possible link definitions and stops the resolution process when it is fully completed. The main method that needs to be implemented is execute (), which is called to process each definition and returns false when the definition is found;
- a function that bypasses the tree from the place the link is located and until it resolves or goes out of scope;
- The PSI elements on which the processDeclarations () method is called during a walk around the PSI tree. If the element is a definition, then it passes the reference to itself to the execute () method. If necessary in accordance with the language rules for defining scopes, an element can pass the PsiScopeProcessor to its child elements.
There is an extension to the PsiReference interface that allows links to use multiple target elements - PsiPolyVariantReference. The target link elements are returned by the multiResolve () method. The “Go to Declaration” action for this type of link allows you to select which element to use for navigation. The implementation of multiResolve () can be based on the PsiScopeProcessor if, instead of stopping the search after the first result found, we continue to collect the remaining target elements.
On the other hand, in IntelliJ IDEA there is an approach to the implementation of the reference system through the Reference Contributor and the Reference Provider.
PsiReferenceContributor checks each PsiElement and, using the appropriate description provided by the user, returns the reference provider object registered for this case (
example ). In turn,
PsiReferenceProvider is a class designed to find links inside a single PSI element of the tree. It returns an array of PsiReference objects (
example ).
The PsiReferenceProvider.getReferencesByElement () method should return a list of links (PsiReference) that are contained in the PsiElement element passed to it. In this case, only one link is returned, but in the general case there may be several of them, with each link having to contain a corresponding textRange (starting index and ending index of finding the link within the text of the PSI element).
Reference Contributor must be registered in the plugin.xml file at the corresponding extension point - com.intellij.psi.referenceContributor.
After that, it is possible to use the results of his work to get a list of links when implementing the PsiElement.getReferences () method. In order not to duplicate this code in each reference element, you can define a base class for custom Psi elements:
public class MyASTWrapperPsiElement extends ASTWrapperPsiElement { public MyASTWrapperPsiElement(@NotNull ASTNode astNode) { super(astNode); } @Override public PsiReference getReference() { PsiReference[] references = getReferences(); return references.length == 0 ? null : references[0]; } @NotNull @Override public PsiReference[] getReferences() {
Code completion
There are two main types of autocompletion that can be used in a user language plugin: simple reference add-on and provider-based add-on.
Simple autocompletion
To populate the add-on list, IDEA calls the PsiReference.getVariants () method either at the link under the cursor, or at the dummy item that fits under the cursor. This method should return an array of objects containing strings, PSI elements, or instances of the LookupElement class. If an instance of PsiElement is found in the returned array, then the corresponding icon will be displayed in the add-on list.
The most common way to implement getVariants () is to use the same tree traversal function as in the PsiReference.resolve () method, but returning all the definitions found.
Autocompletion on the basis of the provider
Implementing an add-on based on the
CompletionContributor interface gives you the most control over the code auto-completion operation.
The main use case of the Completion Contributor consists of calling the extend () method and passing to the “pattern” parameter of the corresponding context in which this option is applied, the provider is passed to the “provider” parameter, which generates the corresponding autocompletion list items.
Example:
CompletionContributor for auto-completion of keywords in MANIFEST.MF files.
The items in the autocompletion list are represented by instances of the
LookupElement interface. These objects are usually created using LookupElementBuilder. For each of these, you can define the following attributes:
- main text, additional text, line with type - additional text is shown immediately after the main text, but is not used to search for matches; it is intended mainly for displaying the list of method parameters. The string containing the type of the complemented expression is right justified in the list of additions, usually shows the returned type of the method or the class containing it;
- icon;
- text attributes;
- A handler for inserting text is a callback method, which is executed when any item from the add-on list is selected. It can be used, for example, to insert brackets after calling the method.
Search for uses
The “Find Usages” action in IDEA is a multi-step process, each step of which requires participation from the plugin: in the form of implementing and registering
FindUsagesProvider at the expansion point com.intellij.lang.findUsagesProvider, as well as the features of implementing the program structure (PsiNamedElement and PsiReference interfaces) .
Example:
implementation of FindUsagesProvider for Properties.
To implement this function, the following steps must be performed:
- before performing the “Find Usages” action, IDEA builds an index of the words represented in each file of the user language. Using the WordsScanner implementation obtained from FindUsagesProvider.getWordsScanner (), IDEA loads the contents of each file and sends it to the word scanner, along with a word processor. The scanner breaks the text into words, determines the context of each word (code, comments, lines) and sends them to the processor. The easiest way to implement a scanner is to use the DefaultWordsScanner class;
- when the user invokes the "Find Usages" action, IDEA identifies the element to which you want to find links. The PSI element under the cursor (or the direct parent in the token tree under the cursor) should be PsiNamedElement or a link to it. IDEA will use the word cache to search for text returned by PsiNamedElement.getName (). If the text range PsiNamedElement includes other text besides the identifier returned by getName (), the getTextOffset () method must be rewritten to return the starting offset of the identifier;
- as soon as an element is found, IDEA calls FindUsagesProvider. canFindUsagesFor () to find out if the action applies to this element;
- when the Find Usages dialog is shown to the user, IDEA calls FindUsagesProvider.getType () and FindUsagesProvider.getDescriptiveName () to determine how to display this element;
- For each file containing the found words, IDEA builds a PSI tree and performs a recursive descent. IDEA breaks the text of each element into words and scans them. If an element is indexed as an identifier, then for each word it is checked whether it points to the required element;
- after all uses are found, the results are shown in the uses panel. The text to display to the user is taken from the FindUsagesProvider.getNodeText () method.
To correctly display the name of the found element in the header of the Find Usages panel, you must provide an implementation of the ElementDescriptionProvider interface. The ElementDescriptionLocation object that is passed to the provider in this case must have the actual type UsageViewLongNameLocation.
Example:
ElementDescriptionProvider for the Properties plugin.
In the next part: refactorings, formatting, etc.
All articles of the cycle: 1 , 2 , 3 , 4 , 5 , 6 , 7 .