震惊!!!编码速度提高10倍的秘诀是....

作者 | 袁华健

handsome code, handsome coder.

前言

初级篇介绍了开发插件必要的环境、编写简单的代码、运行以及打包发布,但是我们当想实现一些高级功能,比如去自动生成代码等功能,就得进一步学习 Intellij IDEA 提供的 sdk 用法。

1、Action

在 Action 中最主要的是两个方法 ,actionPerformed方法和update 方法,在本文中,为了方便理解,我们称呼一个Action为一个菜单项。

1.1 覆盖 actionPerformed 方法

当我们定义的菜单项被点击的时候,actionPerformed方法就会被回调,在初级篇中,我们让插件被点击的时候弹出了一个系统框。

@Override
public void actionPerformed(AnActionEvent e) {
        Project project = e.getData(PlatformDataKeys.PROJECT);
        Messages.showMessageDialog(project, "say hello world ~", "Info",            Messages.getInformationIcon());
}
1.2 覆盖 update 方法

当我们需要自定义的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,那我们可以调用AnActionEventgetPresentation()方法,获取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是否打开

1.3 AnActionEvent

前面讲到AnActionEvent是插件与 IDEA 沟通的桥梁,它记录了 IDEA 当前这个Project的所有状态。当菜单项被点击或打开菜单栏的时候,IDEA 就会通过回调函数将AnActionEvent传递到我们的插件中。

通过AnActionEventgetData方法,我们可以获取到 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这个对象,可以获取我们想要的各种数据,还是非常方便的。

1.4 Actions Group

之前我们一直关注的是单个菜单项也就是单个action,但实际场景中,我们不可能把所有 Action 都放到一个菜单栏里。我们会对功能进行分组。在 IDEA 很多的功能是被汇集到一个组中,就像 New 菜单项点击后,会出现一个组,组里包含了N个菜单项。这个组就我们就称之为 Actions Group

现在我们尝试将我们的 hello world 菜单项也放到这样的组中。

1.4.1 创建简单的组

通过在 plugin.xml 中的部分中添加 元素来完成分组, 元素中不需要 class 属性,但是需要一个唯一的 id。

<group id="com.mars.plugin.groupActions" text="hello world group" popup="true">
</group>
1.4.2 绑定到菜单栏中

和 Action 一样,可以通过 元素绑定到某一个菜单栏中,这里我们依然绑定到 Help菜单栏,并且位于第一位。

<group id="com.mars.plugin.groupActions" text="hello world group" popup="true">
  <add-to-group group-id="HelpMenu" anchor="first"/>
</group>
1.4.3 添加 Action 到组中

将我们之前定义到的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>

运行的效果如下

1.5 小结

至此,我们了解了如何定义一个Action,也知道了如何获取 IDEA 当前运行的一些数据对象的状态,还知道了如何定义一个Action Group。在我们在日常的 IDEA 开发中,最常用的功能实际上就是编辑器,多半的时间都是在编辑器中敲代码,那接下来就介绍一下用于读取和处理文档的数据对象 Editor

2、Editor

对于想自动生成代码或者替换文档的插件来说,必然少不了和Editor打交道,下面介绍Editor中常用的几种对象

2.1 SelectionModel

这个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); //添加一个监听器监听选中文本变化
....
2.2 CaretModel

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(); //返回当前存在于文档中的所有光标,按它们在编辑器中的视觉位置排序。
...
2.2.1 LogicalPosition 与 VisualPosition

LogicalPosition表示插入符号的当前逻辑位置的行和列。逻辑位置将会忽略折叠 。

例如,如果文档的前 10 行被折叠,则文档中的第 10 行的逻辑位置就是行号 10。

VisualPosition表示视觉位置,与逻辑位置不同的是视觉位置会考虑折叠 。

例如,如果文档的前 10 行被折叠,则文档中的第 10 行将在其视觉位置的行号就是 1。

