JetBrains IDE插件开发教程(五)——File Header

前言

本来写教程应该介绍一些知识的,但笔者有新的想法,现在有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 更早

需要实现getStateloadState两个关键的方法

新建一个软件包,然后新建一个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

intellij-community/platform/ide-core/src/com/intellij/openapi/options/Configurable.java at master · JetBrains/intellij-communityhttps://github.com/JetBrains/intellij-community/blob/master/platform/ide-core/src/com/intellij/openapi/options/Configurable.java

里面有几个方法,慢慢使用。

首先需要初始化组件------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 Languagehttps://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-adderhttps://github.com/qe-study/file-header-adder仅仅只是一个demo,不是全部考虑了,用于学习。

相关推荐
harmful_sheep9 小时前
idea相关设置
java·ide·intellij-idea
蓝冰凌11 小时前
vscode 用户级 settings.json
ide·vscode·json
Kapaseker11 小时前
Android CLI 完全指南
android·kotlin
九九落12 小时前
VSCode AI编程完全指南:从环境搭建到实战应用
ide·vscode·ai编程
Coffeeee1 天前
Android功耗优化,哪些代码会让你的机子发热又耗电
android·架构·kotlin
Android打工仔1 天前
Kotlin 编译器是如何把 suspend 函数变成状态机的?
kotlin
Android打工仔1 天前
一个 suspend 函数,为什么会变成一个普通函数?
kotlin
love530love2 天前
将 ChatCut MCP 插件从 Codex 桌面应用移植到 WorkBuddy —— 完整适配实录
ide·人工智能·windows·视频剪辑·ai agent
小李不困还能学2 天前
PyCharm下载安装与配置教程
ide·python·pycharm