Файли
openidea/src/workbench/messages.tsx
T
l0ng-ai 81e0e8e2d0 feat: Align interactions with IntelliJ IDEA (#22)
* feat(nav): Complete file paths and flag unresolved web file references

Path completion in href/src/url()/@import, Markdown links and path-like strings, folders first. HTML and CSS paths that name no file get a 'Cannot resolve file' warning. File references resolve in the backend like IDEA's: relative to the file, /-rooted against the project and resource roots.

* feat: Align editor, workbench, run/debug and VCS interactions with IntelliJ IDEA

Keymap
- Let AltGr characters through instead of firing Ctrl+Alt shortcuts (Windows/Linux)
- Double Shift: strict press/release timing, ignored while typing or after Shift+click
- Chords wait 2s and swallow an unmatched second stroke
- Drop CodeMirror defaults that clash with IDEA keys; ⌘Enter is Split Line, add
  Scroll to Center and Code Block Start/End
- Add missing IDEA defaults (⌃J, ⌘Y, ⇧⌘C, ⌥⌘Y, ⇧⌘M, F9 Resume, ⌘R Rerun, …)

Editor
- Tab inserts at the caret; smart Backspace to the proper indent; Enter continues
  a `//` comment; per-language line comment placement
- Back/Forward records navigations only; ⌘F searches the selected text
- In-place rename; ⌃Space inserts a sole candidate; commit characters only for a
  focused lookup; usage highlighting survives edits; ⌥Enter offers fixes from the
  caret line; ⌘-hover shows declaration info only for navigable targets

Workbench
- Tool windows: primary and split area per side, six Move To anchors, layout migration
- ⌘W closes tool window content; Shift+Esc from the editor; closing a tab
  activates the left neighbour; preview tab and tab limit follow IDEA defaults
- Switcher lists open editors and tool windows; Recent Files Delete removes items
- Search Everywhere tab/scope behaviour; popups close when the window deactivates
- Project view: Copy/Move dialogs on paste, skippable usage search on delete,
  new-file input cancels on blur; Settings search Esc clears the filter
- Status bar breadcrumbs open a children popup; notification timeouts

Run / Debug / Build / Terminal
- Run asks to stop a running single-instance config; Rerun restarts silently;
  "Allow multiple instances" option
- Run chooser: Enter runs, Shift debugs, F4/Delete/digits
- Type into the console for stdin; Stop covers debug sessions
- Log (Shift+click) and temporary (Alt+click) breakpoints; independent suspend/log
  options; Evaluate prefill and history; changed-value highlighting; expandable hints
- Build window opens on failure and jumps to the first error
- Terminal confirms closing a busy tab and lets IDE shortcuts through

VCS
- Correct ours/theirs labels during rebase, cherry-pick and revert
- Smart/Force Checkout; remote branch checkout updates the local branch
- Partial commit by changed block; inline before-commit check results
- F7 continues into the next file; Rollback Lines; Commit and Push; Amend message swap
- Branch delete with Restore; force push confirmation; editable push target;
  update summary with the incoming commits

* chore: Regenerate bindings
2026-10-25 09:34:02 +08:00

150 рядки
4.8 KiB
TypeScript

/**
* Messages (IDEA's `Messages` / `MessageDialogBuilder`): promise-based alert
* dialogs for confirmations, questions and errors, shared by every feature.
*
* const { button, checked } = await ask({ title, body, buttons, checkbox });
* if (await confirm({ title: "Delete", message: "Delete x?", ok: "Delete" })) …
* await showError("Cannot rebase", e.message);
*
* `MessagesHost` (rendered by `OverlayHost`) shows them, newest on top.
*/
import { useState, type ReactNode } from "react";
import { create } from "zustand";
import { t } from "@/platform/i18n";
import { Checkbox, Dialog, type DialogButton, type MessageIconKind } from "@/ui";
export interface MessageButton {
id: string;
label: string;
/** The default button (Enter); at most one. */
primary?: boolean;
}
export interface MessageRequest {
title: string;
body: ReactNode;
/** Declaration order (IDEA `createActions`); the dialog orders them per platform. */
buttons: MessageButton[];
/** Adds a Cancel button (id `cancel`) unless one is declared. Default true. */
cancel?: boolean;
/** "Don't ask again" and similar, shown under the text. */
checkbox?: string;
/** Initial state of `checkbox` (default unchecked). */
checked?: boolean;
/** Closes the dialog from outside (work finished meanwhile): resolves with button `dismissed`. */
signal?: AbortSignal;
/** Defaults to `question`. */
icon?: MessageIconKind;
width?: number;
}
export interface MessageResult {
/** The chosen button's id; `cancel` for Escape and the Cancel button. */
button: string;
checked: boolean;
}
export type Ask = (request: MessageRequest) => Promise<MessageResult>;
interface Pending extends MessageRequest {
id: number;
resolve: (r: MessageResult) => void;
}
const useMessages = create<{ stack: Pending[] }>(() => ({ stack: [] }));
let seq = 0;
/** Shows a message dialog; resolves with the chosen button. */
export const ask: Ask = (request) =>
new Promise((resolve) => {
const id = ++seq;
let settled = false;
const finish = (r: MessageResult) => {
if (settled) return;
settled = true;
request.signal?.removeEventListener("abort", onAbort);
useMessages.setState((st) => ({ stack: st.stack.filter((m) => m.id !== id) }));
resolve(r);
};
const onAbort = () => finish({ button: "dismissed", checked: !!request.checked });
if (request.signal?.aborted) return onAbort();
request.signal?.addEventListener("abort", onAbort);
useMessages.setState((s) => ({ stack: [...s.stack, { ...request, id, resolve: finish }] }));
});
export interface ConfirmOptions {
title: string;
message: ReactNode;
/** Label of the default button. */
ok: string;
icon?: MessageIconKind;
checkbox?: string;
/** Initial state of `checkbox`. */
checked?: boolean;
}
/** OK / Cancel question; resolves `null` when cancelled, else the checkbox state. */
export async function confirmChecked(opts: ConfirmOptions): Promise<{ checked: boolean } | null> {
const r = await ask({
title: opts.title,
body: opts.message,
icon: opts.icon,
checkbox: opts.checkbox,
checked: opts.checked,
buttons: [{ id: "ok", label: opts.ok, primary: true }],
});
return r.button === "ok" ? { checked: r.checked } : null;
}
/** OK / Cancel question; resolves true for OK. */
export async function confirm(opts: Omit<ConfirmOptions, "checkbox" | "checked">): Promise<boolean> {
return (await confirmChecked(opts)) !== null;
}
/** IDEA's `Messages.showErrorDialog`: the error and an OK button. */
export async function showError(title: string, message: ReactNode): Promise<void> {
await ask({
title,
body: message,
icon: "error",
cancel: false,
buttons: [{ id: "ok", label: "OK", primary: true }],
});
}
export function MessagesHost() {
const stack = useMessages((s) => s.stack);
return (
<>
{stack.map((m) => (
<MessageDialog key={m.id} message={m} />
))}
</>
);
}
function MessageDialog({ message }: { message: Pending }) {
const [checked, setChecked] = useState(!!message.checked);
const done = (button: string) => message.resolve({ button, checked });
const declared: DialogButton[] = message.buttons.map((b) => ({
label: b.label,
role: b.primary ? "default" : b.id === "cancel" ? "cancel" : "extra",
autoFocus: b.primary,
onClick: () => done(b.id),
}));
const withCancel = message.cancel !== false && !message.buttons.some((b) => b.id === "cancel");
return (
<Dialog
open
onClose={() => done("cancel")}
title={message.title}
icon={message.icon ?? "question"}
width={message.width ?? 460}
footerStart={message.checkbox && <Checkbox checked={checked} onChange={setChecked} label={message.checkbox} />}
buttons={withCancel ? [...declared, { label: t("common.cancel"), role: "cancel" }] : declared}
>
{message.body}
</Dialog>
);
}