前阵子研究了一下怎么创建自定义的Intellij的Plugin和Android的Lint Rule,惊喜地发现它们有一个共同点就是都用PSI (Program Structure Interface)来呈现代码结构。因此很容易重用部分代码来将某个需求通过Plugin和Lint Rule来实现。
可惜关于PSI的文档十分稀少,所以本来就来讨论Intellij中查看PSI结构的工具来更多得在代码实现中读取和操作PSI对象。
Intellij PSI Viewer
Intellij内置了PSI Viewer,可以从Tools菜单下找到相应的选项来打开窗口,如果当前激活的Intellij窗口是文件编辑窗口还能看到选项直接常看当前文件的PSI结构:
打开PSI Viewer窗口,可以看到如下的视图进行PSI结构查阅,当鼠标指针在文件中移动时,对应的PSI结构也会被加亮来表示它属于哪个类型。这样在代码中就可以知道应该给PsiTreeUtil.getChildrenOfType()
传什么参数了,或者在强制转换时应该用什么类型。
PSI Viewer上下拉列表中有很多类型可以选择,不过在选择类型要自己先明确所选的是正确的类型,否则点击Build PSI Tree按钮后就会获得错误提示。有时候错误信息还特别长,完全无法看到关闭按钮,只能强制重启Intellij了。
解决找到PSI Viewer选项问题
由于日常开发中,大部分时候并不会使用到PSI,所以Intellij的PSI Viewer选项默认并不会在Tools菜单下显示出来,虽然在Find Actions(Command+Shift+A)指令下还是可以搜下“View PSI Structure…”的选项,但也无法打开观察窗口。
根据官方文档的介绍
PSI viewer command is only available when there is at least one plugin module in project.
所以只有当项目中有自定义Plugin项目的时候,PSI Viewer才会起效。创建Plugin项目的流程可参考此处。
好在文档中也提到了解决方案,就是idea.properties
添加一句idea.is.internal=true
,然后就可以在Intellij中任意打开PSI Viewer窗口来常看了。
但千万不要根据上边的文档表述的直接去修改bin/文件夹下的idea.properties,而应该放在个人偏好的文件夹下边,不同系统平台下对应的文件夹位置可以在此处找到。比如Mac OS系统是在~/Library/Preferences/IntelliJIdeaXX
或者 ~/Library/Preferences/IdeaICXX
中(IntelliJIDEA 是IntelliJ IDEA Ultimate Edition, IdeaIC 是IntelliJ IDEA Community Edition; XX是使用的版本号)。
PSI Viewer的缺陷
虽然PSI Viewer是官方提供的工具,但是在使用中也发现很多不方便的地方,比如其窗口是模态的,因此当它被打开时,就不能跟项目窗口进行交互,也就无法边观察PSI结构边更新代码。找到个一种解决办法就是同时打开两个Intellij项目,然后在一个不需要编辑文档的项目窗口中打开PSI Viewer,这样就不会影响另一个在另一个项目窗口中的操作了。
从上边的截图中能看到PSI Viewer只显示代码对应的PSI的类型接口名字,但实际PSI Elements中包含了大量的信息,比如:文本在文件中的位置,父亲元素,子元素等等。要想获得这方面的信息,就得通过调试Plugin从Watch窗口上查阅了。不过下边介绍的另一个插件工具也能帮助我们解决这个烦恼。
PsiViewer插件
PsiViewer是Intellij上的非官方插件。从历史记录上看,该插件发布时间(2004)要早于官方的PSI Viewer的发布时间(2009)年。视图呈现效果也比官方的要好得多。
可以直接在Intellij的插件页面搜索该插件,然后下载安装,重启IDE就可以看到PsiViewer插件显示在右侧边框上。
点开视图,窗口的上半部分就会显示当前文件的PSI结构,于内置的视图差不多。不过更好用地方在于,选择任一PSI元素,在下半部分就会显示该元素的详细信息,极大得方便了开发。
PsiViwer插件的唯一算不上缺点的缺点可能就是只能显示当前文件,但是这不影响我们再文件中输入任意的代码段。同样的也可以同时开两个项目窗口,一个用来查看PSI结构,一个用来编写程序。
PSI实现问题
在实际创建自定义Plugin和Lint Rule,还是发现了一些问题:PSI只是定义了接口(Interface),具体类型实现完全留给平台去决定。所以Intellij的具体实现和Android Lint系统的实现是不一样。
Intellij的实现类是接口名加Impl后缀,Android Lint的实现类则是接口名加Ecj前缀。比如PsiMethod
在前者中的实现就是PsiMethodImpl
,在后者中则是EcjPsiMethod
。更多实现代码可查看两者的代码库:intellij-community at GitHub, android lint at GoogleSource。
好在在代码中都应该直接引用接口类型,访问接口中定义的方法,而不用去关心具体实现类型和方法。
在编写Lint Rule的也发现Android Lint工具并没有实现所有的PSI元素接口。比如就找不到PsiComment
的实现类EcjPsiComment
,所以在Lint代码中就没法找到PsiComment
类型的元素,JavaElementVisitor#visitComment()
方法也不会被系统被调用,必须要自己写代码来检查文本找到注释部分。
所以写自定义Android Lint的时候,如果发现某个类型总是为null
,或者PsiElementVisitor
的方法总是不被调用。就可以去源码库中翻看一下是不是没有实现对应的PSI元素类型了。