Skip to content

Commit 21be614

Browse files
committed
Add alternative QuickPick confirmation dialog
1 parent f2f1dbf commit 21be614

6 files changed

Lines changed: 255 additions & 40 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ You can set the VSCode text editor to use an installed Nerd Font by setting `"ed
167167

168168
Once you have a Nerd Font set for your editor font, to use these icons in your oil view, set `"oil-code.hasNerdFont": true`.
169169

170+
## Confirmation Dialog
171+
172+
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`.
173+
174+
The alternate confirmation dialog provides a QuickPick interface where you can:
175+
176+
- Type `Y` to confirm and apply changes
177+
- Type `N` to cancel and discard changes
178+
- Press `Esc` or click outside to cancel
179+
170180
## Other great extensions
171181

172182
- [vsnetrw](https://github.com/danprince/vsnetrw): Another great option for a split file explorer.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@
160160
"type": "boolean",
161161
"default": false,
162162
"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."
163+
},
164+
"oil-code.enableAlternateConfirmation": {
165+
"type": "boolean",
166+
"default": false,
167+
"description": "Enable alternate confirmation dialog for file operations. When enabled, uses a QuickPick interface instead of the default modal confirmation dialog. Default is false."
163168
}
164169
}
165170
}

src/handlers/onDidSaveTextDocument.ts

Lines changed: 75 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
import { select } from "../commands/select";
1313
import { newline } from "../newline";
1414
import { logger } from "../logger";
15-
import { getEnableWorkspaceEditSetting } from "../utils/settings";
15+
import {
16+
getEnableWorkspaceEditSetting,
17+
getEnableAlternateConfirmationSetting,
18+
} from "../utils/settings";
19+
import { confirmChanges, type Change } from "../ui/confirmChanges";
1620

