feat(vcs): add Preview Diff toggle to the Commit tool window (#1615)

A Preview Diff toggle in the Commit tool window's changes toolbar (and View Options › Show Diff Preview) shows the selected file's working-tree diff in an embedded, resizable pane: beside the changes and the commit message when the window is bottom-docked, below them when side-docked, as in the Local Changes tab. Off by default and remembered; empty states when nothing or several files are selected, or there are no changes.

Co-authored-by: Inf1mum <[email protected]>
Цей коміт міститься в:
Vadim LazukтаInf1mum authored and GitHub committed 2026-11-06 03:18:25 +08:00
1 джерело 800949b7b5
коміт 4830f42168
4 files changed
+201 -7

No files matched your search

+44 -7
Переглянути файл
@@ -55,6 +55,7 @@ import { moveToChangelist } from "./changelistsUi";
import {
messageHistory,
setAmend,
setCommitDiffPreview,
setDiffPreview,
setGroupByDir,
setGroupByStaging,
@@ -357,18 +358,43 @@ export function useIncludedCount(): number {
return useMemo(() => planCommit(status, checks).included.length, [status, checks]);
}
/** The Commit tool window's Commit tab: changes with inclusion checkboxes above the message. */
/**
* The Commit tool window's Commit tab: changes with inclusion checkboxes above the
* message, with an optional embedded preview of the selected file's diff beside them
* (issue #1599).
*/
function ChangesPanel() {
const included = useIncludedCount();
const preview = useCommit((s) => s.commitDiffPreview);
const path = useCommit((s) => (s.selection.length === 1 ? s.selection[0] : null));
const hasChanges = useVcs((s) => s.status.length > 0);
return (
<SplitGroup orientation="vertical" className={styles.split}>
<SplitGroup orientation="horizontal" className={styles.split}>
<SplitPanel defaultSize="62%" minSize="20%">
<ChangesTree mode="commit" />
</SplitPanel>
<SplitHandle />
<SplitPanel defaultSize="38%" minSize="120px">
<CommitPanel included={included} />
<SplitGroup orientation="vertical" className={styles.split}>
<SplitPanel defaultSize="62%" minSize="20%">
<ChangesTree mode="commit" />
</SplitPanel>
<SplitHandle />
<SplitPanel defaultSize="38%" minSize="120px">
<CommitPanel included={included} />
</SplitPanel>
</SplitGroup>
</SplitPanel>
{preview && (
<>
<SplitHandle />
<SplitPanel defaultSize="38%" minSize="120px">
{path ? (
<DiffEditor input={diffInput(workingDiffData(path))} />
) : (
<EmptyState>
<T>{hasChanges ? "Select a file to preview its changes" : "No changes to preview"}</T>
</EmptyState>
)}
</SplitPanel>
</>
)}
</SplitGroup>
);
}
@@ -415,6 +441,7 @@ export function ChangesTree({ mode }: { mode: "commit" | "local" | "dialog" }) {
const groupByDir = useCommit((s) => s.groupByDir);
const groupByStaging = useCommit((s) => s.groupByStaging);
const diffPreview = useCommit((s) => s.diffPreview);
const commitDiffPreview = useCommit((s) => s.commitDiffPreview);
const lists = useChangelists((s) => s.state);
const checks = useCommitChecks();
const checkboxes = mode !== "local";
@@ -511,6 +538,16 @@ export function ChangesTree({ mode }: { mode: "commit" | "local" | "dialog" }) {
])
}
/>
{/* WebStorm's Preview Diff: an embedded diff of the selection beside the changes (#1599). */}
{mode === "commit" && (
<IconButton
icon="editor-preview"
title="Preview Diff"
size="small"
pressed={commitDiffPreview}
onClick={() => setCommitDiffPreview(!commitDiffPreview)}
/>
)}
<span className={styles.sep} />
{mode !== "dialog" && (
<IconButton icon="locate" title="Select Opened File" size="small" onClick={selectOpened} />
+143
Переглянути файл
@@ -0,0 +1,143 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/platform/ipc", async (importOriginal) => {
const real = await importOriginal<typeof import("@/platform/ipc")>();
return { ...real, call: vi.fn() };
});
vi.mock("../DiffEditor", () => ({
DiffEditor: ({ input }: { input: { data: { path: string } } }) => (
<div data-testid="diff-preview" data-path={input.data.path} />
),
}));
const openDiff = vi.hoisted(() => vi.fn());
vi.mock("../diffInput", async (original) => ({
...(await original<typeof import("../diffInput")>()),
openDiff,
}));
import { CommitToolWindow } from "../CommitView";
import { setCommitDiffPreview, useCommit } from "../commitStore";
import { useVcs } from "../store";
/** The splitter between the changes and the preview: the only vertical one in the layout. */
const previewHandle = () =>
screen.queryAllByRole("separator").find((s) => s.getAttribute("aria-orientation") === "vertical");
const modified = (path: string) => ({
path,
relPath: path.slice(3),
staged: null,
unstaged: "modified" as const,
renamedFrom: null,
});
/** Selects the tree's first file the way a user does, so the tree's own state moves too. */
function selectFirstFile() {
const tree = screen.getByRole("tree", { name: "Changes" });
tree.focus();
fireEvent.keyDown(tree, { key: "Home" });
fireEvent.keyDown(tree, { key: "ArrowDown" });
}
describe("Commit tool window Preview Diff (#1599)", () => {
beforeEach(() => {
localStorage.clear();
openDiff.mockClear();
useCommit.setState({ commitDiffPreview: false, selection: [] });
useVcs.setState({ available: true, root: "/p", repo: "/p", roots: ["/p"], status: [] });
});
it("offers a Preview Diff toggle in the changes toolbar, off by default", () => {
render(<CommitToolWindow />);
const toggle = screen.getByRole("button", { name: "Preview Diff" });
expect(toggle).toHaveAttribute("aria-pressed", "false");
expect(screen.queryByTestId("diff-preview")).toBeNull();
});
it("shows an embedded diff pane beside the changes when enabled", () => {
useVcs.setState({ status: [modified("/p/a.ts")] });
render(<CommitToolWindow />);
selectFirstFile();
act(() => setCommitDiffPreview(true));
// Side by side with the changes: the splitter between the panels is vertical.
expect(previewHandle()).toBeTruthy();
expect(screen.getByTestId("diff-preview")).toHaveAttribute("data-path", "/p/a.ts");
expect(screen.getByPlaceholderText("Commit Message")).toBeTruthy();
});
it("previews the selected file and follows the selection", () => {
useVcs.setState({ status: ["a.ts", "b.ts"].map((name) => modified(`/p/${name}`)) });
render(<CommitToolWindow />);
act(() => setCommitDiffPreview(true));
expect(screen.getByText("Select a file to preview its changes")).toBeTruthy();
selectFirstFile();
expect(screen.getByTestId("diff-preview")).toHaveAttribute("data-path", "/p/a.ts");
const tree = screen.getByRole("tree", { name: "Changes" });
fireEvent.keyDown(tree, { key: "ArrowDown" });
expect(screen.getByTestId("diff-preview")).toHaveAttribute("data-path", "/p/b.ts");
fireEvent.keyDown(tree, { key: "ArrowUp" });
expect(screen.getByTestId("diff-preview")).toHaveAttribute("data-path", "/p/a.ts");
});
it("keeps the files list and the commit message accessible while the preview is open", () => {
useVcs.setState({ status: [modified("/p/a.ts")] });
render(<CommitToolWindow />);
selectFirstFile();
act(() => setCommitDiffPreview(true));
expect(screen.getByTestId("diff-preview")).toBeTruthy();
expect(screen.getByRole("tree", { name: "Changes" })).toBeTruthy();
expect(screen.getByPlaceholderText("Commit Message")).toBeTruthy();
expect(screen.getByRole("button", { name: "Commit" })).toBeTruthy();
});
it("offers a resize handle between the changes and the preview", () => {
useVcs.setState({ status: [modified("/p/a.ts")] });
render(<CommitToolWindow />);
selectFirstFile();
act(() => setCommitDiffPreview(true));
const handle = previewHandle();
expect(handle).toBeTruthy();
expect(handle!.getAttribute("aria-valuenow")).not.toBeNull();
// Resizing moves the boundary and keeps both sides rendered.
fireEvent.keyDown(handle!, { key: "ArrowLeft" });
expect(screen.getByTestId("diff-preview")).toBeTruthy();
expect(screen.getByRole("tree", { name: "Changes" })).toBeTruthy();
});
it("hides the pane when disabled and persists the toggle state", () => {
useVcs.setState({ status: [modified("/p/a.ts")] });
render(<CommitToolWindow />);
selectFirstFile();
const toggle = screen.getByRole("button", { name: "Preview Diff" });
act(() => toggle.click());
expect(useCommit.getState().commitDiffPreview).toBe(true);
expect(localStorage.getItem("openidea.vcs.commitDiffPreview")).toBe("true");
expect(screen.getByTestId("diff-preview")).toHaveAttribute("data-path", "/p/a.ts");
act(() => toggle.click());
expect(useCommit.getState().commitDiffPreview).toBe(false);
expect(localStorage.getItem("openidea.vcs.commitDiffPreview")).toBe("false");
expect(screen.queryByTestId("diff-preview")).toBeNull();
expect(previewHandle()).toBeUndefined();
// The tree keeps its selection across the toggling.
expect(screen.getByText("a.ts").closest('[role="treeitem"]')).toHaveAttribute("aria-selected", "true");
});
it("shows an empty state until exactly one file is selected", () => {
useVcs.setState({ status: ["a.ts", "b.ts"].map((name) => modified(`/p/${name}`)) });
render(<CommitToolWindow />);
act(() => setCommitDiffPreview(true));
expect(screen.getByText("Select a file to preview its changes")).toBeTruthy();
act(() => useCommit.setState({ selection: ["/p/a.ts", "/p/b.ts"] }));
expect(screen.getByText("Select a file to preview its changes")).toBeTruthy();
});
it("shows an empty state when there are no changes to preview", () => {
render(<CommitToolWindow />);
act(() => setCommitDiffPreview(true));
expect(screen.getByText("No changes to preview")).toBeTruthy();
expect(screen.queryByTestId("diff-preview")).toBeNull();
});
});
+12
Переглянути файл
@@ -18,6 +18,8 @@ interface CommitState {
groupByStaging: boolean;
/** Whether the Git window's Local Changes tab previews the selection's diff. */
diffPreview: boolean;
/** Whether the Commit tool window previews the selection's diff beside the changes (#1599). */
commitDiffPreview: boolean;
/** User overrides of the inclusion checkboxes (absolute path → state). */
checks: Map<string, CheckState>;
/** Changed blocks left out of the commit (absolute path → `hunkKeys`) of `hunks` files. */
@@ -48,6 +50,7 @@ export const useCommit = create<CommitState>(() => ({
groupByDir: readBool("openidea.vcs.groupByDir", false),
groupByStaging: readBool("openidea.vcs.groupByStaging", false),
diffPreview: readBool("openidea.vcs.diffPreview", true),
commitDiffPreview: readBool("openidea.vcs.commitDiffPreview", false),
checks: new Map(),
excludedHunks: new Map(),
selection: [],
@@ -95,6 +98,15 @@ export function setDiffPreview(v: boolean) {
}
}
export function setCommitDiffPreview(v: boolean) {
useCommit.setState({ commitDiffPreview: v });
try {
localStorage.setItem("openidea.vcs.commitDiffPreview", String(v));
} catch {
// ignore
}
}
const historyKey = () => `openidea.vcs.commitMessages:${useVcs.getState().root ?? ""}`;
const MAX_HISTORY = 20;
+2
Переглянути файл
@@ -1402,6 +1402,7 @@
"No language server available": "没有可用的语言服务器",
"No languages": "没有语言",
"No local changes": "没有本地更改",
"No changes to preview": "没有可预览的更改",
"No matches": "没有匹配项",
"No matching branches": "没有匹配的分支",
"No matching languages": "没有匹配的语言",
@@ -1599,6 +1600,7 @@
"Press a shortcut": "按下快捷键",
"Press a shortcut to find actions": "按下快捷键以查找操作",
"Preview": "预览",
"Preview Diff": "预览差异",
"Preview Replacement": "预览替换",
"Preview failed": "预览失败",
"Previous Bookmark": "上一个书签",