从0到1写一款自动为Markdown标题添加序号的Jetbrains插件

1. markdown-index

最近做了一个Jetbrains的插件,叫markdown-index,它的作用是为Markdown文档的标题自动添加序号,效果如下:

markdown-index使用效果

目前已经可以在Jetbrains全家桶的插件市场中搜索到。

插件已上架

2. 为什么我要做这个插件

我习惯用Markdown写完文章之后给文章标题添加上序号,这样读者阅读起来会更清晰,像这样:

序号让阅读更清晰

之前我都是用Typora写完文章之后,把文章复制到VSCode中,然后使用VSCode中的markdown-index插件给文章标题自动添加序号,然后再复制文章内容进行分发。

本来可以一直沿用这个方式,可是在我最近使用VuePress搭建了个人博客之后,在博客写作这个方向上我慢慢偏向了WebStorm,原因有3个:

  1. 在本地调试的时候我更喜欢一键启动,而不需要每次打开Terminal输入npm run docs:dev命令;

  2. 我配置了git push之后的网站自动部署流,由于平时开发用惯了IDEA,因此WebStorm的git用户界面让我感觉更亲切;

  3. VSCode的markdown-index插件使用尽管已经很方便了,但是还是稍微有点繁琐,因为必须先Command+Shift+p调出command palette,然后选择markdown-index功能。我想直接鼠标右键直接选择markdown-index功能。

综合上面3点原因,我参考了VSCode的markdown-index插件,查阅文档,花了一晚上写了Jetbrains全家桶的markdown-index插件。

下面给大家介绍一下插件从0到1的编写流程以及在查阅官方文档时的一些心得体会。

3. 插件开发前奏

一开始图省事儿,想直接根据网友的插件开发经验来做,但发现要么资料过时,要么是跟着做了不成功,最后索性直接找官方文档了。

因此这个小插件90%的时间都花在了阅读官方文档上了。

3.1. 官方文档

我们一开始肯定不知道官方文档的地址,想直接从Jetbrains门户网站找到插件开发的官方文档也很浪费时间。我提供两种方案:

  1. 使用百度搜索,搜索「Jetbrains插件开发」之类的关键词,找到网友之前分享的开发博客,一般写的详细的博客(可能需要多找几篇)会给出官方地址,然后,抛弃这篇文章,投入官方文档的怀抱吧。

  2. 使用Google搜索,搜索英文关键词,比如「jetbrains plugin development」,一般第一条就是我们要找的结果,这也是我采取的方法(不得不感叹一句,Google搜索英文资料真的是好~)。

现在官方网站就到手了://plugins.jetbrains.com/docs/intellij/getting-started.html

官方文档一般情况下写得都非常详细,尤其是掺杂着各种超链接。大家在读官方文档的时候如果不是十分清楚超链接的含义,尽量不要点,否则跳来跳去很容易把心态搞崩。

3.2. 开发插件的3种方式

官方说明了开发插件的三种方式,分别是:

  1. 使用官方发布在GitHub上的插件模板(Using GitHub Template)

  2. 使用Gradle(Using Gradle)

  3. 使用DevKit(Using DevKit)

我选择的是第一种,原因是我之前从来没有接触过Jetbrains插件的开发,如果从白板开始写起的话太麻烦了,使用官方提供的模板进行填空是最快的方式。

3.3. 使用IntelliJ Platform Plugin Template

官方的模板仓库的地址://github.com/JetBrains/intellij-platform-plugin-template

官方解释说这个仓库预设了项目的脚手架和CI流程,干净又卫生!不管是新手还是老手,都能加快插件开发流程。

需要做的就是三个步骤:

  1. 登陆你的GitHub账号
  2. 点击仓库的Use this template按钮

use-this-template

  1. 用你的IDEA打开它

然后我们下来的参考文档就是这个仓库的README说明了。

3.4. 项目大致结构

项目结构

首先给大家介绍一下项目结构:

  • .github

里面配置了GitHub Actions的工作流,具体来说就是我们自动将插件提交到GitHub之后,GitHub会根据这个工作流为我们自动做一些我们配置的事情,比如安装依赖,比如发布到Jetbrain官方插件库等,默认不需要更改。

  • .run

预设了一些Gradle的配置,使得我们可以在IDEA中直接鼠标点击执行指令,看下面这个图就懂了.

没用过Gradle也没事儿,不影响我们写核心逻辑

  • build

存放编译之后的文件

  • src

我们的核心代码位置

  • 其他

其余都是Gradle的配置文件和其他工具的配置文件,暂时不需要理会,需要的时候再说。

由于项目默认使用Kotlin,我不习惯,我换成了Java,方法很简单,在src/main下面新建java目录,把kotlin的所有目录移动到java目录即可,删掉目录下的Kotlin源文件,src/test同理。

