Contents

使用Gradle编写IntelliJ 插件

1. 简介

在过去几年中, 来自 JetBrains 的IntelliJ  迅速成为 Java 开发人员的顶级 IDE。在我们最近的 Java 现状报告 中,IntelliJ 是 61% 受访者的首选 IDE,高于一年前的 55%。

使 IntelliJ 如此吸引 Java 开发人员的一个特性是能够使用插件扩展和创建新功能。

在本教程中,我们将着眼于使用 Gradle 的新推荐方式编写 IntelliJ 插件,以演示我们可以扩展 IDE 的几种方式。本文是前一篇文章 的重新组合,描述了使用 Plugin Devkit 创建相同的插件。

2. 插件的主要类型

最常见的插件类型包括以下功能:

  • 自定义语言支持:编写、解释和编译用不同语言编写的代码的能力
  • 框架集成:支持Spring等第三方框架
  • 工具集成:与Gradle等外部工具集成
  • 用户界面附加组件:新菜单项、工具窗口、进度条等

插件通常会分为多个类别。例如,  IntelliJ 附带的Git 插件 与系统上安装的git可执行文件进行交互。该插件提供其工具窗口和弹出菜单项,同时还集成到项目创建工作流程、首选项窗口等中。

3.创建一个插件

有两种支持的创建插件的方法。我们将对Gradle 的新项目使用推荐的方式,而不是使用他们的Plugin Devkit

创建基于 Gradle 的插件是通过使用New > Project菜单完成的。

/uploads/intellij_idea_plugins_gradle/1.png

请注意,我们必须包含 Java 和 IntelliJ 平台插件,以确保所需的插件类在类路径中可用。

在撰写本文时, 我们只能使用 JDK 8 来编写 IntelliJ 插件

4. 示例插件

我们将创建一个插件,用于从 IDE 的多个区域快速访问流行的 Stack Overflow 网站。它将包括:

  • 用于访问“提问”页面的“工具”菜单项
  • 文本编辑器和控制台输出中的弹出菜单项,用于在 Stack Overflow 中搜索突出显示的文本

4.1. 创建动作

操作是访问插件的最常见方式。操作由 IDE 中的事件触发,例如单击菜单项或工具栏按钮。

创建动作的第一步是创建一个扩展 AnAction的 Java 类。对于我们的 Stack Overflow 插件,我们将创建两个操作。

第一个操作在新的浏览器窗口中打开提问页面:

public class AskQuestionAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent e) {
        BrowserUtil.browse("https://stackoverflow.com/questions/ask");
    }
}

我们使用内置的 BrowserUtil类来处理在不同操作系统和浏览器上打开网页的所有细微差别。

我们需要两个参数来在 StackOverflow 上执行搜索:语言标签和要搜索的文本。

要获取语言标记,我们将使用 程序结构接口 (PSI)。此 API 解析项目中的所有文件,并提供一种编程方式来检查它们。

在这种情况下,我们使用 PSI 来确定文件的编程语言:

Optional<PsiFile> psiFile = Optional.ofNullable(e.getData(LangDataKeys.PSI_FILE));
String languageTag = psiFile.map(PsiFile::getLanguage)
  .map(Language::getDisplayName)
  .map(String::toLowerCase)
  .map(lang -> "[" + lang + "]")
  .orElse("");

为了获取要搜索的文本,我们将使用 Editor  API 来检索屏幕上突出显示的文本:

Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
CaretModel caretModel = editor.getCaretModel();
String selectedText = caretModel.getCurrentCaret().getSelectedText();

尽管此操作对于编辑器窗口和控制台窗口都是相同的,但访问所选文本的方式是相同的。

现在,我们可以将所有这些放在一个 actionPerformed 声明中:

