Skip to main content
The frontend and the backend talk over Tauri IPC, with three mechanisms: All three are typed end to end: Rust types derive specta::Type, and tauri-specta generates src/bindings.ts.

Commands

A command is a thin adapter in src-tauri/src/commands/<domain>.rs. It resolves the project from the calling window, calls the shared implementation (usually in ide_host::ops), and returns IdeResult<T>:
src-tauri/src/commands/app.rs
Register new commands in the collect_commands! list of commands::builder() (src-tauri/src/commands/mod.rs). The module’s conventions:
  • every command returns IdeResult<T> (it rejects with IdeError in JS);
  • project-scoped commands resolve the project from the calling window;
  • streaming results use a Channel<T> argument; cancellable requests take a UI-allocated RequestId and are cancelled via cancel_request.
Before any command runs, the invoke handler in src-tauri/src/lib.rs checks project trust and, for a remote window, routes it to the server.

Calling a command

The frontend imports commands from @/platform/ipc (which re-exports the bindings) and unwraps the result with call:
Generated functions return { status: "ok", data } | { status: "error", error }. call turns that into a promise that rejects with IpcError, which carries the error’s kind and data. Helpers: isCancelled(e), isNoServer(e), unavailableText(e).

Errors

IdeError (crates/ide-core/src/error.rs) is the single error type that crosses IPC:
ErrorKind is one of notFound, alreadyExists, permissionDenied, invalidInput, outOfSync (document version mismatch; the UI must resync the full text), cancelled, timedOut, unavailable (a required tool such as a language server, git or a debugger is missing), io, protocol, internal. Crates keep rich internal errors and convert at their public edge; the UI switches on kind and shows message. Don’t leak anyhow or crate-specific errors through a command.

Generated bindings

src/bindings.ts is generated by tauri-specta from the registered commands plus IdeEvent. Never edit it.
  • pnpm gen:bindings regenerates it without launching the app (cargo test -p openidea --test export_bindings).
  • Every debug launch also re-exports it, rewriting the file only when it changed.
  • CI regenerates it in the Rust job and fails if the committed file differs. After changing a command signature or an IPC type, commit src/bindings.ts.
Rust doc comments on commands and types become JSDoc in the bindings, so document commands in Rust. 64-bit integers are exported as number.

Text coordinates

Everything that crosses IPC uses the same coordinates (crates/ide-core/src/text.rs):
  • Lines are 0-based.
  • Columns and offsets are UTF-16 code units, matching JavaScript strings, CodeMirror and the LSP default encoding.
  • Pos { line, col }; Range { start, end } is half-open.
Conversion to and from byte offsets lives in ide-text. Convert at the edge of your crate: a tree-sitter byte offset or an LSP position in another encoding must never reach a command’s result.

Streaming with channels

High-volume or incremental results go through a Channel<T> argument, not the event bus. Find in Files:
src-tauri/src/commands/search.rs
On the UI side, create a Channel, set onmessage, and pass it as the argument:
src/platform/textSearch.ts
Make the stream self-terminating (a final Done value) so the UI knows when it ended.

Cancellable requests

The UI allocates the id; the backend registers a token for it:
crates/ide-host/src/ops/search.rs
Details that matter:
  • Ids are shared by all windows of the backend, so each page starts counting at a random point.
  • A cancel that arrives before the command began is remembered for 120 seconds, and the request then starts out cancelled (crates/ide-host/src/requests.rs).
  • Long work checks the CancellationToken and returns ErrorKind::Cancelled (or a partial result) promptly.
  • Tasks the backend starts itself (a language server’s work-done progress) are reported as IdeEvent::Task with a TaskId and cancelled with cancel_task, not cancel_request.

Events

IdeEvent (crates/ide-core/src/event.rs) is one enum of everything the backend tells the UI unasked, serialized as { type: "<camelCase name>", ... }. Feature crates emit through an EventSink:
src-tauri/src/events.rs implements it as TauriSink, which forwards to one window (plus its auxiliary windows) or all windows as the single Tauri event ide://event. Crates take a SharedSink and never depend on Tauri; tests use NullSink. The UI subscribes by type, and the handler gets the narrowed variant:
src/features/project-trust/index.tsx
onIdeEvent returns an unsubscribe function; return it from your feature’s disposer. Listening is window-scoped.

Threads

Long work never blocks the UI thread or a Tauri command thread. Make the command async and move CPU-bound or blocking work onto spawn_blocking or rayon; honour the CancellationToken. Synchronous commands run on the main thread, so keep them trivial.

Remote windows

You don’t write anything special for remote development. In a remote window, src/platform/remote.ts routes the commands the server answers through remote_invoke; each Channel argument is replaced by { "$channel": id } and its values come back tagged with the id. Events arrive as event notifications and are re-emitted locally. What you must do is keep the implementation in ide-host::ops and register it with the server’s Dispatcher (the d.add("search_start", ...) lists in crates/ide-host/src/rpc.rs and the ops modules) so the server can run it. See Remote development.

Checklist: adding a command

  1. Put the logic in the owning crate; convert errors to IdeError at its edge; use IPC coordinates.
  2. Add the shared implementation to ide-host::ops::<domain> if it touches a project; register it with the remote dispatcher if remote windows need it.
  3. Add the #[tauri::command] #[specta::specta] adapter in src-tauri/src/commands/<domain>.rs and list it in collect_commands!.
  4. If it executes anything, leave it out of the restricted-mode allowlist (it is denied in untrusted projects by default).
  5. pnpm gen:bindings, then call it with call(commands.yourCommand(...)).