2.3 Document

通过SelectionModelCaretModel可以很方便的知道光标所选文本的偏移量或者光标所在的偏移量,那下一步就可以使用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); //用指定的字符串替换文档中指定的文本范围。
...

需要注意的是,修改文本的函数,比如insertStringdeleteStringreplaceString等,都应该在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“。

2.4 小结

通过了解EditorSelectionModelCaretModelDocument对象,我们就获取了对文档进行文本操作的能力了。聪明的小朋友应该已经知道如何按照自己的想法去生成一些固定的代码了。

但是仅仅生成固定的代码,我们肯定不能满足。我们还想要自己能动态的获取到文档的信息,然后动态的输出不同的内容。比如一个 Pojo to json 的工具,我需要根据我定义的这个 Java 对象来输出一个 Json,如果完成这个功能,我们的插件需要知道什么?

1、当前这个对象里定义的所有成员

2、如果这个对象是继承自某个父类,那我还需要知道父类里定义的成员

如何解答上面的问题,这就得了解 IDEA 的程序结构接口,通常称为PSI,是IntelliJ Platform 中的一个层,负责解析文件并创建支持平台许多功能的语法和语义代码模型。通过PSI,我们可以解构 Java 的语法模型,获取有用的信息。

3、PSI

3.1 认识 PSI

PSI(Program Structure Interface)是 Intellij Platform 中一个非常重要的概念,在 IDE 所管理的 Project 中,每个目录,Package,源代码和资源文件都会被抽象成对应的PSI对象。每个 PSI 对象可以有自己的parent PSI对象,也可以有child PSI对象,各种PSI对象以树形结构进行排布,最终组合成一个PSI 树,如果你学过一些前端,其实很容易把这个和前端的视图树联系在一块,两者并无差距。

3.1.1 常用的 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 树的组织结构的。

3.2 导航 PSI

PsiElement提供了几种方法,可以在 PSI 树中导航元素

3.2.1 自上而下

如果已知一个顶级元素,比如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)

3.2.2 自下而上

如果持有的是一个低级元素,如何查找到他的顶级元素呢?

在 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();
3.3 修改 PSI

Psi 元素也不是一成不变的,也是可以进行人为的替换、删除、添加。要执行这些操作,可以使用诸如PsiElement.replace()PsiElement.delete()、和PsiElement.add()等方法, 以及在PsiElement接口中定义的其他方法,它允许你在一个单元中处理多个元素操作,或指定树中需要添加元素的确切位置。就像文档操作一样,PSI 修改需要包含在写入操作和命令中(因此只能在事件派发线程中执行)。

3.3.1创建新的PSI

通常添加到树中或替换现有 PSI 元素的 PSI 元素通常是从text中创建的。通常情况下,你可以使用的createFileFromText()方法PsiFileFactory 来创建一个新文件,该文件包含需要添加到树中或用作替换的代码构造,然后去 PSI树中查询需要操作的元素,然后调用元素的add()replace()进行添加或者替换。

如果你想创建一段 Java 代码而非普通的文本文件,那大多数语言都提供了工厂方法,使你可以更轻松地创建符合语法的特定代码。例如,PsiJavaParserFacade该类包含诸如的方法createMethodFromText(),该方法可以根据给定的文本创建 Java 方法。

3.4 小结

这一章我们认识了什么是 PSI、常见的 PSI 元素、PSI 树、PSI 的导航和 PSI 的修改。认识 PSI 后,我们就可以很轻松的回答之前提出的问题,如何去获取到一个类的成员以及父类的成员呢?答案就是调用PsiClass.getAllFields方法,是不是很简单呢?

结束语

至此,我们对 IDEA 提供的 SDK 已经有了一个较为深刻的了解,下一步,就是运用我们所学知识,融会贯通,打造一个属于我们自己的插件啦~


全文完


以下文章您可能也会感兴趣:

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值