Skip to content

Commit 929fab1

Browse files
author
Benjamin Pasero
committed
dialog - extract confirmSave to file dialog servie
1 parent 9134f9f commit 929fab1

14 files changed

Lines changed: 131 additions & 136 deletions

File tree

src/vs/platform/dialogs/common/dialogs.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,23 @@ export interface IFileDialogService {
239239
*/
240240
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
241241

242+
/**
243+
* Shows a confirm dialog for saving 1-N files.
244+
*/
245+
showSaveConfirm(fileNameOrResources: string | URI[]): Promise<ConfirmResult>;
246+
242247
/**
243248
* Shows a open file dialog and returns the chosen file URI.
244249
*/
245250
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
246251
}
247252

253+
export const enum ConfirmResult {
254+
SAVE,
255+
DONT_SAVE,
256+
CANCEL
257+
}
258+
248259
const MAX_CONFIRM_FILES = 10;
249260
export function getConfirmMessage(start: string, resourcesToConfirm: readonly URI[]): string {
250261
const message = [start];

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as nls from 'vs/nls';
77
import { Action } from 'vs/base/common/actions';
88
import { mixin } from 'vs/base/common/objects';
9-
import { IEditorInput, EditorInput, IEditorIdentifier, ConfirmResult, IEditorCommandsContext, CloseDirection } from 'vs/workbench/common/editor';
9+
import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection } from 'vs/workbench/common/editor';
1010
import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
1111
import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen';
1212
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
@@ -22,6 +22,8 @@ import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/
2222
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2323
import { DisposableStore } from 'vs/base/common/lifecycle';
2424
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
25+
import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
26+
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
2527

