Skip to content

Commit f4e89e4

Browse files
committed
Add alternative QuickPick confirmation dialog
Fix linting
1 parent f2f1dbf commit f4e89e4

5 files changed

Lines changed: 216 additions & 39 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/ui/confirmChanges.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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) {
41+
return;
42+
}
43+
finished = true;
44+
try {
45+
qp.hide();
46+
} catch {}
47+
resolve(ok);
48+
};
49+
50+
// Make rows feel non-interactive
51+
disposables.push(
52+
qp.onDidChangeSelection(() => {
53+
qp.selectedItems = [];
54+
}),
55+
qp.onDidChangeActive(() => {
56+
qp.activeItems = [];
57+
}),
58+
59+
// Ignore Enter entirely (only Y/N should close)
60+
qp.onDidAccept(() => {
61+
/* no-op */
62+
}),
63+
64+
// Capture last typed char; accept only Y or N
65+
qp.onDidChangeValue((val) => {
66+
const ch = val.trim().slice(-1).toLowerCase();
67+
qp.value = ""; // keep the field visually empty
68+
if (ch === "y") {
69+
return finish(true);
70+
}
71+
if (ch === "n") {
72+
return finish(false);
73+
}
74+
}),
75+
76+
// Esc or outside click hides -> cancel
77+
qp.onDidHide(() => {
78+
if (!finished) {
79+
resolve(false);
80+
}
81+
})
82+
);
83+
84+
qp.show();
85+
});
86+
87+
disposables.forEach((d) => d.dispose());
88+
qp.dispose();
89+
return decision;
90+
}
91+
92+
function toQuickPickItem(c: Change): vscode.QuickPickItem {
93+
switch (c.kind) {
94+
case "create":
95+
return {
96+
label: "$(diff-added) create",
97+
detail: c.to,
98+
};
99+
case "delete":
100+
return {
101+
label: "$(diff-removed) delete",
102+
detail: c.from,
103+
};
104+
case "modify":
105+
return {
106+
label: "$(edit) modify",
107+
detail: c.from,
108+
};
109+
case "rename":
110+
case "move":
111+
return {
112+
label: `$(diff-renamed) ${c.kind}`,
113+
detail: `${c.from} \u2192 ${c.to}`,
114+
};
115+
case "copy":
116+
return {
117+
label: "$(diff-added) copy",
118+
detail: `${c.from} \u2192 ${c.to}`,
119+
};
120+
}
121+
}

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)