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 更早

需要实现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。

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,不是全部考虑了,用于学习。

相关推荐
熊猫钓鱼>_>14 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 KMPNotifier 实现 OpenHarmony 本地通知引擎
kotlin·ai编程·harmonyos·鸿蒙·openharmony·适配·kmp
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
事圆则缓1 天前
Kotlin 入门与面试:从空安全、扩展函数到协程
安全·面试·kotlin
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
1024奇点1 天前
【仓颉语言入门 · 第26课】
开发语言·ide·开源
golang学习记2 天前
VSCode AI新特性:HydraFusion来了:系统会自己组队干活
ide·人工智能·vscode