跳转至

平台集成专题:JCEF、Terminal、VCS 与 Trusted Projects

本章覆盖几个风险较高、但在复杂插件中很常见的平台集成点:

  • 用 JCEF 在 Swing UI 中嵌入 Chromium 浏览器。
  • 使用 Reworked Terminal API 读取终端、发送输入和扩展终端快捷键。
  • 对接 Version Control System API。
  • 在 Trusted Projects 安全模型下控制危险功能。

这些能力都不是普通插件的默认入口。只有当平台已有 UI、Execution、Settings、Tool Window 或文件 API 无法表达需求时,才应该引入它们。

选择矩阵

需求 优先方案 何时使用本章 API
展示设置、表单、列表、树 Kotlin UI DSL / Swing 平台组件 不需要 JCEF
展示 HTML、Markdown 预览、图表 Web UI JCEF 标准 Swing 表达成本过高,或必须渲染 HTML/JS
运行命令并展示输出 Execution API / Run Configuration 需要复用用户 Terminal tab 或读取 shell block
实现新 VCS 支持 VCS API 目标系统不是 Git/SVN 等内置 VCS
自动导入、执行脚本、启动构建 Trusted Projects 检查 项目未受信任时可能执行外部代码

不要为了“看起来现代”把普通设置页写成 Web UI,也不要为了跑命令绕过 Execution API 直接操作终端。

JCEF 适用边界

JCEF 是 Java Chromium Embedded Framework 的 IntelliJ Platform 封装,用于在 Swing 应用中嵌入 Chromium。

适合:

  • Markdown、HTML、SVG、PDF 或图表预览。
  • 复杂可视化,例如图形、流程图、图片查看器。
  • 需要运行浏览器 JS/CSS 的 UI。

不适合:

  • 普通表单、设置页、列表、树。
  • 可以用平台 Swing 组件实现的 Tool Window。
  • 只为了使用前端框架而重写 IDE UI。

官方建议优先使用 IntelliJ Platform 的 Swing UI 框架;JCEF 是例外工具,不是默认 UI 方案。

JCEF 可用性检查

运行 IDE 可能不支持 JCEF,例如用户使用的 JDK 不含 JCEF,或 JCEF 版本与 IDE 不兼容。使用前必须检查:

if (JBCefApp.isSupported()) {
    createBrowserPanel()
} else {
    createFallbackPanel()
}

fallback 可以是:

  • 打开外部浏览器。
  • 显示只读文本/Markdown。
  • 禁用预览并说明原因。
  • 使用普通 Swing 实现简化视图。

不要假设所有 JetBrains IDE 都能创建 Chromium 组件。

创建 JCEF Browser

简单场景可直接创建 JBCefBrowser

class PreviewPanel(parent: Disposable) : JPanel(BorderLayout()), Disposable {
    private val browser = JBCefBrowser()

    init {
        add(browser.component, BorderLayout.CENTER)
        Disposer.register(parent, this)
    }

    fun loadHtml(html: String) {
        browser.loadHTML(html)
    }

    override fun dispose() {
        Disposer.dispose(browser)
    }
}

复杂场景使用 builder 或自定义 client:

val client = JBCefApp.getInstance().createClient()
val browser = JBCefBrowserBuilder()
    .setClient(client)
    .setUrl("about:blank")
    .build()

Disposer.register(parentDisposable) {
    Disposer.dispose(browser)
    Disposer.dispose(client)
}

规则:

  • 默认 client 随 browser 自动释放。
  • 自定义 JBCefClient 必须显式释放。
  • browser、client、JS query 都实现 disposable 语义。
  • 不要在已 dispose 的 browser 上继续执行 JS。

JavaScript 与插件代码通信

插件到页面:

browser.cefBrowser.executeJavaScript(
    "window.render(${json});",
    browser.cefBrowser.url,
    0,
)

页面到插件使用 JBCefJSQuery

