дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(vcs): Delete… for unversioned files in the Changes view (#1314)
Unversioned files in the commit view could be added or ignored but not deleted. Their context menu now offers Delete…, as IDEA's Changes view does: it asks first and moves the files to the Trash, only when every selected file is unversioned.
Цей коміт міститься в:
1 джерело
f35c781f42
коміт
4906bcb2a1
3 files changed
+83
-3
No files matched your search
@@ -47,7 +47,7 @@ import {
|
||||
toggledCheck,
|
||||
type ChangeNode,
|
||||
} from "./changes";
|
||||
import { applyStash, commitChanges, dropStash, showUnstashDialog, stashChanges } from "./changeOps";
|
||||
import { applyStash, commitChanges, deleteUnversioned, dropStash, showUnstashDialog, stashChanges } from "./changeOps";
|
||||
import { CommitOptionsPopup } from "./CommitOptions";
|
||||
import { DEFAULT_LIST_NAME, useChangelists, withListDefaults } from "./changelists";
|
||||
import { moveToChangelist } from "./changelistsUi";
|
||||
@@ -224,7 +224,7 @@ export function listMenu(id: string, active: boolean, files: readonly FileStatus
|
||||
];
|
||||
}
|
||||
|
||||
function nodeMenu(nodes: ChangeNode[], repos: readonly string[]): MenuItem[] {
|
||||
export function nodeMenu(nodes: ChangeNode[], repos: readonly string[]): MenuItem[] {
|
||||
const files = filesOf(nodes);
|
||||
const paths = files.map((f) => f.path);
|
||||
const first = nodes.find((n) => n.type === "file");
|
||||
@@ -246,7 +246,13 @@ function nodeMenu(nodes: ChangeNode[], repos: readonly string[]): MenuItem[] {
|
||||
items.push(
|
||||
{ kind: "action", action: "vcs.rollback", arg: paths, label: "Rollback…" },
|
||||
...(untracked.length
|
||||
? [{ kind: "action" as const, action: "vcs.addToVcs", arg: untracked.map((f) => f.path) }]
|
||||
? [
|
||||
{ kind: "action" as const, action: "vcs.addToVcs", arg: untracked.map((f) => f.path) },
|
||||
// Only when every selected file is unversioned: versioned ones are rolled back, not deleted.
|
||||
...(untracked.length === files.length
|
||||
? [{ label: "Delete…", icon: "trash", run: () => void deleteUnversioned(untracked.map((f) => f.path)) }]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(toIgnore.length
|
||||
? [
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FileStatus } from "@/bindings";
|
||||
import type { MenuItem } from "@/ui";
|
||||
import type { ChangeNode } from "../changes";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ confirm: vi.fn(), fsDelete: vi.fn(), call: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return { ...real, call: mocks.call, commands: { ...real.commands, fsDelete: mocks.fsDelete } };
|
||||
});
|
||||
vi.mock("../dialogs", async (original) => ({
|
||||
...(await original<typeof import("../dialogs")>()),
|
||||
confirmDialog: mocks.confirm,
|
||||
}));
|
||||
|
||||
const { nodeMenu } = await import("../CommitView");
|
||||
const { deleteUnversioned } = await import("../changeOps");
|
||||
|
||||
const file = (rel: string, unstaged: FileStatus["unstaged"]): ChangeNode => {
|
||||
const f: FileStatus = { path: `/p/${rel}`, relPath: rel, staged: null, unstaged, renamedFrom: null };
|
||||
return { key: f.path, type: "file", label: rel, group: "unversioned", files: [f], path: f.path };
|
||||
};
|
||||
const deleteItem = (menu: MenuItem[]) =>
|
||||
menu.find((i) => i.kind !== "separator" && "label" in i && i.label === "Delete…");
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.fsDelete.mockImplementation((path: string) => path);
|
||||
mocks.call.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
describe("Delete… for unversioned files", () => {
|
||||
it("is offered only when every selected file is unversioned", () => {
|
||||
expect(deleteItem(nodeMenu([file("a.txt", "untracked"), file("b.txt", "untracked")], ["/p"]))).toBeDefined();
|
||||
expect(deleteItem(nodeMenu([file("a.txt", "untracked"), file("c.txt", "modified")], ["/p"]))).toBeUndefined();
|
||||
expect(deleteItem(nodeMenu([file("c.txt", "modified")], ["/p"]))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("asks first, then deletes each file", async () => {
|
||||
mocks.confirm.mockResolvedValueOnce(false);
|
||||
await deleteUnversioned(["/p/a.txt"]);
|
||||
expect(mocks.fsDelete).not.toHaveBeenCalled();
|
||||
|
||||
mocks.confirm.mockResolvedValueOnce(true);
|
||||
await deleteUnversioned(["/p/a.txt", "/p/b.txt"]);
|
||||
expect(mocks.fsDelete.mock.calls.map((c) => c[0])).toEqual(["/p/a.txt", "/p/b.txt"]);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { useChangelists, withListDefaults } from "./changelists";
|
||||
import { runCommitChecks } from "./commitChecks";
|
||||
import { forgetAmendDraft, keepMessageAfterCommit, rememberMessage, useCommit } from "./commitStore";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { notifyFileOperation } from "@/platform/fileOps";
|
||||
import { confirmDialog, openDialog } from "./dialogs";
|
||||
import { hunkKeys } from "./hunks";
|
||||
import { KIND_COLOR, baseName, dirName, effectiveKind, relTo, rootOf, stripSlash } from "./model";
|
||||
@@ -144,6 +145,31 @@ async function partialCommitFiles(paths: readonly string[]): Promise<{ files: Pa
|
||||
return { files, whole };
|
||||
}
|
||||
|
||||
/** Delete… on unversioned files in the Changes view, as in IDEA: they move to the Trash. */
|
||||
export async function deleteUnversioned(paths: readonly string[]) {
|
||||
if (!paths.length) return;
|
||||
const question = paths.length === 1 ? tx`Delete '${baseName(paths[0]!)}'?` : tx`Delete ${paths.length} items?`;
|
||||
const ok = await confirmDialog({
|
||||
title: "Delete",
|
||||
okLabel: "Delete",
|
||||
danger: true,
|
||||
message: (
|
||||
<>
|
||||
{question}
|
||||
<br />
|
||||
<T>Items are moved to the Trash.</T>
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (!ok) return;
|
||||
await runOp("Delete", async () => {
|
||||
for (const path of paths) {
|
||||
await call(commands.fsDelete(path));
|
||||
notifyFileOperation({ kind: "deleted", paths: [path] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Files the Rollback action applies to by default. */
|
||||
export function rollbackCandidates(
|
||||
paths?: readonly string[],
|
||||
|
||||
Reference in new issue
Заблокувати користувача