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 and so on) 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 Go to Declaration
action invoked by Ctrl-B and Ctrl-Click, and it is a prerequisite for implementing the Find Usages
action, the Rename
refactoring and 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, should it point to an undefined class). The resolved element should implement the PsiNamedElement interface.
NOTE While both the referencing element and the referenced element both have a name, only the element which introduces the name (e.g. the definition
int x = 42
) needs to implement the PsiNamedElement interface. It is not necessary for the referencing element at the point of usage (e.g. thex
in the expressionx + 1
) to implement PsiNamedElement.TIP In order to enable more advanced IntelliJ functionality, prefer implementing PsiNameIdentifierOwner over 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, but additional optimizations are possible (for example, performing the tree walk only if the element text is equal to the text of the reference).
Example: Reference to a ResourceBundle in the Properties language plugin
There's 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.
The implementation of resolve based on the standard helper classes contains of the following components:
A class implementing the PsiScopeProcessor interface which gathers the possible declarations for the reference and stops the resolve process when it has successfully completed. The main method which needs to be implemented is
execute()
, which is called to process every declaration encountered during the resolve, and returnstrue
if the resolve needs to be continued orfalse
if the declaration has been found. The methodsgetHint()
andhandleEvent()
are used for internal optimizations and can be left empty in the PsiScopeProcessor implementations 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 (
iterateContent()
in the FileIndex interface obtained from ProjectRootManager.getFileIndex() ).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 theexecute()
method of the PsiScopeProcessor passed to it. Also, if necessary according to the language scoping rules, a PSI element can pass the PsiScopeProcessor to its child elements.
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 a navigation target.
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.
The Quick Definition Lookup
action is based on the same mechanism as Go to Declaration
, so it becomes automatically available for all references that can be resolved by the language plugin.