IntelliJ IDE 插件开发 | (四)开发一个时间管理大师插件

系列文章

  • IntelliJ IDE 插件开发 |(一)快速入门
  • IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化
  • IntelliJ IDE 插件开发 |(三)消息通知与事件监听
  • IntelliJ IDE 插件开发 |(四)开发一个时间管理大师插件

前言

在上篇文章的结尾提到本文将参考 VS Code 中 TimerMaster 插件的实现效果来实现一个在 IDEA 中统计编码情况的插件,TimerMaster 的效果如下:

image-20231225092034655

image-20231225092049204

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

image-20231228112248383

Clip_20231225_092825

由于本文涉及到大部分知识都是在前几篇文章中介绍过的,因此本文只对关键功能点和实现方式进行介绍,一些基础知识和配置就不再讲解,该插件的完整代码已上传到GitHub。

实现思路

在开发前,正如前言中提到的,是参考 TimerMaster 的实现效果。因此首先确认了要实现的功能如下:

  1. 可以统计当天、昨天、过去七天和每天平均的代码活动。
  2. 统计项包括编辑器使用/活跃时间、添加/删除的代码行数、总的键入数、扩展增加了 CV(懂得都懂) 和代码提交活动的统计。
  3. 通过点击右键菜单项后在控制台进行展示。

对于第一点,主要通过本地持久化数据进行实现,当天数据和历史数据分别进行存储(选择了 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'>&nbsp;不设置或者小于 10, 最终都为 10.")}row("活跃间隔(秒): ") {intTextField().bindIntText(model::activeInterval).comment("<icon src='AllIcons.General.Information'>&nbsp;不设置或者小于 30, 最终都为 30.")}
}

image-20231228124547932

存储方式及格式

根据要存储的数据,定义了一个统计数据类,并增加了一个创建时间便于归档:

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>

而如何知道官方给我们提供了哪些扩展点,在上一篇文章中也说明查看官方文档即可,例如复制粘贴的扩展点:

image-20231228132128049

代码提交统计

这里使用的GitPushListener也是在上述官方文档中提供的:

image-20231228132534460

使用方式也很简单:

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:

image-20231228132835555

展示报告

有了以上获取和存储数据的基础,这里就很简单了,关于得到展示数据的方式这里就不介绍了,大家可以去查看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和编辑器操作相关的知识,而关于国际化、主题插件开发等则看情况夹杂在其中进行讲解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/583139.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

未来编程语言什么样?编译解释兼方为王

○、编程语言的未来&#xff1f; 随着科技的飞速发展&#xff0c;编程语言在计算机领域中扮演着至关重要的角色。它们是软件开发的核心&#xff0c;为程序员提供了与机器沟通的桥梁。那么&#xff0c;在技术不断进步的未来&#xff0c;编程语言的走向又将如何呢&#xff1f; …

【银行测试】金融银行-理财项目面试/分析总结(二)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 银行理财相关的项…

CSS 纵向扩展动画

上干货 <template><!-- mouseenter"startAnimation" 表示在鼠标进入元素时触发 startAnimation 方法。mouseleave"stopAnimation" 表示在鼠标离开元素时触发 stopAnimation 方法。 --><!-- 容器元素 --><div class"container&q…

Spark编程范例:Word Count示例解析

Apache Spark是一个强大的分布式计算框架&#xff0c;用于处理大规模数据。Word Count示例是Spark入门教程中的经典示例&#xff0c;旨在展示如何使用Spark来进行简单的文本处理和数据分析。本文将深入解析Word Count示例&#xff0c;以帮助大家更好地理解Spark的基本概念和编程…

关于Echarts的重要属性总结

概要 1.设置图例字体颜色&#xff1a; legend: {//添加位置如下textStyle: {color: #fff // 设置图例文字颜色为白色}} 2.设置序列颜色&#xff1a; series: [{ // 添加位置如下itemStyle: { color: #FFA500 // 设置序列Series颜色}] 3.设置坐标轴单位&#xff1a; xAx…

【Shell编程练习】通过位置变量创建 Linux 系统账户及密码

系列文章目录 输出Hello World 系列文章目录位置变量代码实现运行结果 位置变量 位置变量将以数字方式对变量进行命名&#xff0c;可将命令行参数的值存储到脚本中。要从命令行、函数或脚本执行等处传递参数时&#xff0c;就需要在 Shell 脚本中使用位置参数变量。下表为常用…

LVM逻辑卷与扩容

目录 一.LVM&#xff1a; 1.什么是LVM&#xff1a; 2.LVM的基本核心组件&#xff1a; 3.LVM的基本命令&#xff1a; 二.逻辑卷的创建&#xff1a; 第一步&#xff0c;我们先要为虚拟机添加硬盘 然后我们要添加依赖包 然后我们要进行磁盘分区 再添加好分区后&#xff0…

测试C#使用OpenCvSharp从摄像头获取图片

