简介这是一份面向IntelliJ IDEA插件开发初学者与进阶者的详细源码示例围绕插件中常见的用户交互场景展开包括相关菜单、弹出框以及鼠标右键数据交互等实现方式帮助开发者在较短时间内理解并掌握IDEA插件的开发能力。压缩包共16个文件约10KB以java源码与xml配置为主辅以svg图标资源涵盖插件声明、Action注册、界面组件与项目模块配置等关键内容目录结构清晰便于对照阅读与调试。资源中完整呈现了插件从项目结构搭建到功能注册的实现脉络读者可据此学习事件监听、Action系统、Dialog与Popup创建、Swing组件应用以及资源引用等知识点并借助内置工具完成测试与调试。目前已有785人学习下载适合希望深入理解IDEA插件内部工作原理、提升IDE定制与增强能力的开发者参考。1. 从 idea插件详细源码demo.zip 说起一个压缩包背后到底藏着什么很多人第一次看到idea插件详细源码demo.zip这个文件名第一反应是「下载下来解压看看」结果打开一看目录里一堆build.gradle.kts、plugin.xml、src/main/kotlin完全不知道从哪下手。我在带新人的时候反复遇到这个场景他们能装 IntelliJ IDEA能跑 Java 项目但一碰到插件源码就卡住因为插件的运行方式和普通应用完全不同——它不是main方法启动而是被 IDE 宿主进程加载靠扩展点Extension Point挂载功能。这个标题真正指向的是一套可编译、可调试、可打包的 IntelliJ Platform 插件最小工程。它解决的核心问题是让你从「只会用插件」变成「能改插件、能写插件」。适合两类人——一是想给自己团队做内部效率工具的后端或全栈工程师二是想读懂某个开源插件实现细节、准备二次开发的人。热搜里频繁出现的 idea安装教程、idea社区版、intellij idea 这些词说明大量人卡在环境这一步而插件源码 demo 的价值恰恰在于它把环境、依赖、扩展点注册、运行配置这四件事一次性摊开给你看。接下来我不讲空泛概念直接按「拿到压缩包之后怎么跑起来、怎么改、怎么打包」这条线走一遍。2. 拆开压缩包IntelliJ 插件工程的标准骨架与扩展点机制2.1 一个能跑的插件工程最少需要哪些文件把idea插件详细源码demo.zip解压后你会看到类似这样的结构不同模板略有差异但核心文件固定idea-plugin-demo/ ├── build.gradle.kts ├── settings.gradle.kts ├── gradle.properties └── src/ └── main/ ├── kotlin/ │ └── com/example/demo/ │ └── DemoAction.kt └── resources/ └── META-INF/ └── plugin.xml这里面真正决定「插件能不能被 IDE 识别」的只有两个文件plugin.xml和build.gradle.kts。前者是插件的身份证加功能注册表后者决定编译目标平台版本和依赖。DemoAction.kt只是业务逻辑载体换成 Java 也完全一样。plugin.xml的最小内容长这样idea-plugin idcom.example.demo/id nameDemo Plugin/name vendorexample/vendor dependscom.intellij.modules.platform/depends actions action idcom.example.demo.HelloAction classcom.example.demo.DemoAction textSay Hello descriptionDemo action add-to-group group-idToolsMenu anchorfirst/ /action /actions /idea-plugin逻辑说明id是插件唯一标识打包后不能和已有插件冲突depends声明依赖的平台模块com.intellij.modules.platform是所有 IDE 都有的基础模块如果你要用 Java 相关能力还得加com.intellij.modules.javaactions里注册了一个菜单动作add-to-group把它挂到 Tools 菜单第一位。参数上最容易翻车的是group-id写错了菜单里根本不会出现而且 IDE 不会报错属于典型的「玄学不生效」。2.2 扩展点到底是什么从 Action 到 Service 的注册逻辑IntelliJ 插件的本质是「往宿主 IDE 的扩展点里塞实现」。扩展点由平台或其它插件定义你的插件通过plugin.xml声明「我要往这个点注册一个类」。最常见的三类扩展点用途注册位置com.intellij.action菜单/工具栏动作actionscom.intellij.service全局单例服务applicationServicecom.intellij.projectService项目级服务projectServiceAction 是最直观的入口。一个 Action 类继承AnAction重写actionPerformedclass DemoAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val project e.project ?: return Messages.showInfoMessage(project, Hello from demo plugin, Demo) } }逻辑说明e.project在当前没有打开项目时可能为 null所以必须先判空再使用否则会抛 NPE 并且 IDE 只会在日志里记一笔界面上毫无反应。Messages.showInfoMessage是平台提供的弹窗工具比直接JOptionPane更符合 IDE 风格。参数上AnAction的无参构造在较新平台版本里已经推荐用AnAction()配合Presentation动态设置文本而不是在plugin.xml里写死text这样能支持国际化。Service 的注册稍微不同需要在plugin.xml里写extensions defaultExtensionNscom.intellij applicationService serviceImplementationcom.example.demo.DemoService/ /extensions然后在代码里用ApplicationManager.getApplication().getService(DemoService::class.java)获取。这里有个血泪经验Service 构造函数里不要做耗时操作因为它在 IDE 启动阶段就可能被实例化拖慢启动会被用户骂。2.3 用 Gradle 把工程跑起来的最小命令环境准备只做三件事装 JDK 172022.3 之后平台要求 17、装 IntelliJ IDEA 社区版或 Ultimate、确认 Gradle 能联网拉依赖。然后在工程根目录执行./gradlew runIde这条命令会启动一个「沙箱 IDE」——一个独立的 IDEA 实例里面只装了你当前开发的插件。第一次执行会下载对应版本的平台 SDK体积大概几百 MB国内网络环境下建议配好镜像。参数上runIde默认用的 IDE 版本由build.gradle.kts里的intellij { version.set(2023.2) }决定你可以改成自己日常用的版本但不要跨太多大版本否则 API 不兼容。如果只想编译不启动用./gradlew buildPlugin产物在build/distributions/下是一个 zip可以直接通过 IDE 的「Install Plugin from Disk」安装。注意buildPlugin不会帮你验证扩展点是否写对只有runIde启动后看菜单里有没有出现你的 Action才算真正跑通。3. 从 demo 到可用插件改代码、调 UI、接项目上下文的完整路径3.1 把 Action 挂到编辑器右键菜单并读取当前文件Tools 菜单只是练手真实需求往往是「在编辑器里选中一段代码右键做点什么」。这需要把 Action 挂到EditorPopupMenu并且用UpdateAction控制可用状态action idcom.example.demo.FormatAction classcom.example.demo.FormatAction textFormat Selection add-to-group group-idEditorPopupMenu anchorlast/ /action对应实现class FormatAction : AnAction() { override fun update(e: AnActionEvent) { val editor e.getData(CommonDataKeys.EDITOR) e.presentation.isEnabled editor?.selectionModel?.hasSelection() true } override fun actionPerformed(e: AnActionEvent) { val editor e.getData(CommonDataKeys.EDITOR) ?: return val selected editor.selectionModel.selectedText ?: return // 这里替换成你自己的处理逻辑 val result selected.uppercase() WriteCommandAction.runWriteCommandAction(e.project) { editor.document.replaceString( editor.selectionModel.selectionStart, editor.selectionModel.selectionEnd, result ) } } }逻辑说明update方法每次菜单弹出前都会被调用用来决定这个 Action 是否可点。CommonDataKeys.EDITOR是从事件里取当前编辑器实例的标准方式。修改文档必须包在WriteCommandAction里否则会抛AssertionError: Must not change document outside command这是新手最常见的翻车点之一。参数上selectionStart和selectionEnd是文档偏移量不是行列号别搞混。3.2 用 DialogWrapper 做一个带输入框的配置弹窗很多插件需要用户填参数平台提供了DialogWrapper基类比手写 Swing 省事class ConfigDialog(private val project: Project) : DialogWrapper(project) { private val input JTextField(default, 20) init { title Plugin Config init() } override fun createCenterPanel(): JComponent { val panel JPanel(GridLayout(2, 2)) panel.add(JLabel(Prefix:)) panel.add(input) return panel } fun getValue(): String input.text }调用处val dialog ConfigDialog(project) if (dialog.showAndGet()) { val value dialog.getValue() // 使用 value }逻辑说明init()必须在设置title之后调用否则标题不生效。showAndGet()返回 true 表示用户点了 OKfalse 表示取消。参数上JTextField的列数只是建议宽度实际布局由 LayoutManager 决定。注意不要在 Dialog 里直接操作文档或 PSI应该把值返回给调用方在命令里统一处理否则容易出现线程问题。3.3 读取 PSI让插件理解代码结构而不只是文本如果插件要做代码分析、生成、重构就必须接触 PSIProgram Structure Interface。以读取当前 Java 文件里的类名为例val psiFile e.getData(CommonDataKeys.PSI_FILE) ?: return val javaFile psiFile as? PsiJavaFile ?: return val classes javaFile.classes classes.forEach { cls - println(Class: ${cls.name}, methods: ${cls.methods.size}) }逻辑说明PSI_FILE和EDITOR是两个不同的 DataKey前者给你语法树后者给你文本和光标。PsiJavaFile只在 Java 文件上成立Kotlin 文件要用KtFile所以类型判断不能省。参数上cls.methods返回的是PsiMethod数组包含构造器以外的所有方法构造器要用cls.constructors。这一步是插件从「文本工具」升级为「代码工具」的分水岭也是热搜里代码诊断插件这类需求的底层能力。4. 避坑与排查插件开发里那些 IDE 不报错的沉默失败4.1 Action 菜单里不出现现象runIde启动后Tools 菜单里找不到你注册的 Action日志里也没有明显报错。原因九成是plugin.xml里id重复、group-id拼写错误或者class路径和实际包名不一致。平台对扩展点注册失败的处理是「静默跳过」不会弹窗。解决打开沙箱 IDE 的Help - Show Log in Explorer看idea.log里有没有PluginException或Cannot create extension。另外用CtrlShiftA搜索 Action 的text如果能搜到说明注册成功只是菜单挂载位置不对。4.2 修改文档报 Must not change document outside command现象在 Action 里直接editor.document.insertString(...)控制台抛断言错误。原因IntelliJ 的文档模型要求所有写操作在命令上下文中执行以便支持撤销重做。解决用WriteCommandAction.runWriteCommandAction(project) { ... }包起来。如果是 PSI 修改用WriteCommandAction配合CodeStyleManager或PsiDocumentManager。4.3 插件在社区版能跑Ultimate 上报 ClassNotFound现象依赖了 Java 相关 API在社区版正常换 Ultimate 或反过来就崩。原因不同 IDE 产品包含的平台模块不同com.intellij.modules.java只在支持 Java 的 IDE 里存在。解决在plugin.xml的depends里明确声明所需模块并在build.gradle.kts里用intellij { plugins.set(listOf(java)) }引入对应依赖。不要假设所有 IDE 都一样。4.4 runIde 启动极慢或卡在下载现象第一次./gradlew runIde卡在Downloading intellij...很久。原因Gradle 需要从 JetBrains 仓库拉取完整 IDE 分发包体积大且默认源在境外。解决在build.gradle.kts里配置intellij { version.set(2023.2); type.set(IC) }IC 是社区版比 Ultimate 小很多。同时确认 Gradle 的repositories里加了maven { url uri(https://cache-redirector.jetbrains.com/intellij-dependencies) }这类重定向地址能显著提速。4.5 打包后安装提示插件不兼容现象buildPlugin产物在目标 IDE 上安装时提示Plugin is not compatible with the current version。原因plugin.xml里的idea-version since-build.../和实际 IDE build 号不匹配或者build.gradle.kts里编译用的平台版本远高于目标 IDE。解决用目标 IDE 的Help - About查看 build 号如 232.8660.185把since-build设成对应值。更稳妥的做法是编译时就用目标 IDE 版本作为intellij.version避免 API 差异。5. 进阶技巧用 Gradle 任务链把「改-跑-验-包」压成一条命令走到这里你已经能改代码、能跑沙箱、能打包。但每次改完都要手动runIde、手动点菜单验证效率很低。我自己的习惯是在build.gradle.kts里加一个组合任务把编译、测试、打包串起来tasks.register(verifyPlugin) { dependsOn(buildPlugin) doLast { val dist layout.buildDirectory.dir(distributions).get().asFile val zips dist.listFiles { f - f.extension zip } require(!zips.isNullOrEmpty()) { No plugin zip produced } println(Plugin package ready: ${zips.first().name}) } }逻辑说明dependsOn(buildPlugin)保证先出包doLast里做一次存在性校验避免打包失败但命令返回成功。参数上layout.buildDirectory是 Gradle 7 推荐的路径 API比硬编码build/更安全。再进一步如果你要验证插件在真实 IDE 里的行为可以写一个简单的集成测试用BasePlatformTestCase跑 PSI 相关逻辑class DemoPsiTest : BasePlatformTestCase() { fun testClassCount() { val file myFixture.configureByText( Test.java, class A {} class B {} ) as PsiJavaFile assertEquals(2, file.classes.size) } }逻辑说明BasePlatformTestCase会启动一个轻量平台环境myFixture提供文件配置和编辑器操作。参数上configureByText的第一个参数决定文件类型第二个是内容。这类测试跑起来比runIde快得多适合在 CI 里做回归。最后一个我踩过的坑不要把所有逻辑都塞进 Action 类。Action 应该只负责取上下文和调 Service业务逻辑放 Service 或独立工具类这样单元测试不用启动平台就能跑。我现在的习惯是 Action 里不超过 20 行超过就拆。插件开发最反直觉的地方在于——它看起来是 GUI 编程实际上核心是扩展点注册和线程模型把这两件事理顺剩下的就是普通 Kotlin/Java 业务代码。希望帮到你。本文还有配套的精品资源点击获取