diff --git a/README.md b/README.md index 74346ac..406214e 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,16 @@ You can set the VSCode text editor to use an installed Nerd Font by setting `"ed Once you have a Nerd Font set for your editor font, to use these icons in your oil view, set `"oil-code.hasNerdFont": true`. +## Confirmation Dialog + +By default, oil.code uses a modal confirmation dialog when you save file operations. You can enable an alternate confirmation interface by setting `"oil-code.enableAlternateConfirmation": true`. + +The alternate confirmation dialog provides a QuickPick interface where you can: + +- Type `Y` to confirm and apply changes +- Type `N` to cancel and discard changes +- Press `Esc` or click outside to cancel + ## Other great extensions - [vsnetrw](https://github.com/danprince/vsnetrw): Another great option for a split file explorer. diff --git a/package.json b/package.json index c210cc3..c0d14d2 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,11 @@ "type": "boolean", "default": false, "description": "Enable workspace edit for file move/rename operations. When enabled, VS Code will ask to update references when a file is moved or renamed. Default is false." + }, + "oil-code.enableAlternateConfirmation": { + "type": "boolean", + "default": false, + "description": "Enable alternate confirmation dialog for file operations. When enabled, uses a QuickPick interface instead of the default modal confirmation dialog. Default is false." } } } diff --git a/src/handlers/onDidSaveTextDocument.ts b/src/handlers/onDidSaveTextDocument.ts index 83ae6a3..1c4176b 100644 --- a/src/handlers/onDidSaveTextDocument.ts +++ b/src/handlers/onDidSaveTextDocument.ts @@ -12,7 +12,11 @@ import { import { select } from "../commands/select"; import { newline } from "../newline"; import { logger } from "../logger"; -import { getEnableWorkspaceEditSetting } from "../utils/settings"; +import { + getEnableWorkspaceEditSetting, + getEnableAlternateConfirmationSetting, +} from "../utils/settings"; +import { confirmChanges, type Change } from "../ui/confirmChanges"; export async function onDidSaveTextDocument(document: vscode.TextDocument) { // Check if the saved document is our oil file @@ -134,46 +138,78 @@ export async function onDidSaveTextDocument(document: vscode.TextDocument) { oilState.openAfterSave = undefined; return; } + // Get the alternate confirmation dialog setting + const useAlternateConfirmation = getEnableAlternateConfirmationSetting(); - // Show confirmation dialog - let message = "The following changes will be applied:\n\n"; - if (movedLines.length > 0) { - movedLines.forEach((item) => { - const [originalPath, newPath] = item; - message += `MOVE ${formatPath(originalPath)} → ${formatPath( - newPath - )}\n`; - }); - } - if (copiedLines.length > 0) { - copiedLines.forEach((item) => { - const [originalPath, newPath] = item; - message += `COPY ${formatPath(originalPath)} → ${formatPath( - newPath - )}\n`; - }); - } - if (addedLines.size > 0) { - addedLines.forEach((item) => { - message += `CREATE ${formatPath(item)}\n`; - }); - } - if (deletedLines.size > 0) { - deletedLines.forEach((item) => { - message += `DELETE ${formatPath(item)}\n`; - }); - } - // Show confirmation dialog - const response = await vscode.window.showWarningMessage( - message, - { modal: true }, - "Yes", - "No" - ); - if (response !== "Yes") { - oilState.openAfterSave = undefined; - return; + if (useAlternateConfirmation) { + // Build change list and confirm using Quick Pick + const uiChanges: Change[] = []; + for (const [from, to] of movedLines) { + uiChanges.push({ kind: "move", from, to }); + } + for (const [from, to] of copiedLines) { + uiChanges.push({ kind: "copy", from, to }); + } + for (const p of addedLines) { + uiChanges.push({ kind: "create", to: p }); + } + for (const p of deletedLines) { + uiChanges.push({ kind: "delete", from: p }); + } + const ok = await confirmChanges( + uiChanges.map((c) => ({ + // format paths to relative for nicer display (but still keep full for ops later) + ...(c as any), + from: "from" in c ? formatPath((c as any).from) : undefined, + to: "to" in c ? formatPath((c as any).to) : undefined, + })) as Change[] + ); + if (!ok) { + oilState.openAfterSave = undefined; + return; + } + } else { + // Show confirmation dialog + let message = "The following changes will be applied:\n\n"; + if (movedLines.length > 0) { + movedLines.forEach((item) => { + const [originalPath, newPath] = item; + message += `MOVE ${formatPath(originalPath)} → ${formatPath( + newPath + )}\n`; + }); + } + if (copiedLines.length > 0) { + copiedLines.forEach((item) => { + const [originalPath, newPath] = item; + message += `COPY ${formatPath(originalPath)} → ${formatPath( + newPath + )}\n`; + }); + } + if (addedLines.size > 0) { + addedLines.forEach((item) => { + message += `CREATE ${formatPath(item)}\n`; + }); + } + if (deletedLines.size > 0) { + deletedLines.forEach((item) => { + message += `DELETE ${formatPath(item)}\n`; + }); + } + // Show confirmation dialog + const response = await vscode.window.showWarningMessage( + message, + { modal: true }, + "Yes", + "No" + ); + if (response !== "Yes") { + oilState.openAfterSave = undefined; + return; + } } + logger.debug("Processing changes..."); // Delete files/directories diff --git a/src/ui/confirmChanges.ts b/src/ui/confirmChanges.ts new file mode 100644 index 0000000..82cbba0 --- /dev/null +++ b/src/ui/confirmChanges.ts @@ -0,0 +1,121 @@ +import * as vscode from "vscode"; + +export type Change = + | { kind: "create"; to: string } + | { kind: "delete"; from: string } + | { kind: "rename" | "move"; from: string; to: string } + | { kind: "modify"; from: string } + | { kind: "copy"; from: string; to: string }; + +export async function confirmChanges(changes: Change[]): Promise { + if (changes.length === 0) { + return true; + } + return confirmChangesQuickPick(changes); +} + +async function confirmChangesQuickPick(changes: Change[]): Promise { + const qp = vscode.window.createQuickPick(); + qp.title = "oil.code — Confirm changes"; + qp.matchOnDetail = true; + + // Outside click should cancel -> allow hide on blur + qp.ignoreFocusOut = false; + + // Read-only list feel + qp.items = changes.map(toQuickPickItem); + qp.canSelectMany = false; + + // "Hide" the input and instruct the user + qp.placeholder = "[Y]es [N]o"; + qp.value = ""; + + // We don't want buttons; Y/N only + qp.buttons = []; + + const disposables: vscode.Disposable[] = []; + const decision = await new Promise((resolve) => { + let finished = false; + const finish = (ok: boolean) => { + if (finished) { + return; + } + finished = true; + try { + qp.hide(); + } catch {} + resolve(ok); + }; + + // Make rows feel non-interactive + disposables.push( + qp.onDidChangeSelection(() => { + qp.selectedItems = []; + }), + qp.onDidChangeActive(() => { + qp.activeItems = []; + }), + + // Ignore Enter entirely (only Y/N should close) + qp.onDidAccept(() => { + /* no-op */ + }), + + // Capture last typed char; accept only Y or N + qp.onDidChangeValue((val) => { + const ch = val.trim().slice(-1).toLowerCase(); + qp.value = ""; // keep the field visually empty + if (ch === "y") { + return finish(true); + } + if (ch === "n") { + return finish(false); + } + }), + + // Esc or outside click hides -> cancel + qp.onDidHide(() => { + if (!finished) { + resolve(false); + } + }) + ); + + qp.show(); + }); + + disposables.forEach((d) => d.dispose()); + qp.dispose(); + return decision; +} + +function toQuickPickItem(c: Change): vscode.QuickPickItem { + switch (c.kind) { + case "create": + return { + label: "$(diff-added) create", + detail: c.to, + }; + case "delete": + return { + label: "$(diff-removed) delete", + detail: c.from, + }; + case "modify": + return { + label: "$(edit) modify", + detail: c.from, + }; + case "rename": + case "move": + return { + label: `$(diff-renamed) ${c.kind}`, + detail: `${c.from} \u2192 ${c.to}`, + }; + case "copy": + return { + label: "$(diff-added) copy", + detail: `${c.from} \u2192 ${c.to}`, + }; + } +} diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 3ff3c96..d9d4e75 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -15,6 +15,11 @@ export function getEnableWorkspaceEditSetting(): boolean { return config.get("enableWorkspaceEdit") || false; } +export function getEnableAlternateConfirmationSetting(): boolean { + const config = vscode.workspace.getConfiguration("oil-code"); + return config.get("enableAlternateConfirmation") || false; +} + let restoreAutoSave = false; export async function checkAndDisableAutoSave() {