@Override
public void actionPerformed(@NotNull AnActionEvent e) {
    Optional<PsiFile> psiFile = Optional.ofNullable(e.getData(LangDataKeys.PSI_FILE));
    String languageTag = psiFile.map(PsiFile::getLanguage)
      .map(Language::getDisplayName)
      .map(String::toLowerCase)
      .map(lang -> "[" + lang + "]")
      .orElse("");
    Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
    CaretModel caretModel = editor.getCaretModel();
    String selectedText = caretModel.getCurrentCaret().getSelectedText();
    BrowserUtil.browse("https://stackoverflow.com/search?q=" + languageTag + selectedText);
}

此操作还覆盖了名为update的第二个方法 ,它允许我们在不同条件下启用或禁用该操作。在这种情况下,如果没有选定的文本,我们将禁用搜索操作:

Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
CaretModel caretModel = editor.getCaretModel();
e.getPresentation().setEnabledAndVisible(caretModel.getCurrentCaret().hasSelection());

4.2. 注册动作

编写完操作后, 我们需要将它们注册到 IDE 中。有两种方法可以做到这一点。

第一种方法是使用 plugin.xml 文件,它是在我们开始一个新项目时为我们创建的。

默认情况下,该文件将有一个空的<actions>元素,这是我们将添加我们的动作的地方:

<actions>
    <action
      id="StackOverflow.AskQuestion.ToolsMenu"
      class="com.blogdemo.intellij.stackoverflowplugin.AskQuestionAction"
      text="Ask Question on Stack Overflow"
      description="Ask a Question on Stack Overflow">
        <add-to-group group-id="ToolsMenu" anchor="last"/>
    </action>
    <action
      id="StackOverflow.Search.Editor"
      class="com.blogdemo.intellij.stackoverflowplugin.SearchAction"
      text="Search on Stack Overflow"
      description="Search on Stack Overflow">
        <add-to-group group-id="EditorPopupMenu" anchor="last"/>
    </action>
    <action
      id="StackOverflow.Search.Console"
      class="com.blogdemo.intellij.stackoverflowplugin.SearchAction"
      text="Search on Stack Overflow"
      description="Search on Stack Overflow">
        <add-to-group group-id="ConsoleEditorPopupMenu" anchor="last"/>
    </action>
</actions>

使用 XML 文件注册操作将确保它们在 IDE 启动期间注册,这通常是更可取的。

第二种注册动作的方法是以编程方式使用 ActionManager 类:

ActionManager.getInstance().registerAction("StackOverflow.SearchAction", new SearchAction());

这具有让我们动态注册操作的优势。例如,如果我们编写一个插件来与远程 API 集成,我们可能希望根据调用的 API 版本注册一组不同的操作。

这种方法的缺点是动作不会在启动时注册。我们必须创建一个 ApplicationComponent的实例 来管理操作,这需要更多的编码和 XML 配置。

5. 测试插件

与任何程序一样,编写 IntelliJ 插件需要测试。对于像我们编写的插件这样的小插件,确保插件编译并且我们创建的操作在我们单击它们时按预期工作就足够了。

我们可以通过打开 Gradle 工具窗口并执行runIde任务来手动测试(和调试)我们的插件:

/uploads/intellij_idea_plugins_gradle/3.png

这将启动一个新的 IntelliJ 实例,并激活我们的插件。这样做可以让我们点击我们创建的不同菜单项,并确保打开正确的 Stack Overflow 页面。

如果我们希望进行更传统的单元测试,IntelliJ 提供了一个环境  来运行单元测试。我们可以使用我们想要的任何测试框架编写测试,并且测试使用来自 IDE 的真实的、未模拟的组件运行。

6. 部署插件

Gradle Plugin 提供了一种打包插件的简单方法,因此我们可以安装和分发它们。只需打开 Gradle 工具窗口并执行buildPlugin任务。这将在build/distributions目录中生成一个 ZIP 文件。

生成的 ZIP 文件包含加载到 IntelliJ 中所需的代码和配置文件。我们可以安装在本地,也可以发布到插件仓库  供其他人使用。

下面的屏幕截图显示了正在运行的新 Stack Overflow 菜单项之一:

/uploads/intellij_idea_plugins_gradle/5.png