2628
export class ExecuteCommandAction extends Action {
2729

@@ -597,6 +599,8 @@ export abstract class BaseCloseAllAction extends Action {
597599
label: string,
598600
clazz: string | undefined,
599601
private textFileService: ITextFileService,
602+
private workingCopyService: IWorkingCopyService,
603+
private fileDialogService: IFileDialogService,
600604
protected editorGroupService: IEditorGroupsService
601605
) {
602606
super(id, label, clazz);
@@ -619,7 +623,7 @@ export abstract class BaseCloseAllAction extends Action {
619623
async run(): Promise<any> {
620624

621625
// Just close all if there are no dirty editors
622-
if (!this.textFileService.isDirty()) {
626+
if (!this.workingCopyService.hasDirty) {
623627
return this.doCloseAll();
624628
}
625629

@@ -636,7 +640,7 @@ export abstract class BaseCloseAllAction extends Action {
636640
return undefined;
637641
}));
638642

639-
const confirm = await this.textFileService.confirmSave();
643+
const confirm = await this.fileDialogService.showSaveConfirm(this.workingCopyService.getDirty().map(copy => copy.resource));
640644
if (confirm === ConfirmResult.CANCEL) {
641645
return;
642646
}
@@ -667,9 +671,11 @@ export class CloseAllEditorsAction extends BaseCloseAllAction {
667671
id: string,
668672
label: string,
669673
@ITextFileService textFileService: ITextFileService,
674+
@IWorkingCopyService workingCopyService: IWorkingCopyService,
675+
@IFileDialogService fileDialogService: IFileDialogService,
670676
@IEditorGroupsService editorGroupService: IEditorGroupsService
671677
) {
672-
super(id, label, 'codicon-close-all', textFileService, editorGroupService);
678+
super(id, label, 'codicon-close-all', textFileService, workingCopyService, fileDialogService, editorGroupService);
673679
}
674680

675681
protected doCloseAll(): Promise<any> {
@@ -686,9 +692,11 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction {
686692
id: string,
687693
label: string,
688694
@ITextFileService textFileService: ITextFileService,
695+
@IWorkingCopyService workingCopyService: IWorkingCopyService,
696+
@IFileDialogService fileDialogService: IFileDialogService,
689697
@IEditorGroupsService editorGroupService: IEditorGroupsService
690698
) {
691-
super(id, label, undefined, textFileService, editorGroupService);
699+
super(id, label, undefined, textFileService, workingCopyService, fileDialogService, editorGroupService);
692700
}
693701

694702
protected async doCloseAll(): Promise<any> {

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import 'vs/css!./media/editorgroupview';
77

88
import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup';
9-
import { EditorInput, EditorOptions, GroupIdentifier, ConfirmResult, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext } from 'vs/workbench/common/editor';
9+
import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor } from 'vs/workbench/common/editor';
1010
import { Event, Emitter, Relay } from 'vs/base/common/event';
1111
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1212
import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom';
@@ -50,7 +50,7 @@ import { guessMimeTypes } from 'vs/base/common/mime';
5050
import { extname } from 'vs/base/common/resources';
5151
import { Schemas } from 'vs/base/common/network';
5252
import { EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor';
53-
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
53+
import { IDialogService, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
5454

5555
export class EditorGroupView extends Themable implements IEditorGroupView {
5656

@@ -131,7 +131,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
131131
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
132132
@IKeybindingService private readonly keybindingService: IKeybindingService,
133133
@IMenuService private readonly menuService: IMenuService,
134-
@IContextMenuService private readonly contextMenuService: IContextMenuService
134+
@IContextMenuService private readonly contextMenuService: IContextMenuService,
135+
@IFileDialogService private readonly fileDialogService: IFileDialogService
135136
) {
136137
super(themeService);
137138

@@ -1260,7 +1261,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
12601261
// Switch to editor that we want to handle and confirm to save/revert
12611262
await this.openEditor(editor);
12621263

1263-
const res = await editor.confirmSave();
1264+
const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER });
1265+
const res = await this.fileDialogService.showSaveConfirm(editorResource ? [editorResource] : editor.getName()!);
12641266

12651267
// It could be that the editor saved meanwhile, so we check again
12661268
// to see if anything needs to happen before closing for good.

src/vs/workbench/common/editor.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -415,13 +415,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput {
415415
return false;
416416
}
417417

418-
/**
419-
* Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result.
420-
*/
421-
confirmSave(): Promise<ConfirmResult> {
422-
return Promise.resolve(ConfirmResult.DONT_SAVE);
423-
}
424-
425418
/**
426419
* Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation.
427420
*/
@@ -476,12 +469,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput {
476469
}
477470
}
478471

479-
export const enum ConfirmResult {
480-
SAVE,
481-
DONT_SAVE,
482-
CANCEL
483-
}
484-
485472
export const enum EncodingMode {
486473

487474
/**
@@ -573,10 +560,6 @@ export class SideBySideEditorInput extends EditorInput {
573560
return this.master.isDirty();
574561
}
575562

576-
confirmSave(): Promise<ConfirmResult> {
577-
return this.master.confirmSave();
578-
}
579-
580563
save(): Promise<boolean> {
581564
return this.master.save();
582565
}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { suggestFilename } from 'vs/base/common/mime';
88
import { createMemoizer } from 'vs/base/common/decorators';
99
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
1010
import { basenameOrAuthority, dirname } from 'vs/base/common/resources';
11-
import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult, Verbosity, IModeSupport } from 'vs/workbench/common/editor';
11+
import { EditorInput, IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor';
1212
import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel';
1313
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1414
import { Event, Emitter } from 'vs/base/common/event';
@@ -148,10 +148,6 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup
148148
return false;
149149
}
150150

151-
confirmSave(): Promise<ConfirmResult> {
152-
return this.textFileService.confirmSave([this.resource]);
153-
}
154-
155151
save(): Promise<boolean> {
156152
return this.textFileService.save(this.resource);
157153
}

src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { localize } from 'vs/nls';
77
import { createMemoizer } from 'vs/base/common/decorators';
88
import { dirname } from 'vs/base/common/resources';
99
import { URI } from 'vs/base/common/uri';
10-
import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor';
10+
import { EncodingMode, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor';
1111
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
1212
import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel';
1313
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
@@ -243,10 +243,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
243243
return model.isDirty();
244244
}
245245

246-
confirmSave(): Promise<ConfirmResult> {
247-
return this.textFileService.confirmSave([this.resource]);
248-
}
249-
250246
save(): Promise<boolean> {
251247
return this.textFileService.save(this.resource);
252248
}

src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as nls from 'vs/nls';
77
import { Action } from 'vs/base/common/actions';
88
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
9-
import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, ConfirmResult, IRevertOptions, EditorOptions } from 'vs/workbench/common/editor';
9+
import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, IRevertOptions, EditorOptions } from 'vs/workbench/common/editor';
1010
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1111
import { IEditorModel } from 'vs/platform/editor/common/editor';
1212
import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom';
@@ -160,11 +160,6 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy {
160160
return this.dirty;
161161
}
162162

163-
confirmSave(): Promise<ConfirmResult> {
164-
// TODO
165-
return Promise.resolve(ConfirmResult.DONT_SAVE);
166-
}
167-
168163
save(): Promise<boolean> {
169164
this.setDirty(false);
170165

src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as nls from 'vs/nls';
77
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
8-
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter } from 'vs/platform/dialogs/common/dialogs';
8+
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
99
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
1010
import { IHistoryService } from 'vs/workbench/services/history/common/history';
1111
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -20,8 +20,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
2020
import { IFileService } from 'vs/platform/files/common/files';
2121
import { IOpenerService } from 'vs/platform/opener/common/opener';
2222
import { IHostService } from 'vs/workbench/services/host/browser/host';
23+
import Severity from 'vs/base/common/severity';
2324

24-
export abstract class AbstractFileDialogService {
25+
export abstract class AbstractFileDialogService implements IFileDialogService {
2526

2627
_serviceBrand: undefined;
2728

@@ -34,6 +35,7 @@ export abstract class AbstractFileDialogService {
3435
@IConfigurationService protected readonly configurationService: IConfigurationService,
3536
@IFileService protected readonly fileService: IFileService,
3637
@IOpenerService protected readonly openerService: IOpenerService,
38+
@IDialogService private readonly dialogService: IDialogService
3739
) { }
3840

3941
defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
@@ -78,6 +80,40 @@ export abstract class AbstractFileDialogService {
7880
return this.defaultFilePath(schemeFilter);
7981
}
8082

83+
async showSaveConfirm(fileNameOrResources: string | URI[]): Promise<ConfirmResult> {
84+
if (this.environmentService.isExtensionDevelopment) {
85+
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
86+
}
87+
88+
if (Array.isArray(fileNameOrResources) && fileNameOrResources.length === 0) {
89+
return ConfirmResult.DONT_SAVE;
90+
}
91+
92+
let message: string;
93+
if (typeof fileNameOrResources === 'string' || fileNameOrResources.length === 1) {
94+
message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNameOrResources === 'string' ? fileNameOrResources : resources.basename(fileNameOrResources[0]));
95+
} else {
96+
message = getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNameOrResources.length), fileNameOrResources);
97+
}
98+
99+
const buttons: string[] = [
100+
Array.isArray(fileNameOrResources) && fileNameOrResources.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
101+
nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
102+
nls.localize('cancel', "Cancel")
103+
];
104+
105+
const { choice } = await this.dialogService.show(Severity.Warning, message, buttons, {
106+
cancelId: 2,
107+
detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.")
108+
});
109+
110+
switch (choice) {
111+
case 0: return ConfirmResult.SAVE;
112+
case 1: return ConfirmResult.DONT_SAVE;
113+
default: return ConfirmResult.CANCEL;
114+
}
115+
}
116+
81117
protected abstract addFileSchemaIfNeeded(schema: string): string[];
82118

83119
protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<any> {
@@ -179,4 +215,12 @@ export abstract class AbstractFileDialogService {
179215
protected getFileSystemSchema(options: { availableFileSystems?: readonly string[], defaultUri?: URI }): string {
180216
return options.availableFileSystems && options.availableFileSystems[0] || this.getSchemeFilterForWindow();
181217
}
218+
219+
abstract pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
220+
abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
221+
abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
222+
abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
223+
abstract pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined>;
224+
abstract showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
225+
abstract showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
182226
}

src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { SaveDialogOptions, OpenDialogOptions } from 'electron';
77
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
88
import { IHostService } from 'vs/workbench/services/host/browser/host';
9-
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
9+
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
1010
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
1111
import { IHistoryService } from 'vs/workbench/services/history/common/history';
1212
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -33,8 +33,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
3333
@IConfigurationService configurationService: IConfigurationService,
3434
@IFileService fileService: IFileService,
3535
@IOpenerService openerService: IOpenerService,
36-
@IElectronService private readonly electronService: IElectronService
37-
) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService); }
36+
@IElectronService private readonly electronService: IElectronService,
37+
@IDialogService dialogService: IDialogService
38+
) {
39+
super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService);
40+
}
3841

3942
private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions {
4043
return {
@@ -180,6 +183,16 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
180183
// Don't allow untitled schema through.
181184
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
182185
}
186+
187+
async showSaveConfirm(fileNameOrResources: string | URI[]): Promise<ConfirmResult> {
188+
if (this.environmentService.isExtensionDevelopment) {
189+
if (!this.environmentService.args['extension-development-confirm-save']) {
190+
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
191+
}
192+
}
193+
194+
return super.showSaveConfirm(fileNameOrResources);
195+
}
183196
}
184197

185198
registerSingleton(IFileDialogService, FileDialogService, true);

0 commit comments

Comments
 (0)