Files
openidea/src/platform/ipc.ts
T
l0ng-ai 357b728f1d Editor: quick fixes, completion, navigation while typing, and split-pane drag (#109)
* feat(editor): Drag tabs and files between split panes

HTML5 drag and drop never reaches the webview while native file drop is
enabled, so tab, project-file and changelist drags did nothing. They now
run on pointer events:

- Drag an editor tab onto another pane, or onto a pane's edge to split it.
- Drag files from the Project view into an editor pane (center opens them
  there, an edge opens them in a new split).
- Drag files between changelists in the Commit view.
- "Drag-and-drop with Alt pressed only" applies to tabs and stripe buttons.

* fix(lsp): Deliver server settings and start servers on a supported JDK

- workspace/configuration answers a server's own section (basedpyright)
  from its settings block; basedpyright runs in "standard" mode and keeps
  auto search paths on, so `src/` layouts resolve from `tests/`.
- jdtls: formatting follows the editor indent, calls complete without
  guessed arguments, and JDK internals stay out of completion.
- `max-java` in a language server config picks an installed JDK no newer
  than that (kotlin-language-server fails on JDK 25).
- Server initialization may take up to 5 minutes (first Gradle import).
- `lsp_resend_diagnostics` re-emits stored diagnostics after a reload.
- Spelling only checks declared names (not called methods like
  `substring`) and accepts plurals of known software terms.

* feat(editor): Quick fixes, completion and navigation while typing

Quick fixes
- Errors get a red quick-fix bulb; Apply Quick Fix (⌥⇧↵) fixes the most
  severe problem at the caret, and the error hover offers the fix directly.
- Code generated by a code action is re-indented to the file's style.
- Python: Implement abstract methods (⌃I or the bulb) from the bases.
- Duplicate code actions are listed once.

Completion
- Overloads sharing a name, and a name importable from several modules,
  are no longer merged into one entry.
- Auto-import entries show the module they import from.
- Call snippets without a stop (`new ArrayList<>()`) leave the caret inside
  the parentheses with parameter info, or past them when nothing takes
  arguments; parameter info also opens after server call snippets.
- The selected item is resolved ahead of acceptance, so its auto-import
  is applied (rust-analyzer).

Navigation and diagnostics
- Language server requests wait for pending edits, so Go to Declaration
  right after typing answers for the current text; an empty answer right
  after an edit is retried while the server catches up.
- Diagnostics come back after a window reload; error underlines stay
  visible under typo / unused highlighting.
- Implementation gutter markers no longer refresh in a loop while the
  server reports progress.
- A missing alternative server no longer shows as a server error.

* chore(lint): Skip agent worktrees under .claude

* fix(editor): Skip cancelled LSP requests and limit generated-code reformat

- Requests cancelled while waiting for pending edits are not sent.
- Code generated by an action is reformatted per changed hunk, so an added
  import doesn't reformat the code between it and the generated lines.
- A long busy spell caused by a settle refresh re-runs it only once.
- Files dropped on a file row of another changelist move to that changelist.
- Python Implement methods: a nested class without bases ends the lookup,
  class bodies continue through unindented string lines, and
  `@abstractproperty` stubs become `@property`.
- Resend diagnostics goes through the host ops, and custom servers get
  `max_java`.

* fix(editor): Rank the exact type first in Java constructor completion

jdtls appends the call or body to filterText (`String(${0})`); matching
on the label instead lets typing `String` rank `String` above
`StringBuilder` and the other types sharing its sort text.

* fix(editor): List identical completion items once

kotlin-language-server repeats a class once per classpath entry declaring
it, so `String()` and `String` were listed three times each.

* fix(editor): Keep the internal cancellation message out of the UI text catalog
2026-10-28 09:07:50 +08:00

85 lines
2.8 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";
}
let nextId = 1;
/** 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;
}