References and Resolve
One of the most important and tricky parts in implementing a custom language PSI is resolving references. Resolving references gives users the ability to navigate from a PSI element usage (accessing a variable, calling a method, etc.) to the declaration of that element (the variable's definition, a method declaration, and so on).
This feature is needed in order to support the Ctrl/Cmd+B or clicking the mouse button while holding Ctrl/Cmd key, and it is a prerequisite for implementing the Find Usages action, the Rename Refactoring and Code Completion.action invoked by
ImplementationTextSelectioner registered in
com.intellij.lang.implementationTextSelectioner extension point.
All PSI elements which work as references (for which the
PsiElement.getReference() method and to return a
PsiReference implementation from that method. The
PsiReference 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 fully-qualified class names), in which case it can implement
PsiElement.getReferences() and return the references as an array. To optimize
PsiElement.getReferences() performance, consider implementing
HintedReferenceHost to provide additional hints.
The primary 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, should it point to an undefined class). The resolved element should implement the
PsiNamedElement interface. In order to enable more advanced functionality, prefer implementing
PsiNamedElement where possible.
A counterpart to the
resolve() 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. Still, additional optimizations are possible (for example, performing the tree walk only if the element text is equal to the text of the reference).
Implementing Resolve Logic
There is a set of interfaces that can be used as a base for implementing resolve support, namely the
PsiScopeProcessor interface and the
PsiElement.processDeclarations() method. These interfaces have several extra complexities that are unnecessary for most custom languages (like support for substituting Java generics types). Still, 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.
Please see also Cache Results of Heavy Computations.
The implementation of resolve based on the standard helper classes contains the following components:
A class implements the
PsiScopeProcessorinterface, which gathers the possible declarations for the reference and stops the resolve process when it has successfully completed. The primary method which needs to be implemented is
execute(), which is called to process every declaration encountered during the resolve, and returns
trueif the resolve needs to be continued or
falseif the declaration has been found. The methods
handleEvent()are used for internal optimizations and can be left empty in the
PsiScopeProcessorimplementations for custom languages.
A function which walks the PSI tree up from the reference location until the resolve has successfully completed or until the end of the resolve scope has been reached. If the target of the reference is located in a different file, the file can be located, for example, using
FilenameIndex.getFilesByName()(if the file name is known) or by iterating through all custom language files in the project (
ProjectFileIndexinterface obtained from
The individual PSI elements, on which the
processDeclarations()method is called during the PSI tree walk. If a PSI element is a declaration, it passes itself to the
execute()method of the
PsiScopeProcessorpassed to it. Also, if necessary, according to the language scoping rules, a PSI element can pass the
PsiScopeProcessorto its child elements.
Resolving to Multiple Targets
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 action for such references allows the user to choose a navigation target in a popup. 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.
HighlightedReference to add additional highlighting for non-obvious places (e.g., String literal). Such references will automatically be highlighted using String | Highlighted reference text attributes from (2022.2).