索引
通过索引可以快速查找元素,例如:在代码库中,查找包含某个单词或某个方法的文件。插件开发者可以使用IDE已有的索引来构建和使用自己的索引。
有以下2种索引:
-
文件索引 :基于文件内容构建的索引。通过该索引可以直接搜索到符合指定条件的文件
-
Stud索引 :基于序列化Stub trees 构建。 Stub tree 是PSI tree 的子集,只包含PSI tree中外部可见的声明,以二进制格式存储。该索引可以搜索符合指定条件的PSI元素集。
提示
PSI tree: PSI树,例如在java类中,java类会被解构为一个一个的PsiElement(PSI元素),所有的PsiElement构建为一个PSI树,可以参考文档 PSI元素
提示
可以使用 Index Viewer
插件查看索引的内容和属性。
# Dumb mode
索引在后台进程中进行构建,在构建索引的过程中,DumbService
限制了 只能使用 文本编辑或版本控制等不需要使用索引的功能 ,如果调用了需要使用索引的功能,会抛出 IndexNotReadyException异常。
通过DumbService 可以获取当前项目处于 dumb模式(该模式下不能访问索引)还是 smart模式(该模式下,索引构建完成,可以使用索引),也可以等待项目处于smart模式时再执行操作。
//判断项目处于什么模式下
// dumb模式(该模式下不能访问索引)
// smart模式(该模式下,索引构建完成,可以使用索引)
DumbService.getInstance(project).isDumb();
DumbService.isDumb(project);
//等待项目处于 smart模式 时,再执行业务操作
DumbService.getInstance(project).runWhenSmart(()->{//业务逻辑});
# Gists
有时候会碰到以下情况:
-
不需要使用文件索引,只需要计算文件内容的相关数据,并将数据缓存到硬盘上
-
不需要在索引构建期间执行计算(例如:它会拖慢索引的构建,或者只有一小部分文件曾经需要这些计算数据)
-
可以根据请求延迟重新计算数据,而不会造成明显的性能损失。
大部分情况下可以使用 文件索引 ,但是通过 gists的API 可以延迟计算,并缓存到硬盘上, 可以参考 VirtualFileGist
和 PsiFileGist的文档。
示例:
-
VirtualFileGist :使用 ImageInfoIndex
-
计算图片的大小尺寸并显示到UI上。
-
PsiFileGist : 使用 JavaSimplePropertyGist
-
获取Java类的简单属性
# 提高索引性能
# 性能指标
在2020.2及以后的版本中,索引的性能指标以json格式存储在了沙盒中的logs文件夹中,从2021.1版本开始,以HTML格式存储了另外的一些指标,参考下图:
沙盒路径查看文档:插件开发项目配置
# 避免使用AST
如果可能,尽量使用词法分析器信息而不是解析树。 如果不可能,请使用不会占用大量内存的经量级AST(LighterAST) ,这样可以提升遍历速度。可以通过将传过来的 FileContent参数 转换为 PsiDependentFileContent 然后调用 getLighterAST() 来获取 LighterAST,使用 LighterASTNodeVisitor
和LightTreeUtil来遍历自己需要用到的节点,
继承 LightStubBuilder
来实现轻量级 Stub 索引
If a custom language contains lazy-parseable elements that never or rarely contain any stubs, consider implementing StubBuilder.skipChildProcessingWhenBuildingStubs()
(preferably using Lexer/node text).
考虑使用 NanoXmlUtil
来索引 xml 文件
# 预创建Stubs
如果你使用的编程语言有大量的对所有用户都一样的组件库,你可以注册 PrebuiltStubsProvider
扩展 来预创建Stubs索引,避免每次安装都需要构建Stubs索引。文件索引
文件索引基于Map/Reduce
的数据结构。每一个索引都有一个指定类型的key,一个指定类型的value.
示例:word索引中,key就是word自己。value是跟key关联的,包含了word的context的对象(code, string literal, 或 comment)
在索引中,如果获取索引中某key的value的 type 等于 Void ,则表示不存在跟key相关的数据。
当一个索引实现类对一个文件创建了索引,那么这个索引实现类会收到该文件的内容,并返回一个Map, Map包含了该文件的索引key,及跟key相关的value
访问索引的时候,传入key,就能获取到跟key相关的所有文件。
提示
某些情况,可以不使用索引,而使用 Gist
# 实现文件索引
提示
UI Designer bound forms index
是一个相对简单的索引示例,它存储了 GUI Designer
.form中使用的class的全限定命令
注册索引实现类,如下:
<!--在plugin.xml里注册索引实现类-->
<extensions defaultExtensionNs="com.intellij">
<fileBasedIndex implementation="com.intellij.util.indexing.FileBasedIndexExtension 的子类"></fileBasedIndex>
</extensions>
实现 com.intellij.util.indexing.FileBasedIndexExtension
分为以下几部分:
-
getIndexer() :返回 DataIndexer
-
,它负责根据文件内容构建索引。
-
getKeyDescriptor() :返回以二进制的格式存储key的 KeyDescriptor
,最常用的是 KeyDescriptor的子类 EnumeratorStringDescriptor
-
,它能提升存储效率。
-
getValueExternalizer() :返回以二进制格式存储value的 DataExternalizer
getInputFilter() :只过滤自己需要的文件。可以使用 DefaultFileTypeSpecificInputFilter
-
getName() :返回唯一的索引ID,推荐使用类的全限定命名,防止冲突,例如可以使用:com.example.myplugin.indexing.MyIndex
-
getVersion() :索引的版本。如果当前使用的索引版本和实现类的版本不一致,将会自动重新构建索引
如果获取到的value的type 等于 Void ,则表示不存在该文件。
通过继承 ScalarIndexExtension
可以实现简单的索引。如果索引的Value只有一个,可以继承 SingleEntryFileBasedIndexExtension
来实现
警告
为了保证相同的Value只存储一个,所有索引Value的实现类必须重写 equals() 和 hashCode() 方法。
通过 DataIndexer.map() 方法返回的数据必须只依赖用户输入的参数,不能依赖任何外部元素,否则,你的索引将不能实时更新
在开发的时候,debug索引时,可以把 intellij.idea.indices.debug/intellij.idea.indices.debug.extra.sanity 设置为true
# 访问文件索引
提示
注意:在项目处于 Dumb mode 时,不能访问索引
通过FileBasedIndex
来访问文件索引,主要包括以下操作:
- getAllKeys() 和 processAllKeys() 方法可以获取到项目的文件所有的key。可以让 FileBasedIndexExtension.traceKeyHashToVirtualFileMapping() 方法返回 true ,来优化性能
提示
保证能返回最新项目中所有的key,也可能有最新项目中不包含的key(项目更新时,会更新索引,但不会删除老索引)。
-
getValues() :获取跟Key有关的所有值,但是不包含值所在的文件
-
getContainingFiles() :获取包括Key的所有文件
-
processValues() :可以遍历和访问跟Key有关的所有文件
警告
索引不支持嵌套访问,可能会引起死锁。应该先从索引A中获取到所有结果,然后再在索引B中使用索引A的结果。
# 标准索引
IntelliJ Platform 提供了常用的索引,如下:
# Word Index
通常使用 PsiSearchHelper
来访问
# File Name Index
FilenameIndex
可以快速的根据文件名称查找文件
# File Type Index
FileTypeIndex
可以快速的根据文件类型查找文件
# 向原有的索引根中添加索引
继承 IndexableSetContributor
,并在plugin.xml里注册,如下:
<extensions defaultExtensionNs="com.intellij">
<indexedRootsProvider implementation="com.intellij.util.indexing.IndexableSetContributor 实现类"></indexedRootsProvider>
</extensions>
Stub Trees
Stub Tree是PSI tree的子集,以紧凑的二进制格式进行存储。PSI tree有2种格式,一种是AST(Abstract Syntax Tree:抽象语法树,解析文件构建而成),别一种是从硬盘上反序列化的stub tree , 这2种格式可以互相切换。
Stub Tree只包含一部分节点。通常,它只包含外部文件有访问权限的(public protected default修饰的) 的节点。如果访问Stub Tree不包含的节点,或访问像 PSI element的文本等 Stub tree不支持的操作,都会引起文件的解析由PSI解析切换到 AST backing。
Stub Tree中的每个stub都是一个没有任何行为的bean class 。stub存储了相关PSI Element 相关信息的子集(例如名称,public,final等),还能获取到它的父子stubs
当你想要 stubs 支持你自定义的编辑语言时,你需要确定PSI tree 中的哪些元素需要被存储进 Stubs。通常,Stubs 需要存储外部文件有访问权限的(public protected default修饰的) 方法和成员变量,不需要存储外部文件无访问权限的局部变量,private修改的方法和变量等元素
# 实现Stub索引
一个编程语言想要支持stubs ,需要执行以下几步:
-
改变编辑语言中 ParserDefinition.getFileNodeType() 返回的 Element type 为 自定义的IStubFileElementType
-
的子类
-
在plugin.xml 里,注册 com.intellij.stubElementTypeHolder 扩展。示例:JavaPsiPlugin.xml
-
,摘要如下:
<extensions defaultExtensionNs="com.intellij">
<!-- JavaStubElementTypes 是一个接口, 包含了 编程语言解析器所用到的所有 IElementType 常量 -->
<!-- externalIdPrefix:编辑语言中所有stub element type使用的公共前缀 -->
<stubElementTypeHolder class="com.intellij.psi.impl.java.stubs.JavaStubElementTypes" externalIdPrefix="java."/>
</extensions>
对于要存储在Stub tree中的每种Element type ,你需要参考下面示例的实现步骤:
-
定义 stub 接口 PropertyStub
-
。
-
定义实现类PropertyStubImpl
-
,该接口需要实现上一步定义的接口
-
定义 PSI Element接口 Property
-
接口。
-
定义实现类PropertyImpl
-
接口。另外该实现类需要提供2个构造器,一个参数为 ASTNode 的构造器,和一个参数为 PropertyStub(第一步定义的接口) 的构造器 。
-
定义 PropertyStubElementType
-
,该类需要继承泛型为 PropertyStub(第一步定义的接口) 和 Property接口(第3步定义的接口) 的 IStubElementType接口 (ILightStubElementType<PropertyStub, Property> ),实现创建PSI的 createPsi() 方法和创建stub的createStub() 方法,实现用于序列化存储serialize() 方法和反序列化的deserialize() 方法
-
解析代码的时候,使用 IStubElementType 的子类 PropertyStubElementType(第5步定义的) 作为 element type 常量。参考示例 PropertiesElementTypes
实现 Property.getKey()(示例:PropertyImpl.getKey()
-
)方法, 确保 PSI element 接口中的所有方法在适当的时候访问stub数据 而不是 PSI tree
提示
如果使用 Grammar-Kit
文档了解如何让你的语法整合 stub
默认情况下,所有继承了 StubBasedPsiElement 的PSI element都会存到 stub tree 。如果不需要存储某类型元素,可以在该类型元素中重写 IStubElementType.shouldCreateStub() 方法并返回 false ,这个策略只作用于元素自身,如果该PSI元素的子元素继承了 StubBasedPsiElement,该子元素依然会存储到stub tree 中。
# 序列化数据
推荐使用 StubOutputStream.writeName() 和 StubInputStream.readName() 方法来序列化元素名称等String类型的数据。这些方法可以保证在数据流中同一个数据只存储一次,从而减少 stub tree的占用空间。也可以参考 DataInputOutputUtil
如果需要改变存储Stub的二进制格式(例如:想存储额外的数据或一些新元素),你需要升级通过 IStubFileElementType.getStubVersion() 获取到的 stub 版本。版本升级后,为了避免 Stub 和项目的代码不匹配,会重新构建Stubs和Stub索引 。
必须要保证在 stub tree 中存储的数据只依赖于stub的文件的内容,不依赖于任何外部文件。这是因为外部文件更新后,stub tree 并不会重新构建,从而导致stub tree 中数据不准确。
# Stub索引
在构建 stub tree 的同时,可以把 stub elements 的相关数据存储到其它索引里,这些索引就可以用来查找 PSI elements 。和文件索引不一样,stub索引的value不支持存储自定义数据,只会存储 PSI elements 。stub索引中的key一般都是String类型(例如使用类名),如果需要,也可以使用其它类型。
stub index的类必须继承 AbstractStubIndex
。但是,大多数情况下,如果 stub index 的key的类型是string,可以通过继承 StringStubIndexExtension来实现 ,然后在 plugin.xml 里注册为 com.intellij.stubIndex 类型的扩展。
向某个Stub索引中存储数据,需要实现 IStubElementType.indexStub() 方法 (示例:JavaClassElementType.indexStub()
)。该方法接收 IndexSink 参数,并存储每一个需要存储的元素的key和 索引id
可以使用下面2个方法来访问stub索引:
-
AbstractStubIndex.getAllKeys() :可以获取指定索引,指定项目的所有key (例如:获取项目中所有类的名称)
-
AbstractStubIndex.get() :可以获取指定作用域,指定key(key通常为类名)的 PSI elements集合文章来源:https://www.toymoban.com/news/detail-437188.html
# 相关论坛
Lifecycle of stub creation文章来源地址https://www.toymoban.com/news/detail-437188.html
到了这里,关于索引和PSI存根 (Indexing and PSI Stubs)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!