дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(vcs): open comparisons in independently resizable native windows (#818)
Compare with Local, Compare Versions and the other revision comparisons open in a separate window that belongs to their project window. The window can be moved outside the main window, resized from its edges and corners, and shows the full diff. Edits made there and in the project window are mirrored both ways. Root cause of #798: the comparison was an in-window dialog with a fixed size. Settings and comparison windows now share one auxiliary-window mechanism (`AuxKind`, `open_auxiliary_window`). It handles labels, the title bar, placement over the parent, event forwarding, document-edit mirroring, menu routing, closing together with the parent, and remembered geometry per kind. Co-authored-by: xdpan1234 <[email protected]>
Цей коміт міститься в:
1 джерело
268cf01a86
коміт
0a70b0af01
26 files changed
+918
-198
No files matched your search
@@ -12,6 +12,7 @@ import { t } from "@/platform/i18n";
|
||||
import { closeProject, provideLeaveProjectGuard, useProject } from "@/platform/project";
|
||||
import { maybeEditorService, notifications, type EditorCloseSession } from "@/workbench/contributions";
|
||||
import { ask, type Ask } from "@/workbench/messages";
|
||||
import { AUX_CLOSED, auxiliaryWindow, closeDocumentWindows, type AuxCloseResult } from "@/platform/auxWindow";
|
||||
|
||||
/** Document ids the backend reports dirty in this window. */
|
||||
const dirtyDocs = new Set<number>();
|
||||
@@ -108,6 +109,10 @@ async function onCloseRequested(deps: CloseDeps): Promise<boolean> {
|
||||
closing = true;
|
||||
try {
|
||||
const ctx = await commands.windowCloseContext();
|
||||
if (!(await closeDocumentWindows())) {
|
||||
await (ctx.quitting ? commands.appCancelQuit() : commands.windowGroupCancelClose());
|
||||
return false;
|
||||
}
|
||||
const s = useProject.getState();
|
||||
// Closing the last project (or LightEdit) window closes its project and shows the welcome
|
||||
// screen (to open another one); closing the welcome screen, or quitting, closes the window.
|
||||
@@ -137,13 +142,16 @@ async function onCloseRequested(deps: CloseDeps): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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,
|
||||
);
|
||||
/**
|
||||
* Unsaved changes before this window opens another project in place or closes it. `project` names
|
||||
* the project in the prompt when this window doesn't hold it (a comparison window).
|
||||
*/
|
||||
export async function confirmLeave(
|
||||
deps: CloseDeps = defaultDeps,
|
||||
project = useProject.getState().project?.name ?? null,
|
||||
): Promise<boolean> {
|
||||
if (!(await closeDocumentWindows())) return false;
|
||||
return decideClose({ quitting: false, windows: 2, confirmExit: false, dirty: dirtyCount(), project }, deps);
|
||||
}
|
||||
|
||||
/** Quit: confirm once (if enabled), then let the backend close every window. */
|
||||
@@ -174,11 +182,26 @@ export function installCloseGuard(deps: CloseDeps = defaultDeps): () => void {
|
||||
let disposed = false;
|
||||
if (isTauri()) {
|
||||
void import("@tauri-apps/api/window").then(async ({ getCurrentWindow }) => {
|
||||
// Documents an auxiliary window released are no longer dirty unless this window edits them too.
|
||||
const offAux = await getCurrentWindow().listen<AuxCloseResult>(AUX_CLOSED, ({ payload }) => {
|
||||
if (
|
||||
!payload.closed ||
|
||||
auxiliaryWindow(payload.label)?.parent !== getCurrentWindow().label ||
|
||||
!Array.isArray(payload.documents)
|
||||
)
|
||||
return;
|
||||
const own = new Set(maybeEditorService()?.dirtyDocumentIds?.() ?? []);
|
||||
for (const doc of payload.documents) if (!own.has(doc)) dirtyDocs.delete(doc);
|
||||
});
|
||||
const off = await getCurrentWindow().onCloseRequested(async (event) => {
|
||||
if (!(await onCloseRequested(deps))) event.preventDefault();
|
||||
});
|
||||
if (disposed) off();
|
||||
else unlisten = off;
|
||||
const stop = () => {
|
||||
off();
|
||||
offAux();
|
||||
};
|
||||
if (disposed) stop();
|
||||
else unlisten = stop;
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
|
||||
@@ -20,8 +20,11 @@ import { installLaunchHandling, installLightEditAutoClose, windowActions } from
|
||||
import { WorkspaceDialogHost } from "./WorkspaceDialog";
|
||||
import { installWindowGroups } from "@/platform/windowGroups";
|
||||
|
||||
/** The UI locale of a secondary (Settings) window. */
|
||||
/** The UI locale of a secondary (Settings, comparison) window. */
|
||||
export const activateLocale = installLocaleSync;
|
||||
/** Native menu items of a secondary window that runs actions itself (comparison). */
|
||||
export const activateMenuBridge = installMenuBridge;
|
||||
export { confirmLeave } from "./closeGuard";
|
||||
|
||||
export function activate(): () => void {
|
||||
const disposers = [
|
||||
|
||||
@@ -12,7 +12,7 @@ import { activate as projectTree } from "./project-tree";
|
||||
import { activate as search } from "./search";
|
||||
import { activate as dependencies } from "./dependencies";
|
||||
import { activate as codeInsight } from "./code-insight";
|
||||
import { activate as vcs, activateSettingsPages as vcsPages } from "./vcs";
|
||||
import { activate as vcs, activateComparisonWindow, activateSettingsPages as vcsPages } from "./vcs";
|
||||
import { activate as terminal } from "./terminal";
|
||||
import { activate as run } from "./run";
|
||||
import { activate as runAnything } from "./run-anything";
|
||||
@@ -24,8 +24,8 @@ import { activate as buildTools } from "./build-tools";
|
||||
import { activate as build } from "./build";
|
||||
import { activate as templates, activateSettingsPages as templatesPages } from "./templates";
|
||||
import { activate as todo, activateSettingsPages as todoPages } from "./todo";
|
||||
import { activate as settings, activateSettingsWindow } from "./settings";
|
||||
import { activate as app, activateLocale as appLocale } from "./app";
|
||||
import { activate as settings, activateRuntimeSettings, activateSettingsWindow } from "./settings";
|
||||
import { activate as app, activateLocale as appLocale, activateMenuBridge, confirmLeave } from "./app";
|
||||
import { activate as perf } from "./perf";
|
||||
import { activate as plugins, activateSettingsPages as pluginsPages } from "./plugins";
|
||||
import { activate as tools, activateSettingsPages as toolsPages } from "./tools";
|
||||
@@ -90,20 +90,40 @@ const features: Array<[string, Activate]> = [
|
||||
["remote-dev", remoteDev],
|
||||
];
|
||||
|
||||
export function activateFeatures(): () => void {
|
||||
function activateAll(what: string, list: Array<[string, Activate]>): () => void {
|
||||
const disposers: Array<() => void> = [];
|
||||
for (const [name, activate] of features) {
|
||||
for (const [name, activate] of list) {
|
||||
try {
|
||||
const d = activate();
|
||||
if (d) disposers.push(d);
|
||||
} catch (e) {
|
||||
console.error(`feature ${name} failed to activate`, e);
|
||||
console.error(`${what} of ${name} failed to activate`, e);
|
||||
}
|
||||
}
|
||||
return () => disposers.reverse().forEach((d) => d());
|
||||
}
|
||||
|
||||
export { SettingsWindow, settingsWindowParent } from "./settings";
|
||||
export function activateFeatures(): () => void {
|
||||
return activateAll("feature", features);
|
||||
}
|
||||
|
||||
export { SettingsWindow } from "./settings";
|
||||
export { ComparisonWindow } from "./vcs";
|
||||
|
||||
/**
|
||||
* A comparison window of project window `parent`: the changed files between two revisions and
|
||||
* their diffs, with the editor and Git but no workbench.
|
||||
*/
|
||||
export function activateComparisonWindowFeatures(parent: string): () => void {
|
||||
return activateAll("comparison window", [
|
||||
["locale", appLocale],
|
||||
["settings", activateRuntimeSettings],
|
||||
["menu", activateMenuBridge],
|
||||
["editor", editor],
|
||||
["vcs", vcs],
|
||||
["comparison", () => activateComparisonWindow(parent, (project) => confirmLeave(undefined, project))],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The native Settings window of project window `parent`: only the settings
|
||||
@@ -122,14 +142,5 @@ export function activateSettingsWindowFeatures(parent: string): () => void {
|
||||
["vim", vimPages],
|
||||
["vcs", vcsPages],
|
||||
];
|
||||
const disposers: Array<() => void> = [];
|
||||
for (const [name, activate] of pages) {
|
||||
try {
|
||||
const d = activate();
|
||||
if (d) disposers.push(d);
|
||||
} catch (e) {
|
||||
console.error(`settings pages of ${name} failed to activate`, e);
|
||||
}
|
||||
}
|
||||
return () => disposers.reverse().forEach((d) => d());
|
||||
return activateAll("settings pages", pages);
|
||||
}
|
||||
@@ -40,7 +40,8 @@ import { activatePythonStatus } from "./languages/PythonStatus";
|
||||
import { nativeToAsciiAction } from "./nativeToAscii";
|
||||
import { isImeKey } from "@/platform/ime";
|
||||
|
||||
export { settingsWindowParent } from "./window/bridge";
|
||||
/** Settings-driven runtime state (zoom, fonts, theme) of a secondary window. */
|
||||
export const activateRuntimeSettings = trackRuntimeSettings;
|
||||
export const SettingsWindow = lazyComponent(() => import("./window/SettingsWindow").then((m) => m.SettingsWindow));
|
||||
|
||||
/** Same event as `windows::LAUNCH_EVENT`: a pending launch (page) is waiting. */
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* The comparison window (`comparison-<parent>-<n>`): files that differ between two revisions of
|
||||
* its parent's project, and their diff, in a window of its own (IDEA's Changes Between dialog).
|
||||
*/
|
||||
import clsx from "clsx";
|
||||
import { useEffect } from "react";
|
||||
import { create } from "zustand";
|
||||
import { emitTo } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { editorController } from "@/editor/core/controller";
|
||||
import { AUX_CLOSED, type AuxCloseResult } from "@/platform/auxWindow";
|
||||
import { setContext } from "@/platform/context";
|
||||
import { isImeKey } from "@/platform/ime";
|
||||
import { call, commands, type ComparisonLaunch } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { restoreRemote } from "@/platform/remote";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { Button, Spinner, T, isPopupOpen } from "@/ui";
|
||||
import { editorService, notifications } from "@/workbench/contributions";
|
||||
import { EditorArea } from "@/workbench/EditorArea";
|
||||
import { OverlayHost } from "@/workbench/OverlayHost";
|
||||
import { TitleBar } from "@/workbench/TitleBar";
|
||||
import { useApplyTheme } from "@/workbench/theme";
|
||||
import { diffInput, type DiffData } from "./diffInput";
|
||||
import { ChangesBetweenDialog } from "./operations";
|
||||
import { refreshNow, useVcs } from "./store";
|
||||
import styles from "./common.module.css";
|
||||
|
||||
const useComparison = create<{
|
||||
launch: ComparisonLaunch | null;
|
||||
title: string;
|
||||
full: boolean;
|
||||
error: string | null;
|
||||
}>(() => ({ launch: null, title: "", full: false, error: null }));
|
||||
|
||||
/**
|
||||
* Loads the comparison of this window's launch. `confirmLeave` settles unsaved changes of the
|
||||
* documents edited here, in the named project, before the window closes.
|
||||
*/
|
||||
export function activateComparisonWindow(
|
||||
parent: string,
|
||||
confirmLeave: (project: string | null) => Promise<boolean>,
|
||||
): () => void {
|
||||
let projectName: string | null = null;
|
||||
const native = getCurrentWindow();
|
||||
let disposed = false;
|
||||
let closing = false;
|
||||
let stopClose: (() => void) | undefined;
|
||||
void native
|
||||
.onCloseRequested(async (event) => {
|
||||
if (closing) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
closing = true;
|
||||
const models = [...editorController().manager.byId.values()];
|
||||
let closed = false;
|
||||
try {
|
||||
await Promise.all(models.map((model) => model.sync.whenIdle()));
|
||||
closed = await confirmLeave(projectName);
|
||||
if (closed) {
|
||||
for (const model of models) model.dispose();
|
||||
await Promise.all(
|
||||
models.map((model) =>
|
||||
commands.docClose(model.doc).catch((error) => console.warn("cannot release comparison document", error)),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
notifications().notify({ level: "error", title: "Could not close comparison", body: String(error) });
|
||||
} finally {
|
||||
if (!closed) event.preventDefault();
|
||||
const result: AuxCloseResult = {
|
||||
label: native.label,
|
||||
closed,
|
||||
documents: closed ? models.map((model) => model.doc) : [],
|
||||
};
|
||||
await emitTo(parent, AUX_CLOSED, result).catch((error) =>
|
||||
console.warn("cannot report comparison close", error),
|
||||
);
|
||||
closing = false;
|
||||
}
|
||||
})
|
||||
.then((off) => {
|
||||
if (disposed) off();
|
||||
else stopClose = off;
|
||||
});
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented || isImeKey(event) || isPopupOpen()) return;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
void native.close();
|
||||
}
|
||||
};
|
||||
const onCloseShortcut = (event: KeyboardEvent) => {
|
||||
if (isImeKey(event) || isPopupOpen()) return;
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "w") {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
void native.close();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
window.addEventListener("keydown", onCloseShortcut, true);
|
||||
void (async () => {
|
||||
await restoreRemote();
|
||||
const launch = (await commands.windowTakeLaunch())?.comparison;
|
||||
if (!launch) throw new Error("Missing comparison request");
|
||||
const project = await commands.projectCurrent();
|
||||
if (!project) throw new Error("The comparison's project is no longer open");
|
||||
projectName = project.name;
|
||||
// Settings only: restoring the project's editor session here would overwrite the main window's tabs.
|
||||
useProject.setState({ settings: project.settings });
|
||||
const roots = await call(commands.vcsRoots());
|
||||
useVcs.setState({
|
||||
available: roots.length > 0,
|
||||
root: project.root,
|
||||
roots: roots.map((root) => root.root),
|
||||
repo: launch.repo,
|
||||
});
|
||||
setContext("projectOpen", true);
|
||||
setContext("vcsAvailable", roots.length > 0);
|
||||
await refreshNow();
|
||||
useComparison.setState({ launch });
|
||||
})().catch((error) => useComparison.setState({ error: String(error) }));
|
||||
return () => {
|
||||
disposed = true;
|
||||
stopClose?.();
|
||||
window.removeEventListener("keydown", onKey);
|
||||
window.removeEventListener("keydown", onCloseShortcut, true);
|
||||
};
|
||||
}
|
||||
|
||||
/** The comparison's caption, on the title bar and the native window (Window menu, task switcher). */
|
||||
function showCaption(caption: string) {
|
||||
if (useComparison.getState().title === caption) return;
|
||||
useComparison.setState({ title: caption });
|
||||
void getCurrentWindow()
|
||||
.setTitle(caption)
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
export function ComparisonWindow() {
|
||||
useApplyTheme();
|
||||
const { launch, title, full, error } = useComparison();
|
||||
// macOS overlays traffic lights on the bar; Windows and Linux are frameless, like Settings.
|
||||
const os = usePlatform((s) => s.os);
|
||||
const ownTitleBar = usePlatform((s) => s.overlayTitleBar || s.framelessTitleBar);
|
||||
const showDiff = (data: DiffData) => {
|
||||
void editorService()
|
||||
.openInput(diffInput(data), { focus: true })
|
||||
.then(() => useComparison.setState({ full: true }))
|
||||
.catch((error) => notifications().notify({ level: "error", title: "Could not show diff", body: String(error) }));
|
||||
};
|
||||
useEffect(() => {
|
||||
if (full) editorService().focus?.();
|
||||
}, [full]);
|
||||
return (
|
||||
<main className={clsx(styles.comparisonWindow, os === "linux" && styles.comparisonWindowEdge)}>
|
||||
{ownTitleBar && <TitleBar minimal dialog title={title} />}
|
||||
{error ? (
|
||||
<div className={styles.error} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
) : !launch ? (
|
||||
<div className={styles.comparisonWindowLoading} role="status">
|
||||
<Spinner />
|
||||
<T>Loading…</T>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{full && (
|
||||
<div className={styles.compareToolbar}>
|
||||
<Button onClick={() => useComparison.setState({ full: false })}>Back to Changed Files</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.comparisonWindowBody} hidden={full}>
|
||||
<ChangesBetweenDialog
|
||||
from={launch.from}
|
||||
to={launch.to}
|
||||
scope={launch.scope ?? undefined}
|
||||
native
|
||||
onCaption={showCaption}
|
||||
onShowDiff={showDiff}
|
||||
onClose={() => void getCurrentWindow().close()}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.comparisonWindowBody} hidden={!full}>
|
||||
<EditorArea />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<OverlayHost />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -280,6 +280,64 @@
|
||||
background: var(--bg-list-selected);
|
||||
}
|
||||
|
||||
.comparisonDialog > [data-dialog-body] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.comparisonDialog .comparison {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.comparisonWindow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--bg-dialog, var(--bg-elev));
|
||||
color: var(--fg);
|
||||
}
|
||||
/* Linux: frameless X11 windows have no shadow, so mark the edge in CSS (like the Settings window). */
|
||||
.comparisonWindowEdge {
|
||||
position: relative;
|
||||
}
|
||||
.comparisonWindowEdge::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 0 0 1px var(--border-window);
|
||||
}
|
||||
.comparisonWindowLoading {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.comparisonWindowBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.comparisonWindowBody[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.comparisonWindowBody > .comparisonWindow {
|
||||
flex: 1;
|
||||
}
|
||||
.comparisonWindowBody > .comparison {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.comparisonWindowFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.comparison {
|
||||
display: flex;
|
||||
height: min(520px, 65vh);
|
||||
|
||||
@@ -147,6 +147,8 @@ function conflictsBody(names: string[]): string {
|
||||
}
|
||||
|
||||
/** Settings › Version Control › Log (also used by the Settings window). */
|
||||
export { activateComparisonWindow, ComparisonWindow } from "./ComparisonWindow";
|
||||
|
||||
export function activateSettingsPages(): () => void {
|
||||
return registerSettingsPage({
|
||||
id: "vcs.log",
|
||||
|
||||
@@ -7,9 +7,10 @@ import { commitKey, commitRepo } from "./multiRoot";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ChangedFile, Commit, CompareResult, ResetMode, WorkingTreeChange } from "@/bindings";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { call, commands, nextRequestId } from "@/platform/ipc";
|
||||
import { Checkbox, Dialog, Icon, Input, Spinner, T } from "@/ui";
|
||||
import { call, commands, isTauri, nextRequestId } from "@/platform/ipc";
|
||||
import { Button, Checkbox, Dialog, Icon, Input, Spinner, T } from "@/ui";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { windowBackgroundHex } from "@/workbench/theme";
|
||||
import { diffInput, openDiff, type DiffData, type DiffSide } from "./diffInput";
|
||||
import { openDialog, promptText } from "./dialogs";
|
||||
import { KIND_COLOR, fileCount } from "./model";
|
||||
@@ -460,31 +461,32 @@ export function showCompareWithLocal(commit: Commit) {
|
||||
|
||||
/** Files that differ between `rev` (a commit or branch) and the working tree. */
|
||||
export function showDiffWithWorkingTree(rev: string, label = rev) {
|
||||
openDialog((close) => <ChangesBetweenDialog from={{ rev, label }} to={null} onClose={close} />);
|
||||
showComparison({ rev, label }, null);
|
||||
}
|
||||
|
||||
/** IDEA's Compare Versions of two Git Log commits: files that differ between them, older first. */
|
||||
export function showCompareVersions(older: Commit, newer: Commit) {
|
||||
openDialog((close) => (
|
||||
<ChangesBetweenDialog
|
||||
from={{ rev: older.id, label: short(older.id) }}
|
||||
to={{ rev: newer.id, label: short(newer.id) }}
|
||||
onClose={close}
|
||||
/>
|
||||
));
|
||||
showComparison({ rev: older.id, label: short(older.id) }, { rev: newer.id, label: short(newer.id) });
|
||||
}
|
||||
|
||||
/** Files that differ between two revisions (e.g. two branches from Compare with Current). */
|
||||
export function showCompareRevisions(from: string, to: string, fromLabel = from, toLabel = to) {
|
||||
openDialog((close) => (
|
||||
<ChangesBetweenDialog from={{ rev: from, label: fromLabel }} to={{ rev: to, label: toLabel }} onClose={close} />
|
||||
));
|
||||
showComparison({ rev: from, label: fromLabel }, { rev: to, label: toLabel });
|
||||
}
|
||||
|
||||
export function showPathComparison(paths: string[], rev: string, repositories: string[] | null) {
|
||||
openDialog((close) => (
|
||||
<ChangesBetweenDialog from={{ rev, label: rev }} to={null} scope={{ paths, repositories }} onClose={close} />
|
||||
));
|
||||
showComparison({ rev, label: rev }, null, { paths, repositories });
|
||||
}
|
||||
|
||||
function showComparison(from: Revision, to: Revision | null, scope?: ScopedComparison) {
|
||||
if (!isTauri()) {
|
||||
openDialog((close) => <ChangesBetweenDialog from={from} to={to} scope={scope} onClose={close} />);
|
||||
return;
|
||||
}
|
||||
const comparison = { from, to, scope: scope ?? null, repo: currentRepo() };
|
||||
void call(commands.windowOpenComparison(comparison, windowBackgroundHex())).catch((error) =>
|
||||
notifications().notify({ level: "error", title: "Could not open comparison", body: String(error) }),
|
||||
);
|
||||
}
|
||||
|
||||
interface Revision {
|
||||
@@ -516,11 +518,18 @@ export function ChangesBetweenDialog({
|
||||
to,
|
||||
scope,
|
||||
onClose,
|
||||
native = false,
|
||||
onCaption,
|
||||
onShowDiff,
|
||||
}: {
|
||||
from: Revision;
|
||||
to: Revision | null;
|
||||
scope?: ScopedComparison;
|
||||
onClose: () => void;
|
||||
/** Content of the comparison window rather than a dialog: its caption goes to `onCaption`. */
|
||||
native?: boolean;
|
||||
onCaption?: (caption: string) => void;
|
||||
onShowDiff?: (data: DiffData) => void;
|
||||
}) {
|
||||
const [root] = useState(currentRepoRoot);
|
||||
const [repo] = useState(currentRepo);
|
||||
@@ -607,27 +616,20 @@ export function ChangesBetweenDialog({
|
||||
const skipped = result?.skippedPaths.map((path) => displayPath(path)) ?? [];
|
||||
const skippedNames = skipped.slice(0, 5).join(", ");
|
||||
const skippedLabel = skipped.length > 5 ? tx`${skippedNames} and ${skipped.length - 5} more` : skippedNames;
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={files ? `${title} — ${count}` : title}
|
||||
onClose={onClose}
|
||||
width={input ? 1100 : 680}
|
||||
buttons={[
|
||||
{
|
||||
label: "Show Diff",
|
||||
role: "default",
|
||||
disabled: !current,
|
||||
onClick: () => {
|
||||
if (current) {
|
||||
void openDiff(diffFor(current));
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
{ label: "Close", role: "cancel" },
|
||||
]}
|
||||
>
|
||||
const caption = files ? `${title} — ${count}` : title;
|
||||
useEffect(() => {
|
||||
if (native) onCaption?.(caption);
|
||||
}, [native, onCaption, caption]);
|
||||
const showDiff = () => {
|
||||
if (!current) return;
|
||||
if (onShowDiff) onShowDiff(diffFor(current));
|
||||
else {
|
||||
void openDiff(diffFor(current));
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const contents = (
|
||||
<>
|
||||
{error && (
|
||||
<div className={styles.error} role="alert">
|
||||
{error}
|
||||
@@ -682,6 +684,34 @@ export function ChangesBetweenDialog({
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
if (native)
|
||||
return (
|
||||
<div className={styles.comparisonWindow}>
|
||||
<div className={styles.comparisonWindowBody}>{contents}</div>
|
||||
<div className={styles.comparisonWindowFooter}>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
<Button variant="primary" disabled={!current} onClick={showDiff}>
|
||||
Show Diff
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={caption}
|
||||
onClose={onClose}
|
||||
width={input ? 1100 : 680}
|
||||
resizable="both"
|
||||
className={styles.comparisonDialog}
|
||||
buttons={[
|
||||
{ label: "Show Diff", role: "default", disabled: !current, onClick: showDiff },
|
||||
{ label: "Close", role: "cancel" },
|
||||
]}
|
||||
>
|
||||
{contents}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача