导航、引用、查找用法与重构¶
IDE 级语言体验的核心是“名称和引用关系”。当 PSI 元素能稳定命名、引用能正确 resolve,跳转、补全、查找用法、重命名和安全删除就能沿同一套机制自然展开。
关键接口关系¶
| 能力 | 关键 API |
|---|---|
| 可命名声明 | PsiNamedElement、PsiNameIdentifierOwner |
| 引用 | PsiReference、PsiPolyVariantReference |
| 外部语言中贡献引用 | PsiReferenceContributor、PsiReferenceProvider |
| 补全候选 | PsiReference.getVariants()、CompletionContributor |
| 查找用法 | FindUsagesProvider、WordsScanner |
| 重命名 | setName()、handleElementRename()、NamesValidator |
| 安全删除 | RefactoringSupportProvider、PsiElement.delete() |
| 行号槽导航图标 | LineMarkerProvider、RelatedItemLineMarkerProvider |
这套能力最好从声明、引用、索引、作用域四个维度一起设计,不要只为了一个功能临时拼实现。
可命名元素¶
所有可被引用或重命名的声明元素都应实现 PsiNamedElement。如果声明名只是 PSI 元素的一部分,更常见的是实现 PsiNameIdentifierOwner:
interface ExampleNamedElement : PsiNameIdentifierOwner
实现要点:
getName()返回用户看到的逻辑名称。getNameIdentifier()返回名称所在的 PSI 子元素。getTextOffset()应指向名称开始位置,而不是整个声明开始位置。setName()应通过 Element Factory 创建合法 PSI 节点,再替换旧 AST 节点。
不要手写字符串替换来重命名 PSI。官方建议创建一个 dummy file,让 parser 生成正确节点,再取出要替换的节点。
Element Factory¶
Element Factory 用来构造语言内合法 PSI:
object ExampleElementFactory {
fun createProperty(project: Project, name: String): ExampleProperty {
val file = PsiFileFactory.getInstance(project)
.createFileFromText("dummy.example", ExampleFileType.INSTANCE, "$name = value")
return PsiTreeUtil.findChildOfType(file, ExampleProperty::class.java)!!
}
}
用途:
setName()。- Quick Fix 插入新节点。
- Rename 后替换引用文本。
- 生成 formatter 或 inspection 需要的合法片段。
Reference 与 Resolve¶
引用连接“使用位置”和“声明位置”。单目标引用用 PsiReferenceBase,多目标引用用 PsiPolyVariantReferenceBase。
class ExampleReference(
element: PsiElement,
rangeInElement: TextRange
) : PsiPolyVariantReferenceBase<PsiElement>(element, rangeInElement) {
override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
return ExampleIndex.findDeclarations(element.project, value)
.map { PsiElementResolveResult(it) }
.toTypedArray()
}
override fun getVariants(): Array<Any> {
return ExampleIndex.findAllDeclarations(element.project)
.map {
LookupElementBuilder.create(it)
.withTypeText(it.containingFile.name)
}
.toTypedArray()
}
}
设计原则:
rangeInElement必须精确到引用文本,不要包含引号、前缀或额外语法。resolve()/multiResolve()要快,必要时通过 Stub Index。getVariants()可以提供基础补全,但复杂补全仍应放到CompletionContributor。- 跨语言引用可用
PsiReferenceContributor注入到目标语言元素上。
注册引用贡献:
<extensions defaultExtensionNs="com.intellij">
<psi.referenceContributor
language="JAVA"
implementation="com.example.ExampleReferenceContributor"/>
</extensions>
Find Usages¶
Find Usages 依赖三件事:
- 声明元素是
PsiNamedElement或能从引用 resolve 到PsiNamedElement。 - 文件内容被 WordsScanner 建立词索引。
- 使用位置的
PsiReference能正确 resolve 到声明。
注册:
<extensions defaultExtensionNs="com.intellij">
<lang.findUsagesProvider
language="Example"
implementationClass="com.example.ExampleFindUsagesProvider"/>
</extensions>
典型实现:
class ExampleFindUsagesProvider : FindUsagesProvider {
override fun getWordsScanner(): WordsScanner {
return DefaultWordsScanner(
ExampleLexerAdapter(),
ExampleTokenSets.IDENTIFIERS,
ExampleTokenSets.COMMENTS,
ExampleTokenSets.STRINGS
)
}
override fun canFindUsagesFor(psiElement: PsiElement): Boolean {
return psiElement is ExampleNamedElement
}
override fun getType(element: PsiElement): String = "example symbol"
override fun getDescriptiveName(element: PsiElement): String = (element as PsiNamedElement).name ?: ""
override fun getNodeText(element: PsiElement, useFullName: Boolean): String = getDescriptiveName(element)
}
性能关键:对局部变量、函数参数、局部标签等有限作用域元素,覆写 getUseScope() 返回更窄范围。这样重命名和查找用法不必扫描全项目。
Rename¶
Rename 使用 Find Usages 的同一套查找规则:
- 声明元素调用
PsiNamedElement.setName()。 - 引用位置调用
PsiReference.handleElementRename()。 - 如果引用继承
PsiReferenceBase,可通过ElementManipulator.handleContentChange()处理内部文本替换。
名称校验:
<extensions defaultExtensionNs="com.intellij">
<lang.namesValidator
language="Example"
implementationClass="com.example.ExampleNamesValidator"/>
</extensions>
更细粒度校验:
<extensions defaultExtensionNs="com.intellij">
<renameInputValidator implementation="com.example.ExampleRenameInputValidator"/>
</extensions>
扩展场景:
| 需求 | API |
|---|---|
| 禁止某类元素被重命名 | com.intellij.vetoRenameCondition |
| 标准 UI 但扩展逻辑 | RenamePsiElementProcessor |
| 完全自定义 Rename UI 和流程 | RenameHandler |
| 支持原地重命名 | RefactoringSupportProvider.isMemberInplaceRenameAvailable() |
Safe Delete¶
Safe Delete 也建立在 Find Usages 之上。需要:
- 在
RefactoringSupportProvider中返回isSafeDeleteAvailable()。 - 对可删除 PSI 元素实现
delete()。 - 复杂逻辑用
SafeDeleteProcessorDelegate定制引用搜索和删除流程。
注册:
<extensions defaultExtensionNs="com.intellij">
<lang.refactoringSupport
language="Example"
implementationClass="com.example.ExampleRefactoringSupportProvider"/>
</extensions>
安全删除真正执行前会先找用法。删除时应该删除 AST 中对应节点,而不是直接改 Document 文本。
Line Marker 与导航图标¶
Line Marker 用于在 gutter 放图标,点击跳转到相关元素:
<extensions defaultExtensionNs="com.intellij">
<codeInsight.lineMarkerProvider
language="Example"
implementationClass="com.example.ExampleLineMarkerProvider"/>
</extensions>
最佳实践:
- 返回 marker 的元素必须是最小叶子元素,例如标识符 token。
- 不要在
PsiMethod、函数体、文件等大节点上返回 marker,否则可见区域两阶段扫描会导致图标闪烁。 - 需要用户控制开关时,继承
GutterIconDescriptor。 - 用
RelatedItemLineMarkerProvider和NavigationGutterIconBuilder简化“跳到相关目标”场景。
实战验收清单¶
- 声明元素
getNameIdentifier()和getTextOffset()正确。 - 引用
rangeInElement只覆盖引用名。 resolve()对不存在目标返回空,不抛异常。getVariants()不做全项目 PSI 遍历。- Find Usages 能区分代码、注释、字符串。
- Rename 后声明和所有引用都更新。
- 名称校验符合语言规则,不退回 Java 标识符规则。
- 局部符号覆写
getUseScope()。 - Safe Delete 会先报告用法,再删除正确 AST 节点。
- Line Marker 不闪烁。