val openLinkQuery = JBCefJSQuery.create(browser as JBCefBrowserBase)
openLinkQuery.addHandler { link ->
    if (link.startsWith("http://") || link.startsWith("https://")) {
        BrowserUtil.browse(link)
    } else {
        openProjectFile(project, link)
    }
    null
}

browser.cefBrowser.executeJavaScript(
    """
    window.openPluginLink = function(link) {
      ${openLinkQuery.inject("link")}
    };
    """.trimIndent(),
    browser.cefBrowser.url,
    0,
)

实践建议:

  • JS 输入必须当成不可信数据处理。
  • 不要暴露“执行任意命令”“读任意文件”这类宽接口。
  • 为每个 query 提供最小能力。
  • handler 中涉及项目模型读取时仍需 read action。
  • 页面资源建议通过 request handler 暴露固定 URL,而不是拼接 file://

JCEF 资源、滚动条与调试

JCEF 页面如果由插件分发 HTML/CSS/JS,浏览器不能天然访问插件资源。需要用 request handler 映射资源路径,或生成临时可访问内容。

滚动条:

  • 优先使用 JBCefScrollbarsHelper.buildScrollbarsStyle(),让 JCEF 滚动条接近 IDE 外观。
  • 只有高级透明滚动条需求才考虑 OverlayScrollbars 方案。

调试:

  • Chrome DevTools 默认可通过端口 9222 连接。
  • 端口可通过 registry key ide.browser.jcef.debug.port 调整。
  • 内部模式下可从 JCEF 组件上下文菜单打开 DevTools。
  • 2021.3+ 需要显式启用 ide.browser.jcef.contextMenu.devTools.enabled 才显示菜单入口。

JavaFX 不应作为新插件的浏览器方案。第三方插件使用 JavaFX 早已不推荐;JavaFX Runtime for Plugins 从 2025.1 起不再可用。

Embedded Terminal API 状态

IntelliJ Platform 目前有多种终端实现。官方文档中“Terminal API”指 Reworked Terminal:

  • Reworked Terminal 从 2025.2 起成为默认实现。
  • Terminal API 从 2025.3 起可用。
  • 该 API 仍处于 experimental 状态,未来可能变化。
  • API 由 bundled Terminal 插件提供,插件 ID 是 org.jetbrains.plugins.terminal

依赖声明:

<idea-plugin>
  <depends>org.jetbrains.plugins.terminal</depends>
</idea-plugin>

Gradle 依赖还要声明 bundled plugin:

dependencies {
    intellijPlatform {
        bundledPlugin("org.jetbrains.plugins.terminal")
    }
}

如果目标 IDE 早于 2025.3,不要直接调用新 Terminal API。可以用可选依赖、反射隔离或版本矩阵把能力降级。

获取 Terminal 实例

Reworked Terminal 的入口是 TerminalView。目前它只存在于 Terminal Tool Window 的 tab 中。

常见方式:

val terminal = e.getData(TerminalView.DATA_KEY) ?: return

或通过 tab manager:

val manager = TerminalToolWindowTabsManager.getInstance(project)
val tabs = manager.getTabs()
val tab = manager.createTabBuilder()
    .setWorkingDirectory(project.basePath)
    .build()

注意:

  • 不要假设用户已经打开 Terminal tab。
  • 不要把 Terminal 当作通用进程执行 API。
  • 需要结构化运行、重启、日志、退出码、Run Tool Window 时优先用 Execution API。

终端输出与输入

Terminal 有 regular 和 alternative 两个 output buffer:

Buffer 用途
regular 普通命令和输出
alternative vim、nano、mc 等全屏终端应用

两者都通过 TerminalOutputModel 暴露为只读视图。输出历史会被裁剪,所以 offset 是绝对 offset,不能假设从 0 开始一直可用。

发送输入:

terminal.sendText("echo hello")

更推荐 builder:

