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

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

2. 为什么我要做这个插件
我习惯用Markdown写完文章之后给文章标题添加上序号,这样读者阅读起来会更清晰,像这样:

之前我都是用Typora写完文章之后,把文章复制到VSCode中,然后使用VSCode中的markdown-index插件给文章标题自动添加序号,然后再复制文章内容进行分发。
本来可以一直沿用这个方式,可是在我最近使用VuePress搭建了个人博客之后,在博客写作这个方向上我慢慢偏向了WebStorm,原因有3个:
在本地调试的时候我更喜欢一键启动,而不需要每次打开Terminal输入
npm run docs:dev命令;
我配置了
git push之后的网站自动部署流,由于平时开发用惯了IDEA,因此WebStorm的git用户界面让我感觉更亲切;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门户网站找到插件开发的官方文档也很浪费时间。我提供两种方案:
使用百度搜索,搜索「Jetbrains插件开发」之类的关键词,找到网友之前分享的开发博客,一般写的详细的博客(可能需要多找几篇)会给出官方地址,然后,抛弃这篇文章,投入官方文档的怀抱吧。
使用Google搜索,搜索英文关键词,比如「jetbrains plugin development」,一般第一条就是我们要找的结果,这也是我采取的方法(不得不感叹一句,Google搜索英文资料真的是好~)。

现在官方网站就到手了:https://plugins.jetbrains.com/docs/intellij/getting-started.html
官方文档一般情况下写得都非常详细,尤其是掺杂着各种超链接。大家在读官方文档的时候如果不是十分清楚超链接的含义,尽量不要点,否则跳来跳去很容易把心态搞崩。
3.2. 开发插件的3种方式
官方说明了开发插件的三种方式,分别是:
使用官方发布在GitHub上的插件模板(Using GitHub Template)
使用Gradle(Using Gradle)
使用DevKit(Using DevKit)
我选择的是第一种,原因是我之前从来没有接触过Jetbrains插件的开发,如果从白板开始写起的话太麻烦了,使用官方提供的模板进行填空是最快的方式。
3.3. 使用IntelliJ Platform Plugin Template
官方的模板仓库的地址:https://github.com/JetBrains/intellij-platform-plugin-template
官方解释说这个仓库预设了项目的脚手架和CI流程,干净又卫生!不管是新手还是老手,都能加快插件开发流程。
需要做的就是三个步骤:
- 登陆你的GitHub账号
- 点击仓库的
Use this template按钮

- 用你的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文件。
插件的extensions、actions以及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文档里也说了,更多的详细配置可以查看配置文档,链接为:
https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html?from=IJPluginTemplate。
4. 插件开发过程
4.1. 参考示例代码
现在我们对这个模板项目已经有了直观的感觉了,下面开始写代码了,是不是脑子里还是空空如也,因为有几件事情我们目前压根不知道。
我应该在哪个目录下写Java代码?写完之后怎么调用?调用完了之后怎么和IDEA联动?联动肯定需要知道IDEA提供的api,去哪儿找?
我当时想的就是这几个问题,所以我的第一反应是:作为一个成熟的软件开发商,应该会提供实例代码给我们,我们就能抄参考了。
于是接着读README,还真就给出了一个示例代码仓库,地址为:
https://github.com/JetBrains/intellij-sdk-code-samples
进入一看,示例太多了。。。。于是根据我的需求,我就找了一个名字最相关的,看起来也最简单的项目——editor_basics。

这其实是一个试错的过程,建议一开始看个简单的示例,不需要看懂实际代码,我们的目的是要从例子中找到我们下一步需要了解的概念。
研究了一小会儿之后,我发现我需要了解2个概念。
4.2. Actions
Actions中文的意思是“动作”,举几个例子:
- 菜单中的File | Open File...按钮,点击后触发打开本文文件资源管理器的动作;
- 鼠标右键菜单中Paste按钮点击之后触发粘贴的动作;
- Command + C快捷操作之后,触发复制的动作;
这3个例子说明了几点重要细节,首先Action可以出现在IDE的不同地方,至于出现在哪里,取决于你的注册过程;Action可以有不同的行为,具体的行为是什么取决于你的实现;最后不管是鼠标点击还是快捷键组合都能触发Action。
4.2.1. 注册
在src目录下,创建一个actions目录(其实创不创建不重要,但是我喜欢这种清晰的组织方式),目录上鼠标右键,选择右键菜单中的New | Plugin Devkit | Action(如果你鼠标右键没有这个按钮,那就安装一个Plugin DevKit这个插件),进入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命令,点击运行

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

没错,这个就是插件的测试环境,使用方法和正常的IDEA没有任何区别,只不过这个环境下默认安装了我们刚才编写的插件。
接下来新建或者打开一个已有项目,点击一下鼠标右键看一下「markdown-index」这个Action是否注册成功。

4.2.5. 实现Action
接下来做的就是实现actionPerformed(AnActionEvent e)方法,毫无疑问,我们所需的一切数据都是从e这个对象中获取了。
目标非常的清晰:
- 从
e中获取到当前文件的所有行数据 - 根据行前的
#数量递归添加标号 - 用添加标号之后的文本替换掉原来的文本
那调用的API不知道啊,怎么办?我的办法就是利用IDEA出色的提示功能以及源码的注释。
比如我想获取当前所在的文件,那我肯定会先敲e.get,然后等着提示:

我发现第一个就很像,我就选了,可是让我传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就不说了。有需要的朋友直接看文档,非常清晰。
https://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账号,如果没有的话就注册一个吧,注册地址给上。
https://plugins.jetbrains.com/author/me
然后在右上角点击账号名称,选择Upload plugin,最后上传你的插件jar包,并填写表单即可。

7. 源码分享

