三者端到端都是类型化的:Rust 类型派生
specta::Type,由 tauri-specta 生成 src/bindings.ts。
命令
命令是src-tauri/src/commands/<domain>.rs 里的一层薄适配:从调用它的窗口解析出项目,调用共享实现(通常在 ide_host::ops),返回 IdeResult<T>:
src-tauri/src/commands/app.rs
commands::builder()(src-tauri/src/commands/mod.rs)的 collect_commands! 列表里。该模块的约定:
- 每个命令都返回
IdeResult<T>(在 JS 里以IdeErrorreject); - 项目相关的命令从调用窗口解析项目;
- 流式结果用
Channel<T>参数;可取消的请求接收 UI 分配的RequestId,通过cancel_request取消。
src-tauri/src/lib.rs 里的 invoke handler 都会检查项目信任,远程窗口的命令则会被路由到服务端。
调用命令
前端从@/platform/ipc 导入 commands(它转导出了绑定),用 call 解包结果:
{ status: "ok", data } | { status: "error", error }。call 把它变成一个 promise,出错时以 IpcError reject,其中带有错误的 kind 和 data。辅助函数:isCancelled(e)、isNoServer(e)、unavailableText(e)。
错误
IdeError(crates/ide-core/src/error.rs)是唯一跨 IPC 的错误类型:
ErrorKind 取值:notFound、alreadyExists、permissionDenied、invalidInput、outOfSync(文档版本不一致,UI 必须重新同步全文)、cancelled、timedOut、unavailable(缺少必需的工具,如语言服务器、git 或调试器)、io、protocol、internal。
各 crate 内部可以保留丰富的错误类型,但要在公开边界处转换;UI 根据 kind 分支处理,并展示 message。不要让 anyhow 或 crate 私有的错误穿过命令。
生成的绑定
src/bindings.ts 由 tauri-specta 根据已注册的命令和 IdeEvent 生成,禁止手改。
pnpm gen:bindings不启动应用即可重新生成(cargo test -p openidea --test export_bindings)。- 每次 debug 启动也会重新导出,内容有变化才写文件。
- CI 的 Rust 任务会重新生成并在与提交的文件不同时失败。改了命令签名或 IPC 类型后,要提交
src/bindings.ts。
number。
文本坐标
所有跨 IPC 的数据使用同一套坐标(crates/ide-core/src/text.rs):
- 行号从 0 开始。
- 列和偏移量是 UTF-16 码元,与 JavaScript 字符串、CodeMirror 和 LSP 的默认编码一致。
Pos { line, col };Range { start, end }是左闭右开区间。
ide-text 里。请在自己 crate 的边界处转换:tree-sitter 的字节偏移,或其他编码的 LSP 位置,绝不能出现在命令结果里。
用 channel 流式传输
数据量大或增量产生的结果走Channel<T> 参数,而不是事件总线。以 Find in Files 为例:
src-tauri/src/commands/search.rs
Channel、设置 onmessage,作为参数传入:
src/platform/textSearch.ts
Done 值),这样 UI 才知道它何时结束。
可取消的请求
id 由 UI 分配,后端为它注册一个 token:crates/ide-host/src/ops/search.rs
- 后端所有窗口共用一套 id,所以每个页面从一个随机起点开始计数。
- 在命令开始之前到达的取消会被记住 120 秒,请求开始时就处于已取消状态(
crates/ide-host/src/requests.rs)。 - 长任务要检查
CancellationToken,及时返回ErrorKind::Cancelled(或部分结果)。 - 后端自己发起的任务(比如语言服务器的 work-done progress)以带
TaskId的IdeEvent::Task上报,用cancel_task取消,而不是cancel_request。
事件
IdeEvent(crates/ide-core/src/event.rs)是一个枚举,涵盖后端主动告诉 UI 的所有事情,序列化为 { type: "<camelCase 名称>", ... }。功能 crate 通过 EventSink 发出:
src-tauri/src/events.rs 用 TauriSink 实现它:把事件作为唯一的 Tauri 事件 ide://event 发给某一个窗口(及其辅助窗口)或所有窗口。各 crate 只拿 SharedSink,从不依赖 Tauri;测试里用 NullSink。
UI 按类型订阅,回调拿到的是收窄后的变体:
src/features/project-trust/index.tsx
onIdeEvent 返回一个取消订阅函数,把它放进功能的 disposer 里返回。监听是按窗口隔离的。
线程
长任务绝不阻塞 UI 线程或 Tauri 命令线程。把命令写成async,CPU 密集或阻塞的工作放到 spawn_blocking 或 rayon 上,并响应 CancellationToken。同步命令在主线程上运行,必须非常轻量。
远程窗口
远程开发不需要写任何特殊代码。在远程窗口里,src/platform/remote.ts 会把服务端能处理的命令经 remote_invoke 路由过去;每个 Channel 参数被替换成 { "$channel": id },其值带着 id 传回。事件以 event 通知到达,再在本地重新发出。你需要做的是:把实现放在 ide-host::ops,并注册到服务端的 Dispatcher(crates/ide-host/src/rpc.rs 和各 ops 模块里的 d.add("search_start", ...) 列表),这样服务端才能运行它。见远程开发。
清单:新增一个命令
- 逻辑放在所属的 crate 里;在其边界把错误转成
IdeError;使用 IPC 坐标。 - 如果涉及项目,把共享实现加到
ide-host::ops::<domain>;远程窗口需要的话,注册到远程 dispatcher。 - 在
src-tauri/src/commands/<domain>.rs添加#[tauri::command] #[specta::specta]适配函数,并加入collect_commands!。 - 如果它会执行任何东西,不要把它加进受限模式白名单(未受信任的项目里默认拒绝)。
- 运行
pnpm gen:bindings,然后用call(commands.yourCommand(...))调用。