@Overridepublic 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,那我们可以调用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是否打开
1.3 AnActionEvent前面讲到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这个工具,可以获取我们想要的各种数据,还是非常方便的。
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通过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“。
2.4 小结通过理解Editor的SelectionModel、CaretModel和Document工具,我们就获取了对文档进行文本操作的能力了。聪明的小朋友该当已经知道如何按照自己的想法去天生一些固定的代码了。
但是仅仅天生固定的代码,我们肯定不能知足。我们还想要自己能动态的获取到文档的信息,然后动态的输出不同的内容。比如一个 Pojo to json 的工具,我须要根据我定义的这个 Java 工具来输出一个 Json,如果完成这个功能,我们的插件须要知道什么?
1、当前这个工具里定义的所有成员
2、如果这个工具是继续自某个父类,那我还须要知道父类里定义的成员
如何解答上面的问题,这就得理解 IDEA 的程序构造接口,常日称为PSI,是IntelliJ Platform 中的一个层,卖力解析文件并创建支持平台许多功能的语法和语义代码模型。通过PSI,我们可以解构 Java 的语法模型,获取有用的信息。
3、PSI3.1 认识 PSIPSI(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 已经有了一个较为深刻的理解,下一步,便是利用我们所学知识,交融贯通,打造一个属于我们自己的插件啦~