OpenCvSharp也支持获取摄像头数据&#xff0c;不同于之前测试AForge时使用AForge控件显示摄像头数据流并从中截图图片&#xff0c;OpenCvSharp中显示摄像头数据流需要周期性地从摄像头中截取图片并显示在指定控件中。本文学习C#使用OpenCvSharp从摄像头获取图片的基本方式。  …

MybatisX逆向工程方法

官方文档链接&#xff1a;MybatisX快速开发插件 | MyBatis-Plus (baomidou.com) 使用MybatisX可以快速生成mapper文件&#xff0c;实体类和service及实现 效果 方法&#xff1a;首先下载mybatisX插件 然后创建数据库信息 然后选中表&#xff0c;右键&#xff0c;点击Mybatis…

知识积累 电源模块的介绍

1. 电源模块是什么 电源模块是一种设备或组件&#xff0c;用于提供电能供应给其他电子设备或系统。它通常包括电源转换、稳压、过流保护等功能&#xff0c;以确保输出的电能在一定电压和电流范围内稳定可靠。电源模块可以是线性电源或开关电源&#xff0c;具体类型取决于其工作…

Linux中的gcc\g++使用

文章目录 gcc\g的使用预处理编译汇编链接函数库gcc选项 gcc\g的使用 这里我们需要知道gcc和g实际上是对应的c语言和c编译器&#xff0c;而其他的Java&#xff08;半解释型&#xff09;&#xff0c;PHP&#xff0c;Python等语言实际上是解释型语言&#xff0c;因此我们经常能听…

2013年第二届数学建模国际赛小美赛B题寄居蟹进化出人类的就业模式解题全过程文档及程序

2013年第二届数学建模国际赛小美赛 B题 寄居蟹进化出人类的就业模式 原题再现&#xff1a; 寄居蟹是美国最受欢迎的宠物品种&#xff0c;依靠其他动物的壳来保护。剥去寄居蟹的壳&#xff0c;你会看到它柔软、粉红色的腹部卷曲在头状的蕨类叶子后面。大多数寄居蟹喜欢蜗牛壳&…

【iOS安全】越狱iOS安装Frida | 安装指定版本Frida

越狱iPhone安装Frida 本文的方法适用于已越狱的iPhone手机 打开Cydia&#xff0c;软件源&#xff0c;编辑&#xff08;右上角&#xff09;&#xff0c;添加&#xff08;左上角&#xff09;&#xff1a;https://build.frida.re 然后搜索Frida&#xff0c;点击安装 参考&#x…

mysql原理--MySQL基于规则的优化

设计 MySQL 的大叔依据一些规则&#xff0c;竭尽全力的把一些很糟糕的语句转换成某种可以比较高效执行的形式&#xff0c;这个过程也可以被称作 查询重写 &#xff08;就是人家觉得你写的语句不好&#xff0c;自己再重写一遍&#xff09;。 1.条件化简 我们编写的查询语句的搜…

Centos7:Jenkins+gitlab+node项目启动(2)

Centos7&#xff1a;Jenkinsgitlabnode项目启动(1) Centos7&#xff1a;Jenkinsgitlabnode项目启动(1)-CSDN博客 Centos7&#xff1a;Jenkinsgitlabnode项目启动(2) Centos7&#xff1a;Jenkinsgitlabnode项目启动(2)-CSDN博客 Centos7&#xff1a;Jenkinsgitlabnode项目启…

第十四章 Sentinel实现熔断与限流

Sentinel实现熔断与限流 gitee&#xff1a;springcloud_study: springcloud&#xff1a;服务集群、注册中心、配置中心&#xff08;热更新&#xff09;、服务网关&#xff08;校验、路由、负载均衡&#xff09;、分布式缓存、分布式搜索、消息队列&#xff08;异步通信&#x…

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…

C练习——银行存款

题目&#xff1a;设银行定期存款的年利率 rate为2.25%,已知存款期为n年&#xff0c;存款本金为capital 元,试编程计算并输出n年后本利之和deposit。 解析&#xff1a;利息本金*利率&#xff0c;下一年的本金又是是今年的本利之和 逻辑&#xff1a;注意浮点数&#xff0c;导入…

经典目标检测YOLO系列(一)复现YOLOV1(2)反解边界框及后处理

经典目标检测YOLO系列(一)复现YOLOV1(2)反解边界框及后处理 在上个博客&#xff0c;我们提出了新的YOLOV1架构&#xff0c;这次我们解决前向推理过程中的两个问题。 经典目标检测YOLO系列(一)YOLOV1的复现(1)总体架构 1、边界框的计算 1.1 反解边界框公式的改变 1.1.1 原版…

MFC随对话框大小改变同时改变控件大小

先看一下效果; 初始; 窗口变大,控件也变大; 二个也可以; 窗口变大,控件变大; 默认生成的对话框没有WM_SIZE消息的处理程序;打开类向导,选中WM_SIZE消息,对CxxxDlg类添加该消息的处理程序;默认生成的函数名是OnSize; 添加了以后代码中会有三处变化; 在对话框类的…