Files
openidea/src/platform/ipc.ts
T
l0ng-ai c8e3fe9d2e fix(lsp): Name the running server when it lacks a feature (#201)
ide-lsp returned the same `unavailable` error whether no server was running or a running server didn't implement the request, so the UI said no language server was available, and Invoke Inline Completion said no language server provides inline completion, while jdtls was running fine (it doesn't implement textDocument/inlineCompletion). When servers are running but none supports the feature, the error now names them ("jdtls doesn't support inline completion"), and every explicit action shares one message helper: Invoke Inline Completion, go to declaration / implementation / type declaration, quick documentation, parameter info, rename, reformat, intention actions, highlight usages, call / type hierarchy, find usages, safe delete and workspace symbols. With no server running it still says no language server is available. Background requests while typing stay silent.

Fixes #186

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
2026-10-29 10:35:37 +08:00

102 lines
3.6 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"];
constructor(error: IdeError) {
super(error.message);
this.name = "IpcError";
this.kind = error.kind;
}
}
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;
}
// 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;
}