Files
openidea/src/platform/ipc.ts
T
l0ng-ai ec5193f827 feat(format): external formatting with managed Prettier (#671)
Reformat Code only asked the language server for formatting, so files whose server has no formatting capability (Markdown with marksman, and others depending on the server) could not be formatted.

Languages can now declare an external formatter. Prettier is the first: a managed tool installed on demand, or the project's own node_modules/prettier, an explicit path, or PATH. It runs on the backend copy of the document with the file's path so Prettier resolves the project's config, overrides and ignore files, and its result is applied as one undoable edit. Language servers that format a file keep doing so unless Prettier is preferred in Settings › Languages › Prettier; formatting on save is opt-in. Parsers without range support format the whole file and say so.

Errors carry structured tool data, so a missing Prettier reuses the shared install prompt and Prettier diagnostics or timeouts show as non-sticky notifications.

Fixes #639
2026-11-01 16:54:31 +08:00

109 lines
4.0 KiB
TypeScript

/**
* 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"];
readonly data: IdeError["data"];
constructor(error: IdeError) {
super(error.message);
this.name = "IpcError";
this.kind = error.kind;
this.data = error.data;
}
}
type Result<T> = { status: "ok"; data: T } | { status: "error"; error: IdeError };
/** Unwraps a fallible command result: `await call(commands.docOpen(path))`. */
export async function call<T>(result: Promise<Result<T>>): Promise<T> {
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;
}
/** True when `unavailable` means no server is running for the file, rather than a running server lacking the feature. */
export function isNoServer(e: unknown): boolean {
return e instanceof IpcError && e.kind === "unavailable" && !LACKS_FEATURE.test(e.message);
}
// 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<K extends IdeEvent["type"]> = Extract<IdeEvent, { type: K }>;
type Handler = (event: IdeEvent) => void;
const handlers = new Map<string, Set<Handler>>();
let unlisten: Promise<UnlistenFn> | null = null;
function ensureListening() {
if (unlisten || !isTauri()) return;
// Window-scoped: a global `listen` also receives events emitted to other windows.
unlisten = getCurrentWebviewWindow().listen<IdeEvent>(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<void> {
ensureListening();
await unlisten;
}
/** Subscribes to one backend event type (or `"*"`); returns an unsubscribe function. */
export function onIdeEvent<K extends IdeEvent["type"]>(
type: K | "*",
handler: (event: EventOf<K>) => 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;
}