平台集成专题: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 中唯一。
实现顺序建议:
AbstractVcs主入口。ChangeProvider上报本地变更。FileStatus和Change映射。- Commit / Update / History 等更高级能力。
- 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:
- 先实现
AbstractVcs和ChangeProvider。 - 使用 dirty scope,不全仓库扫描。
- 批量查询外部 VCS 状态。
FilePath、ContentRevision、binary content 和编码处理正确。- 后台任务可取消,错误进入 VCS/notification UI。
Trusted Projects:
- 所有隐式执行代码的功能检查
project.isTrusted()。 - Safe Mode 下禁用自动导入、自动运行、自动下载执行。
- 用户主动触发危险能力时给确认和解释。
- 项目变为 trusted 后再开启功能。