3.5. plugin配置文件

还有一个文件需要单独拿出来说一下,位于src/main/resources/META-INF目录下的plugin.xml文件。

插件的extensionsactions以及listeners都在该文件中进行配置。

这些东西都是个啥先不用管,就是个配置而已,能难到哪去。之后敲代码的时候就知道了,先混个眼熟吧。

<idea-plugin>
  <id>org.jetbrains.plugins.template</id>
  <name>Template</name>
  <vendor>JetBrains</vendor>
  <depends>com.intellij.modules.platform</depends>

  <extensions defaultExtensionNs="com.intellij">
    <applicationService serviceImplementation="..."/>
    <projectService serviceImplementation="..."/>
  </extensions>

  <projectListeners>
    <listener class="..." topic="..."/>
  </projectListeners>
</idea-plugin>

而且,README文档里也说了,更多的详细配置可以查看配置文档,链接为:

//plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html?from=IJPluginTemplate。

4. 插件开发过程

4.1. 参考示例代码

现在我们对这个模板项目已经有了直观的感觉了,下面开始写代码了,是不是脑子里还是空空如也,因为有几件事情我们目前压根不知道。

我应该在哪个目录下写Java代码?写完之后怎么调用?调用完了之后怎么和IDEA联动?联动肯定需要知道IDEA提供的api,去哪儿找?

我当时想的就是这几个问题,所以我的第一反应是:作为一个成熟的软件开发商,应该会提供实例代码给我们,我们就能参考了。

于是接着读README,还真就给出了一个示例代码仓库,地址为:

//github.com/JetBrains/intellij-sdk-code-samples

进入一看,示例太多了。。。。于是根据我的需求,我就找了一个名字最相关的,看起来也最简单的项目——editor_basics

这其实是一个试错的过程,建议一开始看个简单的示例,不需要看懂实际代码,我们的目的是要从例子中找到我们下一步需要了解的概念。

研究了一小会儿之后,我发现我需要了解2个概念。

4.2. Actions

Actions中文的意思是“动作”,举几个例子:

  1. 菜单中的File | Open File…按钮,点击后触发打开本文文件资源管理器的动作;
  2. 鼠标右键菜单中Paste按钮点击之后触发粘贴的动作;
  3. Command + C快捷操作之后,触发复制的动作;

这3个例子说明了几点重要细节,首先Action可以出现在IDE的不同地方,至于出现在哪里,取决于你的注册过程;Action可以有不同的行为,具体的行为是什么取决于你的实现;最后不管是鼠标点击还是快捷键组合都能触发Action

4.2.1. 注册

src目录下,创建一个actions目录(其实创不创建不重要,但是我喜欢这种清晰的组织方式),目录上鼠标右键,选择右键菜单中的New | Plugin Devkit | Action(如果你鼠标右键没有这个按钮,那就安装一个Plugin DevKit这个插件),进入New Action界面。

New Action操作步骤

New Action界面

需要注意的是

  • Name:就是在菜单中实际显示的名称
  • Anchor:菜单中显示的次序,First指排在第一位,Last指排在最后一位

4.2.2. Actions Groups

Action默认是按照Group进行组织的,选择某个Group就意味着要把你的Action放在XX菜单中或者XX工具栏上,这里我选择的是EditorPopupMenu,意思就是编辑器上的右键弹出菜单

我是怎么找到的呢?

因为我的功能需求比较简单,我看了一下Group的大致命名方式,我就尝试性的搜索了一下PopupMenu,由于针对的是编辑器,于是最后找到了EditorPopupMenu,多少有点运气成分,如果各位读者的需求更独特的话就需要多试几次或者阅读官方文档喽。

4.2.3. 实现Action

填写完New Action表单之后,再看一下plugin.xml文件,会发现多了一个配置:

并且actions目录下多了一个PopAction的源文件,在actionPerformed中需要我们写的就是Action的实现。

package com.github.chanmufeng.tesplugin.actions;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;

public class PopAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        // TODO: insert action logic here
    }
}

4.2.4. 测试一下插件

先不着急实现,我们先试一下Action注册的效果。选择Run Plugin命令,点击运行

8

此时你会看到又弹出了一个IDEA!

没错,这个就是插件的测试环境,使用方法和正常的IDEA没有任何区别,只不过这个环境下默认安装了我们刚才编写的插件。

接下来新建或者打开一个已有项目,点击一下鼠标右键看一下「markdown-index」这个Action是否注册成功。

4.2.5. 实现Action

接下来做的就是实现actionPerformed(AnActionEvent e)方法,毫无疑问,我们所需的一切数据都是从e这个对象中获取了。

