简介:这是一份面向IntelliJ IDEA插件开发初学者与进阶者的详细源码示例,围绕插件结构、事件监听、Action系统、Dialog与Popup交互以及Swing组件应用等核心知识点展开,帮助开发者在较短时间内理解IDE扩展机制并上手实践。压缩包共16个文件,约10KB,以java源码与xml配置为主,辅以svg图标、iml模块文件及gitignore等工程辅助文件,分别承载插件逻辑实现、组件注册、界面资源与项目配置等职责,目录组织清晰,便于按模块研读。目前已有785人学习下载。通过研究该demo,读者可掌握菜单项注册、鼠标右键数据交互、弹出框定制等常见交互功能的实现思路,理解项目配置文件与资源管理方式,并借助内置工具完成插件的测试与调试,从而系统提升插件开发能力。
1. 从一份能跑通的 IDEA 插件源码说起:菜单、弹窗、右键交互到底怎么串起来
很多人第一次写 IntelliJ IDEA 插件,卡住的地方不是 Java 语法,而是不知道一个 Action 从注册到被点击、再到弹出对话框,中间到底经过哪些文件。这份ideaPluginProject源码 demo 的价值就在这:它把「相关菜单」「弹出框」「鼠标右键数据交互」这三类最常见的交互入口,用一份能直接导入 IDE 的工程串了起来。你拿到的是一个标准 Gradle/IDEA 插件工程结构,src下是业务代码,resources/META-INF下是plugin.xml和两套图标,.idea与.iml负责工程识别。适合已经会写 Java、想快速把插件跑起来的人,也适合想搞清楚plugin.xml里每个标签到底管什么的人。下面按「结构 → 注册 → 交互 → 排错 → 进阶」的顺序拆,每一步都能对着源码复现。
2. 工程结构与 plugin.xml:插件能被 IDE 认出来的最小闭环
2.1 目录里每个文件到底管什么
先把压缩包解开,对照下面这张表看,能省掉大量「这个文件能不能删」的犹豫。
| 路径 | 作用 | 能不能动 |
|---|---|---|
src/ | Java 源码,Action、Dialog、工具类都在这 | 核心,随便改 |
resources/META-INF/plugin.xml | 插件描述文件,注册 Action、依赖、版本 | 核心,改错直接不加载 |
resources/META-INF/pluginIcon.svg | 亮色主题图标 | 可替换 |
resources/META-INF/pluginIcon_dark.svg | 暗色主题图标 | 可替换 |
ideaPluginProject.iml | 模块配置,声明 SDK 和依赖 | 一般不动 |
.idea/ | 工作区配置,含 artifacts、modules | 不建议手改 |
.idea/artifacts/ideaPluginProject_jar.xml | 打包产物定义 | 打包相关,谨慎 |
plugin.xml是整个插件的入口清单。IDE 启动时扫描这个文件,把里面声明的 Action、扩展点挂到对应位置。源码里pluginIcon.svg和pluginIcon_dark.svg成对出现,是因为新版 IDE 会根据主题自动切换,只放一个在暗色主题下会显示异常,这是很多人第一次提交插件时被审核打回的原因。
2.2 一个 Action 从声明到可点击
插件里「相关菜单」和「右键菜单」本质都是 Action。区别只在注册时挂到哪个group。看下面这段典型注册:
<!-- resources/META-INF/plugin.xml --> <idea-plugin> <id>com.rcc.ideaPluginProject</id> <name>IdeaPluginDemo</name> <vendor>rcc</vendor> <depends>com.intellij.modules.platform</depends> <actions> <!-- 挂到主菜单 Tools 下 --> <action id="com.rcc.demo.HelloAction" class="com.rcc.action.HelloAction" text="Say Hello" description="弹出问候对话框"> <add-to-group group-id="ToolsMenu" anchor="first"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl alt H"/> </action> <!-- 挂到编辑器右键菜单 --> <action id="com.rcc.demo.RightClickAction" class="com.rcc.action.RightClickAction" text="Process Selection" description="处理选中的文本"> <add-to-group group-id="EditorPopupMenu" anchor="last"/> </action> </actions> </idea-plugin>id必须全局唯一,建议用包名倒序;class指向继承AnAction的实现类;add-to-group决定它出现在哪,ToolsMenu是顶部 Tools 菜单,EditorPopupMenu就是编辑器里右键弹出的那一层。anchor控制插入位置,first/last最省事。keyboard-shortcut里的$default表示默认键位方案,写死keymap名在别人机器上可能不生效。
提示:改完
plugin.xml一定要重新加载插件或重启沙箱 IDE,热部署对 Action 注册不生效,这是最常见的「我明明改了却没反应」。
2.3 用沙箱把插件跑起来
IDEA 插件开发不需要你装一个独立 IDE,它自带沙箱运行配置。操作路径是:打开工程 → 右侧 Gradle 面板 →Tasks > intellij > runIde,或者直接点运行配置里的Run Plugin。第一次会下载一个对应版本的 IDE 沙箱,耐心等。
# 命令行方式,等价于点 runIde ./gradlew runIde # 只编译不启动沙箱,用来快速验证语法 ./gradlew buildPluginrunIde会拉起一个全新的 IDE 实例,你注册的菜单和右键项只在这个沙箱里出现,不会污染你日常用的 IDE。buildPlugin产出的是可分发的 zip,在build/distributions下。判断插件是否被正确加载,看沙箱 IDE 启动日志里有没有你的插件名,没有就是plugin.xml写错了。
3. 菜单、弹窗与右键数据交互:三类交互的代码落地
3.1 AnAction 里拿到当前上下文
Action 被点击时,actionPerformed会收到一个AnActionEvent,所有上下文都从它身上取。下面是一个能拿到当前编辑器、选中文本、当前项目的完整写法:
public class RightClickAction extends AnAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { // 当前项目,可能为 null(比如欢迎页触发) Project project = e.getProject(); // 当前编辑器,右键菜单里一般不为 null Editor editor = e.getData(CommonDataKeys.EDITOR); if (project == null || editor == null) { return; } // 选中的文本 String selected = editor.getSelectionModel().getSelectedText(); if (selected == null || selected.isEmpty()) { Messages.showInfoMessage(project, "没有选中任何文本", "提示"); return; } // 处理选中内容 String result = selected.toUpperCase(); Messages.showInfoMessage(project, "处理结果:" + result, "完成"); } @Override public void update(@NotNull AnActionEvent e) { // 控制菜单项是否可点、是否可见 Editor editor = e.getData(CommonDataKeys.EDITOR); boolean hasSelection = editor != null && editor.getSelectionModel().hasSelection(); e.getPresentation().setEnabledAndVisible(hasSelection); } }actionPerformed是点击后的逻辑,update是每次菜单弹出前调用的,用来决定这一项灰不灰、显不显。很多人只写actionPerformed,结果没选中文本时菜单项也能点,点完报空指针,这就是漏了update。CommonDataKeys.EDITOR是取编辑器的标准姿势,别去用FileEditorManager绕一圈,右键场景下前者更直接。
3.2 自定义 Dialog 与 Popup 的选型
「弹出框」在 IDEA 插件里有两套东西,别混。DialogWrapper是模态对话框,适合要用户填表单、点确定的场景;JBPopupFactory是轻量气泡,适合展示信息或做快速选择。源码 demo 里两种都有涉及,选型看交互重量。
public class MyDialog extends DialogWrapper { private final JTextField input = new JTextField(20); protected MyDialog(Project project) { super(project); setTitle("输入内容"); init(); // 必须调用,否则界面不显示 } @Override protected JComponent createCenterPanel() { JPanel panel = new JPanel(new BorderLayout()); panel.add(new JLabel("请输入:"), BorderLayout.WEST); panel.add(input, BorderLayout.CENTER); return panel; } public String getInput() { return input.getText(); } }DialogWrapper的坑集中在init():不调用它,createCenterPanel返回的界面根本不会渲染,你会得到一个空白窗口还找不到原因。createCenterPanel只负责中间区域,按钮区由基类自动生成,想改按钮文案重写createActions。
// 轻量气泡,适合展示结果 JBPopupFactory.getInstance() .createHtmlTextBalloonBuilder("<b>处理完成</b>", MessageType.INFO, null) .setFadeoutTime(3000) .createBalloon() .show(RelativePoint.getCenterOf(editor.getComponent()), Balloon.Position.above);气泡用createHtmlTextBalloonBuilder,setFadeoutTime控制自动消失毫秒数,show的锚点用编辑器组件中心,位置比硬编码坐标稳。模态对话框会阻塞用户操作,气泡不会,展示类信息优先用气泡。
3.3 右键菜单的数据回传
右键交互的完整链路是:用户在编辑器选中文本 → 右键 → 点你的菜单项 → Action 拿到选中内容 → 处理 → 结果回显。回显有两种常见做法,一是上面用的Messages.showInfoMessage,二是把结果写回编辑器或弹出自定义 Dialog。
// 把处理结果替换回编辑器 WriteCommandAction.runWriteCommandAction(project, () -> { Document doc = editor.getDocument(); doc.replaceString( editor.getSelectionModel().getSelectionStart(), editor.getSelectionModel().getSelectionEnd(), result ); });写回编辑器必须包在WriteCommandAction里,直接改Document会抛AssertionError,这是 IDEA 的写保护机制,不是 bug。replaceString的起止位置从SelectionModel取,别自己算偏移量,多光标场景下会算错。
4. 避坑与排查:插件加载失败、Action 不显示、沙箱报错
4.1 插件在沙箱里根本没加载
现象:runIde起来了,但菜单里找不到你的项,日志也没有插件名。原因通常是plugin.xml的<id>与build.gradle里的pluginGroup不一致,或者depends写了一个沙箱版本不支持的模块。解决:把<id>改成和pluginGroup完全一致,depends先用com.intellij.modules.platform这个最基础的,确认能加载后再加别的。
4.2 Action 显示了但一直是灰的
现象:菜单项能看到,但点不动。原因基本都在update方法里,setEnabledAndVisible传了false,或者取Editor时用了错误的DataKey。解决:在update里打日志确认editor是否为 null,右键场景用CommonDataKeys.EDITOR,主菜单场景可能取不到编辑器,要改用e.getData(CommonDataKeys.PROJECT)判断。
4.3 图标在暗色主题下看不见
现象:亮色主题正常,切到暗色主题图标变黑块或消失。原因是只提供了pluginIcon.svg,没提供pluginIcon_dark.svg,或者两个文件内容一样但颜色写死。解决:两个文件都放,暗色版用浅色描边,plugin.xml里不用额外声明,IDE 按文件名自动匹配。
4.4 改 Document 抛 AssertionError
现象:右键处理完想把结果写回编辑器,控制台报AssertionError: Must not change document outside command。原因是没包WriteCommandAction。解决:所有对Document的写操作都套一层WriteCommandAction.runWriteCommandAction(project, () -> {...}),这是硬性要求。
4.5 沙箱启动卡在下载
现象:第一次runIde长时间停在下载 IDE 沙箱。原因是默认下载源慢或版本号写得太具体。解决:在build.gradle里把intellij { version.set("2023.1") }换成一个你本地已装的大版本,或者用localPath指向本地 IDE 安装目录,跳过下载。
5. 进阶:把 demo 改成自己的插件并验证打包产物
5.1 从 demo 派生一个新插件的最小改动集
拿到这份源码后,别急着大改,先做最小改动验证链路通不通。改三处:plugin.xml里的<id>、<name>、<vendor>;build.gradle里的pluginGroup和version;src下包名com.rcc重构成你自己的。改完跑一次runIde,确认沙箱里插件名变了、菜单项还在,说明派生成功。这一步不做,后面出问题你分不清是 demo 本身的问题还是你改出来的问题。
5.2 打包产物怎么验证
./gradlew buildPlugin之后,产物在build/distributions/下,是一个 zip。验证方法不是解压看,而是直接拿沙箱 IDE 的「从磁盘安装插件」功能装这个 zip,重启后看功能是否正常。这一步能暴露很多runIde阶段发现不了的问题,比如资源文件没被打进去、plugin.xml里的路径大小写不一致。
# 打包 ./gradlew buildPlugin # 查看产物内容,确认 plugin.xml 和图标都在 unzip -l build/distributions/*.zip | grep -E "plugin.xml|pluginIcon"unzip -l列出压缩包内容,重点确认META-INF/plugin.xml和两个图标在不在。资源文件缺失是打包阶段最常见的翻车点,runIde时资源从源码目录读,打包后从 jar 里读,路径处理稍有不同就会丢。
5.3 一个我每次都会走的验证习惯
插件开发最坑的地方在于「沙箱里好好的,装到正式 IDE 就崩」。我现在的习惯是:任何一次改动,先runIde验证交互,再buildPlugin打包,最后把 zip 装进一个干净的 IDE 实例跑一遍核心功能。三步都过才算完成,少一步都可能把问题带到用户那边。这份 demo 的结构足够干净,适合拿来当这个流程的起点——把它的 Action 换成你自己的逻辑,把 Dialog 换成你的表单,右键链路原样保留,基本不会在框架层面踩坑。希望这份拆解帮到你,少走几次「明明能跑却装不上」的弯路。
本文还有配套的精品资源,点击获取