跳转至

扩展点、监听器与动态插件

IntelliJ Platform 的插件模型主要由三类机制组成:扩展、监听器和服务。扩展用于把能力挂到平台定义的扩展点;监听器用于订阅事件;服务用于保存运行时状态和封装长期逻辑。

Extensions

扩展是插件接入 IDE 功能最常见的方式。平台或其他插件定义扩展点,当前插件提供实现类或配置数据。

<extensions defaultExtensionNs="com.intellij">
  <toolWindow
      id="Example"
      anchor="right"
      factoryClass="com.example.ExampleToolWindowFactory"/>
  <completion.contributor
      language="JAVA"
      implementationClass="com.example.MyCompletionContributor"/>
</extensions>

defaultExtensionNs 的选择:

含义
com.intellij 扩展 IntelliJ Platform 核心能力
其他插件 ID 扩展某个依赖插件暴露的扩展点

官方文档提到平台和捆绑插件提供了 1700 多个扩展点。实际查找扩展点时,优先使用:

  • plugin.xml 中的代码补全和 Quick Documentation。
  • IntelliJ Platform Extension Point and Listener List。
  • IntelliJ Platform Explorer,查看开源插件如何实现。
  • 依赖插件自己的 plugin.xml

扩展实现的约束

扩展类应保持轻量和无状态:

  • 不要在构造函数做耗时初始化。
  • 不要做静态初始化。
  • 不要用 Kotlin object 作为扩展实现类。
  • 不要把同一个类同时注册为 extension 和 service。
  • 运行时状态放到 Service。
  • 需要按场景禁用时,可以在构造函数抛出 ExtensionNotApplicableException

这些规则不是形式主义。扩展类加载过早或持有状态,会增加启动成本,并影响动态卸载。

自定义 Extension Point

如果你的插件希望被其他插件扩展,可以声明自己的扩展点。

接口型扩展点:

<extensionPoints>
  <extensionPoint name="exampleProvider" interface="com.example.ExampleProvider"/>
</extensionPoints>

Bean 型扩展点:

<extensionPoints>
  <extensionPoint name="exampleRule" beanClass="com.example.ExampleRuleBean"/>
</extensionPoints>

其他插件使用时:

<depends>com.example.myPlugin</depends>

<extensions defaultExtensionNs="com.example.myPlugin">
  <exampleProvider implementation="com.other.OtherProvider"/>
</extensions>

读取扩展:

private val EP_NAME =
    ExtensionPointName.create<ExampleProvider>("com.example.myPlugin.exampleProvider")

fun providers(): List<ExampleProvider> = EP_NAME.extensionList

如果扩展点涉及 Dumb Mode,基类应标记 PossiblyDumbAware,读取时用 DumbService.getDumbAwareExtensions() 过滤可在 Dumb Mode 使用的实现。

Listeners

监听器通过消息总线订阅事件。官方建议优先使用声明式注册,因为实例会延迟到第一次事件发送时创建,减少启动和项目打开开销。

应用级监听器:

<applicationListeners>
  <listener
      class="com.example.MyVfsListener"
      topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</applicationListeners>

项目级监听器:

<projectListeners>
  <listener
      class="com.example.MyToolWindowListener"
      topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"/>
</projectListeners>

监听器要求:

  • 实现类应无状态。
  • 不要实现生命周期接口,例如 Disposable
  • 项目级监听器可以声明接收 Project 的单参数构造函数。
  • 可用 osactiveInTestModeactiveInHeadlessMode 限制激活条件。

需要手动订阅时,务必把连接挂到合适的 Disposable

project.messageBus.connect(disposable).subscribe(
    VirtualFileManager.VFS_CHANGES,
    object : BulkFileListener {
        override fun after(events: List<VFileEvent>) {
            // handle events
        }
    }
)

Components 已废弃

旧插件中的 application/project/module components 不适合新项目:

  • 会影响动态插件能力。
  • 生命周期和初始化时机不如 Service、Extension、Listener 清晰。
  • 新插件应使用 Service 管理状态,Extension 接入功能,Listener 订阅事件。

迁移原则:

旧用途 新方案
保存状态 Service + PersistentStateComponent
运行时逻辑 Application/Project Service
订阅事件 声明式 Listener
扩展平台行为 Extension
项目打开后执行 ProjectActivity

动态插件

动态插件指安装、更新、卸载插件时不需要重启 IDE。现代插件应尽量支持动态加载和卸载。

官方限制包括:

  • 不使用 Components。
  • 所有 <group> 都有唯一 id
  • 只使用支持动态加载的扩展点。
  • 自定义扩展点如满足动态规则,应声明 dynamic="true"
  • 不使用 overrides="true" 的 Service。
  • Configurable 若依赖动态扩展点,应实现 Configurable.WithEpDependencies

自定义动态扩展点:

<extensionPoints>
  <extensionPoint
      name="exampleRule"
      beanClass="com.example.ExampleRuleBean"
      dynamic="true"/>
</extensionPoints>

动态卸载安全规则:

  • 不在长期对象里保存裸 PsiElement,用 SmartPsiElementPointer
  • 不用 FileTypeLanguage 对象作 Map key,改用 getName()getID()
  • Service 需要清理资源时实现 Disposable
  • 插件卸载相关日志在 com.intellij.ide.plugins.DynamicPlugins 分类下。
  • 本地运行 Plugin DevKit 的 dynamic plugin verification inspection。

参考来源