terminal.createSendTextBuilder("echo hello")
    .shouldExecute()
    .useBracketedPasteMode()
    .send()

规则:

  • 执行命令时优先用 shouldExecute(),不要手工拼换行。
  • 用户输入、项目路径、文件名发给 shell 前必须转义或使用安全构造。
  • bracketed paste 能降低 shell 把文本当快捷键解释的风险。

终端快捷键与 Shell Integration

Terminal 的快捷键处理不同于 IDE 其他区域。因为终端会让 shell 处理许多按键,单纯在 plugin.xml 注册 action 不一定生效。

如果 action 要在 Terminal 中通过快捷键触发:

class MyTerminalAction : DumbAwareAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val terminal = e.getData(TerminalView.DATA_KEY) ?: return
        terminal.createSendTextBuilder("pwd")
            .shouldExecute()
            .send()
    }
}

class MyTerminalAllowedActionsProvider : TerminalAllowedActionsProvider {
    override fun getActionIds(): List<String> {
        return listOf("MyPlugin.MyTerminalAction")
    }
}

注册:

<actions>
  <action id="MyPlugin.MyTerminalAction" class="com.example.MyTerminalAction">
    <keyboard-shortcut first-keystroke="shift ENTER" keymap="$default"/>
  </action>
</actions>

<extensions defaultExtensionNs="org.jetbrains.plugins.terminal">
  <allowedActionsProvider implementation="com.example.MyTerminalAllowedActionsProvider"/>
</extensions>

Shell Integration 可读取命令 block、当前命令、工作目录、退出码等信息:

val shellIntegration = terminal.shellIntegrationDeferred.await()
shellIntegration.addCommandExecutionListener(listener, disposable)

限制:

  • 只支持 Bash、Zsh、PowerShell。
  • 依赖用户 shell 配置。
  • 依赖 Terminal 设置中的 Shell Integration。
  • await() 可能永远无法成功,必须可取消并设置合理等待策略。

VCS Integration 基础概念

如果只是读取 Git 状态或显示当前分支,优先使用现有 Git/VCS API,不要实现新 VCS。只有要让 IDE 支持一种新的版本控制系统时,才需要完整 VCS integration plugin。

核心模型:

类型 含义
FilePath 磁盘或仓库中的文件/目录路径,可表示本地不存在的路径
VcsRevisionNumber VCS 修订号
ContentRevision 文件某个修订版本的内容
FileStatus 文件相对 VCS 的状态,影响 UI 颜色
Change 一次创建、修改、删除、移动/重命名
ChangeList 一组相关 change,分 local 和 committed

FilePath 不等同于 VirtualFile。删除文件、远程仓库文件、历史版本文件都可能没有本地 VirtualFile

注册 VCS

主入口是 AbstractVcs,通过 com.intellij.vcs 扩展点注册:

<extensions defaultExtensionNs="com.intellij">
  <vcs
      name="example"
      vcsClass="com.example.vcs.ExampleVcs"/>
</extensions>

name 必须与 AbstractVcs.getName() 返回值一致,并且在 IDE 中唯一。

实现顺序建议:

  1. AbstractVcs 主入口。
  2. ChangeProvider 上报本地变更。
  3. FileStatusChange 映射。
  4. Commit / Update / History 等更高级能力。
  5. UI 集成,例如工具窗口、action、settings。

不要一开始就实现所有 VCS extension。先让 IDE 能正确识别 dirty scope 和 local changes。

ChangeProvider 与 Dirty Scope

ChangeProvider 负责跟踪 working copy 中的用户修改。它与 VcsDirtyScopeManager 协作:

  • 文件变化、状态失效或插件主动调用会把路径加入 dirty scope。
  • 平台异步在后台调用 ChangeProvider.getChanges()
  • 插件把结果报告给 ChangelistBuilder

伪代码:

