Skip to content

Commit fc10a34

Browse files
authored
Merge pull request microsoft#154457 from microsoft/ben/flying-fowl
2 parents 31a22e7 + 37c6c1c commit fc10a34

File tree

5 files changed

+110
-69
lines changed

5 files changed

+110
-69
lines changed

src/vs/workbench/browser/parts/editor/editorActions.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -573,24 +573,31 @@ abstract class AbstractCloseAllAction extends Action {
573573
override async run(): Promise<void> {
574574

575575
// Depending on the editor and auto save configuration,
576-
// split dirty editors into buckets
576+
// split editors into buckets for handling confirmation
577577

578578
const dirtyEditorsWithDefaultConfirm = new Set<IEditorIdentifier>();
579579
const dirtyAutoSaveOnFocusChangeEditors = new Set<IEditorIdentifier>();
580580
const dirtyAutoSaveOnWindowChangeEditors = new Set<IEditorIdentifier>();
581-
const dirtyEditorsWithCustomConfirm = new Map<string /* typeId */, Set<IEditorIdentifier>>();
581+
const editorsWithCustomConfirm = new Map<string /* typeId */, Set<IEditorIdentifier>>();
582582

583583
for (const { editor, groupId } of this.editorService.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: this.excludeSticky })) {
584-
if (!editor.isDirty() || editor.isSaving()) {
585-
continue; // only interested in dirty editors that are not in the process of saving
584+
let confirmClose = false;
585+
if (editor.closeHandler) {
586+
confirmClose = editor.closeHandler.showConfirm(); // custom handling of confirmation on close
587+
} else {
588+
confirmClose = editor.isDirty() && !editor.isSaving(); // default confirm only when dirty and not saving
589+
}
590+
591+
if (!confirmClose) {
592+
continue;
586593
}
587594

588595
// Editor has custom confirm implementation
589-
if (typeof editor.confirm === 'function') {
590-
let customEditorsToConfirm = dirtyEditorsWithCustomConfirm.get(editor.typeId);
596+
if (typeof editor.closeHandler?.confirm === 'function') {
597+
let customEditorsToConfirm = editorsWithCustomConfirm.get(editor.typeId);
591598
if (!customEditorsToConfirm) {
592599
customEditorsToConfirm = new Set();
593-
dirtyEditorsWithCustomConfirm.set(editor.typeId, customEditorsToConfirm);
600+
editorsWithCustomConfirm.set(editor.typeId, customEditorsToConfirm);
594601
}
595602

596603
customEditorsToConfirm.add({ editor, groupId });
@@ -619,7 +626,7 @@ abstract class AbstractCloseAllAction extends Action {
619626
if (dirtyEditorsWithDefaultConfirm.size > 0) {
620627
const editors = Array.from(dirtyEditorsWithDefaultConfirm.values());
621628

622-
await this.revealDirtyEditors(editors); // help user make a decision by revealing editors
629+
await this.revealEditorsToConfirm(editors); // help user make a decision by revealing editors
623630

624631
const confirmation = await this.fileDialogService.showSaveConfirm(editors.map(({ editor }) => {
625632
if (editor instanceof SideBySideEditorInput) {
@@ -642,12 +649,12 @@ abstract class AbstractCloseAllAction extends Action {
642649
}
643650

644651
// 2.) Show custom confirm based dialog
645-
for (const [, editorIdentifiers] of dirtyEditorsWithCustomConfirm) {
652+
for (const [, editorIdentifiers] of editorsWithCustomConfirm) {
646653
const editors = Array.from(editorIdentifiers.values());
647654

648-
await this.revealDirtyEditors(editors); // help user make a decision by revealing editors
655+
await this.revealEditorsToConfirm(editors); // help user make a decision by revealing editors
649656

650-
const confirmation = await firstOrDefault(editors)?.editor.confirm?.(editors);
657+
const confirmation = await firstOrDefault(editors)?.editor.closeHandler?.confirm?.(editors);
651658
if (typeof confirmation === 'number') {
652659
switch (confirmation) {
653660
case ConfirmResult.CANCEL:
@@ -683,7 +690,7 @@ abstract class AbstractCloseAllAction extends Action {
683690
return this.doCloseAll();
684691
}
685692

686-
private async revealDirtyEditors(editors: ReadonlyArray<IEditorIdentifier>): Promise<void> {
693+
private async revealEditorsToConfirm(editors: ReadonlyArray<IEditorIdentifier>): Promise<void> {
687694
try {
688695
const handledGroups = new Set<GroupIdentifier>();
689696
for (const { editor, groupId } of editors) {

src/vs/workbench/browser/parts/editor/editorGroupView.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,16 +1322,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
13221322
//#region closeEditor()
13231323

13241324
async closeEditor(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions): Promise<boolean> {
1325-
return this.doCloseEditorWithDirtyHandling(editor, options);
1325+
return this.doCloseEditorWithConfirmationHandling(editor, options);
13261326
}
13271327

1328-
private async doCloseEditorWithDirtyHandling(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions, internalOptions?: IInternalEditorCloseOptions): Promise<boolean> {
1328+
private async doCloseEditorWithConfirmationHandling(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions, internalOptions?: IInternalEditorCloseOptions): Promise<boolean> {
13291329
if (!editor) {
13301330
return false;
13311331
}
13321332

1333-
// Check for dirty and veto
1334-
const veto = await this.handleDirtyClosing([editor]);
1333+
// Check for confirmation and veto
1334+
const veto = await this.handleCloseConfirmation([editor]);
13351335
if (veto) {
13361336
return false;
13371337
}
@@ -1461,7 +1461,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
14611461
return this.model.closeEditor(editor, internalOptions?.context)?.editorIndex;
14621462
}
14631463

1464-
private async handleDirtyClosing(editors: EditorInput[]): Promise<boolean /* veto */> {
1464+
private async handleCloseConfirmation(editors: EditorInput[]): Promise<boolean /* veto */> {
14651465
if (!editors.length) {
14661466
return false; // no veto
14671467
}
@@ -1470,15 +1470,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
14701470

14711471
// To prevent multiple confirmation dialogs from showing up one after the other
14721472
// we check if a pending confirmation is currently showing and if so, join that
1473-
let handleDirtyClosingPromise = this.mapEditorToPendingConfirmation.get(editor);
1474-
if (!handleDirtyClosingPromise) {
1475-
handleDirtyClosingPromise = this.doHandleDirtyClosing(editor);
1476-
this.mapEditorToPendingConfirmation.set(editor, handleDirtyClosingPromise);
1473+
let handleCloseConfirmationPromise = this.mapEditorToPendingConfirmation.get(editor);
1474+
if (!handleCloseConfirmationPromise) {
1475+
handleCloseConfirmationPromise = this.doHandleCloseConfirmation(editor);
1476+
this.mapEditorToPendingConfirmation.set(editor, handleCloseConfirmationPromise);
14771477
}
14781478

14791479
let veto: boolean;
14801480
try {
1481-
veto = await handleDirtyClosingPromise;
1481+
veto = await handleCloseConfirmationPromise;
14821482
} finally {
14831483
this.mapEditorToPendingConfirmation.delete(editor);
14841484
}
@@ -1489,12 +1489,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
14891489
}
14901490

14911491
// Otherwise continue with the remainders
1492-
return this.handleDirtyClosing(editors);
1492+
return this.handleCloseConfirmation(editors);
14931493
}
14941494

1495-
private async doHandleDirtyClosing(editor: EditorInput, options?: { skipAutoSave: boolean }): Promise<boolean /* veto */> {
1496-
if (!editor.isDirty() || editor.isSaving()) {
1497-
return false; // editor must be dirty and not saving
1495+
private async doHandleCloseConfirmation(editor: EditorInput, options?: { skipAutoSave: boolean }): Promise<boolean /* veto */> {
1496+
if (!this.shouldConfirmClose(editor)) {
1497+
return false; // no veto
14981498
}
14991499

15001500
if (editor instanceof SideBySideEditorInput && this.model.contains(editor.primary)) {
@@ -1531,10 +1531,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
15311531
// on auto-save configuration.
15321532
// However, make sure to respect `skipAutoSave` option in case the automated
15331533
// save fails which would result in the editor never closing.
1534+
// Also, we only do this if no custom confirmation handling is implemented.
15341535
let confirmation = ConfirmResult.CANCEL;
15351536
let saveReason = SaveReason.EXPLICIT;
15361537
let autoSave = false;
1537-
if (!editor.hasCapability(EditorInputCapabilities.Untitled) && !options?.skipAutoSave) {
1538+
if (!editor.hasCapability(EditorInputCapabilities.Untitled) && !options?.skipAutoSave && !editor.closeHandler) {
15381539

15391540
// Auto-save on focus change: save, because a dialog would steal focus
15401541
// (see https://github.com/microsoft/vscode/issues/108752)
@@ -1554,15 +1555,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
15541555
}
15551556
}
15561557

1557-
// No auto-save on focus change: ask user
1558+
// No auto-save on focus change or custom confirmation handler: ask user
15581559
if (!autoSave) {
15591560

1560-
// Switch to editor that we want to handle and confirm to save/revert
1561+
// Switch to editor that we want to handle for confirmation
15611562
await this.doOpenEditor(editor);
15621563

15631564
// Let editor handle confirmation if implemented
1564-
if (typeof editor.confirm === 'function') {
1565-
confirmation = await editor.confirm();
1565+
if (typeof editor.closeHandler?.confirm === 'function') {
1566+
confirmation = await editor.closeHandler.confirm();
15661567
}
15671568

15681569
// Show a file specific confirmation
@@ -1578,11 +1579,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
15781579
}
15791580
}
15801581

1581-
// It could be that the editor saved meanwhile or is saving, so we check
1582+
// It could be that the editor's choice of confirmation has changed
1583+
// given the check for confirmation is long running, so we check
15821584
// again to see if anything needs to happen before closing for good.
1583-
// This can happen for example if autoSave: onFocusChange is configured
1585+
// This can happen for example if `autoSave: onFocusChange` is configured
15841586
// so that the save happens when the dialog opens.
1585-
if (!editor.isDirty() || editor.isSaving()) {
1587+
if (!this.shouldConfirmClose(editor)) {
15861588
return confirmation === ConfirmResult.CANCEL ? true : false;
15871589
}
15881590

@@ -1595,7 +1597,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
15951597
// we handle the dirty editor again but this time ensuring to
15961598
// show the confirm dialog
15971599
// (see https://github.com/microsoft/vscode/issues/108752)
1598-
return this.doHandleDirtyClosing(editor, { skipAutoSave: true });
1600+
return this.doHandleCloseConfirmation(editor, { skipAutoSave: true });
15991601
}
16001602

16011603
return editor.isDirty(); // veto if still dirty
@@ -1621,6 +1623,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
16211623
}
16221624
}
16231625

1626+
private shouldConfirmClose(editor: EditorInput): boolean {
1627+
if (editor.closeHandler) {
1628+
return editor.closeHandler.showConfirm(); // custom handling of confirmation on close
1629+
}
1630+
1631+
return editor.isDirty() && !editor.isSaving(); // editor must be dirty and not saving
1632+
}
1633+
16241634
//#endregion
16251635

16261636
//#region closeEditors()
@@ -1632,8 +1642,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
16321642

16331643
const editors = this.doGetEditorsToClose(args);
16341644

1635-
// Check for dirty and veto
1636-
const veto = await this.handleDirtyClosing(editors.slice(0));
1645+
// Check for confirmation and veto
1646+
const veto = await this.handleCloseConfirmation(editors.slice(0));
16371647
if (veto) {
16381648
return false;
16391649
}
@@ -1714,8 +1724,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
17141724
return true;
17151725
}
17161726

1717-
// Check for dirty and veto
1718-
const veto = await this.handleDirtyClosing(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options));
1727+
// Check for confirmation and veto
1728+
const veto = await this.handleCloseConfirmation(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options));
17191729
if (veto) {
17201730
return false;
17211731
}
@@ -1795,7 +1805,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
17951805
this.doCloseEditor(editor, false, { context: EditorCloseContext.REPLACE });
17961806
closed = true;
17971807
} else {
1798-
closed = await this.doCloseEditorWithDirtyHandling(editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE });
1808+
closed = await this.doCloseEditorWithConfirmationHandling(editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE });
17991809
}
18001810

18011811
if (!closed) {
@@ -1815,7 +1825,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
18151825
if (activeReplacement.forceReplaceDirty) {
18161826
this.doCloseEditor(activeReplacement.editor, false, { context: EditorCloseContext.REPLACE });
18171827
} else {
1818-
await this.doCloseEditorWithDirtyHandling(activeReplacement.editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE });
1828+
await this.doCloseEditorWithConfirmationHandling(activeReplacement.editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE });
18191829
}
18201830
}
18211831

src/vs/workbench/common/editor/editorInput.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@ import { EditorInputCapabilities, Verbosity, GroupIdentifier, ISaveOptions, IRev
1111
import { isEqual } from 'vs/base/common/resources';
1212
import { ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
1313

14+
export interface IEditorCloseHandler {
15+
16+
/**
17+
* If `true`, will call into the `confirm` method to ask for confirmation
18+
* before closing the editor.
19+
*/
20+
showConfirm(): boolean;
21+
22+
/**
23+
* Allows an editor to control what should happen when the editor
24+
* (or a list of editor of the same kind) is being closed.
25+
*
26+
* By default a file specific dialog will open if the editor is
27+
* dirty and not in the process of saving.
28+
*
29+
* If the editor is not dealing with files or another condition
30+
* should be used besides dirty state, this method should be
31+
* implemented to show a different dialog.
32+
*
33+
* @param editors if more than one editor is closed, will pass in
34+
* each editor of the same kind to be able to show a combined dialog.
35+
*/
36+
confirm(editors?: ReadonlyArray<IEditorIdentifier>): Promise<ConfirmResult>;
37+
}
38+
1439
/**
1540
* Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part.
1641
* Each editor input is mapped to an editor that is capable of opening it through the Platform facade.
@@ -45,6 +70,12 @@ export abstract class EditorInput extends AbstractEditorInput {
4570

4671
private disposed: boolean = false;
4772

73+
/**
74+
* Optional: subclasses can override to implement
75+
* custom confirmation on close behavior.
76+
*/
77+
readonly closeHandler?: IEditorCloseHandler;
78+
4879
/**
4980
* Unique type identifier for this input. Every editor input of the
5081
* same class should share the same type identifier. The type identifier
@@ -168,20 +199,6 @@ export abstract class EditorInput extends AbstractEditorInput {
168199
return null;
169200
}
170201

171-
/**
172-
* Optional: if this method is implemented, allows an editor to
173-
* control what should happen when the editor (or a list of editors
174-
* of the same kind) is dirty and there is an intent to close it.
175-
*
176-
* By default a file specific dialog will open. If the editor is
177-
* not dealing with files, this method should be implemented to
178-
* show a different dialog.
179-
*
180-
* @param editors if more than one editor is closed, will pass in
181-
* each editor of the same kind to be able to show a combined dialog.
182-
*/
183-
confirm?(editors?: ReadonlyArray<IEditorIdentifier>): Promise<ConfirmResult>;
184-
185202
/**
186203
* Saves the editor. The provided groupId helps implementors
187204
* to e.g. preserve view state of the editor and re-open it

src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@ import { IFileService } from 'vs/platform/files/common/files';
1414
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1515
import { ILabelService } from 'vs/platform/label/common/label';
1616
import { IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor';
17-
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
17+
import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput';
1818
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
1919
import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
2020
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
2121
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2222
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
2323
import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
2424
import { assertType } from 'vs/base/common/types';
25-
import { Event } from 'vs/base/common/event';
2625

2726
export class MergeEditorInputData {
2827
constructor(
@@ -33,7 +32,7 @@ export class MergeEditorInputData {
3332
) { }
3433
}
3534

36-
export class MergeEditorInput extends AbstractTextResourceEditorInput implements ILanguageSupport {
35+
export class MergeEditorInput extends AbstractTextResourceEditorInput implements ILanguageSupport, IEditorCloseHandler {
3736

3837
static readonly ID = 'mergeEditor.Input';
3938

@@ -125,8 +124,6 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
125124
this._store.add(input1);
126125
this._store.add(input2);
127126
this._store.add(result);
128-
129-
this._store.add(Event.fromObservable(this._model.hasUnhandledConflicts)(() => this._onDidChangeDirty.fire(undefined)));
130127
}
131128

132129
this._ignoreUnhandledConflictsForDirtyState = undefined;
@@ -146,21 +143,25 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
146143
// ---- FileEditorInput
147144

148145
override isDirty(): boolean {
149-
const textModelDirty = Boolean(this._outTextModel?.isDirty());
150-
if (textModelDirty) {
146+
return Boolean(this._outTextModel?.isDirty());
147+
}
148+
149+
override readonly closeHandler = this;
150+
151+
showConfirm(): boolean {
152+
if (this.isDirty()) {
151153
// text model dirty -> 3wm is dirty
152154
return true;
153155
}
154156
if (!this._ignoreUnhandledConflictsForDirtyState) {
155-
// unhandled conflicts -> 3wm is dirty UNLESS we explicitly set this input
156-
// to ignore unhandled conflicts for the dirty-state. This happens only
157-
// after confirming to ignore unhandled changes
157+
// unhandled conflicts -> 3wm asks to confirm UNLESS we explicitly set this input
158+
// to ignore unhandled conflicts. This happens only after confirming to ignore unhandled changes
158159
return Boolean(this._model && this._model.hasUnhandledConflicts.get());
159160
}
160161
return false;
161162
}
162163

163-
override async confirm(editors?: ReadonlyArray<IEditorIdentifier>): Promise<ConfirmResult> {
164+
async confirm(editors?: ReadonlyArray<IEditorIdentifier>): Promise<ConfirmResult> {
164165

165166
const inputs: MergeEditorInput[] = [this];
166167
if (editors) {

0 commit comments

Comments
 (0)