/** * Typed access to the Rust backend. * * `bindings.ts` is generated by tauri-specta; never edit it. Use `call` to turn * its `{status, data | error}` results into plain promises that reject with * `IpcError`, and `onIdeEvent` to subscribe to the backend event bus. */ import type { UnlistenFn } from "@tauri-apps/api/event"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { commands, type IdeError, type IdeEvent, type RequestId } from "@/bindings"; export { commands }; export type * from "@/bindings"; export class IpcError extends Error { readonly kind: IdeError["kind"]; constructor(error: IdeError) { super(error.message); this.name = "IpcError"; this.kind = error.kind; } } type Result = { status: "ok"; data: T } | { status: "error"; error: IdeError }; /** Unwraps a fallible command result: `await call(commands.docOpen(path))`. */ export async function call(result: Promise>): Promise { const r = await result; if (r.status === "error") throw new IpcError(r.error); return r.data; } export function isCancelled(e: unknown): boolean { return e instanceof IpcError && e.kind === "cancelled"; } /** Backend `unavailable` message when servers run but none has the feature: "jdtls doesn't support inline completion". */ const LACKS_FEATURE = /^\S.* doesn't support \S.*$/; /** * What to tell the user when a language-server feature is `unavailable`: the * running server that lacks it, so a working server doesn't read as missing; * `fallback` when no server is running. */ export function unavailableText(e: unknown, fallback = "No language server available"): string { return e instanceof IpcError && e.kind === "unavailable" && LACKS_FEATURE.test(e.message) ? e.message : fallback; } // Ids are shared by every window of the backend and outlive a reload there (a cancel that // arrives before its request began is remembered), so each page starts at a random point. let nextId = 1 + Math.floor(Math.random() * 0x7000_0000); /** Allocates an id for a cancellable request (see `commands.cancelRequest`). */ export function nextRequestId(): RequestId { return nextId++ as RequestId; } export const IDE_EVENT = "ide://event"; type EventOf = Extract; type Handler = (event: IdeEvent) => void; const handlers = new Map>(); let unlisten: Promise | null = null; function ensureListening() { if (unlisten || !isTauri()) return; // Window-scoped: a global `listen` also receives events emitted to other windows. unlisten = getCurrentWebviewWindow().listen(IDE_EVENT, ({ payload }) => { handlers.get(payload.type)?.forEach((h) => h(payload)); handlers.get("*")?.forEach((h) => h(payload)); }); } /** Resolves once backend events reach this window's handlers. */ export async function ideEventsReady(): Promise { ensureListening(); await unlisten; } /** Subscribes to one backend event type (or `"*"`); returns an unsubscribe function. */ export function onIdeEvent( type: K | "*", handler: (event: EventOf) => void, ): () => void { ensureListening(); let set = handlers.get(type); if (!set) handlers.set(type, (set = new Set())); const h = handler as Handler; set.add(h); return () => set.delete(h); } /** Test helper: dispatch an event as if it came from the backend. */ export function emitLocalIdeEvent(event: IdeEvent) { handlers.get(event.type)?.forEach((h) => h(event)); handlers.get("*")?.forEach((h) => h(event)); } export function isTauri(): boolean { return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; }