作者 | 袁华健
handsome code, handsome coder.
初级篇介绍了开发插件必要的环境、编写简单的代码、运行以及打包发布,但是我们当想实现一些高级功能,比如去自动生成代码等功能,就得进一步学习 Intellij IDEA 提供的 sdk 用法。
在 Action 中最主要的是两个方法 , actionPerformed
方法和 update
方法,在本文中,为了方便理解,我们称呼一个 Action
为一个菜单项。
当我们定义的菜单项被点击的时候, actionPerformed
方法就会被回调,在初级篇中,我们让插件被点击的时候弹出了一个系统框。
@Override public void actionPerformed(AnActionEvent e) { Project project = e.getData(PlatformDataKeys.PROJECT); Messages.showMessageDialog(project, "say hello world ~", "Info", Messages.getInformationIcon()); }
当我们需要自定义的 Action
只在特定的场景下可用或者出现,那么就需要覆盖 update
方法, update
方法会在我们点击菜单项或者菜单项的父菜单被打开的时候被回调。 update
方法用于判断是否展示我们自定义的菜单项。
假设我们现在打算让之前定义的 hello world 菜单项只在一个文件被打开的时候可用,那就可以重写 update
方法,判断编辑器是否被打开。
@Override public void update(@NotNull AnActionEvent e) { Editor editor = e.getData(PlatformDataKeys.EDITOR); e.getPresentation().setEnabled(editor != null); }
我们可以通过 update
的参数 AnActionEvent
去获取 IDEA 当前的一个状态, AnActionEvent
是插件与 IDEA 沟通的一个桥梁,我们所有的开发都基于这个 AnActionEvent
,后续会接着讲。OK,那我们可以通过 AnActionEvent 去获取当前的编辑器对象 Editor
,如果 Editor
没有被打开,则 Editor == null
,那我们可以调用 AnActionEvent
的 getPresentation
()方法,获取 Presentation
对象,该对象用于控制菜单项的外观设置。
Presentation 主要的几个方法
1. setIcon(@Nullable Icon icon) //设置图标 2. setText(String text) //设置菜单项展示文本 3. setDescription(String description) //设置菜单项功能描述 4. setEnabled(boolean enabled) //设置是否可用 5. setVisible(boolean visible) //设置是否可见 ...
当 editor 为空时, enable=false
,我们运行看一下效果。
发现菜单项就变成了灰色。
需要注意的是,虽然菜单项变成灰色,但是不代表不可以点击,点击时仍然会回调 actionPerformed
方法,这时需要在 actionPerformed
同样需要去判断Editor是否打开
前面讲到 AnActionEvent
是插件与 IDEA 沟通的桥梁,它记录了 IDEA 当前这个 Project
的所有状态。当菜单项被点击或打开菜单栏的时候,IDEA 就会通过回调函数将 AnActionEvent
传递到我们的插件中。
通过 AnActionEvent
的 getData
方法,我们可以获取到 IDEA 的定义的各种数据对象。这些数据对象的 key 被定义在 CommonDataKeys
和它的子类中,一般使用 PlatformDataKeys
就可以获取到常用的数据对象。
public static final DataKey<Project> PROJECT = DataKey.create("project"); public static final DataKey<Editor> EDITOR = DataKey.create("editor"); public static final DataKey<Editor> HOST_EDITOR = DataKey.create("host.editor"); public static final DataKey<Caret> CARET = DataKey.create("caret"); public static final DataKey<Editor> EDITOR_EVEN_IF_INACTIVE = DataKey.create("editor.even.if.inactive"); public static final DataKey<Navigatable> NAVIGATABLE = DataKey.create("Navigatable"); public static final DataKey<Navigatable[]> NAVIGATABLE_ARRAY = DataKey.create("NavigatableArray"); public static final DataKey<VirtualFile> VIRTUAL_FILE = DataKey.create("virtualFile"); public static final DataKey<VirtualFile[]> VIRTUAL_FILE_ARRAY = DataKey.create("virtualFileArray"); public static final DataKey<PsiElement> PSI_ELEMENT = DataKey.create("psi.Element"); public static final DataKey<PsiFile> PSI_FILE = DataKey.create("psi.File"); public static final DataKey<String> FILE_TEXT = DataKey.create("fileText");
举几个常用的例子
Project project = e.getData(PlatformDataKeys.PROJECT);
通过 Project 可以获取当前打开的工程名称、工程绝对路径、工程是否被初始化
Editor editor = e.getData(PlatformDataKeys.EDITOR);
通过 Editor 可以获取当前编辑器中的文档内容,光标位置、选中文字、编辑器设置等等。
PsiFile file = e.getData(PlatformDataKeys.PSI_FILE);
通过 PsiFile 可以获取到当前打开的文档类型、文档名称、是否只读、字符集等等。
可以发现通过 AnActionEvent
这个对象,可以获取我们想要的各种数据,还是非常方便的。
之前我们一直关注的是单个菜单项也就是单个 action
,但实际场景中,我们不可能把所有 Action 都放到一个菜单栏里。我们会对功能进行分组。在 IDEA 很多的功能是被汇集到一个组中,就像 New 菜单项点击后,会出现一个组,组里包含了N个菜单项。这个组就我们就称之为 Actions Group
现在我们尝试将我们的 hello world 菜单项也放到这样的组中。
通过在 plugin.xml
中的
<group id="com.mars.plugin.groupActions" text="hello world group" popup="true"> </group>
和 Action 一样,可以通过
<group id="com.mars.plugin.groupActions" text="hello world group" popup="true"> <add-to-group group-id="HelpMenu" anchor="first"/> </group>
将我们之前定义到的 action
直接添加到 group
中,就算是完成一个完整的 Actions Group
。
<group id="com.mars.plugin.groupActions" text="hello world group" popup="true"> <add-to-group group-id="HelpMenu" anchor="first"/> <action id="com.mars.plugin.HelloAction" class="com.mars.plugin.com.mars.plugin.HelloAction" text="show Hello world" description="show Hello world with popwindow"/> </group>
运行的效果如下
至此,我们了解了如何定义一个 Action
,也知道了如何获取 IDEA 当前运行的一些数据对象的状态,还知道了如何定义一个 Action Group
。在我们在日常的 IDEA 开发中,最常用的功能实际上就是编辑器,多半的时间都是在编辑器中敲代码,那接下来就介绍一下用于读取和处理文档的数据对象 Editor
。
对于想自动生成代码或者替换文档的插件来说,必然少不了和 Editor
打交道,下面介绍 Editor
中常用的几种对象
这个 SelectionModel
记录了光标选中的文本以及坐标信息。通过 Editor 对象可以获取到 SelectionModel
。
Editor editor = e.getData(PlatformDataKeys.EDITOR); SelectionModel selectionModel = editor.getSelectionModel();
经常使用的方法有
1. getSelectionStart(); //返回所选文本范围的文档中的起始偏移量 2. getSelectionEnd(); //返回所选文本范围的文档中的结尾便宜量 3. getSelectedText(); //返回光标选中的文本 4. hasSelection(); //检查当前是否选择了文本范围。 5. removeSelection(); // 移除选中 6. selectLineAtCaret(); //在插入符号位置选择整行文本。 7. addSelectionListener(@NotNull SelectionListener listener); //添加一个监听器监听选中文本变化 ....
CaretModel
用于记录当前光标所处位置,以及可以控制光标移动。同样通过 Editor
对象获取
Editor editor = e.getData(PlatformDataKeys.EDITOR); CaretModel caretModel = editor.getCaretModel();
常见方法有
1. moveToOffset(int offset); //移动光标到指定的offset位置 2. moveToLogicalPosition(@NotNull LogicalPosition pos); //移动光标到指定的逻辑位置,如果该位置在折叠区域中,则将扩展该区域。 3. moveToVisualPosition(@NotNull VisualPosition pos); //移动光标到是视觉位置 4. getOffset(); //返回文档中插入符号的偏移量。 5. getAllCarets(); //返回当前存在于文档中的所有光标,按它们在编辑器中的视觉位置排序。 ...
LogicalPosition
表示插入符号的当前逻辑位置的行和列。逻辑位置将会忽略折叠 。
例如,如果文档的前 10 行被折叠,则文档中的第 10 行的逻辑位置就是行号 10。
VisualPosition
表示视觉位置,与逻辑位置不同的是视觉位置会考虑折叠 。
例如,如果文档的前 10 行被折叠,则文档中的第 10 行将在其视觉位置的行号就是 1。
通过 SelectionModel
和 CaretModel
可以很方便的知道光标所选文本的偏移量或者光标所在的偏移量,那下一步就可以使用 Document
对象根据偏移量来对文档进行处理。 Document
表示加载到内存中并可以在 IDEA 文本编辑器中打开的文本文件的内容。同样也是通过 Editor
获取。
Editor editor = e.getData(PlatformDataKeys.EDITOR); Document document = editor.getDocument();
常用的方法有
1. getText(); //返回当前文档的文本副本 2. getTextLength(); //返回文本的长度 3. getLineCount(); //返回文档的行数 4. getLineNumber(int offset); //返回偏移量所处的行号 5. getLineStartOffset(int line); //返回具有指定编号的行的起始偏移量 6. insertString(int offset, @NotNull CharSequence s); //将指定的文本插入文档中的指定偏移处。 7. deleteString(int startOffset, int endOffset); //从文档中删除指定范围的文本。 8. replaceString(int startOffset, int endOffset, @NotNull CharSequence s); //用指定的字符串替换文档中指定的文本范围。 ...
需要注意的是,修改文本的函数,比如 insertString
、 deleteString
、 replaceString
等,都应该在 WriteCommandAction
中以异步方式执行。
@Override public void actionPerformed(AnActionEvent e) { Project project = e.getData(PlatformDataKeys.PROJECT); Editor editor = e.getData(PlatformDataKeys.EDITOR); Document document = editor.getDocument(); SelectionModel selectionModel = editor.getSelectionModel(); //获取光标选择的文本,并替换成"hello" WriteCommandAction.runWriteCommandAction(project, () -> document.replaceString(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd(), "hello")); }
上述代码获取了光标选取的文本,并在 WriteCommandAction
中将选择的文本更新成”hello“。
通过了解 Editor
的 SelectionModel
、 CaretModel
和 Document
对象,我们就获取了对文档进行文本操作的能力了。聪明的小朋友应该已经知道如何按照自己的想法去生成一些固定的代码了。
但是仅仅生成固定的代码,我们肯定不能满足。我们还想要自己能动态的获取到文档的信息,然后动态的输出不同的内容。比如一个 Pojo to json 的工具,我需要根据我定义的这个 Java 对象来输出一个 Json,如果完成这个功能,我们的插件需要知道什么?
1、当前这个对象里定义的所有成员
2、如果这个对象是继承自某个父类,那我还需要知道父类里定义的成员
如何解答上面的问题,这就得了解 IDEA 的程序结构接口,通常称为 PSI
,是IntelliJ Platform 中的一个层,负责解析文件并创建支持平台许多功能的语法和语义代码模型。通过 PSI
,我们可以解构 Java 的语法模型,获取有用的信息。
PSI(Program Structure Interface)
是 Intellij Platform 中一个非常重要的概念,在 IDE 所管理的 Project 中,每个目录,Package,源代码和资源文件都会被抽象成对应的 PSI
对象。每个 PSI 对象可以有自己的 parent PSI
对象,也可以有 child PSI
对象,各种PSI对象以树形结构进行排布,最终组合成一个PSI 树,如果你学过一些前端,其实很容易把这个和前端的视图树联系在一块,两者并无差距。
在 PSI 树中,所有的 PSI 对象都实现了 PsiElement
接口, PsiElement
是PSI 树的所有元素的通用基本接口。不同的 PsiElement 实现提供了不同的能力,介绍几个在插件开发中常见的 PsiElement
。
PsiFile 表示一个文件。
1. FileType getFileType(); //返回文件类型 2. PsiFile[] getPsiRoots(); //如果文件包含多种散布的语言,则返回每种语言的PSI树的根。(例如,一个JSPX文件包含JSP,XML和Java树) ....
PsiClass 表示 Java 类或接口。
1. PsiClass getSuperClass(); //返回此类的基类 2. PsiClass[] getInterfaces(); //返回此类实现的接口 3. PsiField[] getFields(); //返回类中的字段列表 4. PsiMethod[] getMethods(); //返回类中的方法列表 5. PsiField[] getAllFields(); //返回类及其所有超类中的字段列表。 ....
PsiMethod 表示 Java 方法或构造函数。
1. PsiType getReturnType(); //获取方法的返回类型 2. PsiParameterList getParameterList(); //获取方法的形参列表 3. boolean isConstructor(); //是否为构造器方法 4. PsiMethod[] findSuperMethods(); //查找所有超类中的方法实现。 ...
PsiDeclarationStatement 表示 Java 局部变量或类声明语句。
PsiElement[] getDeclaredElements(); //返回语句声明的元素
PsiLocalVariable 表示 Java 局部变量。
1. PsiType getType(); //返回变量的类型 ...
了解了 PsiElement
的各种实现,我们再看一下最常见的创建对象语句是如何体现 PSI 树的组织结构的。
PsiElement
提供了几种方法,可以在 PSI 树中导航元素
如果已知一个顶级元素,比如 PsiFile
,那如何查找到一个局部变量,最常用方法是使用 visitor
。
使用 visitor
的时候,你需要创建一个类 (通常是一个匿名内部类)扩展基本访问者类,覆盖处理你需要的元素的方法,并将访问者实例传递给 PsiElement.accept()
。
访问者的基类是特定于语言的。例如,如果你需要处理 Java 文件中的元素,则需要扩展 JavaRecursiveElementVisitor
并覆盖与你所使用的 Java 元素类型相对应的方法。
下段代码显示了使用 visitor
查找所有 Java 局部变量声明:
file.accept(new JavaRecursiveElementVisitor() { @Override public void visitLocalVariable(PsiLocalVariable variable) { super.visitLocalVariable(variable); System.out.println("Found a variable at offset " + variable.getTextRange().getStartOffset()); } });
除此之外,也可以使用 PsiElement
提供的 API 去导航下级元素。比如我持有一个 PsiClass
,就可以调用 PsiClass.getMethods()
获取方法列表。IDEA 还提供了 PsiTreeUtil
工具类,其中包含多个了用于 PSI 树导航的通用方法,(例如, findChildrenOfType
)
如果持有的是一个低级元素,如何查找到他的顶级元素呢?
在 IDEA 中如果已获知偏移量,则可以通过调用找到相应的 PSI 元素 PsiFile.findElementAt()
,此方法返回树的最低级别的元素(例如,标识符)。如果要确定元素的顶级元素,则可以通过调用 PsiTreeUtil.getParentOfType()
来执行自下而上的导航,这个方法会一直向上查找,直到找到你指定的类型的元素。例如,要查找包含方法,可以调用 PsiTreeUtil.getParentOfType(element,PsiMethod.class)。
你还可以使用 PsiElement 提供的特定的导航方法。例如,要查找包含方法的类,可以使用 PsiMethod.getContainingClass()
。
以下代码段显示了如何一起使用这些调用:
PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE); PsiElement element = psiFile.findElementAt(offset); PsiMethod containingMethod = PsiTreeUtil.getParentOfType(element, PsiMethod.class); PsiClass containingClass = containingMethod.getContainingClass();
Psi 元素也不是一成不变的,也是可以进行人为的替换、删除、添加。要执行这些操作,可以使用诸如 PsiElement.replace()
、 PsiElement.delete()
、和 PsiElement.add()
等方法, 以及在 PsiElement
接口中定义的其他方法,它允许你在一个单元中处理多个元素操作,或指定树中需要添加元素的确切位置。就像文档操作一样,PSI 修改需要包含在写入操作和命令中(因此只能在事件派发线程中执行)。
通常添加到树中或替换现有 PSI 元素的 PSI 元素通常是从 text
中创建的。通常情况下,你可以使用的 createFileFromText()
方法 PsiFileFactory
来创建一个新文件,该文件包含需要添加到树中或用作替换的代码构造,然后去 PSI树中查询需要操作的元素,然后调用元素的 add()
或 replace()
进行添加或者替换。
如果你想创建一段 Java 代码而非普通的文本文件,那大多数语言都提供了工厂方法,使你可以更轻松地创建符合语法的特定代码。例如, PsiJavaParserFacade
该类包含诸如的方法 createMethodFromText()
,该方法可以根据给定的文本创建 Java 方法。
这一章我们认识了什么是 PSI、常见的 PSI 元素、PSI 树、PSI 的导航和 PSI 的修改。认识 PSI 后,我们就可以很轻松的回答之前提出的问题,如何去获取到一个类的成员以及父类的成员呢?答案就是调用 PsiClass.getAllFields
方法,是不是很简单呢?
至此,我们对 IDEA 提供的 SDK 已经有了一个较为深刻的了解,下一步,就是运用我们所学知识,融会贯通,打造一个属于我们自己的插件啦~
全文完
以下文章您可能也会感兴趣:
后端的缓存系统浅谈
从 React 到 Preact 迁移指南
如何成为一名数据分析师:数据的初步认知
复杂业务状态的处理:从状态模式到 FSM
聊聊移动端跨平台数据库 Realm
苹果在医疗健康领域的三个 Kit
响应式编程(下):Spring 5
响应式编程(上):总览
Web 与 App 数据交互原理和实现
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。