Implementing Poly Symbols
The core element of the framework is a PolySymbol
. It is identified through name
and qualifiedKind
properties. The symbol has a very generic meaning and may represent a variable in some language, or an endpoint of some web server, or a file.
The symbol lifecycle is limited to a single read action. To ensure its survival between read actions, use PolySymbol.createPointer()
to create a symbol pointer. Provided the symbol remains valid, dereferencing the pointer will return a new instance of the symbol. It should be noted that during a write action, the symbol might not survive a PSI tree commit. Therefore, creating a pointer prior to the commit and dereferencing it post-commit is advised.
Symbols, which share some common characteristics, should be grouped using the same qualifiedKind
. The qualifiedKind
consists of a namespace
, which roughly indicates a language or a framework the symbol belongs to, and a kind
, which roughly indicates what the symbol's basic characteristics are.
Examples:
a CSS property:
namespace: CSS
,kind: properties
a Java class:
namespace: Java
,kind: classes
a plugin extension:
namespace: ij-plugin
,kind: extensions
A Poly Symbol can originate from source code analysis, or it can be a symbol statically defined through Web Types (JSON) or some other custom format. In both cases, such a symbol can have some source
defined. Each symbol is treated by the framework the same, regardless of their origin.
Consumers of symbols should avoid casting the PolySymbol
to some other specialized interface, as it prevents third party symbol providers from customizing symbols or providing additional symbols.
General Properties
PolySymbol
has a number of properties which are used across IDE features:
qualifiedKind
Describes which group of symbols (kind) within the particular language or concept (namespace) the symbol belongs to.
name
The name of the symbol. If the symbol does not have a pattern, the name will be used as-is for matching.
origin
Specifies where this symbol comes from. Besides descriptive information like framework, library, version, or default icon, it also provides an interface to load symbol types and icons.
icon
An optional icon associated with the symbol, which is going to be used across the IDE. If none is specified, a default icon of the
origin
will be used and if that’s not available, a default icon for symbolnamespace
andkind
.priority
Symbols with higher priority will have precedence over those with lower priority when matching is performed. Symbols with higher priority will also show higher on the completion list.
apiStatus
Documents API status of the symbol. It is one of the sub-interfaces of
PolySymbolApiStatus
:Stable
,Experimental
orDeprecated
. Deprecated symbols are appropriately highlighted in the code editor, code completion, and quick documentation.modifiers
A set of symbol modifiers. The framework contains constants for many modifiers known from various programming languages. However, implementations are free to define other modifiers using
PolySymbolModifier.get
.When a match is performed over a sequence of symbols, use
PolySymbolMatchCustomizer
to customize how modifiers from different symbols in the sequence are merged for the resultingPolySymbolMatch
modifiers.psiContext
A
PsiElement
, which is a file or an element, which can be used to roughly locate the source of the symbol within a project to provide a context for loading additional information, like types. If the symbol isPsiSourcedPolySymbol
(see PsiSourcedPolySymbol ), thenpsiContext
is equal tosource
.presentation
Returns
TargetPresentation
used bySearchTarget
andRenameTarget
. Default implementations ofPolySymbolRenameTarget
andPolySymbolSearchTarget
use thepresentation
property.searchTarget
Implement to provide usage search for the symbol. In most cases the implementation would simply call
PolySymbolSearchTarget.create
.Symbol can also implement the
SearchTarget
interface directly and override its methods, in which casePolySymbolSearchTarget
returned bysearchTarget
property is ignored. If the returned target is not aPolySymbolSearchTarget
, a dedicatedUsageSearcher
needs to be implemented to handle it.searchTarget
Implement to provide rename refactoring for the symbol. In most cases the implementation would simply call
PolySymbolRenameTarget.create
.Symbol can also implement the
RenameTarget
interface directly and override its methods, in which casePolySymbolRenameTarget
returned byrenameTarget
property is ignored. If the returned target is not aPolySymbolRenameTarget
, a dedicatedRenameUsageSearcher
needs to be implemented to handle it.
Query Related Properties
The following properties are related to name matching and code completion queries:
queryScope
When a pattern is being evaluated, matched symbols can provide additional scope for further resolution in the pattern. By default, the
queryScope
returns the symbol itself if it implements thePolySymbolScope
interface.extension
Specifies whether the symbol is an extension. When matched along with a non-extension symbol, it can provide or override some properties of the symbol, or it can extend its scope contents.
Methods
get(property: PolySymbolProperty<T>)
Accessor for various symbol properties. Plugins can use properties to provide additional information on the symbol. All properties supported by IDEs are defined through
PROP_*
constants of thePolySymbol
interface. Check their documentation for further reference. To ensure that results are properly cast, use thePolySymbolProperty.tryCast
method for returned values.getDocumentationTarget(location: PsiElement?)
Used by the Poly Symbols framework to get a
DocumentationTarget
, which handles documentation rendering for the symbol. The additionallocation
parameter allows calculating more specific properties for the symbol documentation, like inferred generic parameters.By default,
PolySymbolDocumentationTarget.create
method should be used to build the documentation target for the symbol. It allows for documentation to be further customized byPolySymbolDocumentationCustomizer
s.getNavigationTargets(project: Project)
Override to provide navigation targets for the symbol. The
SymbolNavigationService
may be used to create navigation targets.isEquivalentTo(symbol: Symbol)
Returns true if two symbols are the same or equivalent for resolve purposes.
matchContext(context: PolyContext)
Allows filtering out symbol from query results if the context is not matched. By default, only the current symbol framework from the
origin
property is checked.createPointer()
Returns the pointer to the symbol, which can survive between read actions. The dereferenced symbol should be valid, for example, any PSI-based properties should return valid
PsiElement
s.getModificationCount()
Symbols can be used in
CachedValue
s as dependencies. If a symbol instance can mutate over time, it should properly implement this method.
PsiSourcedPolySymbol
A symbol should implement PsiSourcedPolySymbol
if its declaration is a regular PsiElement
, for example, a variable or a declared type. Once a symbol implements this interface, it can be searched and refactored together with the PSI element declaration. In case a symbol is:
a part of
PsiElement
(for instance, being part of a string literal)spans multiple PSI elements
does not correlate one-to-one with a PSI element
contribution of a dedicated declaration provider instead of implementing this interface is recommended.
Properties
source
The
PsiElement
, which is the symbol declaration.
PolySymbolWithPattern
The PolySymbolWithPattern
represents a symbol, which might consist of other symbols and provides a pattern, which describes the relationship.
Properties
pattern
The pattern to match names against. As a result of pattern matching, a
PolySymbolMatch
will be created. A pattern may specify that a reference to other Poly Symbols is expected in some part of it. For such places, appropriate segments with referenced Poly Symbols will be created, and navigation, validation, and refactoring support are available out-of-the-box.
CompositePolySymbol
PolySymbolMatch
and some special symbols can have a name, which consists of other Poly Symbols.
Properties
nameSegments
List of
PolySymbolNameSegment
. Each segment describes a range in the symbol name. Segments can be built of other Poly Symbols and/or have related matching problems - missing the required part, unknown symbol name or be a duplicate of another segment. See the Model Queries Example section for an example.
PolySymbolScope
Each PolySymbol
can contain other Poly Symbols, in which case it should implement PolySymbolScope
. For instance, an HTML element symbol would contain some HTML attribute symbols, or a JavaScript class symbol would contain field and method symbols.
When configuring queries, PolySymbolScope
s contributed by PolySymbolQueryScopeContributor
for the given location are added to a PolySymbolQueryStack
] to create an initial scope for symbol resolve. During pattern matching with symbol sequences, all matched symbols' query scopes (PolySymbol.queryScope
) are added to the stack allowing for extending scope matching.
Methods
getSymbols()
Returns symbols within the scope. If the provided
name
isnull
, no pattern evaluation will happen, and all symbols of a particular kind and from a particular namespace will be returned.getCodeCompletions()
Returns code completions for symbols within the scope.
isExclusiveFor()
When scope is exclusive for a particular namespace and kind, resolve will not continue down the stack during pattern matching.
createPointer()
Returns the pointer to the symbol scope, which can survive between read actions. The dereferenced symbol scope should be valid.
getModificationCount()
Symbol scopes are used in CachedValues as dependencies for query executors. If a symbol scope instance can mutate over time, it should properly implement this method.
When implementing a scope containing many elements, an extension of PolySymbolScopeWithCache
is advised. This structure caches the list of symbols and uses an efficient cache mechanism to speed up queries. On extension of this class, it's only necessary to override initialize()
and provide parameters to the super constructor to specify the caching strategy for the results.
Model Queries
Poly Symbols can contain patterns, which allow composing them from other Poly Symbols. Such symbols should implement the PolySymbolWithPattern
interface.
To find which symbols match available patterns, we need to make a match query. One can also run a code completion query, which will produce a list of valid completions in the provided context, or a symbol list query, which will list all available symbols.
To perform a query, create a PolySymbolQueryExecutor
using PolySymbolQueryExecutorFactory
. The query executor will be configured by all registered PolySymbolQueryScopeContributor
and PolySymbolQueryConfigurator
extensions based on the provided PSI context. Scope contributors will provide initial Poly Symbol scopes, and configurators will provider rules for calculating Poly Symbols context, and rules for symbol names conversion.
The result of the match query is a list of PolySymbol
s. Some of them might be PolySymbolMatch
es. Such objects represent complex matches when patterns are used. Poly Symbol Match has nameSegments
property, which precisely describes how segments of the name relate to referenced Poly Symbols and whether there are any problems with resolution or the name itself.
When working with code completion, one can query for the list of code completions. To properly calculate completions, a position in the current text under completion is required. As a result, a list of PolySymbolCodeCompletionItem
will be provided.
Example
Let's consider a Vue directive.
It is a special HTML attribute processed by the Vue framework at runtime or during compilation, which results in additional code being attached to the DOM element. Its structure looks as follows:
An example of how a Vue directive might be declared in Web Types is here. Once a match query is run on v-on:click.once.alt
, we will get a PolySymbolMatch
with the following segments:
v-
: Vue directive pattern symbolon
: Vueon
directive:
click
: DOMclick
event symbol.
once
: Vueon
directiveonce
modifieralt
: Vueon
directivealt
modifier
Patterns
Usually one would create such elements using Web Types, but sometimes there might be a need to do that programmatically.
To simplify resolution and make it less ambiguous, a segment to match is selected by taking everything up to static prefixes of the following patterns. Thus, if we want to have symbol references and regular expressions in the pattern, they either have to terminate the pattern or must be followed by a static text. A regular pattern static prefix is also considered a static text.
There are seven types of patterns:
String match: try to match an exact text, the match is case-sensitive.
Regular expression match: try to match a regular expression, the match can be case-insensitive.
Symbol reference placeholder: a symbol reference resolve will be attempted when this pattern is reached. A resolve will be made by the symbols provider from an enclosing complex pattern. If none of the symbols match the segment, the segment will have a
MatchProblem.UNKNOWN_SYMBOL
problem reported. The matched symbol might be aPolySymbolMatch
itself, which allows for nesting patterns.Pattern sequence: a sequence of patterns. If some patterns are not matched, an empty segment with
MatchProblem.MISSING_REQUIRED_PART
will be created.Complex pattern: this pattern is called complex because it makes several things:
The provided patterns are treated as alternatives.
It can have symbols resolver, which is used by nested symbol reference placeholder patterns.
It allows adding an extra scope to resolve stack.
A complex pattern might be optional, in which case its absence is not reported as an error in an enclosing sequence or complex pattern.
The match can be repeated, and any duplicate segments might have a
MatchProblem.DUPLICATED
problem set.It can override proximity and priority, which by default is based on priority and proximity of matched symbols.
Completion auto popup: a special pattern, which works only in code completion queries. It delimits the place where when creating code completion items, pattern evaluation should be stopped and
...
added. Selecting such items will result in adding the prefix part, and then another code completion popup will open. The pattern can be sticky, which means that the prefix will be shown in the nested code completion list.Single symbol reference (since 2023.2): try to match text against the symbol name, but put a reference to another element.
Query Context
When performing queries, some symbols should be excluded and others included in particular contexts. For instance, if we have an Angular project, none of the Vue components should be available. PolyContext
is created using rules provided by PolySymbolQueryConfigurator
s with the addition of custom PolyContextProvider
. As a result, for each kind of context, there is at most a single name assigned. PolyContext
can also be used outside the PolySymbolQueryExecutor
as an efficient way to determine whether to enable or disable particular functionality in the IDE based on PSI or VFS context.
Query stack
The stack is used as a scope for resolving symbols. All scopes provided by PolySymbolQueryConfigurator
s together with the list of additional scopes passed as arguments to the query create an initial query stack. Each time a symbol is matched, the list returned by queryScope
property is added to the stack for any subsequent matches further right the pattern.
Declarations
To provide locations of declarations of Poly Symbols, which are not PsiSourcedPolySymbol
s, a dedicated PolySymbolDeclarationProvider
should be registered. It should return a list of PolySymbolDeclaration
s in a particular PsiElement
at a particular offset. Symbols may implement the PolySymbolDeclaredInPsi
interface to avoid boilerplate related to declaration building.
References and Code Completion
Usually, it is enough to provide a PolySymbolQueryScopeContributor
for the supported language features. However, when implementing integration for a language feature, reference providers and code completions need to be implemented from scratch.
To provide references, a PsiPolySymbolReferenceProvider
should be registered. If references resolve to a single PolySymbol
, even if it may be a composite PolySymbol
, the getReferencedSymbol
method should be implemented. If the symbol reference is offset within the PsiElement
, for example, within a string literal, the getReferencedSymbolNameOffset
should also be implemented. If there are multiple symbol references within the PsiElement
and it is not possible to use a PolySymbolWithPattern
to represent that, getOffsetsToReferencedSymbols
should be overridden instead. The PsiPolySymbolReferenceProvider
should use PolySymbolQueryExecutor
to produce the list of the symbols at the given location.
Together with references, completion providers should also be implemented. Due to the nature of Poly Symbol patterns, providers need to be implemented separately from reference contributions. A regular CompletionContributor
should be registered, which in turn should register a pattern for completion provider, which extends PolySymbolsCompletionProviderBase
.
Documentation, Navigation, Find Usages, and Rename Refactoring
These platform features are based on the reference resolve, so once the reference is resolved to a PolySymbol
, it is up to implementations of the interface to provide particular support. Override getDocumentationTarget
, getNavigationTargets
methods, or searchTarget
, renameTarget
properties to inform the platform about how it should use the symbol. To customize symbols documentation generically, PolySymbolDocumentationCustomizer
should be implemented.
Semantic highlighting
Poly Symbol framework supports semantic highlighting of symbols. The simplest method is to return the name of TextAttributesKey
from the PolySymbol.get
method, when PolySymbol.PROP_IJ_TEXT_ATTRIBUTES_KEY
property value is requested. For a more generic approach, PolySymbolHighlightingCustomizer
may be implemented.