mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(vcs): delete changed files from the Changes view
Delete… (⌫ / Delete) in the Commit view's Changes tree was only offered for unversioned files. It now applies to any selected files still on disk, moving them to the Trash; a tracked file then shows as a deleted change.
This commit is contained in:
@@ -48,7 +48,7 @@ import {
|
||||
toggledCheck,
|
||||
type ChangeNode,
|
||||
} from "./changes";
|
||||
import { applyStash, commitChanges, deleteUnversioned, dropStash, showUnstashDialog, stashChanges } from "./changeOps";
|
||||
import { applyStash, commitChanges, deleteFiles, dropStash, showUnstashDialog, stashChanges } from "./changeOps";
|
||||
import { CommitOptionsPopup } from "./CommitOptions";
|
||||
import { DEFAULT_LIST_NAME, useChangelists, withListDefaults } from "./changelists";
|
||||
import { moveToChangelist } from "./changelistsUi";
|
||||
@@ -226,14 +226,16 @@ export function listMenu(id: string, active: boolean, files: readonly FileStatus
|
||||
}
|
||||
|
||||
/** Paths Delete… applies to: only when every selected file is unversioned (versioned ones are rolled back). */
|
||||
export function deletableUnversioned(nodes: ChangeNode[]): string[] {
|
||||
/** The selected files that still exist in the working tree (Delete… removes them from disk). */
|
||||
export function deletableFiles(nodes: ChangeNode[]): string[] {
|
||||
const files = filesOf(nodes);
|
||||
return files.length && files.every((f) => f.unstaged === "untracked") ? files.map((f) => f.path) : [];
|
||||
const onDisk = (f: FileStatus) => f.unstaged !== "deleted" && !(f.staged === "deleted" && !f.unstaged);
|
||||
return files.length && files.every(onDisk) ? files.map((f) => f.path) : [];
|
||||
}
|
||||
|
||||
export function nodeMenu(nodes: ChangeNode[], repos: readonly string[]): MenuItem[] {
|
||||
const files = filesOf(nodes);
|
||||
const deletable = deletableUnversioned(nodes);
|
||||
const deletable = deletableFiles(nodes);
|
||||
const paths = files.map((f) => f.path);
|
||||
const first = nodes.find((n) => n.type === "file");
|
||||
const untracked = files.filter((f) => f.unstaged === "untracked");
|
||||
@@ -254,18 +256,16 @@ export function nodeMenu(nodes: ChangeNode[], repos: readonly string[]): MenuIte
|
||||
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) }]
|
||||
: []),
|
||||
...(deletable.length
|
||||
? [
|
||||
{ kind: "action" as const, action: "vcs.addToVcs", arg: untracked.map((f) => f.path) },
|
||||
...(deletable.length
|
||||
? [
|
||||
{
|
||||
label: "Delete…",
|
||||
icon: "trash",
|
||||
shortcut: formatBinding(isMacHost() ? "Backspace" : "Delete"),
|
||||
run: () => void deleteUnversioned(deletable),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Delete…",
|
||||
icon: "trash",
|
||||
shortcut: formatBinding(isMacHost() ? "Backspace" : "Delete"),
|
||||
run: () => void deleteFiles(deletable),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(toIgnore.length
|
||||
@@ -534,7 +534,7 @@ export function ChangesTree({ mode }: { mode: "commit" | "local" | "dialog" }) {
|
||||
} else if ((e.key === "Delete" || e.key === "Backspace") && !e.metaKey) {
|
||||
const nodes = selectedNodes();
|
||||
const group = nodes.find((x) => x.type === "group" && x.changelist);
|
||||
const deletable = group ? [] : deletableUnversioned(nodes);
|
||||
const deletable = group ? [] : deletableFiles(nodes);
|
||||
if (group?.changelist) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -542,7 +542,7 @@ export function ChangesTree({ mode }: { mode: "commit" | "local" | "dialog" }) {
|
||||
} else if (deletable.length) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void deleteUnversioned(deletable);
|
||||
void deleteFiles(deletable);
|
||||
}
|
||||
} else if (e.key === "F4") {
|
||||
const n = selectedNodes().find((x) => x.type === "file");
|
||||
|
||||
+16
-16
@@ -13,11 +13,11 @@ vi.mock("../dialogs", async (original) => ({
|
||||
confirmDialog: mocks.confirm,
|
||||
}));
|
||||
|
||||
const { deletableUnversioned, nodeMenu } = await import("../CommitView");
|
||||
const { deleteUnversioned } = await import("../changeOps");
|
||||
const { deletableFiles, nodeMenu } = await import("../CommitView");
|
||||
const { deleteFiles } = await import("../changeOps");
|
||||
|
||||
const file = (rel: string, unstaged: FileStatus["unstaged"]): ChangeNode => {
|
||||
const f: FileStatus = { path: `/p/${rel}`, relPath: rel, staged: null, unstaged, renamedFrom: null };
|
||||
const file = (rel: string, unstaged: FileStatus["unstaged"], staged: FileStatus["staged"] = null): ChangeNode => {
|
||||
const f: FileStatus = { path: `/p/${rel}`, relPath: rel, staged, unstaged, renamedFrom: null };
|
||||
return { key: f.path, type: "file", label: rel, group: "unversioned", files: [f], path: f.path };
|
||||
};
|
||||
const deleteItem = (menu: MenuItem[]) =>
|
||||
@@ -29,30 +29,30 @@ beforeEach(() => {
|
||||
mocks.call.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
describe("Delete… for unversioned files", () => {
|
||||
it("is offered only when every selected file is unversioned", () => {
|
||||
describe("Delete… in the Changes view", () => {
|
||||
it("is offered when every selected file is still on disk", () => {
|
||||
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();
|
||||
expect(deleteItem(nodeMenu([file("a.txt", "untracked"), file("c.txt", "modified")], ["/p"]))).toBeDefined();
|
||||
expect(deleteItem(nodeMenu([file("c.txt", "modified")], ["/p"]))).toBeDefined();
|
||||
expect(deleteItem(nodeMenu([file("d.txt", null, "added")], ["/p"]))).toBeDefined();
|
||||
expect(deleteItem(nodeMenu([file("c.txt", "modified"), file("e.txt", "deleted")], ["/p"]))).toBeUndefined();
|
||||
expect(deleteItem(nodeMenu([file("f.txt", null, "deleted")], ["/p"]))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shows the Delete key and gives the key handler the same paths", () => {
|
||||
expect(deleteItem(nodeMenu([file("a.txt", "untracked")], ["/p"]))).toMatchObject({ shortcut: "⌫" });
|
||||
expect(deletableUnversioned([file("a.txt", "untracked"), file("b.txt", "untracked")])).toEqual([
|
||||
"/p/a.txt",
|
||||
"/p/b.txt",
|
||||
]);
|
||||
expect(deletableUnversioned([file("a.txt", "untracked"), file("c.txt", "modified")])).toEqual([]);
|
||||
expect(deletableUnversioned([])).toEqual([]);
|
||||
expect(deletableFiles([file("a.txt", "untracked"), file("c.txt", "modified")])).toEqual(["/p/a.txt", "/p/c.txt"]);
|
||||
expect(deletableFiles([file("a.txt", "untracked"), file("e.txt", "deleted")])).toEqual([]);
|
||||
expect(deletableFiles([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("asks first, then deletes each file", async () => {
|
||||
mocks.confirm.mockResolvedValueOnce(false);
|
||||
await deleteUnversioned(["/p/a.txt"]);
|
||||
await deleteFiles(["/p/a.txt"]);
|
||||
expect(mocks.fsDelete).not.toHaveBeenCalled();
|
||||
|
||||
mocks.confirm.mockResolvedValueOnce(true);
|
||||
await deleteUnversioned(["/p/a.txt", "/p/b.txt"]);
|
||||
await deleteFiles(["/p/a.txt", "/p/b.txt"]);
|
||||
expect(mocks.fsDelete.mock.calls.map((c) => c[0])).toEqual(["/p/a.txt", "/p/b.txt"]);
|
||||
});
|
||||
});
|
||||
@@ -153,7 +153,8 @@ async function partialCommitFiles(paths: readonly string[]): Promise<{ files: Pa
|
||||
}
|
||||
|
||||
/** Delete… on unversioned files in the Changes view, as in IDEA: they move to the Trash. */
|
||||
export async function deleteUnversioned(paths: readonly string[]) {
|
||||
/** Delete… in the Changes view: moves the files to the Trash (a tracked file then shows as deleted). */
|
||||
export async function deleteFiles(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({
|
||||
|
||||
Reference in New Issue
Block a user