1721
export async function onDidSaveTextDocument(document: vscode.TextDocument) {
1822
// Check if the saved document is our oil file
@@ -134,46 +138,78 @@ export async function onDidSaveTextDocument(document: vscode.TextDocument) {
134138
oilState.openAfterSave = undefined;
135139
return;
136140
}
141+
// Get the alternate confirmation dialog setting
142+
const useAlternateConfirmation = getEnableAlternateConfirmationSetting();
137143

138-
// Show confirmation dialog
139-
let message = "The following changes will be applied:\n\n";
140-
if (movedLines.length > 0) {
141-
movedLines.forEach((item) => {
142-
const [originalPath, newPath] = item;
143-
message += `MOVE ${formatPath(originalPath)}${formatPath(
144-
newPath
145-
)}\n`;
146-
});
147-
}
148-
if (copiedLines.length > 0) {
149-
copiedLines.forEach((item) => {
150-
const [originalPath, newPath] = item;
151-
message += `COPY ${formatPath(originalPath)}${formatPath(
152-
newPath
153-
)}\n`;
154-
});
155-
}
156-
if (addedLines.size > 0) {
157-
addedLines.forEach((item) => {
158-
message += `CREATE ${formatPath(item)}\n`;
159-
});
160-
}
161-
if (deletedLines.size > 0) {
162-
deletedLines.forEach((item) => {
163-
message += `DELETE ${formatPath(item)}\n`;
164-
});
165-
}
166-
// Show confirmation dialog
167-
const response = await vscode.window.showWarningMessage(
168-
message,
169-
{ modal: true },
170-
"Yes",
171-
"No"
172-
);
173-
if (response !== "Yes") {
174-
oilState.openAfterSave = undefined;
175-
return;
144+
if (useAlternateConfirmation) {
145+
// Build change list and confirm using Quick Pick
146+
const uiChanges: Change[] = [];
147+
for (const [from, to] of movedLines) {
148+
uiChanges.push({ kind: "move", from, to });
149+
}
150+
for (const [from, to] of copiedLines) {
151+
uiChanges.push({ kind: "copy", from, to });
152+
}
153+
for (const p of addedLines) {
154+
uiChanges.push({ kind: "create", to: p });
155+
}
156+
for (const p of deletedLines) {
157+
uiChanges.push({ kind: "delete", from: p });
158+
}
159+
const ok = await confirmChanges(
160+
uiChanges.map((c) => ({
161+
// format paths to relative for nicer display (but still keep full for ops later)
162+
...(c as any),
163+
from: "from" in c ? formatPath((c as any).from) : undefined,
164+
to: "to" in c ? formatPath((c as any).to) : undefined,
165+
})) as Change[]
166+
);
167+
if (!ok) {
168+
oilState.openAfterSave = undefined;
169+
return;
170+
}
171+
} else {
172+
// Show confirmation dialog
173+
let message = "The following changes will be applied:\n\n";
174+
if (movedLines.length > 0) {
175+
movedLines.forEach((item) => {
176+
const [originalPath, newPath] = item;
177+
message += `MOVE ${formatPath(originalPath)}${formatPath(
178+
newPath
179+
)}\n`;
180+
});
181+
}
182+
if (copiedLines.length > 0) {
183+
copiedLines.forEach((item) => {
184+
const [originalPath, newPath] = item;
185+
message += `COPY ${formatPath(originalPath)}${formatPath(
186+
newPath
187+
)}\n`;
188+
});
189+
}
190+
if (addedLines.size > 0) {
191+
addedLines.forEach((item) => {
192+
message += `CREATE ${formatPath(item)}\n`;
193+
});
194+
}
195+
if (deletedLines.size > 0) {
196+
deletedLines.forEach((item) => {
197+
message += `DELETE ${formatPath(item)}\n`;
198+
});
199+
}
200+
// Show confirmation dialog
201+
const response = await vscode.window.showWarningMessage(
202+
message,
203+
{ modal: true },
204+
"Yes",
205+
"No"
206+
);
207+
if (response !== "Yes") {
208+
oilState.openAfterSave = undefined;
209+
return;
210+
}
176211
}
212+
177213
logger.debug("Processing changes...");
178214

179215
// Delete files/directories

src/test/extension.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,64 @@ suite("oil.code", () => {
4343
// Setup and teardown for Sinon stubs
4444
let showWarningMessageStub: sinon.SinonStub;
4545
let executeCommandSpy: sinon.SinonStub;
46+
let createQuickPickStub: sinon.SinonStub;
4647

47-
setup(() => {
48+
setup(async () => {
4849
// Stub vscode.window.showWarningMessage to automatically return a response
4950
// This avoids blocking dialogs during tests
5051
showWarningMessageStub = sinon.stub(vscode.window, "showWarningMessage");
5152
// Default to "Yes" response for dialogs
5253
showWarningMessageStub.resolves("Yes");
54+
55+
// Stub createQuickPick so confirmation UI immediately accepts
56+
createQuickPickStub = sinon
57+
.stub(vscode.window, "createQuickPick")
58+
.callsFake(() => {
59+
const acceptHandlers: Array<() => void> = [];
60+
const hideHandlers: Array<() => void> = [];
61+
const triggerButtonHandlers: Array<
62+
(btn: vscode.QuickInputButton) => void
63+
> = [];
64+
65+
const qp: any = {
66+
title: "",
67+
matchOnDetail: false,
68+
ignoreFocusOut: false,
69+
canSelectMany: false,
70+
items: [],
71+
buttons: [],
72+
onDidTriggerButton: (cb: (btn: vscode.QuickInputButton) => void) => {
73+
triggerButtonHandlers.push(cb);
74+
return { dispose() {} } as vscode.Disposable;
75+
},
76+
onDidAccept: (cb: () => void) => {
77+
acceptHandlers.push(cb);
78+
return { dispose() {} } as vscode.Disposable;
79+
},
80+
onDidHide: (cb: () => void) => {
81+
hideHandlers.push(cb);
82+
return { dispose() {} } as vscode.Disposable;
83+
},
84+
show: () => {
85+
// Simulate pressing Enter to accept immediately
86+
setTimeout(() => {
87+
acceptHandlers.forEach((cb) => cb());
88+
}, 0);
89+
},
90+
hide: () => {
91+
hideHandlers.forEach((cb) => cb());
92+
},
93+
dispose: () => {},
94+
};
95+
96+
return qp as vscode.QuickPick<vscode.QuickPickItem>;
97+
});
5398
});
5499

55100
teardown(async () => {
56101
// Restore the original methods after each test
57102
showWarningMessageStub.restore();
103+
createQuickPickStub.restore();
58104
executeCommandSpy?.restore();
59105

60106
await vscode.commands.executeCommand("oil-code.close");

src/ui/confirmChanges.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import * as vscode from "vscode";
2+
3+
export type Change =
4+
| { kind: "create"; to: string }
5+
| { kind: "delete"; from: string }
6+
| { kind: "rename" | "move"; from: string; to: string }
7+
| { kind: "modify"; from: string }
8+
| { kind: "copy"; from: string; to: string };
9+
10+
export async function confirmChanges(changes: Change[]): Promise<boolean> {
11+
if (changes.length === 0) {
12+
return true;
13+
}
14+
return confirmChangesQuickPick(changes);
15+
}
16+
17+
async function confirmChangesQuickPick(changes: Change[]): Promise<boolean> {
18+
const qp = vscode.window.createQuickPick<vscode.QuickPickItem>();
19+
qp.title = "oil.code — Confirm changes";
20+
qp.matchOnDetail = true;
21+
22+
// Outside click should cancel -> allow hide on blur
23+
qp.ignoreFocusOut = false;
24+
25+
// Read-only list feel
26+
qp.items = changes.map(toQuickPickItem);
27+
qp.canSelectMany = false;
28+
29+
// "Hide" the input and instruct the user
30+
qp.placeholder = "[Y]es [N]o";
31+
qp.value = "";
32+
33+
// We don't want buttons; Y/N only
34+
qp.buttons = [];
35+
36+
const disposables: vscode.Disposable[] = [];
37+
const decision = await new Promise<boolean>((resolve) => {
38+
let finished = false;
39+
const finish = (ok: boolean) => {
40+
if (finished) return;
41+
finished = true;
42+
try {
43+
qp.hide();
44+
} catch {}
45+
resolve(ok);
46+
};
47+
48+
// Make rows feel non-interactive
49+
disposables.push(
50+
qp.onDidChangeSelection(() => {
51+
qp.selectedItems = [];
52+
}),
53+
qp.onDidChangeActive(() => {
54+
qp.activeItems = [];
55+
}),
56+
57+
// Ignore Enter entirely (only Y/N should close)
58+
qp.onDidAccept(() => {
59+
/* no-op */
60+
}),
61+
62+
// Capture last typed char; accept only Y or N
63+
qp.onDidChangeValue((val) => {
64+
const ch = val.trim().slice(-1).toLowerCase();
65+
qp.value = ""; // keep the field visually empty
66+
if (ch === "y") return finish(true);
67+
if (ch === "n") return finish(false);
68+
}),
69+
70+
// Esc or outside click hides -> cancel
71+
qp.onDidHide(() => {
72+
if (!finished) resolve(false);
73+
})
74+
);
75+
76+
qp.show();
77+
});
78+
79+
disposables.forEach((d) => d.dispose());
80+
qp.dispose();
81+
return decision;
82+
}
83+
84+
function toQuickPickItem(c: Change): vscode.QuickPickItem {
85+
switch (c.kind) {
86+
case "create":
87+
return {
88+
label: "$(diff-added) create",
89+
detail: c.to,
90+
};
91+
case "delete":
92+
return {
93+
label: "$(diff-removed) delete",
94+
detail: c.from,
95+
};
96+
case "modify":
97+
return {
98+
label: "$(edit) modify",
99+
detail: c.from,
100+
};
101+
case "rename":
102+
case "move":
103+
return {
104+
label: `$(diff-renamed) ${c.kind}`,
105+
detail: `${c.from} \u2192 ${c.to}`,
106+
};
107+
case "copy":
108+
return {
109+
label: "$(diff-added) copy",
110+
detail: `${c.from} \u2192 ${c.to}`,
111+
};
112+
}
113+
}

src/utils/settings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export function getEnableWorkspaceEditSetting(): boolean {
1515
return config.get<boolean>("enableWorkspaceEdit") || false;
1616
}
1717

18+
export function getEnableAlternateConfirmationSetting(): boolean {
19+
const config = vscode.workspace.getConfiguration("oil-code");
20+
return config.get<boolean>("enableAlternateConfirmation") || false;
21+
}
22+
1823
let restoreAutoSave = false;
1924

2025
export async function checkAndDisableAutoSave() {

0 commit comments

Comments
 (0)