Java IDE 插件开发(IntelliJ IDEA Plugin)
IntelliJ Platform 是 JetBrains 旗下所有 IDE(IntelliJ IDEA、PyCharm、WebStorm 等)的底层平台。通过开发插件,可以扩展 IDE 的功能,满足特定开发场景的需求。本文档系统介绍 IntelliJ IDEA 插件开发的核心概念与实践方法。
1. IntelliJ Platform 概述
1.1 插件开发环境搭建
1.1.1 开发工具准备
开发 IntelliJ 插件需要安装 IntelliJ IDEA Ultimate 或 Community Edition(推荐 Ultimate,因为 Community 版不包含部分 Java 调试功能)。所有 JetBrains IDE 共享同一底层平台,插件可跨 IDE 运行,但须声明兼容性。
1.1.2 Plugin DevKit 方式
Plugin DevKit 是 IntelliJ 内置的插件开发框架,适合快速原型开发:
- 在 IntelliJ IDEA 中创建新项目,选择 IDE Plugin 模板。
- 项目自动生成 plugin.xml 配置文件和基础项目结构。
- 通过 Run Plugin 运行配置(即 runIde 任务)启动测试 IDE 实例。
1.1.3 Gradle IntelliJ Plugin 方式(推荐生产使用)
基于 Gradle 的构建方式是官方推荐的生产级方案,支持依赖管理、版本控制、CI/CD 集成。
在 build.gradle.kts 中配置:
plugins {
id("java")
id("org.jetbrains.intellij") version "1.17.4"
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
// IntelliJ Platform 配置
intellij {
pluginName.set("MyPlugin")
version.set("2023.3") // 目标 IDE 版本
type.set("IC") // IC=Community, IU=Ultimate
plugins.set(listOf(
"com.intellij.java", // Java 语言支持
"org.jetbrains.kotlin" // Kotlin 支持
))
}
// 运行配置 - 启动测试 IDE 实例
tasks {
runIde {
ideDir.set(file("C:\\Program Files\\JetBrains\\IntelliJ IDEA 2023.3"))
jvmArgs("-Xmx2G")
}
patchPluginXml {
sinceBuild.set("233")
untilBuild.set("243.*")
}
}
dependencies {
implementation("com.google.gson:gson:2.10.1")
testImplementation("junit:junit:4.13.2")
}常用 Gradle 任务:
| 任务 | 说明 |
|---|---|
runIde | 启动测试 IDE 实例,加载当前插件 |
buildPlugin | 构建可发布的插件 ZIP 包 |
publishPlugin | 发布插件到 JetBrains Marketplace |
verifyPlugin | 验证插件配置和结构完整性 |
1.1.4 plugin.xml 基础配置
每个插件都需要 META-INF/plugin.xml 作为插件描述文件:
<idea-plugin>
<id>com.example.myplugin</id>
<name>My Plugin</name>
<version>1.0.0</version>
<vendor email="support@example.com" url="https://example.com">Example Inc.</vendor>
<description><![CDATA[
<h1>My Plugin</h1>
这是一个用于简化日常开发的 IntelliJ 插件。
]]></description>
<change-notes><![CDATA[
<h3>v1.0.0</h3>
<ul>
<li>初始版本发布</li>
<li>支持代码自动补全增强</li>
</ul>
]]></change-notes>
<!-- 依赖声明 -->
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.java</depends>
<!-- 扩展点注册 -->
<extensions defaultExtensionNs="com.intellij">
<toolWindow factoryClass="com.example.MyToolWindowFactory" id="MyToolWindow" />
</extensions>
<!-- 动作注册 -->
<actions>
<action id="MyAction" class="com.example.MyAction" text="My Action">
<add-to-group group-id="ToolsMenu" anchor="first"/>
<keyboard-shortcut keymap="$default" first-keystroke="control alt M"/>
</action>
</actions>
</idea-plugin>1.1.5 runIde 调试
右键 build.gradle.kts > Run > 选择 runIde 任务。此时会启动一个新的 IDE 实例,其中已加载并启用当前开发的插件。可以在插件代码中设置断点进行调试。
Java 9 模块系统注意事项:
// gradle.properties 中配置 JVM 参数
org.gradle.jvmargs=--add-exports=java.base/jdk.internal.vm=ALL-UNNAMED1.2 插件项目结构
一个标准的 IntelliJ 插件项目结构如下:
my-plugin/
├── build.gradle.kts # Gradle 构建脚本
├── settings.gradle.kts # Gradle 设置
├── gradle.properties # Gradle 属性配置
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/plugin/
│ │ ├── actions/ # Action 动作类
│ │ ├── inspections/ # 代码检查
│ │ ├── listeners/ # 监听器
│ │ ├── tools/ # ToolWindow 工具窗口
│ │ ├── psi/ # PSI 相关工具
│ │ └── ui/ # UI 组件(Dialog、Panel)
│ ├── resources/
│ │ ├── META-INF/
│ │ │ ├── plugin.xml # 插件描述符(必需)
│ │ │ └── pluginIcon.svg # 插件图标(推荐)
│ │ ├── inspectionDescriptions/ # 检查项描述 HTML
│ │ └── messages/ # 国际化资源 bundle
│ └── kotlin/
│ └── com/example/plugin/ # Kotlin 源码(可选)
└── test/
└── src/
└── java/
└── com/example/plugin/ # 单元测试1.2.1 pluginIcon.svg
插件图标放置在 META-INF/pluginIcon.svg,尺寸建议为 40x40 或 80x80 的 SVG 图标。
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<rect width="40" height="40" rx="8" fill="#3278FF"/>
<text x="20" y="28" font-size="24" fill="white" text-anchor="middle" font-family="sans-serif">P</text>
</svg>1.3 插件类型
根据功能分类,IntelliJ 插件可以分为以下主要类型:
1.3.1 主题插件
修改 IDE 外观和配色方案:
<!-- 注册主题 -->
<extensions defaultExtensionNs="com.intellij">
<themeProvider id="my-dark-theme" path="/themes/my-dark-theme.theme.json"/>
<colorSchemeProvider path="/colorSchemes/my-scheme.xml"/>
</extensions>1.3.2 语言支持插件
为新的编程语言提供语法高亮、代码补全、导航等功能:
<extensions defaultExtensionNs="com.intellij">
<lang.syntaxHighlighterFactory
language="MyLanguage"
implementationClass="com.example.MySyntaxHighlighterFactory"/>
<lang.completionContributor
language="MyLanguage"
implementationClass="com.example.MyCompletionContributor"/>
<lang.parserDefinition
language="MyLanguage"
implementationClass="com.example.MyParserDefinition"/>
</extensions>1.3.3 工具插件
在 IDE 中添加工具窗口(ToolWindow),如数据库浏览器、HTTP 客户端等:
<extensions defaultExtensionNs="com.intellij">
<toolWindow
id="MyTool"
anchor="right"
icon="/icons/myTool.svg"
factoryClass="com.example.MyToolWindowFactory"/>
</extensions>1.3.4 框架集成插件
集成特定开发框架(如 Spring、MyBatis、Quarkus),提供导航、提示和代码生成功能。
1.3.5 代码分析插件
扩展 IDE 的代码检查能力,提供自定义的 Lint 规则和安全扫描:
<extensions defaultExtensionNs="com.intellij">
<localInspection
language="java"
groupPath="Java"
groupName="Custom Inspections"
displayName="My Custom Inspection"
enabledByDefault="true"
level="WARNING"
implementationClass="com.example.MyInspection"/>
</extensions>1.3.6 自定义组件插件
通过 Swing 和 IntelliJ UI Framework 扩展 IDE 界面组件,如自定义编辑器、表格视图等。
2. plugin.xml 配置详解
2.1 插件基本信息
plugin.xml 中的 <idea-plugin> 元素包含以下核心配置项:
<idea-plugin>
<!-- 唯一标识符,建议使用反向域名,发布后不可更改 -->
<id>com.example.myplugin</id>
<!-- 插件显示名称 -->
<name>My Plugin</name>
<!-- 版本号,建议遵循语义化版本规范 -->
<version>1.0.0</version>
<!-- 供应商信息 -->
<vendor
email="developer@example.com"
url="https://github.com/example/myplugin">
Example Corporation
</vendor>
<!-- 插件描述,支持 HTML 格式 -->
<description><![CDATA[
<h2>功能概述</h2>
<ul>
<li>自动代码补全增强</li>
<li>智能模板生成</li>
</ul>
]]></description>
<!-- 版本更新说明 -->
<change-notes><![CDATA[
<h3>1.0.0</h3>
<ul>
<li>Initial release</li>
</ul>
]]></change-notes>
<!-- 兼容的 IDE 版本范围 -->
<idea-version since-build="233" until-build="243.*"/>
</idea-plugin>2.2 依赖声明
通过 <depends> 标签声明插件对 IntelliJ 平台模块或其他插件的依赖:
<!-- 基础平台依赖(所有插件必需) -->
<depends>com.intellij.modules.platform</depends>
<!-- Java 语言支持(如需操作 Java PSI) -->
<depends>com.intellij.modules.java</depends>
<!-- 所有模块(插件可在所有 JetBrains IDE 中运行) -->
<depends>com.intellij.modules.all</depends>
<!-- 依赖其他插件 -->
<depends>com.intellij.database</depends>
<depends>org.jetbrains.kotlin</depends>
<depends>com.intellij.spring</depends>| 依赖标识 | 说明 |
|---|---|
com.intellij.modules.platform | 基础平台,所有插件必须声明 |
com.intellij.modules.java | Java 语言支持模块 |
com.intellij.modules.all | 跨 IDE 兼容 |
com.intellij.modules.xml | XML 支持模块 |
com.intellij.modules.vcs | 版本控制集成模块 |
com.intellij.modules.lang | 语言处理基础模块 |
com.intellij.spring | Spring 框架支持插件 |
org.jetbrains.kotlin | Kotlin 支持插件 |
2.3 扩展点注册
扩展点(Extension Point)是 IntelliJ 平台提供的插件集成接口。通过 <extensions> 注册自定义实现:
<extensions defaultExtensionNs="com.intellij">
<!-- 工具窗口 -->
<toolWindow
id="DatabaseBrowser"
anchor="right"
factoryClass="com.example.DatabaseBrowserFactory"/>
<!-- 代码检查 -->
<localInspection
language="JAVA"
shortName="MyInspection"
displayName="My Custom Inspection"
groupName="Custom"
implementationClass="com.example.MyInspection"
level="WARNING"
enabledByDefault="true"/>
<!-- 应用级别服务 -->
<applicationService
serviceInterface="com.example.MyService"
serviceImplementation="com.example.MyServiceImpl"/>
<!-- 项目级别服务 -->
<projectService
serviceImplementation="com.example.MyProjectService"/>
<!-- 项目打开时的启动活动 -->
<postStartupActivity
implementation="com.example.MyStartupActivity"/>
<!-- 编辑器操作处理 -->
<typedHandler
implementation="com.example.MyTypedHandler"/>
<!-- 文件类型注册 -->
<fileType
name="MyFile"
implementationClass="com.example.MyFileType"
fieldName="INSTANCE"
extensions="myf"/>
</extensions>2.3.1 defaultExtensionNs
defaultExtensionNs 指定扩展点的命名空间,常用值:
| 命名空间 | 说明 |
|---|---|
com.intellij | IntelliJ 平台核心扩展点 |
com.intellij.java | Java 语言插件扩展点 |
org.jetbrains.kotlin | Kotlin 插件扩展点 |
com.intellij.database | 数据库插件扩展点 |
2.4 动作注册
动作(Action)是用户通过菜单、工具栏或快捷键触发的操作:
<actions>
<!-- 定义动作并添加到菜单组 -->
<action
id="com.example.ShowHelloAction"
class="com.example.ShowHelloAction"
text="Say Hello"
description="Display a hello message"
icon="/icons/hello.svg">
<!-- 添加到菜单组 -->
<add-to-group
group-id="ToolsMenu"
anchor="first"/>
<!-- 添加快捷键 -->
<keyboard-shortcut
keymap="$default"
first-keystroke="control alt H"
second-keystroke="control H"/>
</action>
<!-- 分组动作 -->
<group
id="com.example.MyGroup"
text="My Plugin"
icon="/icons/group.svg"
popup="true">
<add-to-group group-id="MainMenu" anchor="after" relative-to-action="ToolsMenu"/>
<action
id="com.example.ActionA"
class="com.example.ActionA"
text="Action A"/>
<action
id="com.example.ActionB"
class="com.example.ActionB"
text="Action B"/>
<separator/>
<reference id="com.example.ShowHelloAction"/>
</group>
</actions>2.4.1 常用 group-id
| 组 ID | 位置 |
|---|---|
MainMenu | 主菜单栏 |
FileMenu | 文件菜单 |
EditMenu | 编辑菜单 |
ViewMenu | 查看菜单 |
NavigateMenu | 导航菜单 |
CodeMenu | 代码菜单 |
AnalyzeMenu | 分析菜单 |
RefactoringMenu | 重构菜单 |
ToolsMenu | 工具菜单 |
WindowMenu | 窗口菜单 |
HelpMenu | 帮助菜单 |
EditorPopupMenu | 编辑器右键菜单 |
ProjectViewPopupMenu | 项目视图右键菜单 |
ToolbarRunGroup | 工具栏运行组 |
2.4.2 anchor 位置
| anchor | 说明 |
|---|---|
first | 组内最前 |
last | 组内最后 |
before | 在指定动作之前 |
after | 在指定动作之后 |
2.5 应用级别与项目级别组件
2.5.1 应用级别组件(Application Service)
应用级别组件在 IDE 生命周期内只初始化一次,所有项目共享。适合管理全局配置、缓存和工具类。
<extensions defaultExtensionNs="com.intellij">
<!-- 基于接口的服务注册 -->
<applicationService
serviceInterface="com.example.GlobalCacheService"
serviceImplementation="com.example.GlobalCacheServiceImpl"/>
<!-- 基于类的服务注册 -->
<applicationService
serviceImplementation="com.example.AppSettingsManager"/>
</extensions>// Kotlin 示例:应用级别服务
class AppSettingsManager {
private val settings = mutableMapOf<String, String>()
fun getSetting(key: String): String? = settings[key]
fun setSetting(key: String, value: String) {
settings[key] = value
}
companion object {
fun getInstance(): AppSettingsManager =
ApplicationManager.getApplication().getService(AppSettingsManager::class.java)
}
}2.5.2 项目级别组件(Project Service)
项目级别组件在每个项目中单独初始化,适合管理项目相关的数据和状态。
<extensions defaultExtensionNs="com.intellij">
<projectService
serviceImplementation="com.example.ProjectBookmarkManager"/>
</extensions>// Java 示例:项目级别服务
public class ProjectBookmarkManager {
private final Project project;
private final List<String> bookmarks = new ArrayList<>();
public ProjectBookmarkManager(Project project) {
this.project = project;
}
public void addBookmark(String path) {
bookmarks.add(path);
}
public List<String> getBookmarks() {
return Collections.unmodifiableList(bookmarks);
}
public static ProjectBookmarkManager getInstance(Project project) {
return project.getService(ProjectBookmarkManager.class);
}
}3. Actions 与扩展点
3.1 AnAction 开发
AnAction 是 IntelliJ 平台中用户触发操作的基本单元。通过实现 AnAction 类并注册到 plugin.xml 中,可以在菜单、工具栏或快捷键中注册操作。
3.1.1 基本 AnAction 实现
// Java 示例
public class ShowHelloAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
// 获取当前项目实例
Project project = e.getProject();
if (project == null) return;
// 显示消息对话框
Messages.showInfoMessage(project,
"欢迎使用 My Plugin!",
"Hello Plugin");
}
@Override
public void update(@NotNull AnActionEvent e) {
// 动态控制动作的启用/禁用状态
Project project = e.getProject();
boolean enabled = project != null && !project.isDisposed();
e.getPresentation().setEnabledAndVisible(enabled);
}
}// Kotlin 示例
class ShowHelloAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
Messages.showInfoMessage(project, "欢迎使用 My Plugin!", "Hello Plugin")
}
override fun update(e: AnActionEvent) {
// 在后台线程执行更新
e.presentation.isEnabledAndVisible = e.project != null && !e.project!!.isDisposed
}
// 声明在后台线程执行 update(避免阻塞 EDT)
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}3.1.2 ActionUpdateThread 线程策略
从 IntelliJ Platform 2022.3 开始,必须显式声明 update 方法的执行线程:
| 枚举值 | 说明 |
|---|---|
ActionUpdateThread.EDT | 在 Event Dispatch Thread 执行,适合轻量更新 |
ActionUpdateThread.BGT | 在后台线程执行,适合需要读取文件或 PSI 的耗时操作 |
ActionUpdateThread.BGT_IN_EDT | 过渡模式,后台执行但某些场景回退到 EDT |
@Override
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}3.1.3 从 AnActionEvent 获取上下文
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
// 项目实例
Project project = e.getProject();
// 编辑器实例
Editor editor = e.getData(CommonDataKeys.EDITOR);
// 当前选中的文件
VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE);
// PSI 文件
PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
// 当前选中的元素
PsiElement psiElement = e.getData(CommonDataKeys.PSI_ELEMENT);
// 编辑器选中的文本
String selectedText = e.getData(CommonDataKeys.EDITOR)
.getSelectionModel()
.getSelectedText();
}3.2 ToolWindow 开发
ToolWindow(工具窗口)是 IDE 侧边栏、底部栏或顶部栏中的可停靠面板。
3.2.1 ToolWindowFactory 实现
// Java 示例:实现 ToolWindowFactory
public class MyToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// 创建内容管理器
ContentManager contentManager = toolWindow.getContentManager();
// 创建面板
SimpleToolWindowPanel panel = new SimpleToolWindowPanel(false, true);
// 添加树组件
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
DefaultMutableTreeNode child = new DefaultMutableTreeNode("Node 1");
root.add(child);
JTree tree = new JTree(root);
panel.setContent(new JScrollPane(tree));
// 创建内容并添加到窗口
Content content = ContentFactory.getInstance()
.createContent(panel, "Browser", false);
contentManager.addContent(content);
// 定时刷新
startPeriodicRefresh(project, toolWindow, panel);
}
private void startPeriodicRefresh(Project project, ToolWindow toolWindow, SimpleToolWindowPanel panel) {
Disposable disposable = () -> {};
// 使用 ApplicationManager 的定时任务
ApplicationManager.getApplication().invokeLater(() -> {
if (!project.isDisposed() && toolWindow.isAvailable()) {
// 刷新逻辑
panel.revalidate();
panel.repaint();
}
});
}
}// Kotlin 示例:ToolWindow 实现
class MyToolWindowFactory : ToolWindowFactory {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val contentManager = toolWindow.contentManager
val panel = SimpleToolWindowPanel(false, true)
// 构建树结构
val root = DefaultMutableTreeNode("Project Files")
val tree = JTree(root)
panel.setContent(JScrollPane(tree))
val content = ContentFactory.getInstance()
.createContent(panel, "Files", false)
contentManager.addContent(content)
// ToolWindow 定时刷新
val disposable = Disposer.newDisposable()
Disposer.register(project.disposable, disposable)
SimpleTimer.getInstance().setUp({
if (!project.isDisposed && toolWindow.isAvailable) {
// 重新加载树数据
reloadTreeData(project, root)
}
}, disposable, TimeUnit.SECONDS.toMillis(30))
}
private fun reloadTreeData(project: Project, root: DefaultMutableTreeNode) {
ApplicationManager.getApplication().executeOnPooledThread {
// 后台加载数据
root.removeAllChildren()
val nodes = loadProjectNodes(project)
nodes.forEach { root.add(it) }
// 切回 EDT 更新 UI
ApplicationManager.getApplication().invokeLater {
(root.children() as Enumeration<*>).toList()
}
}
}
private fun loadProjectNodes(project: Project): List<DefaultMutableTreeNode> {
// 模拟加载数据
return listOf(
DefaultMutableTreeNode("Controller"),
DefaultMutableTreeNode("Service"),
DefaultMutableTreeNode("Repository")
)
}
}3.2.2 plugin.xml 注册 ToolWindow
<extensions defaultExtensionNs="com.intellij">
<!-- 基础注册 -->
<toolWindow
id="MyToolWindow"
anchor="right"
icon="/icons/toolIcon.svg"
factoryClass="com.example.MyToolWindowFactory"/>
<!-- 带条件注册(仅特定项目类型可见) -->
<toolWindow
id="DatabaseExplorer"
anchor="left"
factoryClass="com.example.DatabaseExplorerFactory"
conditionClass="com.example.IsDatabaseProjectCondition"/>
</extensions>3.3 Listener 监听器
IntelliJ Platform 提供了丰富的监听器接口,用于响应 IDE 中的各种事件。
3.3.1 项目打开/关闭监听
// Kotlin 示例
class MyProjectManagerListener : ProjectManagerListener {
override fun projectOpened(project: Project) {
println("项目打开: ${project.name}")
// 初始化项目相关资源
}
override fun projectClosed(project: Project) {
println("项目关闭: ${project.name}")
// 清理资源
}
}<extensions defaultExtensionNs="com.intellij">
<projectManagerListener
implementation="com.example.MyProjectManagerListener"/>
</extensions>3.3.2 文件保存监听
// Java 示例
public class MyDocumentListener implements BulkFileListener {
@Override
public void before(@NotNull List<? extends VirtualFile> files) {
// 文件保存前触发
}
@Override
public void after(@NotNull List<? extends VirtualFile> files) {
// 文件保存后触发
for (VirtualFile file : files) {
if ("java".equals(file.getExtension())) {
// 对 Java 文件做后处理
System.out.println("文件已保存: " + file.getPath());
}
}
}
}<extensions defaultExtensionNs="com.intellij">
<fileDocumentManagerListener
implementation="com.example.MyDocumentListener"/>
</extensions>3.3.3 编辑器事件监听
// Kotlin 示例:编辑器按键事件监听
class MyTypedHandler : TypedHandlerDelegate() {
override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
// 当用户输入特定字符时触发
if (c == '.' && file.fileType.name == "JAVA") {
// 在 Java 文件中输入 "." 时触发自动补全
println("检测到点号输入,准备代码补全")
}
return Result.CONTINUE
}
}<extensions defaultExtensionNs="com.intellij">
<typedHandler implementation="com.example.MyTypedHandler"/>
</extensions>3.3.4 PsiTreeChangeListener
监听 PSI 树的变化,可用于实时更新缓存或触发校验:
// Java 示例
public class MyPsiTreeChangeListener implements PsiTreeChangeListener {
@Override
public void beforeChildAddition(@NotNull PsiTreeChangeEvent event) {
// 子节点添加前
}
@Override
public void afterChildAddition(@NotNull PsiTreeChangeEvent event) {
// 子节点添加后 - 例如,当在类中添加新方法时触发
PsiElement child = event.getChild();
if (child instanceof PsiMethod) {
System.out.println("新方法添加: " + ((PsiMethod) child).getName());
}
}
@Override
public void beforeChildRemoval(@NotNull PsiTreeChangeEvent event) {
// 子节点移除前
}
@Override
public void afterChildRemoval(@NotNull PsiTreeChangeEvent event) {
// 子节点移除后
}
@Override
public void beforeChildReplacement(@NotNull PsiTreeChangeEvent event) {
// 子节点替换前
}
@Override
public void afterChildReplacement(@NotNull PsiTreeChangeEvent event) {
// 子节点替换后
}
@Override
public void beforeChildMovement(@NotNull PsiTreeChangeEvent event) {
// 子节点移动前
}
@Override
public void afterChildMovement(@NotNull PsiTreeChangeEvent event) {
// 子节点移动后
}
@Override
public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
// 任意子节点变更
}
@Override
public void beforePropertyChange(@NotNull PsiTreeChangeEvent event) {
// 属性变更前
}
@Override
public void afterPropertyChange(@NotNull PsiTreeChangeEvent event) {
// 属性变更后
}
}// Kotlin 示例:注册 PsiTreeChangeListener
class MyPsiListenerRegistrar : ProjectActivity {
override suspend fun execute(project: Project) {
val messageBusConnection = project.messageBus.connect()
messageBusConnection.subscribe(
PsiTreeChangeListener.TOPIC,
object : PsiTreeChangeListener {
override fun afterChildAddition(event: PsiTreeChangeEvent) {
println("PsiTree 子节点已添加")
}
}
)
}
}<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="com.example.MyPsiListenerRegistrar"/>
</extensions>4. PSI(程序结构接口)
4.1 PSI 基础
PSI(Program Structure Interface,程序结构接口)是 IntelliJ Platform 的核心抽象,用于表示源代码的结构化视图。PSI 将源代码解析为树状结构,每个节点代表代码中的一个语法元素。
4.1.1 核心 PSI 类型
| 类型 | 说明 |
|---|---|
PsiFile | 代表一个源文件,是 PSI 树的根节点 |
PsiElement | 所有 PSI 节点的基类 |
PsiClass | 代表一个 Java/Kotlin 类、接口或枚举 |
PsiMethod | 代表一个方法或函数 |
PsiField | 代表一个字段或属性 |
PsiParameter | 代表方法参数 |
PsiStatement | 代表一条语句 |
PsiExpression | 代表一个表达式 |
PsiReference | 代表引用关系(如变量引用、方法调用) |
PsiIdentifier | 代表标识符 |
PsiComment | 代表注释 |
PsiAnnotation | 代表注解 |
PsiModifierList | 修饰符列表(public、static 等) |
PsiTypeElement | 类型元素 |
PsiImportStatement | 导入语句 |
PsiPackageStatement | 包声明语句 |
4.1.2 PSI 树与 AST 的关系
PSI 树构建在底层 AST(Abstract Syntax Tree,抽象语法树)之上。二者的关系如下:
源代码
↓ 词法分析
Token 流
↓ 语法分析
AST(抽象语法树) ← 底层语法表示
↓ PSI 封装
PSI 树 ← 高层语义表示,提供语言级别的 APIPSI 层相比 AST 的优势:
- 提供了面向开发者的语义 API(如
PsiClass.getMethods())。 - 支持跨文件引用解析(如
PsiReference.resolve())。 - 与 IntelliJ 的索引系统、代码分析系统深度集成。
4.1.3 从文件获取 PSI
// Java 示例
PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
if (psiFile instanceof PsiJavaFile) {
PsiJavaFile javaFile = (PsiJavaFile) psiFile;
PsiClass[] classes = javaFile.getClasses();
for (PsiClass clazz : classes) {
System.out.println("类名: " + clazz.getName());
System.out.println("包名: " + javaFile.getPackageName());
}
}// Kotlin 示例
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return
if (psiFile is PsiJavaFile) {
val packageName = psiFile.packageName
for (clazz in psiFile.classes) {
println("Class: ${clazz.name}")
println("Methods: ${clazz.methods.joinToString { it.name }}")
println("Fields: ${clazz.fields.joinToString { it.name }}")
println("Annotations: ${clazz.annotations.joinToString { it.qualifiedName }}")
// 遍历修饰符
val modifierList = clazz.modifierList
if (modifierList != null && modifierList.hasModifierProperty(PsiModifier.PUBLIC)) {
println("这是一个 public 类")
}
}
}4.2 PSI 遍历与搜索
4.2.1 PsiTreeUtil 遍历
// Java 示例:使用 PsiTreeUtil 遍历 PSI 树
import com.intellij.psi.util.PsiTreeUtil;
public class PsiTraversalExample {
public void traverseMethods(PsiFile file) {
// 递归查找所有 PsiMethod 节点
Collection<PsiMethod> methods = PsiTreeUtil.findChildrenOfType(file, PsiMethod.class);
for (PsiMethod method : methods) {
System.out.println("方法: " + method.getName()
+ " 返回值: " + method.getReturnType().getPresentableText());
}
}
public void findParentClass(PsiElement element) {
// 查找最近的父级 PsiClass
PsiClass containingClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (containingClass != null) {
System.out.println("所属类: " + containingClass.getQualifiedName());
}
}
public void collectAnnotations(PsiClass psiClass) {
// 收集类及其所有成员的注解
List<PsiAnnotation> annotations = new ArrayList<>();
annotations.addAll(Arrays.asList(psiClass.getAnnotations()));
for (PsiMethod method : psiClass.getMethods()) {
annotations.addAll(Arrays.asList(method.getAnnotations()));
}
annotations.forEach(a ->
System.out.println("注解: " + a.getQualifiedName())
);
}
}// Kotlin 示例:使用 Lambda 过滤
fun findPublicMethods(psiClass: PsiClass): List<PsiMethod> {
return psiClass.methods
.filter { it.hasModifierProperty(PsiModifier.PUBLIC) }
.filter { it.name.startsWith("get") || it.name.startsWith("set") }
.toList()
}
fun findDeprecatedClasses(project: Project): List<PsiClass> {
val scope = GlobalSearchScope.allScope(project)
return PsiTreeUtil.findChildrenOfType(
PsiManager.getInstance(project).findFile(
// 搜索范围中的某个文件
),
PsiClass::class.java
).filter { clazz ->
clazz.annotations.any { it.qualifiedName == "java.lang.Deprecated" }
}
}4.2.2 PsiSearchHelper 搜索
// Java 示例:使用 PsiSearchHelper 进行全局搜索
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.UsageSearchContext;
import com.intellij.psi.search.searches.ReferencesSearch;
public class SearchExample {
public void findUsages(PsiClass psiClass) {
// 使用 ReferencesSearch 查找引用
Collection<PsiReference> references = ReferencesSearch
.search(psiClass, GlobalSearchScope.projectScope(psiClass.getProject()))
.findAll();
for (PsiReference ref : references) {
System.out.println("引用位置: " + ref.getElement().getText());
}
}
public void searchByText(Project project, String keyword) {
PsiSearchHelper searchHelper = PsiSearchHelper.getInstance(project);
PsiSearchHelper.SearchCostResult cost = searchHelper.isCheapEnoughToSearch(keyword,
GlobalSearchScope.allScope(project), null, null);
if (cost == PsiSearchHelper.SearchCostResult.TOO_MANY_TO_SEARCH) {
System.out.println("搜索结果过多,需要缩小搜索范围");
return;
}
searchHelper.processUsagesInText(keyword,
GlobalSearchScope.projectScope(project),
(file, startOffset, endOffset) -> {
System.out.println("找到匹配: " + file.getName()
+ " 偏移: " + startOffset + "-" + endOffset);
return true; // 继续搜索
});
}
public void findImplementations(PsiClass interfaceClass) {
// 查找接口的所有实现类
Collection<PsiClass> implementations = new ArrayList<>();
JavaPsiFacade.getInstance(interfaceClass.getProject())
.findClasses(interfaceClass.getQualifiedName(), GlobalSearchScope.allScope(interfaceClass.getProject()));
// 通过 PsiClass 的 findImplementations 方法
Collection<PsiClass> allImpls = new ArrayList<>();
PsiClassType type = JavaPsiFacade.getElementFactory(interfaceClass.getProject())
.createType(interfaceClass);
// 使用 ClassInheritorsSearch
com.intellij.psi.search.searches.ClassInheritorsSearch
.search(interfaceClass)
.forEach(allImpls::add);
}
}// Kotlin 示例:Find Usages 集成
class UsageSearchExample {
fun findMethodUsages(method: PsiMethod) {
val usages = ReferencesSearch.search(method).findAll()
val usageGroups = usages.groupBy { it.element.containingFile }
usageGroups.forEach { (file, refs) ->
println("文件: ${file.name}")
refs.forEach { ref ->
val element = ref.element
println(" 行 ${element.textOffset} - ${element.text}")
}
}
}
fun searchForSymbol(project: Project, symbolName: String) {
val searchHelper = PsiSearchHelper.getInstance(project)
val scope = GlobalSearchScope.projectScope(project)
// 处理文本引用
searchHelper.processUsagesInText(symbolName, scope) { file, startOffset, endOffset ->
println("找到引用: ${file.name} 在偏移 $startOffset")
true
}
}
}4.3 PSI 修改
PSI 修改必须在 WriteCommandAction 中执行,以确保与 IntelliJ 的撤销(Undo)系统和索引机制兼容。
4.3.1 WriteCommandAction 基础
// Java 示例:WriteCommandAction 基本用法
public class PsiModificationExample {
public void addMethodToClass(Project project, PsiClass targetClass, String methodName) {
WriteCommandAction.runWriteCommandAction(project, () -> {
// 创建新方法
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
PsiMethod newMethod = factory.createMethod(methodName, PsiType.VOID);
// 添加方法体
String body = "{\n System.out.println(\"Hello from " + methodName + "\");\n}";
PsiCodeBlock codeBlock = factory.createCodeBlockFromText(body, null);
newMethod.getBody().replace(codeBlock);
// 添加 Javadoc 注释
PsiComment comment = factory.createCommentFromText("/**\n * " + methodName + " 方法\n */\n", null);
targetClass.addBefore(comment, targetClass.getLBrace());
// 将方法添加到类中
targetClass.add(newMethod);
});
}
public void removeField(PsiClass targetClass, String fieldName) {
Project project = targetClass.getProject();
WriteCommandAction.runWriteCommandAction(project, () -> {
PsiField field = targetClass.findFieldByName(fieldName, false);
if (field != null) {
field.delete();
}
});
}
public void modifyMethod(PsiMethod method, String newBody) {
Project project = method.getProject();
WriteCommandAction.runWriteCommandAction(project, () -> {
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
PsiCodeBlock codeBlock = factory.createCodeBlockFromText("{\n" + newBody + "\n}", null);
if (method.getBody() != null) {
method.getBody().replace(codeBlock);
}
});
}
}// Kotlin 示例:使用 WriteCommandAction
class PsiWriter(private val project: Project) {
fun addField(clazz: PsiClass, fieldName: String, fieldType: String) {
WriteCommandAction.runWriteCommandAction(project) {
val factory = JavaPsiFacade.getElementFactory(project)
val field = factory.createField(fieldName, factory.createTypeFromText(fieldType, null))
clazz.add(field)
// 在字段上添加 @Inject 注解
val annotation = factory.createAnnotationFromText("@Inject", null)
field.modifierList?.addAfter(annotation, null)
}
}
fun addImport(psiFile: PsiJavaFile, qualifiedName: String) {
WriteCommandAction.runWriteCommandAction(project) {
psiFile.importList?.add(
JavaPsiFacade.getElementFactory(project)
.createImportStatement(qualifiedName)
)
}
}
fun insertBeforeElement(anchor: PsiElement, newElement: PsiElement) {
WriteCommandAction.runWriteCommandAction(project) {
anchor.parent?.addBefore(newElement, anchor)
}
}
}4.3.2 代码模板创建
// Java 示例:从代码模板创建类
public class CodeTemplateExample {
public PsiClass createClassFromTemplate(Project project, String className) {
return WriteCommandAction.runWriteCommandAction(project, () -> {
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
// 创建类
PsiClass psiClass = factory.createClass(className);
// 添加私有字段
PsiField idField = factory.createField("id", PsiType.LONG);
idField.getModifierList().setModifierProperty(PsiModifier.PRIVATE, true);
psiClass.add(idField);
// 添加 getter 方法
PsiMethod getter = factory.createMethod("getId", PsiType.LONG);
String getterBody = "{ return this.id; }";
getter.getBody().replace(
factory.createCodeBlockFromText(getterBody, null)
);
psiClass.add(getter);
// 添加 setter 方法
PsiMethod setter = factory.createMethod("setId", PsiType.VOID);
PsiParameter param = factory.createParameter("id", PsiType.LONG);
setter.getParameterList().add(param);
String setterBody = "{ this.id = id; }";
setter.getBody().replace(
factory.createCodeBlockFromText(setterBody, null)
);
psiClass.add(setter);
return psiClass;
});
}
}5. 代码生成与检查
5.1 代码检查(LocalInspectionTool)
自定义代码检查允许在 IDE 中标记代码问题,并提供快速修复(QuickFix)。
5.1.1 基础 LocalInspectionTool 实现
// Java 示例:自定义代码检查
public class MyCustomInspection extends LocalInspectionTool {
@Override
public @NotNull String getDisplayName() {
return "检测未使用的私有方法";
}
@Override
public @NotNull String getShortName() {
return "UnusedPrivateMethod";
}
@NotNull
@Override
public String getGroupDisplayName() {
return "Custom Inspections";
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
public @NotNull HighlightDisplayLevel getDefaultLevel() {
// WARNING、ERROR、WEAK WARNING、INFO
return HighlightDisplayLevel.WARNING;
}
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);
// 只检查私有方法
if (!method.hasModifierProperty(PsiModifier.PRIVATE)) return;
// 排除构造方法、getter、setter
if (method.isConstructor()) return;
if (method.getName().startsWith("get") || method.getName().startsWith("set")) return;
// 检查方法是否被引用
if (method.getReference() == null || !isMethodUsed(method)) {
holder.registerProblem(
method.getNameIdentifier(),
"私有方法 '" + method.getName() + "' 未被使用",
ProblemHighlightType.LIKE_UNUSED_SYMBOL,
new DeleteMethodQuickFix(method)
);
}
}
private boolean isMethodUsed(PsiMethod method) {
// 通过引用搜索判断是否被使用
return ReferencesSearch.search(method).findAll().size() > 0;
}
};
}
// 快速修复实现 - 删除未使用的私有方法
private static class DeleteMethodQuickFix implements LocalQuickFix {
private final PsiMethod method;
public DeleteMethodQuickFix(PsiMethod method) {
this.method = method;
}
@Override
public @NotNull String getFamilyName() {
return "删除未使用的私有方法";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
WriteCommandAction.runWriteCommandAction(project, () -> {
// 删除方法前的注释
PsiElement firstChild = method.getFirstChild();
if (firstChild instanceof PsiComment) {
firstChild.delete();
}
method.delete();
});
}
}
}// Kotlin 示例:简化版代码检查
class MagicNumberInspection : LocalInspectionTool() {
override fun getDisplayName() = "检测魔法数字(Magic Number)"
override fun getShortName() = "MagicNumber"
override fun getGroupDisplayName() = "Custom Inspections"
override fun getDefaultLevel() = HighlightDisplayLevel.WARNING
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : JavaElementVisitor() {
override fun visitLiteralExpression(expression: PsiLiteralExpression) {
super.visitLiteralExpression(expression)
val value = expression.value
if (value is Int && value != 0 && value != 1 && value != -1) {
// 检查是否在常量声明或注解中
val parent = expression.parent
if (parent !is PsiVariable || parent.hasModifierProperty(PsiModifier.FINAL)) {
// 排除 final 变量初始化和注解参数
val grandParent = parent.parent
if (grandParent !is PsiAnnotation) {
holder.registerProblem(
expression,
"魔法数字 '${value}' 应定义为命名常量",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
ExtractConstantQuickFix(expression)
)
}
}
}
}
}
}
private class ExtractConstantQuickFix(
private val expression: PsiLiteralExpression
) : LocalQuickFix {
override fun getFamilyName() = "提取为常量"
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
// QuickFix 实现:将魔法数字提取为常量
WriteCommandAction.runWriteCommandAction(project) {
// 提取常量逻辑
}
}
}
}5.1.2 plugin.xml 注册检查
<extensions defaultExtensionNs="com.intellij">
<localInspection
language="JAVA"
shortName="UnusedPrivateMethod"
displayName="检测未使用的私有方法"
groupName="Custom Inspections"
groupPath="Java"
enabledByDefault="true"
level="WARNING"
implementationClass="com.example.MyCustomInspection"/>
<localInspection
language="JAVA"
shortName="MagicNumber"
displayName="检测魔法数字"
groupName="Custom Inspections"
enabledByDefault="false"
level="WEAK WARNING"
implementationClass="com.example.MagicNumberInspection"/>
</extensions>5.1.3 检查描述文件
在 resources/inspectionDescriptions/ 目录下创建与该检查 shortName 同名的 HTML 文件,提供详细的检查说明:
<!-- resources/inspectionDescriptions/UnusedPrivateMethod.html -->
<html>
<body>
<p>检测项目中未被使用的私有方法。私有方法如果没有任何调用方引用,应被移除或标记为待处理。</p>
<p><strong>示例:</strong></p>
<pre><code>
// 不好的写法:
private void helper() {
// 从未被调用
}
// 好的写法:移除未使用的方法
</code></pre>
<p>此检查提供了"删除未使用的私有方法"快速修复。</p>
</body>
</html>5.2 代码生成(无代码不生成)
IntelliJ 平台本身不主张在文档中生成完整代码。代码生成主要依赖模板机制和 PSI 操作。实际开发中通过上述 PSI 修改(WriteCommandAction + PsiElementFactory)在运行时动态生成代码。
5.3 意图操作(IntentionAction)
意图操作(Intention Action)在编辑器中通过 ALT+ENTER 快捷键触发,为用户提供上下文相关的代码改进建议。
5.3.1 基础 IntentionAction 实现
// Java 示例:意图操作
public class ConvertToLambdaIntention implements IntentionAction {
@Override
public @NotNull String getText() {
return "转换为 Lambda 表达式";
}
@Override
public @NotNull String getFamilyName() {
return "Lambda 转换";
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
// 仅在 Java 文件中可用
if (!(file instanceof PsiJavaFile)) return false;
// 获取光标位置的元素
int offset = editor.getCaretModel().getOffset();
PsiElement element = file.findElementAt(offset);
if (element == null) return false;
// 检查是否位于匿名内部类上
PsiAnonymousClass anonymousClass = PsiTreeUtil.getParentOfType(element, PsiAnonymousClass.class);
if (anonymousClass == null) return false;
// 检查是否实现了函数式接口
return isFunctionalInterface(anonymousClass);
}
private boolean isFunctionalInterface(PsiAnonymousClass anonymousClass) {
PsiClassType[] interfaces = anonymousClass.getBaseClassTypes();
if (interfaces.length != 1) return false;
PsiClass interfaceClass = interfaces[0].resolve();
return interfaceClass != null && hasSingleAbstractMethod(interfaceClass);
}
private boolean hasSingleAbstractMethod(PsiClass psiClass) {
return Arrays.stream(psiClass.getMethods())
.filter(m -> m.hasModifierProperty(PsiModifier.ABSTRACT))
.count() == 1;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
// 执行意图操作
int offset = editor.getCaretModel().getOffset();
PsiElement element = file.findElementAt(offset);
PsiAnonymousClass anonymousClass = PsiTreeUtil.getParentOfType(element, PsiAnonymousClass.class);
if (anonymousClass != null) {
ConvertToLambda(project, anonymousClass);
}
}
private void ConvertToLambda(Project project, PsiAnonymousClass anonymousClass) {
WriteCommandAction.runWriteCommandAction(project, () -> {
// 实际的 Lambda 转换逻辑
// 使用 PSI 操作将匿名内部类替换为 Lambda 表达式
});
}
@Override
public boolean startInWriteAction() {
return false; // invoke 内部自行管理 write action
}
@Override
public @NotNull IntentionActionMetaData getMetaData() {
return IntentionActionMetaData.EMPTY;
}
}// Kotlin 示例:意图操作
class AddSerialVersionUidIntention : IntentionAction {
override fun getText() = "添加 serialVersionUID 字段"
override fun getFamilyName() = "Serialization"
override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean {
if (file !is PsiJavaFile) return false
val offset = editor.caretModel.offset
val element = file.findElementAt(offset) ?: return false
val psiClass = PsiTreeUtil.getParentOfType(element, PsiClass::class.java) ?: return false
// 检查是否实现 Serializable
val serializable = JavaPsiFacade.getInstance(project)
.findClass("java.io.Serializable", GlobalSearchScope.allScope(project))
?: return false
val isSerializable = psiClass.supers.any { it == serializable } ||
psiClass.implementsList?.referencedTypes?.any { it.resolve() == serializable } == true
if (!isSerializable) return false
// 检查是否已有 serialVersionUID
return psiClass.fields.none { it.name == "serialVersionUID" }
}
override fun invoke(project: Project, editor: Editor, file: PsiFile) {
val offset = editor.caretModel.offset
val element = file.findElementAt(offset) ?: return
val psiClass = PsiTreeUtil.getParentOfType(element, PsiClass::class.java) ?: return
WriteCommandAction.runWriteCommandAction(project) {
val factory = JavaPsiFacade.getElementFactory(project)
val field = factory.createFieldFromTemplate(
"private static final long serialVersionUID = 1L;",
null
)
psiClass.add(field)
}
}
override fun startInWriteAction() = false
}5.3.2 plugin.xml 注册意图操作
<extensions defaultExtensionNs="com.intellij">
<intentionAction>
<className>com.example.ConvertToLambdaIntention</className>
<category>Java/Lambda</category>
<descriptionDirectoryName>ConvertToLambda</descriptionDirectoryName>
</intentionAction>
<intentionAction>
<className>com.example.AddSerialVersionUidIntention</className>
<category>Java/Serialization</category>
<descriptionDirectoryName>AddSerialVersionUid</descriptionDirectoryName>
</intentionAction>
</extensions>5.4 代码格式化
// Java 示例:代码格式化
public class CodeFormattingExample {
public void reformatCode(Project project, PsiFile file) {
WriteCommandAction.runWriteCommandAction(project, () -> {
// 使用 CodeStyleManager 格式化
CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
codeStyleManager.reformat(file);
// 仅格式化指定范围
TextRange range = file.getTextRange();
codeStyleManager.reformatText(file, range.getStartOffset(), range.getEndOffset());
});
}
public void adjustIndent(Project project, PsiFile file, PsiElement element) {
WriteCommandAction.runWriteCommandAction(project, () -> {
CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
codeStyleManager.adjustLineIndent(file, element.getTextOffset());
});
}
public void formatImports(PsiJavaFile javaFile) {
Project project = javaFile.getProject();
WriteCommandAction.runWriteCommandAction(project, () -> {
// 优化导入语句
ImportHelper importHelper = new ImportHelper();
importHelper.optimizeImports(project, javaFile, true, false);
});
}
}6. 插件 UI 开发
6.1 Dialog 与 Panel
IntelliJ 平台提供了丰富的 UI 组件,构建在 Swing 之上,但经过了主题和样式的适配。
6.1.1 DialogWrapper 对话框
// Java 示例:自定义对话框
public class MySettingsDialog extends DialogWrapper {
private final JBTextField nameField;
private final JBPasswordField passwordField;
private final JCheckBox enableCheckBox;
private final ComboBox<String> modeComboBox;
private final Project project;
public MySettingsDialog(@Nullable Project project) {
super(project);
this.project = project;
setTitle("插件设置");
setSize(500, 400);
// 初始化 UI 组件
nameField = new JBTextField();
passwordField = new JBPasswordField();
enableCheckBox = new JCheckBox("启用高级功能");
modeComboBox = new ComboBox<>(new String[]{"自动", "手动", "半自动"});
// 设置对话框初始值
PropertiesComponent properties = PropertiesComponent.getInstance(project);
nameField.setText(properties.getValue("plugin.name", ""));
enableCheckBox.setSelected(properties.getBoolean("plugin.enabled", false));
// 初始化
init();
}
@Override
protected @Nullable JComponent createCenterPanel() {
// 创建表单面板
FormBuilder formBuilder = FormBuilder.createFormBuilder()
.addLabeledComponent("名称:", nameField, 1, false)
.addLabeledComponent("密码:", passwordField, 1, false)
.addComponent(enableCheckBox, 1)
.addLabeledComponent("模式:", modeComboBox, 1, false)
.addComponentFillVertically(new JPanel(), 0);
return formBuilder.getPanel();
}
@Override
protected void doOKAction() {
// 自定义验证
String name = nameField.getText().trim();
if (name.isEmpty()) {
Messages.showErrorDialog("名称不能为空", "输入错误");
return;
}
if (name.length() < 3) {
Messages.showWarningDialog("名称长度至少为 3 个字符", "输入警告");
return;
}
// 保存设置
PropertiesComponent properties = PropertiesComponent.getInstance(project);
properties.setValue("plugin.name", name);
properties.setValue("plugin.enabled", enableCheckBox.isSelected());
super.doOKAction();
}
@Override
public @Nullable JComponent getPreferredFocusedComponent() {
return nameField;
}
}6.1.2 Validation(输入验证)
// Java 示例:带验证的对话框
public class ConnectionDialog extends DialogWrapper {
private final JBTextField hostField;
private final JBTextField portField;
private final ValidationPanel validationPanel;
public ConnectionDialog() {
super(true);
setTitle("数据库连接配置");
hostField = new JBTextField("localhost");
portField = new JBTextField("3306");
validationPanel = new ValidationPanel();
validationPanel.registerValidator(new Validator<JBTextField>(hostField) {
@Override
public @Nullable ValidationInfo validate(@NotNull JBTextField field) {
String text = field.getText().trim();
if (text.isEmpty()) {
return new ValidationInfo("主机名不能为空", field);
}
// 验证主机名格式
if (!text.matches("^[a-zA-Z0-9.-]+$")) {
return new ValidationInfo("主机名格式不正确", field);
}
return null;
}
});
validationPanel.registerValidator(new Validator<JBTextField>(portField) {
@Override
public @Nullable ValidationInfo validate(@NotNull JBTextField field) {
String text = field.getText().trim();
if (text.isEmpty()) {
return new ValidationInfo("端口不能为空", field);
}
try {
int port = Integer.parseInt(text);
if (port < 1 || port > 65535) {
return new ValidationInfo("端口范围应为 1-65535", field);
}
} catch (NumberFormatException e) {
return new ValidationInfo("端口号必须是数字", field);
}
return null;
}
});
init();
}
@Override
protected @Nullable JComponent createCenterPanel() {
FormBuilder formBuilder = FormBuilder.createFormBuilder()
.addLabeledComponent("主机:", hostField)
.addLabeledComponent("端口:", portField);
JPanel panel = formBuilder.getPanel();
panel.setLayout(new BorderLayout());
panel.add(validationPanel, BorderLayout.SOUTH);
return panel;
}
@Override
protected @NotNull List<ValidationInfo> doValidateAll() {
return validationPanel.getValidators().stream()
.map(v -> v.validate())
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}6.2 Configuration(状态持久化)
6.2.1 PersistentStateComponent——XML 持久化
适用于需要持久保存复杂配置的场景,配置以 XML 格式存储在 IDE 配置目录中。
// Java 示例:应用级别状态持久化
@State(
name = "MyPluginSettings",
storages = @Storage("myPluginSettings.xml")
)
public class MyPluginSettings implements PersistentStateComponent<MyPluginSettings.State> {
public static class State {
public String serverUrl = "https://api.example.com";
public int timeoutSeconds = 30;
public boolean autoSync = true;
public List<String> recentFiles = new ArrayList<>();
public Map<String, String> customMappings = new HashMap<>();
}
private State state = new State();
// 从 XML 反序列化
@Override
public State getState() {
return state;
}
// 序列化到 XML
@Override
public void loadState(@NotNull State state) {
this.state = state;
}
// 获取应用级别实例
public static MyPluginSettings getInstance() {
return ApplicationManager.getApplication().getService(MyPluginSettings.class);
}
// 业务方法
public String getServerUrl() {
return state.serverUrl;
}
public void setServerUrl(String serverUrl) {
state.serverUrl = serverUrl;
}
public void addRecentFile(String filePath) {
if (!state.recentFiles.contains(filePath)) {
state.recentFiles.add(0, filePath);
// 限制数量
if (state.recentFiles.size() > 10) {
state.recentFiles.remove(state.recentFiles.size() - 1);
}
}
}
}// Kotlin 示例:项目级别状态持久化
@State(
name = "ProjectBookmarkSettings",
storages = [Storage("projectBookmarkSettings.xml")]
)
class ProjectBookmarkSettings : PersistentStateComponent<ProjectBookmarkSettings.State> {
data class State(
var bookmarks: MutableList<String> = mutableListOf(),
var lastSelectedPath: String = ""
)
private var state = State()
override fun getState() = state
override fun loadState(state: State) {
this.state = state
}
fun addBookmark(path: String) {
if (path !in state.bookmarks) {
state.bookmarks.add(path)
}
}
fun removeBookmark(path: String) {
state.bookmarks.remove(path)
}
fun getBookmarks(): List<String> = state.bookmarks.toList()
companion object {
fun getInstance(project: Project): ProjectBookmarkSettings {
return project.getService(ProjectBookmarkSettings::class.java)
}
}
}6.2.2 PropertiesComponent——轻量持久化
适用于存储简单键值对,不需要创建 State 类:
// Java 示例
public class ConfigManager {
private final Project project;
public ConfigManager(Project project) {
this.project = project;
}
public String getSetting(String key, String defaultValue) {
return PropertiesComponent.getInstance(project).getValue(key, defaultValue);
}
public void setSetting(String key, String value) {
PropertiesComponent.getInstance(project).setValue(key, value);
}
public boolean getBoolean(String key, boolean defaultValue) {
return PropertiesComponent.getInstance(project).getBoolean(key, defaultValue);
}
public void setBoolean(String key, boolean value) {
PropertiesComponent.getInstance(project).setValue(key, value);
}
}6.2.3 应用级别 vs 项目级别
| 特性 | 应用级别(Application Level) | 项目级别(Project Level) |
|---|---|---|
| 作用范围 | 所有项目共享 | 单个项目隔离 |
| 存储文件 | %CONFIG%/options/xxx.xml | .idea/xxx.xml |
| 典型用途 | 全局设置、插件配置 | 项目特定设置、书签 |
| 注册方式 | <applicationService> | <projectService> |
| 获取实例 | ApplicationManager.getApplication().getService() | project.getService() |
6.3 Notifications
6.3.1 NotificationGroup 通知
// Java 示例:注册通知组
public class MyNotifier {
// 注册通知组(在插件初始化时注册一次)
private static final NotificationGroup NOTIFICATION_GROUP = new NotificationGroup(
"My Plugin Notifications",
NotificationDisplayType.BALLOON, // 气泡样式
true // 在日志中保留
);
public static void showInfo(Project project, String title, String content) {
Notification notification = NOTIFICATION_GROUP.createNotification(
title,
content,
NotificationType.INFORMATION,
null // 通知点击时的监听器
);
notification.notify(project);
}
public static void showWarning(Project project, String content) {
Notification notification = NOTIFICATION_GROUP.createNotification(
"My Plugin",
content,
NotificationType.WARNING,
(notification1, event) -> {
// 点击通知时执行的操作
Messages.showInfoMessage("通知被点击");
}
);
notification.notify(project);
}
public static void showError(Project project, String title, String content) {
Notification notification = NOTIFICATION_GROUP.createNotification(
title,
content,
NotificationType.ERROR,
null
);
notification.notify(project);
}
}// Kotlin 示例:通知工具类
object Notifier {
private val GROUP = NotificationGroup(
"Plugin Notification Group",
NotificationDisplayType.BALLOON,
true
)
fun info(project: Project?, message: String) {
GROUP.createNotification(
"Plugin Info",
message,
NotificationType.INFORMATION,
null
).notify(project)
}
fun error(project: Project?, message: String) {
GROUP.createNotification(
"Plugin Error",
message,
NotificationType.ERROR,
null
).notify(project)
}
}6.3.2 StatusBar 通知
// Java 示例:状态栏通知
public class StatusBarNotifier {
public static void showInStatusBar(Project project, String text) {
if (project == null || project.isDisposed()) return;
// 获取状态栏
StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
if (statusBar == null) return;
// 创建并显示通知
statusBar.setInfo(text);
// 或者在状态栏显示带图标的通知
DefaultStatusBarWidget widget = new DefaultStatusBarWidget("MyWidget", project) {
@Override
public @NotNull String ID() {
return "MyWidget";
}
};
statusBar.addWidget(widget, "before", project);
}
}6.3.3 通知类型
| 通知类型 | 说明 | 视觉效果 |
|---|---|---|
NotificationType.INFORMATION | 信息提示 | 蓝色气泡 |
NotificationType.WARNING | 警告提示 | 黄色气泡 |
NotificationType.ERROR | 错误提示 | 红色气泡 |
| 显示样式 | 说明 |
|---|---|
NotificationDisplayType.BALLOON | 气泡弹出显示,自动消失 |
NotificationDisplayType.STICKY_BALLOON | 固定气泡,用户手动关闭 |
NotificationDisplayType.TOOL_WINDOW | 在工具窗口中显示 |
NotificationDisplayType.NONE | 不显示,仅记录到事件日志 |