Files
openidea/docs/plugin_p0.md
T
l0ng-ai fd1f465aeb feat(plugins): extension points, capability consent, async runtime and plugin API 1.0
Rework the plugin system per docs/plugin_plan.md (P0–P8):

- Extension-point registry (ide-extension) so new contribution types register
  without touching the kernel; languages, run detectors, providers, views and
  UI contributions go through it.
- Per-plugin capability consent with a risk-graded ledger; process
  capabilities are pinned to resolved binary paths and hashes.
- Async, actor-per-plugin Wasm runtime with lazy activation, cancellation,
  events, long-running tasks and idle reclamation.
- Plugin host moved into ide-host, shared by local and remote projects.
- Distribution: install/package tooling (openidea-plugin bin) and a static
  market index.
- WIT frozen as openidea:[email protected]; Rust SDK with mock host.
- Ten official validation plugins under plugins/official.
2026-11-04 02:41:46 +08:00

160 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件系统 P0 验证结论
对应 `docs/plugin_plan.md` 第 5 节 P0。实验代码保留为回归测试:
`crates/ide-plugin/tests/p0_probe.rs`,探针组件在 `crates/ide-plugin/tests/fixtures/p0-probe/`
(`build.sh` 生成 `dist/min.wasm` 与 `dist/full.wasm`)。环境:wasmtime 49.0.5、wasmtime-wasi 49(p2)、
wit-bindgen 0.63、`wasm32-wasip2`,Apple Silicon,debug 构建(release 数字括注)。
## 1. 可选导出能否按接口绑定 —— 可以
测试:`optional_exports_bind_per_interface`。
- `Component::get_export_index(None, "openidea:probe/[email protected]")` 在**编译后、实例化前**就能判断
组件有没有导出某个接口。安装时据此算出插件实现了哪些 Provider,写进贡献索引,启动时不需要实例化。
- `bindgen!` 生成的整 world 绑定(`ProbeHostPre::new`)要求所有导出都在,缺一个就失败;
但每个导出接口各有一个 `GuestIndices::new(&instance_pre)`,缺失时返回 `Err`,存在时正常 `load` 和调用。
- 结论:**不需要退回“基础 world + 多个组合 world”**。宿主用一个包含全部可选导出的 world 生成绑定,
只用到每个接口的 `GuestIndices`,不用整 world 的 `*Pre`。插件作者侧:SDK 为每个 Provider 提供
feature 或宏,只导出实现了的接口(wit-bindgen 按 world 生成,SDK 内置几个常用组合 world 即可,
宿主不关心组合方式)。
对设计的影响:无,4.3 节的“可选导出”成立。
## 2. async 调用、取消与实例复用
测试:`spinning_guest_yields_to_the_executor`、`cancelled_instance_cannot_be_reused_but_reinstantiates_fast`、
`async_host_import_does_not_block`。
- wasmtime 49 里 `Config::async_support` 已废弃且无效果,async 能力由 `async` cargo feature 和调用方式决定。
- `store.epoch_deadline_async_yield_and_update(1)` + `call_async`:死循环的 guest 每个 epoch tick 让出一次,
**单线程**执行器上的其他任务照常推进。长调用不再独占线程。
- async 宿主导入(`imports: { "...sleep-ms": async }`)等待期间不阻塞线程。
- guest 返回 `Err`(不是 trap)后实例照常可用。
- **取消(丢弃 future)后实例不能复用**:下一次调用返回 `Trap::CannotEnterComponent`。
无论取消发生在 guest 计算中还是 async 宿主导入的等待中,结果相同。原因:wasmtime 在 fiber 被
dispose 时把错误传回 `catch_traps`,组件 store 被标记为 `trapped`(`may_enter() == false`)。
这是 wasmtime 的有意行为,不能绕开。
- **重建很便宜**:从已有的 `InstancePre` 新建 store 并实例化,加上第一次调用,约 **50µs**(release 约 40µs)。
代价在于丢失实例内存里的状态,而不是时间。
结论与对 4.5 节的修订:
1. 取消 = 丢弃 future + 丢弃 store;下次调用前从 `InstancePre` 重建。不需要冷编译。
2. 两段式超时仍然保留,但理由从“避免冷启动”改为“保留实例状态”:
- **软超时**(编辑路径 50ms):调用方不再等待、结果丢弃,调用在 actor 内继续执行完,实例和状态保留;
同一插件的后续调用排队,新请求到来时旧请求如仍在排队则直接丢弃(不进入 guest)。
- **硬超时**(编辑路径 500ms,命令 5s):丢弃 future,重建实例,计入熔断计数。
3. 用户主动取消(例如 AI Completion 的旧请求)优先走**协作式取消**(见第 3 节),实例不受影响;
协作式取消在宽限期内没有返回时,再升级为硬取消。
4. 插件需要跨调用保留的状态一律走 `storage`;实例内存只当缓存。SDK 文档写明这一点。
5. fuel 保留作为死循环兜底;与 epoch 让出同时启用没有冲突。
## 3. WASI p2 上的流式与任务模型 —— 可行,不依赖 p3
测试:`pollable_task_streams_and_cancels_cooperatively`。
- 宿主定义 `task` 资源,方法 `next-ready() -> pollable` 返回 `wasi:io/poll.pollable`(宿主用
`wasmtime_wasi::p2::subscribe` 包装自定义 `Pollable`)。guest 调 `pollable.block()` 时,wasmtime 在
async 宿主里等待该 future,执行器线程不被占用。
- guest 循环“等待 → 取数据 → `report` 进度 → 检查 `is-cancelled`”,宿主推送数据并在最后设置取消标志;
guest 正常返回,**实例保持可用**。
- 流式输出(例如 AI Completion 逐块返回、Kubernetes 日志)用同一模式的反方向:宿主提供
`output-stream` 风格的资源(`write(chunk)` 是 async 宿主导入),或者 guest 通过 `task.emit(chunk)` 推送;
两者都只需要 p2 资源 + pollable。
- 结论:4.3 节 `tasks` 接口定为:
```wit
resource task {
report: func(done: u32, total: option<u32>);
is-cancelled: func() -> bool;
/// 取消或有新输入时就绪,供 guest 在等待时让出。
cancelled: func() -> pollable;
}
```
`net` 的流式响应体直接复用 `wasi:io/streams.input-stream`,不自造类型。
## 4. `syntax`、`index` 接口
依据:`ide-syntax`(`StructureService`:`node_at`、`syntax_errors`、`locals`、`identifiers_named`、
`structural_ranges`,全部基于 `ide-language` 的 `SyntaxCache` 增量树,0 基行、UTF-16 列)和 `ide-index`
(`ProjectIndex`:`find_files`、`find_symbols`、`symbols_named`,后台 worker + redb 持久化)。
Solidity 与 MyBatis Navigator 两个原型需要的能力如下,接口按它们的最小需求定稿。
### 4.1 `syntax`
两个方向,分开设计:
- **读宿主的树(import)**:适用于宿主有 grammar 的语言(MyBatis Navigator 读 Java、XML)。
```wit
interface syntax {
resource tree {
/// 0 基行、UTF-16 列;返回包含该位置的最内层命名节点。
node-at: func(pos: position) -> option<node>;
/// tree-sitter 查询语法;只返回被捕获的节点,最多 `limit` 个。
query: func(source: string, range: option<range>, limit: u32) -> result<list<capture>, error>;
errors: func(limit: u32) -> list<range>;
}
record node { kind: string, range: range, text: string, parent-kind: option<string> }
record capture { name: string, node: node }
/// 打开的文档用增量缓存树;未打开的文件现场解析(受 `project.read` 约束)。
parse: func(path: string) -> result<tree, error>;
}
```
不暴露节点句柄的游走 API(`parent`/`children` 逐个跨边界调用代价高);查询一次返回结果列表。
- **插件提供语法树(export,P3)**:宿主没有 grammar 的语言(Solidity)。宿主不加载插件提供的原生
grammar;插件在 Wasm 里自带解析器,导出 `syntax-provider`,按需返回**扁平结果**而不是树:
```wit
interface syntax-provider {
highlights: func(text: string) -> list<token>; // range + 语义类别(映射到 --syn-*)
outline: func(text: string) -> list<outline-item>;
folding: func(text: string) -> list<range>;
}
```
宿主把结果转换成现有的 `OutlineItem`、`FoldRange`、`SyntaxHighlights`,编辑器侧无感知。
这条路径在编辑路径上,适用 4.5 节的编辑路径预算。
原因:tree-sitter 的 Wasm grammar 需要 `tree-sitter` 的 wasm 特性和额外的 wasmtime 实例,
且 grammar 不能表达大纲和折叠;扁平结果把实现方式留给插件。
### 4.2 `index`
- **查宿主的符号索引(import)**:
```wit
interface index {
find-symbols: func(query: string, types-only: bool, limit: u32) -> list<symbol>;
symbols-named: func(name: string, kind: option<symbol-kind>, limit: u32) -> list<symbol>;
find-files: func(query: string, limit: u32) -> list<string>;
/// 插件自定义索引的查询,见下。
lookup: func(index: string, key: string, limit: u32) -> list<entry>;
}
record entry { key: string, value: string, path: string, range: range }
```
- **插件自定义索引(export + 声明)**:MyBatis Navigator 要“Mapper 接口方法 ↔ XML 语句 id”的跨文件映射,
宿主的符号索引不认识 XML 语句。做法:
```toml
[[contributes.indexes]]
id = "statements"
files = ["**/*Mapper.xml", "**/*Mapper.java"]
version = 1 # 改变索引格式时递增,宿主据此重建
```
插件导出 `indexer.index-file(path, text) -> list<entry>`;宿主在**后台**(复用 `ide-index` 的 worker
调度和 redb 存储,按插件 id + 索引 id 分表)对匹配文件调用,文件变化时增量更新,插件禁用或卸载时删表。
插件运行时只能 `lookup` 自己的索引。索引任务是后台任务,适用后台预算并上报进度。
对设计的影响:4.3 节的 `syntax`、`index` 按上面两节定稿;新增 `contributes.indexes` 扩展点(P6)。
## 5. 文档修正
`plugins/README.md` 第 4 节原文暗示插件语言里的 `language-servers`、`debugger` 会生效。实际上插件语言
只经 `languages` 命令进入前端的语言列表(文件检测、注释、缩进、借用的 CodeMirror 模式),不进入后端
`LanguageRegistry`,因此 LSP、DAP、后端语言检测都不生效。README 已改为如实说明,P3 完成后再恢复。
## 6. 对计划的修订汇总
| 位置 | 原设计 | 修订 |
| -------------------- | ------------------------ | ------------------------------------------------------------ |
| 4.3 可选导出 | 待验证,备选组合 world | 按接口 `GuestIndices` 绑定,安装时用 `get_export_index` 记录 |
| 4.5 超时 | 两段式用于避免冷启动 | 重建约 50µs;两段式用于保留实例状态;用户取消优先协作式 |
| 4.3 `tasks` | `report`、`is-cancelled` | 增加 `cancelled() -> pollable` |
| 4.3 `syntax`/`index` | 待定 | 见第 4 节 |
以上修订已同步到 `docs/plugin_plan.md`。