IntelliJ IDE 插件开发 | (四)开发一个时间管理大师插件
系列文章
- IntelliJ IDE 插件开发 |(一)快速入门
- IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化
- IntelliJ IDE 插件开发 |(三)消息通知与事件监听
- IntelliJ IDE 插件开发 |(四)开发一个时间管理大师插件
前言
在上篇文章的结尾提到本文将参考 VS Code 中 TimerMaster 插件的实现效果来实现一个在 IDEA 中统计编码情况的插件,TimerMaster 的效果如下:


本文最终实现的插件效果如下(增加了 CV 操作和代码提交的统计😎):


由于本文涉及到大部分知识都是在前几篇文章中介绍过的,因此本文只对关键功能点和实现方式进行介绍,一些基础知识和配置就不再讲解,该插件的完整代码已上传到GitHub。
实现思路
在开发前,正如前言中提到的,是参考 TimerMaster 的实现效果。因此首先确认了要实现的功能如下:
- 可以统计当天、昨天、过去七天和每天平均的代码活动。
- 统计项包括编辑器使用/活跃时间、添加/删除的代码行数、总的键入数、扩展增加了 CV(懂得都懂) 和代码提交活动的统计。
- 通过点击右键菜单项后在控制台进行展示。
对于第一点,主要通过本地持久化数据进行实现,当天数据和历史数据分别进行存储(选择了 JSON 格式进行存储),然后按需取用进行统计即可。
对于第二点,涉及的功能较多,这里分开进行介绍:
-
编辑器使用/活跃时间
增加一个对项目生命周期的监听器,在启动事件中注册一个定时任务,每隔 n 秒(可配置)将使用时间增加 n 秒。增加一个对文档和游标位置的监听器,只要有文档操作或者游标的移动都认为是活跃状态,然后更新最新操作时间,在定时任务中会将当前时间和最新操作时间进行比较,如果不大于 m 秒(可配置)就认为编辑器处于活跃状态,并将活跃时间增加 n 秒。
-
添加/删除的代码行数、总的键入数
通过增加对文档的监听器,根据变更内容中的换行数目得到添加/删除的代码行数,同时根据变更内容长度是否为 1 或者为空白符来决定是否增加键入数。
-
CV 统计
增加一个对复制粘贴事件的监听器,分别在复制和粘贴事件中增加相应的次数。
-
代码提交统计
监听自带 GIT 插件提供的 PUSH 事件监听器,在提交完成事件中增加对提交提交次数的统计。
对于第三点,只需要增加一个 action 并绑定到右键菜单,然后通过 ToolWindowFactory 获取到控制台视图来输出计算得到的结果即可。
根据个人需求,统计每天最早/最晚运行时间、活跃时间段、数据云存储等也很容易实现,本文就不进行拓展了。
代码实现及讲解
配置界面
在实现思路中提到了定时器间隔和代码活跃时间间隔是可配置的,由于界面比较简单,这里使用了 Kotlin UI DSL 的方式(参考第二篇文章使用 Swing UI 的拖拽方式也很容易实现),对应代码及效果如下:
// TimerMasterConfig 中部分代码private var panel = panel { row("更新间隔(秒): ") {intTextField().bindIntText(model::updateInterval).comment("<icon src='AllIcons.General.Information'> 不设置或者小于 10, 最终都为 10.")}row("活跃间隔(秒): ") {intTextField().bindIntText(model::activeInterval).comment("<icon src='AllIcons.General.Information'> 不设置或者小于 30, 最终都为 30.")}
}

