дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
201 рядки
7.0 KiB
TypeScript
201 рядки
7.0 KiB
TypeScript
/**
|
|
* Compare entry points (IDEA's Compare With…, Compare Files, Compare with
|
|
* Clipboard) and the diff viewer's own actions (F7 / ⇧F7, F4).
|
|
*/
|
|
import { activeEditor } from "@/editor/api";
|
|
import { registerActions } from "@/platform/actions";
|
|
import { isTauri } from "@/platform/ipc";
|
|
import { useProject } from "@/platform/project";
|
|
import { maybeEditorService, notifications } from "@/workbench/contributions";
|
|
import { focusedDiffNavigator } from "./diffNavigator";
|
|
import { openDiff, type DiffData } from "./diffInput";
|
|
import { baseName } from "./model";
|
|
|
|
function argPath(arg: unknown): string | undefined {
|
|
if (typeof arg === "string") return arg;
|
|
if (arg && typeof arg === "object" && "path" in arg && typeof (arg as { path: unknown }).path === "string")
|
|
return (arg as { path: string }).path;
|
|
return undefined;
|
|
}
|
|
|
|
function argPaths(arg: unknown): string[] {
|
|
if (Array.isArray(arg)) return arg.filter((x): x is string => typeof x === "string");
|
|
if (arg && typeof arg === "object" && "paths" in arg && Array.isArray((arg as { paths: unknown }).paths))
|
|
return ((arg as { paths: unknown[] }).paths ?? []).filter((x): x is string => typeof x === "string");
|
|
const p = argPath(arg);
|
|
return p ? [p] : [];
|
|
}
|
|
|
|
/** Project-relative path for labels. */
|
|
export function displayPath(path: string, root = useProject.getState().project?.root ?? null): string {
|
|
return root && path.startsWith(root + "/") ? path.slice(root.length + 1) : path;
|
|
}
|
|
|
|
/** Diff of two files on disk; both sides are live, editable documents. */
|
|
export function compareFilesData(leftPath: string, rightPath: string): DiffData {
|
|
return {
|
|
path: rightPath,
|
|
left: { type: "file", path: leftPath, label: displayPath(leftPath) },
|
|
right: { type: "worktree", label: displayPath(rightPath) },
|
|
};
|
|
}
|
|
|
|
/** Clipboard ↔ the file (editable) or ↔ the editor selection. */
|
|
export function clipboardDiffData(path: string, clipboard: string, selection: string | null): DiffData {
|
|
return {
|
|
path,
|
|
title: `Clipboard ↔ ${baseName(path)}${selection ? " (selection)" : ""}`,
|
|
left: { type: "text", text: clipboard, label: "Clipboard" },
|
|
right: selection ? { type: "text", text: selection, label: "Selection" } : { type: "worktree", label: displayPath(path) },
|
|
};
|
|
}
|
|
|
|
export interface TextsArg {
|
|
path: string;
|
|
title?: string;
|
|
left: { text: string; label: string };
|
|
right: { text: string; label: string };
|
|
line?: number;
|
|
}
|
|
|
|
export function parseTextsArg(arg: unknown): TextsArg | null {
|
|
if (!arg || typeof arg !== "object") return null;
|
|
const a = arg as Record<string, unknown>;
|
|
const side = (v: unknown) =>
|
|
v && typeof v === "object" && typeof (v as { text?: unknown }).text === "string"
|
|
? { text: (v as { text: string }).text, label: String((v as { label?: unknown }).label ?? "") }
|
|
: null;
|
|
const left = side(a.left);
|
|
const right = side(a.right);
|
|
if (typeof a.path !== "string" || !left || !right) return null;
|
|
return {
|
|
path: a.path,
|
|
title: typeof a.title === "string" ? a.title : undefined,
|
|
left,
|
|
right,
|
|
line: typeof a.line === "number" ? a.line : undefined,
|
|
};
|
|
}
|
|
|
|
function info(title: string) {
|
|
notifications().notify({ group: "vcs", level: "info", title });
|
|
}
|
|
|
|
async function pickFile(near: string): Promise<string | null> {
|
|
if (!isTauri()) return null;
|
|
const { open } = await import("@tauri-apps/plugin-dialog");
|
|
const dir = near.slice(0, near.lastIndexOf("/")) || undefined;
|
|
const picked = await open({ multiple: false, directory: false, title: "Compare With", defaultPath: dir });
|
|
return (Array.isArray(picked) ? picked[0] : picked) ?? null;
|
|
}
|
|
|
|
const contextPath = (arg: unknown) => argPath(arg) ?? maybeEditorService()?.activePath() ?? undefined;
|
|
|
|
export function registerCompareActions(): () => void {
|
|
return registerActions([
|
|
{
|
|
id: "vcs.openDiff",
|
|
title: "Open Diff",
|
|
description: "Opens the diff viewer for a `DiffData` argument (e.g. a pull request file).",
|
|
category: "Diff",
|
|
icon: "diff",
|
|
when: "projectOpen",
|
|
hidden: true,
|
|
run: ({ arg }) => {
|
|
const data = arg as DiffData | undefined;
|
|
if (data && typeof data.path === "string" && data.left && data.right) return openDiff(data);
|
|
},
|
|
},
|
|
{
|
|
id: "diff.nextDifference",
|
|
title: "Next Difference",
|
|
category: "Diff",
|
|
icon: "arrow-down",
|
|
when: "diffEditorFocus",
|
|
run: () => focusedDiffNavigator()?.next(),
|
|
},
|
|
{
|
|
id: "diff.prevDifference",
|
|
title: "Previous Difference",
|
|
category: "Diff",
|
|
icon: "arrow-up",
|
|
when: "diffEditorFocus",
|
|
run: () => focusedDiffNavigator()?.prev(),
|
|
},
|
|
{
|
|
id: "diff.jumpToSource",
|
|
title: "Jump to Source",
|
|
category: "Diff",
|
|
icon: "external-link",
|
|
when: "diffEditorFocus",
|
|
run: () => focusedDiffNavigator()?.jumpToSource(),
|
|
},
|
|
{
|
|
id: "diff.compareWith",
|
|
title: "Compare With…",
|
|
category: "Diff",
|
|
icon: "diff",
|
|
when: "projectOpen",
|
|
keywords: ["diff", "compare files"],
|
|
run: async ({ arg }) => {
|
|
const path = contextPath(arg);
|
|
if (!path) return info("Open or select a file to compare");
|
|
const other = await pickFile(path);
|
|
if (!other || other === path) return;
|
|
await openDiff(compareFilesData(path, other));
|
|
},
|
|
},
|
|
{
|
|
id: "diff.compareFiles",
|
|
title: "Compare Files",
|
|
category: "Diff",
|
|
icon: "diff",
|
|
when: "projectOpen",
|
|
run: async ({ arg }) => {
|
|
const paths = argPaths(arg).filter((p) => !p.endsWith("/"));
|
|
if (paths.length !== 2) return info("Select two files to compare");
|
|
await openDiff(compareFilesData(paths[0]!, paths[1]!));
|
|
},
|
|
},
|
|
{
|
|
id: "diff.compareWithClipboard",
|
|
title: "Compare with Clipboard",
|
|
category: "Diff",
|
|
icon: "diff",
|
|
when: "projectOpen",
|
|
run: async ({ arg }) => {
|
|
const path = contextPath(arg);
|
|
if (!path) return info("Open or select a file to compare with the clipboard");
|
|
let clipboard: string;
|
|
try {
|
|
clipboard = (await navigator.clipboard?.readText()) ?? "";
|
|
} catch {
|
|
return info("The clipboard could not be read");
|
|
}
|
|
const ed = activeEditor();
|
|
const sel = ed && ed.ctx.path === path ? ed.view.state.selection.main : null;
|
|
const selection = sel && !sel.empty ? ed!.view.state.sliceDoc(sel.from, sel.to) : null;
|
|
await openDiff(clipboardDiffData(path, clipboard, selection));
|
|
},
|
|
},
|
|
{
|
|
// Parameterised: { path, title?, left: { text, label }, right: { text, label }, line? }.
|
|
id: "diff.compareTexts",
|
|
title: "Compare Texts",
|
|
category: "Diff",
|
|
hidden: true,
|
|
run: async ({ arg }) => {
|
|
const a = parseTextsArg(arg);
|
|
if (!a) return;
|
|
await openDiff({
|
|
path: a.path,
|
|
title: a.title,
|
|
line: a.line,
|
|
left: { type: "text", text: a.left.text, label: a.left.label },
|
|
right: { type: "text", text: a.right.text, label: a.right.label },
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
}
|