дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
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]>
Цей коміт міститься в:
1 джерело
800949b7b5
коміт
4830f42168
4 files changed
+201
-7
No files matched your search
@@ -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} />
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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": "上一个书签",
|
||||
|
||||
Reference in new issue
Заблокувати користувача