class ExampleChangeProvider : ChangeProvider {
    override fun getChanges(
        dirtyScope: VcsDirtyScope,
        builder: ChangelistBuilder,
        progress: ProgressIndicator,
        addGate: ChangeListManagerGate,
    ) {
        val files = dirtyScope.dirtyFiles
        val directories = dirtyScope.recursivelyDirtyDirectories

        val statuses = vcsClient.status(files, directories)
        for (status in statuses) {
            progress.checkCanceled()
            when (status.kind) {
                ADDED, MODIFIED, DELETED -> builder.processChange(status.toChange())
                UNVERSIONED -> builder.processUnversionedFile(status.file)
                IGNORED -> builder.processIgnoredFile(status.file)
                LOCALLY_DELETED -> builder.processLocallyDeletedFile(status.file)
            }
        }
    }
}

性能原则:

  • 能批量查询 VCS 状态就不要逐文件调用外部命令。
  • 大仓库必须使用 dirty scope,不要每次扫描整个项目。
  • 定期检查取消。
  • binary revision 用 BinaryContentRevision
  • 文本 revision 内容要处理正确编码。

Trusted Projects 安全模型

从 2021.2.4/2021.3.1 起,IDE 会在第一次打开项目时询问用户是否信任项目。用户选择 Safe Mode 时,插件不能自动或意外执行潜在危险功能。

检查项目是否受信任:

if (!project.isTrusted()) {
    disableDangerousFeature()
    return
}

Java:

if (!TrustedProjects.isTrusted(project)) {
  return;
}

等待信任后执行:

TrustedProjects.whenProjectTrusted(project) {
    enableProjectImport()
}

监听状态变化:

class ExampleTrustListener : TrustStateListener {
    override fun onProjectTrusted(project: Project) {
        project.service<ExampleService>().enableDangerousFeatures()
    }
}

什么算危险功能

官方判断标准是:如果某个功能可能执行恶意代码,并且用户并不明显知道代码会被执行,那么 Safe Mode 下必须禁用,并且启用时需要确认。

需要受 Trusted Projects 保护的常见插件能力:

  • 自动导入 Gradle/Maven/npm/pip 等会执行脚本的项目。
  • 打开项目后自动运行外部命令。
  • 自动启动语言服务器、构建工具、debug adapter。
  • 自动执行仓库内脚本、hook、生成器。
  • 自动加载项目内二进制、native library 或插件。
  • JCEF 页面直接访问项目内脚本并执行。

不一定需要额外确认:

  • 用户明确点击 Run/Debug 执行自己的代码。
  • 用户明确触发“执行这个命令”的 action。
  • 只读取文件、显示文本、解析静态配置且不执行代码。

关键是“用户是否能明显理解这会执行项目代码”。如果不明显,就按危险功能处理。

平台集成 QA 清单

JCEF:

  • 调用 JBCefApp.isSupported() 并提供 fallback。
  • browser、client、query 都正确 dispose。
  • JS query 只暴露最小能力。
  • 页面资源通过受控 handler 暴露。
  • 滚动条、主题、缩放和 DevTools 调试路径验证过。

Terminal:

  • 目标 IDE 版本满足 2025.3+,或有降级路径。
  • 声明 org.jetbrains.plugins.terminal 依赖。
  • 不把 Terminal 当通用进程管理器。
  • 发送命令时处理 shell escaping 和 bracketed paste。
  • Shell Integration 失败时不阻塞。

VCS:

  • 先实现 AbstractVcsChangeProvider
  • 使用 dirty scope,不全仓库扫描。
  • 批量查询外部 VCS 状态。
  • FilePathContentRevision、binary content 和编码处理正确。
  • 后台任务可取消,错误进入 VCS/notification UI。

Trusted Projects:

  • 所有隐式执行代码的功能检查 project.isTrusted()
  • Safe Mode 下禁用自动导入、自动运行、自动下载执行。
  • 用户主动触发危险能力时给确认和解释。
  • 项目变为 trusted 后再开启功能。

参考来源