实现自定义语言 PSI 中最重要和最棘手的部分之一是解析References。解析References使用户能够从 PSI 元素的使用(访问变量、调用方法等)导航到该元素的声明(变量的定义、方法声明等)。其实就是实现Navigate | Declaration or Usages功能,这个功能是实现Find Usages、Rename、Code Completion的先决条件。
引用功能是自定义语言支持实现中最重要的部分之一。解析引用意味着能够从元素的使用到它的声明、完成、重命名重构、查找用法等。
View | Quick Definition 也需要依赖此功能,如果需要知道文档的确切的范围,需要实现com.intellij.lang.implementationTextSelectioner扩展点。
一、References and Resolve
1、Psi References
所有的PSI elements如果想实现引用功能都需要持有PsiReference属性,这个属性是由PsiElement.getReference()方法返回的。一个Element可以拥有多个PsiReference(例如,一个字符串文字可以包含多个子字符串)PsiElement.getReferences()
, 上述这个方法的性能如果有问题的话,可以使用HintedReferenceHost工具类进行优化。
PsiReference的主要接口是resolve()
,它返回引用指向的元素,或者null
如果无法解析对有效元素的引用,已解析的元素应该实现PsiNamedElement接口或PsiNameIdentifierOwner接口。
虽然引用元素和被引用元素都可能有名称,但只有引入名称的元素(例如,定义
int x = 42
)需要实现PsiNamedElement
。使用点的引用元素(例如,表达式中的 the )x
不应x + 1
实现PsiNamedElement
,因为它没有名称。
与resolve()
方法对应方法isReferenceTo()
,它检查引用是否解析为指定的元素。后一种方法可以通过调用resolve()
并将结果与传递的 PSI 元素进行比较来实现。可参考示例:
public class ResourceBundleReference extends PsiReferenceBase<PsiElement>
implements PsiPolyVariantReference, BundleNameEvaluator, ResolvingHint {
private static final Function<PropertiesFile, PsiElement> PROPERTIES_FILE_PSI_ELEMENT_FUNCTION =
PropertiesFile::getContainingFile;
private final String myBundleName;
public ResourceBundleReference(final PsiElement element) {
this(element, false);
}
public ResourceBundleReference(final PsiElement element, boolean soft) {
super(element, soft);
myBundleName = getValue().replace('/', '.');
}
public ResourceBundleReference(final PsiElement element, TextRange textRange, boolean soft) {
super(element, textRange, soft);
myBundleName = getValue().replace('/', '.');
}
@Override
public boolean canResolveTo(Class<? extends PsiElement> elementClass) {
return ReflectionUtil.isAssignable(PsiFile.class, elementClass);
}
@Override
@Nullable
public PsiElement resolve() {
ResolveResult[] resolveResults = multiResolve(false);
return resolveResults.length == 1 ? resolveResults[0].getElement() : null;
}
@Override
public ResolveResult @NotNull [] multiResolve(final boolean incompleteCode) {
PropertiesReferenceManager referenceManager = PropertiesReferenceManager.getInstance(myElement.getProject());
List<PropertiesFile> propertiesFiles = referenceManager.findPropertiesFiles(myElement.getResolveScope(), myBundleName, this);
return PsiElementResolveResult.createResults(ContainerUtil.map(propertiesFiles, PROPERTIES_FILE_PSI_ELEMENT_FUNCTION));
}
@Override
@NotNull
public String getCanonicalText() {
return myBundleName;
}
@Override
public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
if (newElementName.endsWith(PropertiesFileType.DOT_DEFAULT_EXTENSION)) {
newElementName = newElementName.substring(0, newElementName.lastIndexOf(PropertiesFileType.DOT_DEFAULT_EXTENSION));
}
final String currentValue = getValue();
final char packageDelimiter = getPackageDelimiter();
final int index = currentValue.lastIndexOf(packageDelimiter);
if (index != -1) {
newElementName = currentValue.substring(0, index) + packageDelimiter + newElementName;
}
return super.handleElementRename(newElementName);
}
private char getPackageDelimiter() {
return getValue().indexOf('/') != -1 ? '/' : '.';
}
@Override
public PsiElement bindToElement(@NotNull final PsiElement element) throws IncorrectOperationException {
if (!(element instanceof PropertiesFile)) {
throw new IncorrectOperationException();
}
final String name = ResourceBundleManager.getInstance(element.getProject()).getFullName((PropertiesFile)element);
return name != null ? super.handleElementRename(name) : element;
}
@Override
public boolean isReferenceTo(@NotNull PsiElement element) {
if (element instanceof PropertiesFile) {
final String name = ResourceBundleManager.getInstance(element.getProject()).getFullName((PropertiesFile)element);
if (name != null && name.equals(myBundleName)) {
return true;
}
}
return false;
}
@Override
public Object @NotNull [] getVariants() {
final ProjectFileIndex projectFileIndex = ProjectFileIndex.getInstance(getElement().getProject());
final PropertiesReferenceManager referenceManager = PropertiesReferenceManager.getInstance(getElement().getProject());
final Set<String> bundleNames = new HashSet<>();
final List<LookupElement> variants = new SmartList<>();
PropertiesFileProcessor processor = (baseName, propertiesFile) -> {
if (!bundleNames.add(baseName)) return true;
final LookupElementBuilder builder =
LookupElementBuilder.create(baseName)
.withIcon(AllIcons.Nodes.ResourceBundle);
boolean isInContent = projectFileIndex.isInContent(propertiesFile.getVirtualFile());
variants.add(isInContent ? PrioritizedLookupElement.withPriority(builder, Double.MAX_VALUE) : builder);
return true;
};
referenceManager.processPropertiesFiles(myElement.getResolveScope(), processor, this);
return variants.toArray(LookupElement.EMPTY_ARRAY);
}
@Override
public String evaluateBundleName(final PsiFile psiFile) {
return BundleNameEvaluator.DEFAULT.evaluateBundleName(psiFile);
}
}
2、Resolve实现逻辑
PsiScopeProcessor接口和PsiElement.processDeclarations()方法可以作为实现解析支持的基础。插件可以放弃标准接口并提供自己的、不同的 resolve 实现。resolve 实现包含以下组件:
1、一个PsiScopeProcessor接口实现类,它收集引用的可能声明,并在成功完成时停止解析过程。需要实现的主要方法是execute()
,调用它来处理解析过程中遇到的每一个声明,true
如果需要继续解析或者false
找到声明则返回。方法getHint()
和handleEvent()
用于内部优化,在PsiScopeProcessor
自定义语言的实现中可以留空;
2、一个函数,它从参考位置向上遍历 PSI 树,直到解析成功完成或到达解析范围的末尾。如果引用的目标位于不同的文件中,则可以定位该文件;
3、在 PSI 树遍历时,每个PSI 元素都会调用processDeclarations(),
如果 PSI 元素是一个声明,它会将自身传递给传递给它的execute()
方法PsiScopeProcessor
。此外,如果需要,根据语言范围规则,PSI 元素可以将PsiScopeProcessor
传递给它的子元素。
3、多个目标References
PsiReference(PsiPolyVariantReference)接口允许Reference解析到多个目标上,通过multiResolve()
方法返回这些目标。multiResolve()
也可以基于PsiScopeProcessor实现。
4、额外突出显示
Settings | Editor | Color Scheme | Language Defaults中的设置是通过HighlightedReference来实现的。
二、示例
1、定义命名元素类
public interface SimpleNamedElement extends PsiNameIdentifierOwner {
}
public abstract class SimpleNamedElementImpl extends ASTWrapperPsiElement implements SimpleNamedElement {
public SimpleNamedElementImpl(@NotNull ASTNode node) {
super(node);
}
}
为生成的 PSI 元素定义辅助方法
修改SimplePsiImplUtil
以支持添加到简单语言的 PSI 类的新方法。
public class SimplePsiImplUtil {
// ...
public static String getName(SimpleProperty element) {
return getKey(element);
}
public static PsiElement setName(SimpleProperty element, String newName) {
ASTNode keyNode = element.getNode().findChildByType(SimpleTypes.KEY);
if (keyNode != null) {
SimpleProperty property =
SimpleElementFactory.createProperty(element.getProject(), newName);
ASTNode newKeyNode = property.getFirstChild().getNode();
element.getNode().replaceChild(keyNode, newKeyNode);
}
return element;
}
public static PsiElement getNameIdentifier(SimpleProperty element) {
ASTNode keyNode = element.getNode().findChildByType(SimpleTypes.KEY);
return keyNode != null ? keyNode.getPsi() : null;
}
// ...
}
定义一个元素工厂
package org.intellij.sdk.language.psi;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import org.intellij.sdk.language.SimpleFileType;
public class SimpleElementFactory {
public static SimpleProperty createProperty(Project project, String name) {
SimpleFile file = createFile(project, name);
return (SimpleProperty) file.getFirstChild();
}
public static SimpleFile createFile(Project project, String text) {
String name = "dummy.simple";
return (SimpleFile) PsiFileFactory.getInstance(project).
createFileFromText(name, SimpleFileType.INSTANCE, text);
}
}
更新语法并重新生成解析器
现在通过用下面的行替换定义来对Simple.bnf语法文件进行相应的更改。property
不要忘记在更新文件后重新生成解析器!右键单击Simple.bnf文件并选择Generate Parser Code。
property ::= (KEY? SEPARATOR VALUE?) | KEY {
mixin="org.intellij.sdk.language.psi.impl.SimpleNamedElementImpl"
implements="org.intellij.sdk.language.psi.SimpleNamedElement"
methods=[getKey getValue getName setName getNameIdentifier]
}
2、定义Reference
public class SimpleReference extends PsiReferenceBase<PsiElement> implements PsiPolyVariantReference {
private final String key;
public SimpleReference(@NotNull PsiElement element, TextRange textRange) {
super(element, textRange);
key = element.getText().substring(textRange.getStartOffset(), textRange.getEndOffset());
}
@Override
public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
Project project = myElement.getProject();
final List<SimpleProperty> properties = SimpleUtil.findProperties(project, key);
List<ResolveResult> results = new ArrayList<>();
for (SimpleProperty property : properties) {
results.add(new PsiElementResolveResult(property));
}
return results.toArray(new ResolveResult[results.size()]);
}
@Nullable
@Override
public PsiElement resolve() {
ResolveResult[] resolveResults = multiResolve(false);
return resolveResults.length == 1 ? resolveResults[0].getElement() : null;
}
@Override
public Object @NotNull [] getVariants() {
Project project = myElement.getProject();
List<SimpleProperty> properties = SimpleUtil.findProperties(project);
List<LookupElement> variants = new ArrayList<>();
for (final SimpleProperty property : properties) {
if (property.getKey() != null && property.getKey().length() > 0) {
variants.add(LookupElementBuilder
.create(property).withIcon(SimpleIcons.FILE)
.withTypeText(property.getContainingFile().getName())
);
}
}
return variants.toArray();
}
}
定义Reference Contributor
public class SimpleReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralExpression.class),
new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element,
@NotNull ProcessingContext context) {
PsiLiteralExpression literalExpression = (PsiLiteralExpression) element;
String value = literalExpression.getValue() instanceof String ?
(String) literalExpression.getValue() : null;
if ((value != null && value.startsWith(SIMPLE_PREFIX_STR + SIMPLE_SEPARATOR_STR))) {
TextRange property = new TextRange(SIMPLE_PREFIX_STR.length() + SIMPLE_SEPARATOR_STR.length() + 1,
value.length() + 1);
return new PsiReference[]{new SimpleReference(element, property)};
}
return PsiReference.EMPTY_ARRAY;
}
});
}
}
<extensions defaultExtensionNs="com.intellij">
<psi.referenceContributor language="JAVA"
implementation="org.intellij.sdk.language.SimpleReferenceContributor"/>
</extensions>
3、定义Refactoring Support Provide
public class SimpleRefactoringSupportProvider extends RefactoringSupportProvider {
@Override
public boolean isMemberInplaceRenameAvailable(@NotNull PsiElement elementToRename, @Nullable PsiElement context) {
return (elementToRename instanceof SimpleProperty);
}
}
<extensions defaultExtensionNs="com.intellij">
<lang.refactoringSupport
language="Simple"
implementationClass="org.intellij.sdk.language.SimpleRefactoringSupportProvider"/>
</extensions>
至此,Simple语言此时支持了重构功能