前言
本来写教程应该介绍一些知识的,但笔者有新的想法,现在有AI了,
给AI提需求,AI就可以解决。
那么,不妨干中学,边写实际的项目(demo),一边学习,一边写教程。
正文
需求
确定需求,对于一个文件,比如kt文件,可以在文件的开始地方写关于自己的东西,比如作者,日期之类的。
那么就写一个插件来实现这个。
都2026年的,jetbrian IDE其实早就有这个能力,不需要写插件,好像通过模板就可以了。
当然,笔者希望这样搞,
1、在设置里面有这个插件相关的东西,设置作者、公司、日期格式
2、可以保存
3、右键打开的文件,点击插件的按钮就可以在文件出现对于的信息。
流程其实就是这个流程。开始搞事情。
确定关键的数据类型
初始化项目,不必多说。
需要把作者、公司、日期格式持久保存,这就需要实现一个接口PersistentStateComponent
看一下反编译代码
java
package com.intellij.openapi.components;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface PersistentStateComponent<T> {
@Nullable T getState();
void loadState(@NotNull T var1);
default void noStateLoaded() {
}
default void initializeComponent() {
}
}
有四个关键的方法。
getState:返回当前要保存的状态对象T,IDE 会在关闭时自动将其序列化为 XML
loadState:IDE 启动时自动调用,从 XML 中恢复之前保存的状态
noStateLoaded:默认方法,首次使用(没有历史状态)时触发,可在这里初始化默认值
initializeComponent:默认方法,组件初始化时调用,比 loadState 更早
需要实现getState 和loadState两个关键的方法
新建一个软件包,然后新建一个HeaderSettings.kt文件,代码如下
Kotlin
package com.github.qepresent.fileheaderadder.settings
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.XmlSerializerUtil
// Service 告诉IDE这个类是应用级单例服务。整个IDE里只有一个实例
@Service
/*
State 标记为可持久化状态State 标记为可持久化状态
name 这个状态在内部注册表里的唯一标识名,
storages 指定序列化后的数据存到FileHeaderSettings.xml文件里面。IDE 关闭时自动保存,下次启动时自动读取
*/
@State(
name = "com.github.qepresent.fileheaderadder.settings.HeaderSettings",
storages = [Storage("FileHeaderSettings.xml")]
)
// 泛型写自己,意思是 这个组件的状态类型就是我自己这个类
class HeaderSettings: PersistentStateComponent<HeaderSettings> {
// var 可读写属性,默认值从系统属性user.name读取当前操作系统用户名,读不到就返回unknown
var author: String= System.getProperty("user.name", "unknown")
var company: String=""
var dateFormat: String="yyyy-MM-dd"
//IDE 要保存设置到磁盘时,会调用这个方法,直接返回this
override fun getState(): HeaderSettings=this
// IDE 启动时,从XML读出数据,构造一个临时的HeaderSettings,copyBean用反射把state字段的值拷贝到this实例上
override fun loadState(state: HeaderSettings) {
XmlSerializerUtil.copyBean(state, this)
}
companion object {
fun getInstance(): HeaderSettings=com.intellij.openapi.components.service()
}
}
初始化了三个属性,实现前面两个关键的方法。
添加可视化组件
author和company使用input,dateFormat使用下拉选择框。
大致就是一个面板,面板有一个Form------表单,然后放对应的组件。
Kotlin
package com.github.qepresent.fileheaderadder.settings
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import javax.swing.JPanel
class HeaderSettingsComponent {
val panel: JPanel
private val authorField= JBTextField()
private val companyField= JBTextField()
private val dateFormatCombo= ComboBox<String>()
init {
val nowFormat=arrayOf(
"yyyy-MM-dd",
"yyyy/MM/dd",
"yyyy年MM月dd日",
"yyyy/MM/dd HH:mm:ss",
)
nowFormat.forEach {
dateFormatCombo.addItem(it)
}
panel = FormBuilder.createFormBuilder()
// 标签文本 实际组件 垂直间距系数 标签位置
// 垂直间距系数,1 是标准间距,0 表示紧贴上一行
// 标签位置,false = 标签在左边(水平并排),true = 标签在上方(垂直堆叠)
.addLabeledComponent("Author", authorField,1,false)
.addLabeledComponent("Company", companyField,1,false)
.addLabeledComponent("DateFormat", dateFormatCombo,1,false)
// 把前面的表单内容顶到面板顶部,下方空白自动填满
.addComponentFillVertically(JPanel(), 0)
.panel
}
var author: String
get() = authorField.text
set(value) {
authorField.text = value
}
var company: String
get() = companyField.text
set(value) {
companyField.text = value
}
var dateFormat: String
get() = dateFormatCombo.selectedItem as?String?:"yyyy-MM-dd"
set(value) {
dateFormatCombo.selectedItem = value
}
}
上面代码关键的一个方法是addLabeledComponent,看看源码
java
public FormBuilder addLabeledComponent(
@Label @NotNull String labelText,
@NotNull JComponent component,
int topInset,
boolean labelOnTop) {
JLabel label = createLabelForComponent(labelText, component);
return this.addLabeledComponent((JComponent)label, component, topInset, labelOnTop);
}
需要传递四个参数
第一个参数:标签文本
第二个参数:组件对象
第三个参数:垂直间距系数,1 是标准行间距;0 表示紧贴上一行,不额外留空
第四个参数:标签位置,false = 标签在左边(水平并排);true = 标签在上方
联动数据与组件
前面定义了数据和ui组件。
现在需要合并一下,启动IDE的时候,把数据放到组件里面;关闭IDE的时候,把组件里面的数据保存,还有对更新的考虑。
这就需要实现另外一个接口------Configurable。
里面有几个方法,慢慢使用。
首先需要初始化组件------createComponent,
中间过程,如果有修改,判断保存按钮是否可以点击------isModified,
如果可以保存------apply,
或者重置------reset。
关键的东西简单进行了表述。
代码如下
Kotlin
package com.github.qepresent.fileheaderadder.settings
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.util.NlsContexts
import javax.swing.JComponent
// 实现了Configurable接口。成为IDE设置系统的一个页面控制器。
class HeaderSettingsConfigurable: Configurable {
private var settingsComponent: HeaderSettingsComponent?=null
// IDE 左侧设置列表里显示的名字
override fun getDisplayName(): @NlsContexts.ConfigurableName String {
return "File Header"
}
/*
判断用户修改了设置
1. 从磁盘读取已保存的配置
2. 从UI输入框读取当前值
3. 比较任意字段是否相同,如果有不同,返回true
3.IDE根据返回值决定是否点亮Apply按钮
*/
override fun isModified(): Boolean {
val settings= HeaderSettings.getInstance()
val component=settingsComponent?:return false
return component.author!=settings.author||
component.company!=settings.company||
component.dateFormat!=settings.dateFormat
}
/*
创建ui界面
1.当点击File Header设置页面时候,IDE调用在这个方法
2. 实例化表单组件HeaderSettingsComponent
3。 返回panel
*/
override fun createComponent(): JComponent {
settingsComponent = HeaderSettingsComponent()
return settingsComponent!!.panel
}
/*
保存设置到磁盘
当用户点击Apply。IDE调用这个方法
把输入框里面的值写入到HeaderSettings的对象
HeaderSettings自动序列化到FileHeaderSettings.xml文件,下次IDE读取
*/
override fun apply() {
val settings= HeaderSettings.getInstance()
val component = settingsComponent?:return
settings.author=component.author
settings.company=component.company
settings.dateFormat=component.dateFormat
}
/*
重置UI为已保存的值
1 当点击Cancel或者打开设置页面,IDE会调用这个方法
2 把磁盘保存的值填到输入框
*/
override fun reset() {
val settings = HeaderSettings.getInstance()
val component = settingsComponent?:return
component.author=settings.author
component.company=settings.company
component.dateFormat=settings.dateFormat
}
/*
设置页面关闭时释放 UI 组件引用
*/
override fun disposeUIResources() {
settingsComponent = null
}
}
触发Action
现在已经有数据了,接下来就需要把数据写到文件中。
大致流程
1、需要打开文件。
2、右键点击触发。
3、header信息写入文件。
直接给出代码
Kotlin
package com.github.qepresent.fileheaderadder.actions
import com.github.qepresent.fileheaderadder.settings.HeaderSettings
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.psi.PsiFile
import java.text.SimpleDateFormat
import java.util.Date
class FileHeader : AnAction(){
override fun update(e: AnActionEvent) {
// 只在文本编辑器里显示这个右键菜单
val editor = e.getData(CommonDataKeys.EDITOR)
e.presentation.isEnabledAndVisible = editor != null
}
override fun actionPerformed(e: AnActionEvent) {
// 获取项目
val project=e.getData(CommonDataKeys.PROJECT)
// 获取编辑器
val editor=e.getData(CommonDataKeys.EDITOR)?:return
// 获取文件
val psiFile=e.getData(CommonDataKeys.PSI_FILE)?:return
val header=getHeader(psiFile)
WriteCommandAction.runWriteCommandAction(project) {
val offset=calculateOffset(editor.document)
editor.document.insertString(offset,header)
}
}
private fun getHeader(psiFile: PsiFile):String{
//数据
val settings= HeaderSettings.getInstance()
val fileName=psiFile.name
val date= SimpleDateFormat(settings.dateFormat).format(Date())
return when(psiFile.language.displayName){
"Java","Kotlin","Groovy"->generateJavaStyle(settings,fileName,date)
else -> generateGenericStyle(settings,fileName,date)
}
}
private fun generateJavaStyle(settings: HeaderSettings,fileName: String,date: String):String{
return buildString{
appendLine("/**")
appendLine("author ${settings.author }")
if(settings.company.isNotEmpty())appendLine("company ${settings.company}")
appendLine("date $date")
appendLine("file $fileName")
appendLine("*/")
appendLine()
}
}
private fun generateGenericStyle(s: HeaderSettings, fileName: String, date: String): String {
return buildString {
appendLine("/*")
appendLine(" * Author: ${s.author}")
if (s.company.isNotEmpty()) appendLine(" * Company: ${s.company}")
appendLine(" * Date: $date")
appendLine(" * File: $fileName")
appendLine(" */")
appendLine()
}
}
private fun calculateOffset(document: Document): Int{
val text = document.text
// 考虑开头
if(text.startsWith("#!")){
val firstNewLine=text.indexOf('\n')
if(firstNewLine!=-1){
return firstNewLine + 1
}
}
return 0
}
}
这个代码,意思是很显然的,写的时候,使用了appendLine,这个方法。
笔者表示不知道。搜一搜
appendln | Core API -- Kotlin Programming Language
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.text/appendln.html原来是标准库里面的函数,appendLine 是 Kotlin 标准库 中的函数,用于向 StringBuilder(或其他可追加对象)添加内容,并在末尾自动加上换行符。
plugin.xml的配置
有两个配置
1、设置里面的配置
2、右键和Action相关的配置。
那么直接给出关键的配置
XML
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.github.qepresent.fileheaderadder.settings.HeaderSettings"/>
<applicationConfigurable
parentId="tools"
instance="com.github.qepresent.fileheaderadder.settings.HeaderSettingsConfigurable"
id="FileHeaderSettings"
displayName="File Header"/>
</extensions>
<actions>
<action
id="com.github.qepresent.fileheaderadder"
class="com.github.qepresent.fileheaderadder.actions.FileHeader"
text="Add File Header"
description="Insert file header with author,date,company"
>
<!-- 编辑器右键菜单-->
<add-to-group group-id="EditorPopupMenu" anchor="last"/>
</action>
测试一下
启动插件,
进入设置,如下

笔者修改了,可以发现下面三个按钮都可以点击。
打开一个java文件,在最下面可以发现action。

点击action,结果如下

没问题。
源码参考
当前demo的源码参考。
qe-study/file-header-adder
https://github.com/qe-study/file-header-adder仅仅只是一个demo,不是全部考虑了,用于学习。