存储方式及格式
根据要存储的数据,定义了一个统计数据类,并增加了一个创建时间便于归档:
data class StatisticsData(var runTime: Long = 0,var activeTime: Long = 0,var keyCount: Long = 0,var addLineCount: Long = 0,var removeLineCount: Long = 0,var copyCount: Long = 0,var pasteCount: Long = 0,var pushCount: Long = 0,var createDate: String = Utils.getTodayYmd()
)
同时为了便于解析和处理,在进行本地持久化的时候,使用了 GSON 库来进行数据的 JSON (反)序列化操作。
编辑器运行/活跃时间
在前文中提到这里是通过在项目启动事件中增加定时任务来实现的,先展示代码:
class ProjectStartListener: ProjectActivity, Disposable {private val state = TimerMasterState.getInstance()override suspend fun execute(project: Project) {while (true) {delay(TimeUnit.SECONDS.toMillis(state.updateInterval.toLong()))run {// 避免多个项目运行时间统计多次, 增加满足以下规则才进行时间统计: // 当前项目与配置信息一致, 或配置信息为空, 或配置信息内的项目不处于打开状态val projectPath = project.locationHashval firstOrNull = ProjectManager.getInstance().openProjects.firstOrNull { it.locationHash == state.runProjectPath }// 判断编辑器是否处于活跃状态val active = (System.currentTimeMillis() - state.activeTime) / 1000 <= state.activeIntervalstate.runProjectPath.takeIf { it == projectPath || it.isBlank() || firstOrNull == null }?.let { state.runProjectPath = projectPathval data = Utils.parse(state.statisticsData, TypeToken.get(StatisticsData::class.java))if (data.createDate == Utils.getTodayYmd()) {// 计算运行和活跃时间data.runTime += state.updateIntervalif (active) {data.activeTime += state.updateInterval}state.statisticsData = Utils.stringify(data)} else {// 存储的如果不是当日的数据, 则将数据加入到历史数据, 然后再初始化数据val arr = Utils.parse(state.historyData, object : TypeToken<MutableList<String>>() {})arr.add(state.statisticsData)state.historyData = Utils.stringify(arr)val newData = StatisticsData()newData.runTime = state.updateInterval.toLong()if (active) {newData.activeTime = state.updateInterval.toLong()}state.statisticsData = Utils.stringify(newData)}}}}}override fun dispose() {}}
增加以下配置:
<extensions defaultExtensionNs="com.intellij"><postStartupActivityimplementation="cn.butterfly.timermaster.listener.ProjectStartListener"/>
</extensions>
这里需要注意的是我们可能会同时打开多个项目,因此为了避免运行和活跃时间被多次计算,这里通过使用 runProjectPath 来存储当前项目路径的 hash 值,只有该值为空(暂无运行的项目)或者该值不空但打开的项目中没有等于该值的项目(项目打开后又关闭)时才会更新该值为当前运行项目的路径 hash 值。相应地,也只有满足以上条件才会统计运行和活跃时间,同时根据 createDate 字段来决定是在当日数据上进行累积,还是归档该数据然后初始化今日数据。
添加/删除的代码行数、总的键入数
这里是在编辑器创建事件中,对相应的文档和光标移动增加监听器来统计添加/删除的代码行数、总的键入数,此外还会更新编辑器活跃时间(Utils.initData() 中进行记录),代码如下:
class EditorListener: EditorFactoryListener, BulkAwareDocumentListener, CaretListener {private val state = TimerMasterState.getInstance()private val fileSet = mutableSetOf<String>()override fun editorCreated(event: EditorFactoryEvent) {// 避免重复增加监听器val file = FileDocumentManager.getInstance().getFile(event.editor.document) ?: returnif (file.path in fileSet) {return}fileSet.add(file.path)// 监听编辑操作event.editor.document.addDocumentListener(this)// 监听光标移动事件event.editor.caretModel.addCaretListener(this)}override fun documentChangedNonBulk(event: DocumentEvent) {val data = Utils.initData()event.takeIf { (it.oldFragment.isNotEmpty() or it.newFragment.isNotEmpty()) or !it.isWholeTextReplaced }?.let {// 只对字符长度为 1 和非空空白符的情况进行统计if (it.newFragment.isNotEmpty() && (it.newFragment.length == 1 || it.newFragment.trim().isEmpty())) {++data.keyCount}// 根据文档代码段变更信息判断是新增还是删除行if (it.oldFragment.contains('\n')) {data.removeLineCount += it.oldFragment.count { item -> item == '\n' }}if (it.newFragment.contains('\n')) {data.addLineCount += it.newFragment.count { item -> item == '\n' }}}state.statisticsData = Utils.stringify(data)}override fun caretPositionChanged(event: CaretEvent) {state.statisticsData = Utils.stringify(Utils.initData())}}
增加以下配置:
<extensions defaultExtensionNs="com.intellij"><editorFactoryListener implementation="cn.butterfly.timermaster.listener.EditorListener"/>
</extensions>
结合注释,代码不难理解,这里不再介绍。
CV 统计
关于复制粘贴的统计也很简单,官方给我们提供了相应的扩展点,我们增加相应的实现即可:
class CopyPasteListener: CopyPastePreProcessor {private val state = TimerMasterState.getInstance()override fun preprocessOnCopy(p0: PsiFile?, p1: IntArray?, p2: IntArray?, p3: String?): String? {val data = Utils.initData()++data.copyCountstate.statisticsData = Utils.stringify(data)return null}override fun preprocessOnPaste(p0: Project?, p1: PsiFile?, p2: Editor?, text: String?, p4: RawText?): String {val data = Utils.initData()++data.pasteCountstate.statisticsData = Utils.stringify(data)return text ?: ""}}
增加以下配置:
<extensions defaultExtensionNs="com.intellij"><copyPastePreProcessorimplementation="cn.butterfly.timermaster.listener.CopyPasteListener"/>
</extensions>
而如何知道官方给我们提供了哪些扩展点,在上一篇文章中也说明查看官方文档即可,例如复制粘贴的扩展点:

代码提交统计
这里使用的GitPushListener也是在上述官方文档中提供的:

使用方式也很简单:
class GitListener: GitPushListener {private val state = TimerMasterState.getInstance()override fun onCompleted(repository: GitRepository, pushResult: GitPushRepoResult) {val data = Utils.initData()++data.pushCountstate.statisticsData = Utils.stringify(data)}}
增加以下配置:
<depends>Git4Idea</depends><projectListeners><listener class="cn.butterfly.timermaster.listener.GitListener"topic="git4idea.push.GitPushListener"/>
</projectListeners>
build.gradle.kts中也需要增加以下配置:
intellij {// 用到的插件plugins.set(listOf("Git4Idea"))
}
由于GitPushListener是自带 GIT 插件所提供的监听器,因此还增加了第一行,其中Git4Idea是 GIT 的插件 id:

展示报告
有了以上获取和存储数据的基础,这里就很简单了,关于得到展示数据的方式这里就不介绍了,大家可以去查看TimerMasterOutputAction的源码,这里主要介绍如何在控制台进行报告的输出。
其实控制台视图也可以看作是第二篇文章中提到的侧边栏,因此也需要继承实现ToolWindowFactory:
class ConsoleWindow: ToolWindowFactory {override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {if (Utils.getConsoleViews()[project] == null) {Utils.createToolWindow(project, toolWindow)}Utils.toolWindows[project] = toolWindow}}// Utils 中的方法
fun createToolWindow(project: Project, toolWindow: ToolWindow) {val consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).consoleconsoleViews[project] = consoleViewval content = toolWindow.contentManager.factory.createContent(consoleView.component, "TimerMaster Output", false)content.component.isVisible = truecontent.isCloseable = truetoolWindow.contentManager.addContent(content)
}// 在控制台中显示信息
fun consoleInfo(project: Project, msg: String) {if (consoleViews[project] == null) {ToolWindowManager.getInstance(project).getToolWindow("TimerMaster Console")?.let { createToolWindow(project, it) }}consoleViews[project]?.clear()consoleViews[project]?.print(msg, ConsoleViewContentType.NORMAL_OUTPUT)// 显示控制台窗口, 减去手动点击侧边栏按钮的操作toolWindows[project]?.activate(null, false)
}
这里需要注意的是由于可能多个项目都打开了该控制台视图,因此需要使用一个 map 继续保存,避免内容错乱。
最后以一个动图结尾:

总结
本文通过实现一个对代码活动统计的插件算是对前几篇文章的一个总结,目前只在本地运行测试了几天,可能有一些不妥的实现方式和 BUG,欢迎一起交流讨论。另外截止到本篇,关于 IntelliJ 平台插件开发的基础知识也算告一段落,后续文章则优先讲解关于虚拟文件、PSI和编辑器操作相关的知识,而关于国际化、主题插件开发等则看情况夹杂在其中进行讲解。
相关文章:
IntelliJ IDE 插件开发 | (四)开发一个时间管理大师插件
系列文章 IntelliJ IDE 插件开发 |(一)快速入门IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化IntelliJ IDE 插件开发 |(三)消息通知与事件监听IntelliJ IDE 插件开发 |(四)开发一…...
【ChatGPT 默认强化学习策略】PPO 近端策略优化算法
PPO 近端策略优化算法 PPO 概率比率裁剪 演员-评论家算法演员-评论家算法:多智能体强化学习核心框架概率比率裁剪:逐步进行变化的方法PPO 目标函数的设计重要性采样KL散度 PPO 概率比率裁剪 演员-评论家算法 论文链接:https://arxiv.org…...
【银行测试】金融银行-理财项目面试/分析总结(二)
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 银行理财相关的项…...
张江智荟毁约offer
毕业8年后,找工作被国企歧视学历!已经收到了offer,在入职前一周被通知要撤回offer,拒绝录用,理由居然是他们只要本科211以上的人 这是我今天(2023-12-26)亲身经历的事,听说过面试前…...
ubuntu 系统终端颜色设置
1 开启终端颜色 # 第一步: 在 ~/.bashrc 中设置 force_color_promptyes# 第二步: 执行 source ~/.bashrc2 对于精减的 .bashrc 在 ~/.bashrc 中添加以下内容,再执行 source ~/.bashrc : # uncomment for a colored prompt, if…...
【Vue】class与style绑定
✨ 专栏介绍 在当今Web开发领域中,构建交互性强、可复用且易于维护的用户界面是至关重要的。而Vue.js作为一款现代化且流行的JavaScript框架,正是为了满足这些需求而诞生。它采用了MVVM架构模式,并通过数据驱动和组件化的方式,使…...
大厂前端面试题总结(百度、字节跳动、腾讯、小米.....),附上热乎面试经验!
先简单介绍下自己,我“平平无奇小天才”一枚,毕业于南方普通985普通学生,有幸去百度、字节面试,感觉大公司就是不一样,印象最深的是字节,所以有必要总结一下面试经验,以及面试中遇到的一些问题&…...
EXPLORING DIFFUSION MODELS FOR UNSUPERVISED VIDEO ANOMALY DETECTION 论文阅读
EXPLORING DIFFUSION MODELS FOR UNSUPERVISED VIDEO ANOMALY DETECTION 论文阅读 ABSTRACT1. INTRODUCTION2. RELATEDWORK3. METHOD4. EXPERIMENTAL ANALYSIS AND RESULTS4.1. Comparisons with State-Of-The-Art (SOTA)4.2. Diffusion Model Analysis4.3. Qualitative Result…...
当 ML 遇到 DevOps:如何理解 MLOps
近年来,人工智能 (AI) 和机器学习 (ML) 已经席卷全球,几乎成为任何行业的重要组成部分,从零售和娱乐到医疗保健和银行业。这些技术能够通过分析大量数据实现运营自动化、降低成本和促进决策&…...
vue+element+springboot实现多张图片上传
1.需求说明 2.实现思路 3.el-upload组件主要属性说明 4.前端传递MultipartFile数组与服务端接收说明 5.完整代码 1.需求说明 动态模块新增添加动态功能,支持多张图片上传.实现过程中对el-upload组件不是很熟悉,踩了很多坑,当然也参考过别的文章,发现处理很…...
react使用useState更新数组失败
失败案例: const [addBox, setAddBox] useState([])const itemAdd (item) >{addBox.push(item);setAddBox(addBox)console.log(addBox,点击添加按钮)} 原因:react的useState hook监听的是浅监听 在 React 中,使用 useState Hook 来更新…...
《LIO-SAM阅读笔记》3.后端优化
前言: LIO-SAM后端优化部分写在了mapOptimization.cpp文件中,本部分主要进行了激光帧的scan-to-map匹配,回环检测以及关键帧的因子图优化。本部分主要有两个环节同步进行,一个单独开辟了回环检测线程,另外一个是lidar…...
mac下jd-gui提示没有找到合适的jdk版本
mac下jd-gui提示jdk有问题 背景解决看一下是不是真有问题了方法一:修改启动脚本方法二:设置launchd环境变量 扩展动态切jdk脚本(.bash_profile) 背景 配置了动态jdk后,再次使用JD-GUI提示没有找到合适的jdk版本。 解决 看一下是不是真有问题…...
FlinkSQL窗口实例分析
Windowing TVFs Windowing table-valued functions (Windowing TVFs),即窗口表值函数 注意:窗口函数不可以单独使用,需要聚合函数,按照 window_start、window_end 分区,即存在:group by window_start,wind…...
18-网络安全框架及模型-信息系统安全保障模型
信息系统安全保障模型 1 基本概念 信息系统安全保障是针对信息系统在运行环境中所面临的各种风险,制定信息系统安全保障策略,设计并实现信息系统安全保障架构或模型,采取工程、技术、管理等安全保障要素,将风险减少至预定可接受的…...
Android 提取(备份)apk(安装包)
Android 提取(备份)apk(安装包) 一、通过安卓代码的方式 主要分三步: 根据应用找到包名根据包名获得apk提取apk 提取apk代码 private static final String BACKUP_PATH "/sdcard/backup1/"; private static final String APK ".apk";pri…...
gRPC-Go基础(4)metadata和超时设置
文章目录 0. 简介1. metadata1.1 metadata结构1.2 metadata创建1.3 客户端处理metadata1.4 服务端处理metadata1.5 metadata的传输 2. 超时设置2.1 客户端输出超时信息2.2 服务端端接收超时信息 3. 小结 0. 简介 Go在多个go routine之间传递数据使用的是Go SDK提供的context包…...
语言模型:从n-gram到神经网络的演进
目录 1 前言2 语言模型的两个任务2.1 自然语言理解2.2 自然语言生成 3 n-gram模型4 神经网络语言模型5 结语 1 前言 语言模型是自然语言处理领域中的关键技术之一,它致力于理解和生成人类语言。从最初的n-gram模型到如今基于神经网络的深度学习模型,语言…...
docker compose 部署 grafana + loki + vector 监控kafka消息
Centos7 随笔记录记录 docker compose 统一管理 granfana loki vector 监控kafka 信息。 当然如果仅仅是想通过 Grafana 监控kafka,推荐使用 Grafana Prometheus 通过JMX监控kafka 目录 1. 目录结构 2. 前提已安装Docker-Compose 3. docker-compose 自定义服…...
kubeadm创建k8s集群
kubeadm来快速的搭建一个k8s集群: 二进制搭建适合大集群,50台以上。 kubeadm更适合中下企业的业务集群。 部署框架 master192.168.10.10dockerkubelet kubeadm kubectl flannelnode1192.168.10.20dockerkubelet kubeadm kubectl flannelnode2192.168.1…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
Ubuntu系统多网卡多相机IP设置方法
目录 1、硬件情况 2、如何设置网卡和相机IP 2.1 万兆网卡连接交换机,交换机再连相机 2.1.1 网卡设置 2.1.2 相机设置 2.3 万兆网卡直连相机 1、硬件情况 2个网卡n个相机 电脑系统信息,系统版本:Ubuntu22.04.5 LTS;内核版本…...
HTML中各种标签的作用
一、HTML文件主要标签结构及说明 1. <!DOCTYPE html> 作用:声明文档类型,告知浏览器这是 HTML5 文档。 必须:是。 2. <html lang“zh”>. </html> 作用:包裹整个网页内容,lang"z…...
在Spring Boot中集成RabbitMQ的完整指南
前言 在现代微服务架构中,消息队列(Message Queue)是实现异步通信、解耦系统组件的重要工具。RabbitMQ 是一个流行的消息中间件,支持多种消息协议,具有高可靠性和可扩展性。 本博客将详细介绍如何在 Spring Boot 项目…...
以太网PHY布局布线指南
1. 简介 对于以太网布局布线遵循以下准则很重要,因为这将有助于减少信号发射,最大程度地减少噪声,确保器件作用,最大程度地减少泄漏并提高信号质量。 2. PHY设计准则 2.1 DRC错误检查 首先检查DRC规则是否设置正确,然…...
Python爬虫(52)Scrapy-Redis分布式爬虫架构实战:IP代理池深度集成与跨地域数据采集
目录 一、引言:当爬虫遭遇"地域封锁"二、背景解析:分布式爬虫的两大技术挑战1. 传统Scrapy架构的局限性2. 地域限制的三种典型表现 三、架构设计:Scrapy-Redis 代理池的协同机制1. 分布式架构拓扑图2. 核心组件协同流程 四、技术实…...
