mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-11 02:31:38 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15f618f089 |
No files matched your search
@@ -0,0 +1,138 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
worktree: vi.fn(),
|
||||
openDiff: vi.fn(),
|
||||
openFile: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
commands: { ...real.commands, vcsDiffWithWorktree: mocks.worktree },
|
||||
};
|
||||
});
|
||||
vi.mock("../diffInput", async (original) => {
|
||||
const real = await original<typeof import("../diffInput")>();
|
||||
return { ...real, openVcsDiff: mocks.openDiff };
|
||||
});
|
||||
vi.mock("../DiffEditor", () => ({
|
||||
DiffEditor: ({ input }: { input: unknown }) => <div data-testid="diff">{JSON.stringify(input)}</div>,
|
||||
}));
|
||||
vi.mock("@/workbench/contributions", async (original) => {
|
||||
const real = await original<typeof import("@/workbench/contributions")>();
|
||||
return { ...real, maybeEditorService: () => ({ openFile: mocks.openFile }) };
|
||||
});
|
||||
import { useProject } from "@/platform/project";
|
||||
import { comparisonDiffData, type ComparisonFile } from "../operations";
|
||||
import { CompareToolWindow, useCompareTool } from "../compareTool";
|
||||
import { useVcs } from "../store";
|
||||
|
||||
const modified: ComparisonFile = {
|
||||
path: "src/app.ts",
|
||||
kind: "modified",
|
||||
oldPath: null,
|
||||
root: "/workspace/client",
|
||||
revision: "aaa",
|
||||
added: 3,
|
||||
removed: 1,
|
||||
};
|
||||
const from = { rev: "main", label: "main" };
|
||||
|
||||
describe("comparisonDiffData", () => {
|
||||
it("puts the revision on the left and the working tree on the right", () => {
|
||||
const data = comparisonDiffData(modified, from, null, false);
|
||||
expect(data.path).toBe("/workspace/client/src/app.ts");
|
||||
expect(data.left).toEqual({ type: "rev", rev: "aaa", label: "main", path: undefined });
|
||||
expect(data.right).toEqual({ type: "worktree" });
|
||||
});
|
||||
|
||||
it("swaps both sides, for worktree and revision diffs alike", () => {
|
||||
const worktree = comparisonDiffData(modified, from, null, true);
|
||||
expect(worktree.left).toEqual({ type: "worktree" });
|
||||
expect(worktree.right).toEqual({ type: "rev", rev: "aaa", label: "main", path: undefined });
|
||||
const revisions = comparisonDiffData(modified, from, { rev: "feature", label: "feature" }, true);
|
||||
expect(revisions.left).toEqual({ type: "rev", rev: "feature", label: "feature" });
|
||||
expect(revisions.right).toEqual({ type: "rev", rev: "aaa", label: "main", path: undefined });
|
||||
});
|
||||
|
||||
it("keeps the one-sided markers on their file after a swap", () => {
|
||||
const added: ComparisonFile = { ...modified, kind: "added", path: "src/new.ts" };
|
||||
expect(comparisonDiffData(added, from, null, false).left).toEqual({
|
||||
type: "text",
|
||||
text: "",
|
||||
label: "(does not exist)",
|
||||
});
|
||||
expect(comparisonDiffData(added, from, null, true).right).toEqual({
|
||||
type: "text",
|
||||
text: "",
|
||||
label: "(does not exist)",
|
||||
});
|
||||
const deleted: ComparisonFile = { ...modified, kind: "deleted", path: "src/old.ts" };
|
||||
expect(comparisonDiffData(deleted, from, null, false).right).toEqual({
|
||||
type: "text",
|
||||
text: "",
|
||||
label: "(deleted)",
|
||||
});
|
||||
expect(comparisonDiffData(deleted, from, null, true).left).toEqual({
|
||||
type: "text",
|
||||
text: "",
|
||||
label: "(deleted)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CompareToolWindow", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useProject.setState({ project: { root: "/workspace", name: "workspace" } as never });
|
||||
useVcs.setState({ available: true, root: "/workspace", roots: ["/workspace/client"], repo: "/workspace/client" });
|
||||
mocks.worktree.mockResolvedValue({
|
||||
status: "ok",
|
||||
data: [
|
||||
{ ...modified, added: 3, removed: 1 },
|
||||
{ path: "src/new.ts", kind: "added", oldPath: null, root: "/workspace/client", revision: "aaa" },
|
||||
{ path: "src/old.ts", kind: "deleted", oldPath: null, root: "/workspace/client", revision: "aaa" },
|
||||
],
|
||||
});
|
||||
useCompareTool.getState().show(from, null);
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
useCompareTool.getState().close();
|
||||
useProject.setState({ project: null });
|
||||
});
|
||||
|
||||
it("lists the worktree diff and jumps to the selected source file", async () => {
|
||||
render(<CompareToolWindow />);
|
||||
const jump = screen.getByRole("button", { name: "Jump to Source" });
|
||||
expect(jump).toBeDisabled();
|
||||
fireEvent.mouseDown(await screen.findByText("app.ts"), { button: 0 });
|
||||
expect(jump).toBeEnabled();
|
||||
fireEvent.click(jump);
|
||||
expect(mocks.openFile).toHaveBeenCalledWith("/workspace/client/src/app.ts", { focus: true });
|
||||
});
|
||||
|
||||
it("has no Jump to Source for a file deleted from the working tree", async () => {
|
||||
render(<CompareToolWindow />);
|
||||
fireEvent.mouseDown(await screen.findByText("old.ts"), { button: 0 });
|
||||
expect(screen.getByRole("button", { name: "Jump to Source" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("swaps the sides of the shown diff", async () => {
|
||||
mocks.openDiff.mockResolvedValue(undefined);
|
||||
render(<CompareToolWindow />);
|
||||
fireEvent.mouseDown(await screen.findByText("app.ts"), { button: 0 });
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Show Diff" }));
|
||||
expect(mocks.openDiff).toHaveBeenLastCalledWith(expect.objectContaining({ right: { type: "worktree" } }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Swap Sides" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Show Diff" }));
|
||||
expect(mocks.openDiff).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
left: { type: "worktree" },
|
||||
right: expect.objectContaining({ type: "rev", label: "main" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* The Compare tool window: `Show Diff with Working Tree` shows the worktree diff
|
||||
* in the sidebar instead of a dialog, with Jump to Source and Swap Sides (#2442).
|
||||
*/
|
||||
import { create } from "zustand";
|
||||
import { Button } from "@/ui";
|
||||
import { maybeEditorService, notifications, registerToolWindow } from "@/workbench/contributions";
|
||||
import { showToolWindow } from "@/workbench/toolwindows/store";
|
||||
import { openVcsDiff, type DiffData } from "./diffInput";
|
||||
import { ChangesBetweenDialog, type ComparisonFile, type Revision } from "./operations";
|
||||
import { repositoryPath } from "./repositoryPaths";
|
||||
import styles from "./common.module.css";
|
||||
|
||||
interface CompareToolState {
|
||||
target: { from: Revision; to: Revision | null } | null;
|
||||
swapped: boolean;
|
||||
current: ComparisonFile | null;
|
||||
show: (from: Revision, to: Revision | null) => void;
|
||||
close: () => void;
|
||||
toggleSwapped: () => void;
|
||||
select: (file: ComparisonFile | null) => void;
|
||||
}
|
||||
|
||||
export const useCompareTool = create<CompareToolState>()((set) => ({
|
||||
target: null,
|
||||
swapped: false,
|
||||
current: null,
|
||||
show: (from, to) => set({ target: { from, to }, swapped: false, current: null }),
|
||||
close: () => set({ target: null, swapped: false, current: null }),
|
||||
toggleSwapped: () => set((s) => ({ swapped: !s.swapped })),
|
||||
select: (current) => set({ current }),
|
||||
}));
|
||||
|
||||
let disposeCompareWindow: (() => void) | null = null;
|
||||
|
||||
function ensureCompareWindow() {
|
||||
disposeCompareWindow ??= registerToolWindow({
|
||||
id: "compare",
|
||||
title: "Compare",
|
||||
icon: "diff",
|
||||
anchor: "left",
|
||||
order: 2,
|
||||
component: CompareToolWindow,
|
||||
});
|
||||
}
|
||||
|
||||
function disposeCompareWindowNow() {
|
||||
disposeCompareWindow?.();
|
||||
disposeCompareWindow = null;
|
||||
}
|
||||
|
||||
/** Worktree diff of `rev` in the sidebar's Compare tool window (no dialog). */
|
||||
export function showWorktreeComparison(rev: string, label: string) {
|
||||
useCompareTool.getState().show({ rev, label }, null);
|
||||
ensureCompareWindow();
|
||||
showToolWindow("compare", true);
|
||||
}
|
||||
|
||||
export function CompareToolWindow() {
|
||||
const target = useCompareTool((s) => s.target);
|
||||
const swapped = useCompareTool((s) => s.swapped);
|
||||
const current = useCompareTool((s) => s.current);
|
||||
const toggleSwapped = useCompareTool((s) => s.toggleSwapped);
|
||||
const select = useCompareTool((s) => s.select);
|
||||
const close = useCompareTool((s) => s.close);
|
||||
if (!target) return null;
|
||||
const jumpToSource = () => {
|
||||
if (!current || current.kind === "deleted") return;
|
||||
void maybeEditorService()?.openFile(repositoryPath(current.root, current.path), { focus: true });
|
||||
};
|
||||
const showDiff = (data: DiffData) => {
|
||||
void openVcsDiff(data).catch((error) =>
|
||||
notifications().notify({ level: "error", title: "Could not show diff", body: String(error) }),
|
||||
);
|
||||
};
|
||||
const handleClose = () => {
|
||||
close();
|
||||
disposeCompareWindowNow();
|
||||
};
|
||||
return (
|
||||
<div className={styles.comparisonWindow}>
|
||||
<div className={styles.compareToolbar}>
|
||||
<Button size="small" onClick={jumpToSource} disabled={!current || current.kind === "deleted"}>
|
||||
Jump to Source
|
||||
</Button>
|
||||
<Button size="small" variant={swapped ? "primary" : "secondary"} onClick={toggleSwapped}>
|
||||
Swap Sides
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.comparisonWindowBody}>
|
||||
<ChangesBetweenDialog
|
||||
key={target.from.rev}
|
||||
from={target.from}
|
||||
to={target.to}
|
||||
native
|
||||
swapped={swapped}
|
||||
onSelect={select}
|
||||
onShowDiff={showDiff}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { reloadLog, useGitLog } from "./logStore";
|
||||
import { conflictedFiles, currentRepo, currentRepoRoot, refreshNow, repoName, repoOf, useVcs } from "./store";
|
||||
import styles from "./common.module.css";
|
||||
import { ComparisonTree } from "./ComparisonTree";
|
||||
import { showWorktreeComparison } from "./compareTool";
|
||||
import { buildFileTree, type FileTreeNode } from "./fileTree";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { displayPath } from "./compareData";
|
||||
@@ -507,9 +508,9 @@ export function showCompareWithLocal(commit: Commit) {
|
||||
showDiffWithWorkingTree(commit.id, short(commit.id));
|
||||
}
|
||||
|
||||
/** Files that differ between `rev` (a commit or branch) and the working tree. */
|
||||
/** Files that differ between `rev` (a commit or branch) and the working tree, in the Compare tool window. */
|
||||
export function showDiffWithWorkingTree(rev: string, label = rev) {
|
||||
showComparison({ rev, label }, null);
|
||||
showWorktreeComparison(rev, label);
|
||||
}
|
||||
|
||||
/** IDEA's Compare Versions of two Git Log commits: files that differ between them, older first. */
|
||||
@@ -537,7 +538,7 @@ function showComparison(from: Revision, to: Revision | null, scope?: ScopedCompa
|
||||
);
|
||||
}
|
||||
|
||||
interface Revision {
|
||||
export interface Revision {
|
||||
rev: string;
|
||||
label: string;
|
||||
}
|
||||
@@ -547,13 +548,43 @@ interface ScopedComparison {
|
||||
repositories: string[] | null;
|
||||
}
|
||||
|
||||
interface ComparisonFile extends ChangedFile {
|
||||
export interface ComparisonFile extends ChangedFile {
|
||||
root: string;
|
||||
revision: string;
|
||||
added?: number;
|
||||
removed?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff content of one comparison file. `swapped` exchanges the two sides
|
||||
* (the Compare tool window's Swap Sides), for worktree and revision diffs alike.
|
||||
*/
|
||||
export function comparisonDiffData(
|
||||
file: ComparisonFile,
|
||||
from: Revision,
|
||||
to: Revision | null,
|
||||
swapped: boolean,
|
||||
): DiffData {
|
||||
const missing = (label: string): DiffSide => ({ type: "text", text: "", label });
|
||||
const path = repositoryPath(file.root, file.path);
|
||||
const left: DiffSide =
|
||||
file.kind === "added" || file.kind === "untracked"
|
||||
? missing("(does not exist)")
|
||||
: {
|
||||
type: "rev",
|
||||
rev: file.revision,
|
||||
label: from.label,
|
||||
path: file.oldPath ? repositoryPath(file.root, file.oldPath) : undefined,
|
||||
};
|
||||
const right: DiffSide =
|
||||
file.kind === "deleted"
|
||||
? missing("(deleted)")
|
||||
: !to
|
||||
? { type: "worktree" }
|
||||
: { type: "rev", rev: to.rev, label: to.label };
|
||||
return swapped ? { path, left: right, right: left } : { path, left, right };
|
||||
}
|
||||
|
||||
export function comparisonEmpty(revision: string, repositories: number): string {
|
||||
return repositories === 1
|
||||
? tx`No changes against ${revision} in 1 repository`
|
||||
@@ -569,6 +600,8 @@ export function ChangesBetweenDialog({
|
||||
native = false,
|
||||
onCaption,
|
||||
onShowDiff,
|
||||
swapped = false,
|
||||
onSelect,
|
||||
}: {
|
||||
from: Revision;
|
||||
to: Revision | null;
|
||||
@@ -578,13 +611,21 @@ export function ChangesBetweenDialog({
|
||||
native?: boolean;
|
||||
onCaption?: (caption: string) => void;
|
||||
onShowDiff?: (data: DiffData) => void;
|
||||
/** Exchange the two sides (the Compare tool window's Swap Sides). */
|
||||
swapped?: boolean;
|
||||
/** Reports the selected file (the Compare tool window's Jump to Source / Swap Sides). */
|
||||
onSelect?: (file: ComparisonFile | null) => void;
|
||||
}) {
|
||||
const [root] = useState(currentRepoRoot);
|
||||
const [repo] = useState(currentRepo);
|
||||
const [files, setFiles] = useState<ComparisonFile[] | null>(null);
|
||||
const [result, setResult] = useState<CompareResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [current, setCurrent] = useState<ComparisonFile | null>(null);
|
||||
const [current, setCurrentState] = useState<ComparisonFile | null>(null);
|
||||
const setCurrent = (file: ComparisonFile | null) => {
|
||||
setCurrentState(file);
|
||||
onSelect?.(file);
|
||||
};
|
||||
const fromRev = from.rev;
|
||||
const toRev = to?.rev ?? null;
|
||||
useEffect(() => {
|
||||
@@ -633,27 +674,7 @@ export function ChangesBetweenDialog({
|
||||
};
|
||||
});
|
||||
}, [files, result, scope]);
|
||||
const diffFor = (f: ComparisonFile): DiffData => {
|
||||
const missing = (label: string): DiffSide => ({ type: "text", text: "", label });
|
||||
return {
|
||||
path: repositoryPath(f.root, f.path),
|
||||
left:
|
||||
f.kind === "added" || f.kind === "untracked"
|
||||
? missing("(does not exist)")
|
||||
: {
|
||||
type: "rev",
|
||||
rev: f.revision,
|
||||
label: from.label,
|
||||
path: f.oldPath ? repositoryPath(f.root, f.oldPath) : undefined,
|
||||
},
|
||||
right:
|
||||
f.kind === "deleted"
|
||||
? missing("(deleted)")
|
||||
: !to
|
||||
? { type: "worktree" }
|
||||
: { type: "rev", rev: to.rev, label: to.label },
|
||||
};
|
||||
};
|
||||
const diffFor = (f: ComparisonFile): DiffData => comparisonDiffData(f, from, to, swapped);
|
||||
const input = current ? diffInput(diffFor(current)) : null;
|
||||
const title = to
|
||||
? tx`Changes between ${from.label} and ${to.label}`
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
"Astro Language Server needs Node.js. Install it or select its executable in Settings › Languages & Frameworks › Node.js.": "Astro Language Server needs Node.js. Install it or select its executable in Settings › Languages & Frameworks › Node.js.",
|
||||
"Astro Language Server needs a TypeScript language-service library. Install typescript@6 in the project or select a TypeScript lib directory in Settings.": "Astro Language Server needs a TypeScript language-service library. Install typescript@6 in the project or select a TypeScript lib directory in Settings.",
|
||||
"Cannot show current revision": "Cannot show current revision",
|
||||
"Compare": "Compare",
|
||||
"Compare with Revision…": "Compare with Revision…",
|
||||
"Swap Sides": "Swap Sides",
|
||||
"Show Current Revision": "Show Current Revision",
|
||||
"Search file revisions": "Search file revisions",
|
||||
"File revisions": "File revisions",
|
||||
|
||||
@@ -674,6 +674,7 @@
|
||||
"Commit…": "提交…",
|
||||
"Compact mode": "紧凑模式",
|
||||
"Companion object": "伴生对象",
|
||||
"Compare": "对比",
|
||||
"Compare Files": "比较文件",
|
||||
"Compare Texts": "比较文本",
|
||||
"Compare With": "比较对象",
|
||||
@@ -2514,6 +2515,7 @@
|
||||
"Surround With…": "包围方式…",
|
||||
"Surround selection on typing quote or brace": "输入引号或括号时包围所选内容",
|
||||
"Suspend on": "挂起时机",
|
||||
"Swap Sides": "交换两侧",
|
||||
"Switch to Auto-Commit": "切换到自动提交",
|
||||
"Switcher": "切换器",
|
||||
"Switcher (Backward)": "切换器(向后)",
|
||||
|
||||
Reference in new issue
Block a user