说明:源码贴出来,希望能给想要做插件的朋友一些参考
完~
从0到1写一款自动为Markdown标题添加序号的Jetbrains插件的更多相关文章
- 初步学习nodejs,业余用node写个一个自动创建目录和文件的小脚本,希望对需要的人有所帮助
初步学习nodejs,业余用node写个一个自动创建目录和文件的小脚本,希望对需要的人有所帮助,如果有bug或者更好的优化方案,也请批评与指正,谢谢,代码如下: var fs = require('f ...
- Android Oreo 8.0 新特性实战 Autosizing TextView --自动缩放TextView
Android Oreo 8.0 新特性实战 Autosizing TextView --自动缩放TextView 8.0出来很久了,这个新特性已经用了很久了,但是一直没有亲自去试试.这几天新的需求来 ...
- 用Python写一款属于自己的 简易zip压缩软件 附完成图(适合初学者)
一.软件描述 用Python tkinter模块写一款属于自己的压缩软件.zip文件格式是通用的文档压缩标准,在ziplib模块中,使用ZipFile来操作zip文件,具有功能:zip压缩功能,zip ...
- 用weexplus从0到1写一个app
说明 基于wexplus开发app是来新公司才接触的,之前只是用过weex体验过写demo,当时就被用vue技术栈来开发app的开发体验惊艳到了,这个开发体验比react native要好很多,对于我 ...
- 如何手写一款KOA的中间件来实现断点续传
本文实现的断点续传只是我对断点续传的一个理解.其中有很多不完善的地方,仅仅是记录了一个我对断点续传一个实现过程.大家应该也会发现我用的都是一些H5的api,老得浏览器不会支持,以及我并未将跨域考虑入内 ...
- HDFS+ClickHouse+Spark:从0到1实现一款轻量级大数据分析系统
在产品精细化运营时代,经常会遇到产品增长问题:比如指标涨跌原因分析.版本迭代效果分析.运营活动效果分析等.这一类分析问题高频且具有较高时效性要求,然而在人力资源紧张情况,传统的数据分析模式难以满足.本 ...
- 「懒惰的美德」我用 python 写了个自动生成给文档生成索引的脚本
我用 python 写了一个自动生成索引的脚本 简介:为了刷算法题,建了一个 GitHub仓库:PiperLiu / ACMOI_Journey,记录自己的刷题轨迹,并总结一下方法.心得.想到一个需求 ...
- 从0到1搭建一款Vue可配置视频播放器组件(Npm已发布)
前言 话不多说,这篇文章主要讲述如何从0到1搭建一款适用于Vue.js的自定义配置视频播放器.我们平时在PC端网站上观看视频时,会看到有很多丰富样式的视频播放器,而我们自己写的video标签样式却是那 ...
- 【Parcel 2 + Vue 3】从0到1搭建一款极快,零配置的Vue3项目构建工具
前言 一周时间,没见了,大家有没有想我啊!哈哈!我知道肯定会有的.言归正传,我们切入正题.上一篇文章中我主要介绍了使用Vite2+Vue3+Ts如何更快的入手项目.那么,今天我将会带领大家认识一个新的 ...
随机推荐
- 【.NET 6】多线程的几种打开方式和代码演示
前言: 多线程无处不在,平常的开发过程中,应该算是最常用的基础技术之一了.以下通过Thread.ThreadPool.再到Task.Parallel.线程锁.线程取消等方面,一步步进行演示多线程的一些 ...
- 批处理(bat、cmd)命令总结
2021-07-21 初稿 注释与回显 rem 回显 @取消单行回显 rem 注释有三种方式 :: %content% rem rem @取消单行回显,echo off取消后面的回显 @echo of ...
- String-StringBuffer-StringBuilder,Comparable-comparator
String 1.String是final类,不可被继承 2.内部是value[]的数组 private final char value[]; 3.不可变字符串 String s1 = " ...
- Oracle 创建表空间及用户授权、dmp数据导入、表空间、用户删除
1.创建表空间 // 创建表空间 物理位置为'C:\app\admin\oradata\NETHRA\NETHRA.DBF',初始大小100M,当空间不足时自动扩展步长为10M create tabl ...
- Zabbix 5.0:通过LLD方式自动化监控阿里云RDS
Blog:博客园 个人 之前做了RDS监控,由于 RDS 实例梳理增多,手动添加的方式已经不够效率,故改为LLD(Low-level discovery)方式做监控. 什么是LLD LLD(Low-l ...
- android studio 初印象
ANSROID STUDIO sdk 目录 build-tools目录,存放各版本Android的各种编译工具. docs目录,存放开发说明文档. extras\android目录,存放兼容低版本的新 ...
- go-zero微服务实战系列(十、分布式事务如何实现)
在分布式应用场景中,分布式事务问题是不可回避的,在目前流行的微服务场景下更是如此.比如在我们的商城系统中,下单操作涉及创建订单和库存扣减操作两个操作,而订单服务和商品服务是两个独立的微服务,因为每个微 ...
- Collection集合和Collection集合常用功能
Collection集合常用功能 方法: boolean add(E e); 向集合中添加元素 boolean remove(E e); 删除集合中的某个元素 void clear(); 清空集合所有 ...
- Map集合和Map常用子类
Map集合 java.util.Map<K,V>集合 Map集合的特点: 1.Map集合是一个双列集合,一个元素包含两个值(Key,Value) 2.Map集合中的元素,key和value ...
- Linux系列之管理用户环境变量
前言 环境变量控制你在Linux工作环境中的外观.行为和感觉.一共有两种类型的变量: 环境变量:这些是内置于系统中的进程范围的变量,控制着系统的外观和行为.因为是进程范围的,所以它们被任何子shell ...