дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
- workbench/messages: one promise-based ask/confirm/confirmChecked/showError host replaces the per-feature confirm dialogs (app, services, vcs, project tree, keymap, run, database, find, LSP choice popup). - Commit tool window: Commit | Shelf | Stash tabs as the title, IDEA's toolbar (Update Project, View Options, Select Opened File), Unversioned collapsed, Commit / Commit and Push on one row, empty-message error. - Commit Options popup: author, sign-off, before-commit checks, Git hooks; vcs_commit takes CommitOptions (--author, --no-verify). - Remove from Recent Projects asks first; refused history rewrites show an error dialog. Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu
172 рядки
5.3 KiB
TypeScript
172 рядки
5.3 KiB
TypeScript
/**
|
|
* Window close / quit flow.
|
|
*
|
|
* Closing a window (traffic light, Quit closing every window) runs
|
|
* `decideClose`: confirm exit when this is the last window and
|
|
* `general.confirmExit` is on, then Save All / Don't Save / Cancel when
|
|
* documents have unsaved changes. Quit confirms once, then the backend closes
|
|
* every window, each of which runs only its unsaved-changes check.
|
|
*/
|
|
import { commands, isTauri, onIdeEvent, type Settings } from "@/platform/ipc";
|
|
import { t } from "@/platform/i18n";
|
|
import { provideLeaveProjectGuard, useProject } from "@/platform/project";
|
|
import { maybeEditorService, notifications } from "@/workbench/contributions";
|
|
import { ask, type Ask } from "@/workbench/messages";
|
|
|
|
/** Document ids the backend reports dirty in this window. */
|
|
const dirtyDocs = new Set<number>();
|
|
|
|
export function dirtyCount(): number {
|
|
return dirtyDocs.size;
|
|
}
|
|
|
|
export function confirmExitEnabled(settings: Settings | null | undefined): boolean {
|
|
return settings?.general?.confirmExit === true;
|
|
}
|
|
|
|
export interface CloseInput {
|
|
quitting: boolean;
|
|
windows: number;
|
|
confirmExit: boolean;
|
|
dirty: number;
|
|
project: string | null;
|
|
}
|
|
|
|
export interface CloseDeps {
|
|
ask: Ask;
|
|
saveAll: () => Promise<void>;
|
|
disableConfirmExit: () => Promise<void>;
|
|
onSaveFailed: (e: unknown) => void;
|
|
}
|
|
|
|
/** Asks whatever must be asked; resolves true when the window may close. */
|
|
export async function decideClose(input: CloseInput, deps: CloseDeps): Promise<boolean> {
|
|
if (!input.quitting && input.windows <= 1 && input.confirmExit) {
|
|
const r = await deps.ask({
|
|
title: t("app.exit.title"),
|
|
body: t("app.exit.body"),
|
|
buttons: [{ id: "exit", label: t("app.exit.confirm"), primary: true }],
|
|
checkbox: t("app.exit.dontAsk"),
|
|
});
|
|
if (r.button !== "exit") return false;
|
|
if (r.checked) await deps.disableConfirmExit().catch(() => {});
|
|
}
|
|
if (input.dirty > 0) {
|
|
const r = await deps.ask({
|
|
title: t("app.unsaved.title"),
|
|
body: t("app.unsaved.body", { count: input.dirty, project: input.project ?? t("app.title.default") }),
|
|
buttons: [
|
|
{ id: "discard", label: t("app.unsaved.discard") },
|
|
{ id: "save", label: t("app.unsaved.save"), primary: true },
|
|
],
|
|
icon: "warning",
|
|
});
|
|
if (r.button === "cancel") return false;
|
|
if (r.button === "save") {
|
|
try {
|
|
await deps.saveAll();
|
|
} catch (e) {
|
|
deps.onSaveFailed(e);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const defaultDeps: CloseDeps = {
|
|
ask,
|
|
saveAll: async () => {
|
|
await maybeEditorService()?.saveAll();
|
|
},
|
|
disableConfirmExit: async () => {
|
|
const r = await commands.settingsSet("global", "general.confirmExit", "false");
|
|
if (r.status === "ok") useProject.setState({ settings: r.data });
|
|
},
|
|
onSaveFailed: (e) =>
|
|
notifications().notify({
|
|
level: "error",
|
|
title: t("app.unsaved.saveFailed"),
|
|
body: e instanceof Error ? e.message : String(e),
|
|
}),
|
|
};
|
|
|
|
let closing = false;
|
|
|
|
async function onCloseRequested(deps: CloseDeps): Promise<boolean> {
|
|
if (closing) return false;
|
|
closing = true;
|
|
try {
|
|
const ctx = await commands.windowCloseContext();
|
|
const s = useProject.getState();
|
|
const ok = await decideClose(
|
|
{
|
|
quitting: ctx.quitting,
|
|
windows: ctx.windows,
|
|
confirmExit: confirmExitEnabled(s.settings),
|
|
dirty: dirtyCount(),
|
|
project: s.project?.name ?? null,
|
|
},
|
|
deps,
|
|
);
|
|
if (!ok && ctx.quitting) await commands.appCancelQuit();
|
|
return ok;
|
|
} finally {
|
|
closing = false;
|
|
}
|
|
}
|
|
|
|
/** Unsaved changes before this window opens another project in place or closes it. */
|
|
export function confirmLeave(deps: CloseDeps = defaultDeps): Promise<boolean> {
|
|
const s = useProject.getState();
|
|
return decideClose(
|
|
{ quitting: false, windows: 2, confirmExit: false, dirty: dirtyCount(), project: s.project?.name ?? null },
|
|
deps,
|
|
);
|
|
}
|
|
|
|
/** Quit: confirm once (if enabled), then let the backend close every window. */
|
|
export async function quitApp(deps: CloseDeps = defaultDeps): Promise<void> {
|
|
if (!isTauri()) return;
|
|
if (confirmExitEnabled(useProject.getState().settings)) {
|
|
const ok = await decideClose({ quitting: false, windows: 1, confirmExit: true, dirty: 0, project: null }, deps);
|
|
if (!ok) return;
|
|
}
|
|
await commands.appQuit();
|
|
}
|
|
|
|
export function installCloseGuard(deps: CloseDeps = defaultDeps): () => void {
|
|
const unsubs = [
|
|
onIdeEvent("docDirty", (e) => {
|
|
if (e.dirty) dirtyDocs.add(e.doc);
|
|
else dirtyDocs.delete(e.doc);
|
|
}),
|
|
useProject.subscribe((s, prev) => {
|
|
if (s.project?.root !== prev.project?.root) dirtyDocs.clear();
|
|
}),
|
|
provideLeaveProjectGuard(() => confirmLeave(deps)),
|
|
];
|
|
let unlisten: (() => void) | undefined;
|
|
let disposed = false;
|
|
if (isTauri()) {
|
|
void import("@tauri-apps/api/window").then(async ({ getCurrentWindow }) => {
|
|
const off = await getCurrentWindow().onCloseRequested(async (event) => {
|
|
if (!(await onCloseRequested(deps))) event.preventDefault();
|
|
});
|
|
if (disposed) off();
|
|
else unlisten = off;
|
|
});
|
|
}
|
|
return () => {
|
|
disposed = true;
|
|
unlisten?.();
|
|
unsubs.forEach((u) => u());
|
|
};
|
|
}
|
|
|
|
/** Tests only. */
|
|
export function _setDirtyForTests(ids: number[]) {
|
|
dirtyDocs.clear();
|
|
ids.forEach((i) => dirtyDocs.add(i));
|
|
}
|