目标非常的清晰:

  1. e中获取到当前文件的所有行数据
  2. 根据行前的#数量递归添加标号
  3. 用添加标号之后的文本替换掉原来的文本

那调用的API不知道啊,怎么办?我的办法就是利用IDEA出色的提示功能以及源码的注释。

比如我想获取当前所在的文件,那我肯定会先敲e.get,然后等着提示:

image-20220827161401837

我发现第一个就很像,我就选了,可是让我传DataKey类型的参数,我不知道该怎么传,我就点进去,看看注释,发现了新大陆:

继续往下推,就获得了所有我想获得的对象,如果这招对你行不通,那就去看官方文档或者上文提到的示例代码,肯定有一个适合你。

插件的核心功能到此为止其实已经结束了,但是我当时又稍微折腾了一下。

4.3. Services

有代码洁癖的人肯定受不了把所有代码写在一个方法里,至少封装一下方法吧。还记得一开始项目模板为我们提供了一个services目录吗,我当时就猜测这个目录就是专门放我们编写的服务的,对于大型插件来说这是必须的。于是我又简单翻了一下官方文档。

发现我真是个小天才!Services确实是干这个的,而且跟Spring Bean的使用方法非常类似。

4.3.1. 分类

Services分类如下:

  • 重量级Service
    • application-level services(Application级别的Service)
    • project-level services(Project级别的Service)
    • module-level services(Module级别的Service,在多模块项目下不建议使用)
  • 轻量级Service

先说说重量级Service,分成了三个级别,目的是为了控制不同粒度下的数据权限。

Application级别的Service全局只有一个访问点,也就是说IDEA不管打开几个项目,Service的实例对象只有一个。

Project级别的Service在每个项目下只有一个访问点,如果IDEA打开了3个项目,就会生成3个实例。

Module级别的Service在每个模块下都会有一个访问点。

4.3.2. 重量级Service的使用场景

重量级Service适合比较规整的项目,比如严格定义XXServiceInterface并且有一个或多个实现类XXXServiceImplementation

重量级Service必须在plugin.xml中进行注册,在xml标签中直接定义Service的作用范围,如下:

<extensions defaultExtensionNs="com.intellij">
  <!-- Declare the application-level service -->
  <applicationService
      serviceInterface="mypackage.MyApplicationService"
      serviceImplementation="mypackage.MyApplicationServiceImpl"/>

  <!-- Declare the project-level service -->
  <projectService
      serviceInterface="mypackage.MyProjectService"
      serviceImplementation="mypackage.MyProjectServiceImpl"/>
</extensions>

4.3.3. 轻量级Service的使用场景

没那么多苛刻条件,不需要继承关系,就比如我这个插件,我只是想让某些方法抽离出来而已,没必要搞的继承这么复杂。因此我选用的也是该类Service。

轻量级Service不需要在plugin.xml文件中注册,但是该类Service必须被final修饰,并在类头部添加@Service注解。举个例子:

@Service
public final class ProjectService {

  private final Project myProject;

  public ProjectService(Project project) {
    myProject = project;
  }

  public void someServiceMethod(String parameter) {
    AnotherService anotherService = myProject.getService(AnotherService.class);
    String result = anotherService.anotherServiceMethod(parameter, false);
    // do some more stuff
  }
}

4.3.4. 如何获取Service实例

重量级Service就不说了。有需要的朋友直接看文档,非常清晰。

//plugins.jetbrains.com/docs/intellij/plugin-services.html#retrieving-a-service

轻量级Service直接用本插件的代码来做演示:

 // 获取自己编写的MarkdownIndexService
 MarkdownIndexService markdownIndexService =
   ApplicationManager.getApplication().getService(MarkdownIndexService.class);

轻量级Service实例的生命周期范围和调用者保持一致,以上面为例,我用的getApplication().getService,那么MarkdownIndexService的作用范围就是Application

5. Listeners

简单提一句Linsteners,在这个插件里没有使用到,从名字上很好理解,就是监听器,想想就知道肯定有个回调函数,你可以在其中捕获到某些IDEA的操作行为,然后添加自己的逻辑。

是不是很简单?

6. 插件发布

插件写完了,接下来我们发布到plugin repository,让更多的人看到我们的插件。

6.1. 修改插件图标

使用你钟意的图标替换掉src/main/resources/META-INF目录下的pluginIcon.svg文件即可。

6.2. 发布插件

首先你需要登陆Jetbrains账号,如果没有的话就注册一个吧,注册地址给上。

//plugins.jetbrains.com/author/me

然后在右上角点击账号名称,选择Upload plugin,最后上传你的插件jar包,并填写表单即可。

7. 源码分享


完~