diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 0258fa407dd..64f783471ba 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -239,12 +239,23 @@ export interface IFileDialogService { */ showSaveDialog(options: ISaveDialogOptions): Promise; + /** + * Shows a confirm dialog for saving 1-N files. + */ + showSaveConfirm(fileNameOrResources: string | URI[]): Promise; + /** * Shows a open file dialog and returns the chosen file URI. */ showOpenDialog(options: IOpenDialogOptions): Promise; } +export const enum ConfirmResult { + SAVE, + DONT_SAVE, + CANCEL +} + const MAX_CONFIRM_FILES = 10; export function getConfirmMessage(start: string, resourcesToConfirm: readonly URI[]): string { const message = [start]; diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 178c61b28b6..0c2e0156455 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { mixin } from 'vs/base/common/objects'; -import { IEditorInput, EditorInput, IEditorIdentifier, ConfirmResult, IEditorCommandsContext, CloseDirection } from 'vs/workbench/common/editor'; +import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection } from 'vs/workbench/common/editor'; import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -22,6 +22,8 @@ import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export class ExecuteCommandAction extends Action { @@ -597,6 +599,8 @@ export abstract class BaseCloseAllAction extends Action { label: string, clazz: string | undefined, private textFileService: ITextFileService, + private workingCopyService: IWorkingCopyService, + private fileDialogService: IFileDialogService, protected editorGroupService: IEditorGroupsService ) { super(id, label, clazz); @@ -619,7 +623,7 @@ export abstract class BaseCloseAllAction extends Action { async run(): Promise { // Just close all if there are no dirty editors - if (!this.textFileService.isDirty()) { + if (!this.workingCopyService.hasDirty) { return this.doCloseAll(); } @@ -636,7 +640,7 @@ export abstract class BaseCloseAllAction extends Action { return undefined; })); - const confirm = await this.textFileService.confirmSave(); + const confirm = await this.fileDialogService.showSaveConfirm(this.workingCopyService.getDirty().map(copy => copy.resource)); if (confirm === ConfirmResult.CANCEL) { return; } @@ -667,9 +671,11 @@ export class CloseAllEditorsAction extends BaseCloseAllAction { id: string, label: string, @ITextFileService textFileService: ITextFileService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IFileDialogService fileDialogService: IFileDialogService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(id, label, 'codicon-close-all', textFileService, editorGroupService); + super(id, label, 'codicon-close-all', textFileService, workingCopyService, fileDialogService, editorGroupService); } protected doCloseAll(): Promise { @@ -686,9 +692,11 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { id: string, label: string, @ITextFileService textFileService: ITextFileService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IFileDialogService fileDialogService: IFileDialogService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(id, label, undefined, textFileService, editorGroupService); + super(id, label, undefined, textFileService, workingCopyService, fileDialogService, editorGroupService); } protected async doCloseAll(): Promise { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 60c90b3c842..281a231ebf3 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { EditorInput, EditorOptions, GroupIdentifier, ConfirmResult, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; 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'; import { extname } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -131,7 +131,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IFileDialogService private readonly fileDialogService: IFileDialogService ) { super(themeService); @@ -1260,7 +1261,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Switch to editor that we want to handle and confirm to save/revert await this.openEditor(editor); - const res = await editor.confirmSave(); + const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); + const res = await this.fileDialogService.showSaveConfirm(editorResource ? [editorResource] : editor.getName()!); // It could be that the editor saved meanwhile, so we check again // to see if anything needs to happen before closing for good. diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 4ac59c9c71c..87782783b9f 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -415,13 +415,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return false; } - /** - * Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result. - */ - confirmSave(): Promise { - return Promise.resolve(ConfirmResult.DONT_SAVE); - } - /** * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ @@ -476,12 +469,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } } -export const enum ConfirmResult { - SAVE, - DONT_SAVE, - CANCEL -} - export const enum EncodingMode { /** @@ -573,10 +560,6 @@ export class SideBySideEditorInput extends EditorInput { return this.master.isDirty(); } - confirmSave(): Promise { - return this.master.confirmSave(); - } - save(): Promise { return this.master.save(); } diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index 466216f9840..28ecc5b4b23 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -8,7 +8,7 @@ import { suggestFilename } from 'vs/base/common/mime'; import { createMemoizer } from 'vs/base/common/decorators'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; -import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult, Verbosity, IModeSupport } from 'vs/workbench/common/editor'; +import { EditorInput, IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; @@ -148,10 +148,6 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup return false; } - confirmSave(): Promise { - return this.textFileService.confirmSave([this.resource]); - } - save(): Promise { return this.textFileService.save(this.resource); } diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 2695c40d587..104bebe8fb3 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { createMemoizer } from 'vs/base/common/decorators'; import { dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor'; +import { EncodingMode, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; @@ -243,10 +243,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return model.isDirty(); } - confirmSave(): Promise { - return this.textFileService.confirmSave([this.resource]); - } - save(): Promise { return this.textFileService.save(this.resource); } diff --git a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts index 62711b74fc8..1a43978f692 100644 --- a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts +++ b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, ConfirmResult, IRevertOptions, EditorOptions } from 'vs/workbench/common/editor'; +import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, IRevertOptions, EditorOptions } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; @@ -160,11 +160,6 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy { return this.dirty; } - confirmSave(): Promise { - // TODO - return Promise.resolve(ConfirmResult.DONT_SAVE); - } - save(): Promise { this.setDirty(false); diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index f8d717d76a5..cead0c626ba 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter } from 'vs/platform/dialogs/common/dialogs'; +import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -20,8 +20,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import Severity from 'vs/base/common/severity'; -export abstract class AbstractFileDialogService { +export abstract class AbstractFileDialogService implements IFileDialogService { _serviceBrand: undefined; @@ -34,6 +35,7 @@ export abstract class AbstractFileDialogService { @IConfigurationService protected readonly configurationService: IConfigurationService, @IFileService protected readonly fileService: IFileService, @IOpenerService protected readonly openerService: IOpenerService, + @IDialogService private readonly dialogService: IDialogService ) { } defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined { @@ -78,6 +80,40 @@ export abstract class AbstractFileDialogService { return this.defaultFilePath(schemeFilter); } + async showSaveConfirm(fileNameOrResources: string | URI[]): Promise { + if (this.environmentService.isExtensionDevelopment) { + return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) + } + + if (Array.isArray(fileNameOrResources) && fileNameOrResources.length === 0) { + return ConfirmResult.DONT_SAVE; + } + + let message: string; + if (typeof fileNameOrResources === 'string' || fileNameOrResources.length === 1) { + message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNameOrResources === 'string' ? fileNameOrResources : resources.basename(fileNameOrResources[0])); + } else { + message = getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNameOrResources.length), fileNameOrResources); + } + + const buttons: string[] = [ + 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"), + nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"), + nls.localize('cancel', "Cancel") + ]; + + const { choice } = await this.dialogService.show(Severity.Warning, message, buttons, { + cancelId: 2, + detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.") + }); + + switch (choice) { + case 0: return ConfirmResult.SAVE; + case 1: return ConfirmResult.DONT_SAVE; + default: return ConfirmResult.CANCEL; + } + } + protected abstract addFileSchemaIfNeeded(schema: string): string[]; protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise { @@ -179,4 +215,12 @@ export abstract class AbstractFileDialogService { protected getFileSystemSchema(options: { availableFileSystems?: readonly string[], defaultUri?: URI }): string { return options.availableFileSystems && options.availableFileSystems[0] || this.getSchemeFilterForWindow(); } + + abstract pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise; + abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise; + abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise; + abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise; + abstract pickFileToSave(options: ISaveDialogOptions): Promise; + abstract showSaveDialog(options: ISaveDialogOptions): Promise; + abstract showOpenDialog(options: IOpenDialogOptions): Promise; } diff --git a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts index a4873580a8d..f4452c6dc6f 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts @@ -6,7 +6,7 @@ import { SaveDialogOptions, OpenDialogOptions } from 'electron'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -33,8 +33,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, @IOpenerService openerService: IOpenerService, - @IElectronService private readonly electronService: IElectronService - ) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService); } + @IElectronService private readonly electronService: IElectronService, + @IDialogService dialogService: IDialogService + ) { + super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService); + } private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions { return { @@ -180,6 +183,16 @@ export class FileDialogService extends AbstractFileDialogService implements IFil // Don't allow untitled schema through. return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]); } + + async showSaveConfirm(fileNameOrResources: string | URI[]): Promise { + if (this.environmentService.isExtensionDevelopment) { + if (!this.environmentService.args['extension-development-confirm-save']) { + return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) + } + } + + return super.showSaveConfirm(fileNameOrResources); + } } registerSingleton(IFileDialogService, FileDialogService, true); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index f7409aa72ca..7961fcdb09d 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -9,7 +9,7 @@ import { Emitter, AsyncEmitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; +import { IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService, FileOperationError, FileOperationResult, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; @@ -24,9 +24,9 @@ import { Schemas } from 'vs/base/common/network'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources'; -import { getConfirmMessage, IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { coalesce } from 'vs/base/common/arrays'; @@ -232,7 +232,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } private async confirmBeforeShutdown(): Promise { - const confirm = await this.confirmSave(); + const confirm = await this.fileDialogService.showSaveConfirm(this.getDirty()); // Save if (confirm === ConfirmResult.SAVE) { @@ -496,49 +496,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return result.results.length === 1 && !!result.results[0].success; } - async confirmSave(resources?: URI[]): Promise { - if (this.environmentService.isExtensionDevelopment) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) - } - - const resourcesToConfirm = this.getDirty(resources); - if (resourcesToConfirm.length === 0) { - return ConfirmResult.DONT_SAVE; - } - - const message = resourcesToConfirm.length === 1 - ? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", basename(resourcesToConfirm[0])) - : getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm); - - const buttons: string[] = [ - resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"), - nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"), - nls.localize('cancel', "Cancel") - ]; - - const { choice } = await this.dialogService.show(Severity.Warning, message, buttons, { - cancelId: 2, - detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.") - }); - - switch (choice) { - case 0: return ConfirmResult.SAVE; - case 1: return ConfirmResult.DONT_SAVE; - default: return ConfirmResult.CANCEL; - } - } - - async confirmOverwrite(resource: URI): Promise { - const confirm: IConfirmation = { - message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), - detail: nls.localize('irreversible', "A file or folder with the same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - return (await this.dialogService.confirm(confirm)).confirmed; - } - saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise; saveAll(resources: URI[], options?: ISaveOptions): Promise; saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise { @@ -849,6 +806,17 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } } + private async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: nls.localize('irreversible', "A file or folder with the same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))), + primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + return (await this.dialogService.confirm(confirm)).confirmed; + } + private suggestFileName(untitledResource: URI): URI { const untitledFileName = this.untitledTextEditorService.suggestFileName(untitledResource); const remoteAuthority = this.environmentService.configuration.remoteAuthority; diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index bfc329ca941..d07dc93f41e 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { Event, IWaitUntil } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IEncodingSupport, ConfirmResult, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor'; import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult, FileOperation } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -129,14 +129,6 @@ export interface ITextFileService extends IDisposable { * Move a file. If the file is dirty, its contents will be preserved and restored. */ move(source: URI, target: URI, overwrite?: boolean): Promise; - - /** - * Brings up the confirm dialog to either save, don't save or cancel. - * - * @param resources the resources of the files to ask for confirmation or null if - * confirming for all dirty resources. - */ - confirmSave(resources?: URI[]): Promise; } export class FileOperationWillRunEvent implements IWaitUntil { diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 7a37e7ca8ab..e105588731a 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -39,7 +39,6 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ConfirmResult } from 'vs/workbench/common/editor'; import { assign } from 'vs/base/common/objects'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; @@ -315,16 +314,6 @@ export class NativeTextFileService extends AbstractTextFileService { protected getWindowCount(): Promise { return this.electronService.getWindowCount(); } - - async confirmSave(resources?: URI[]): Promise { - if (this.environmentService.isExtensionDevelopment) { - if (!this.environmentService.args['extension-development-confirm-save']) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) - } - } - - return super.confirmSave(resources); - } } export interface IEncodingOverride { diff --git a/src/vs/workbench/services/textfile/test/textFileService.test.ts b/src/vs/workbench/services/textfile/test/textFileService.test.ts index 152212b71a2..db7e2bd8033 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.test.ts @@ -7,12 +7,11 @@ import * as sinon from 'sinon'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; -import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestContextService, TestFileService, TestElectronService, TestFilesConfigurationService } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestContextService, TestFileService, TestElectronService, TestFilesConfigurationService, TestFileDialogService } from 'vs/workbench/test/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ConfirmResult } from 'vs/workbench/common/editor'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -22,6 +21,7 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { Schemas } from 'vs/base/common/network'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; class ServiceAccessor { constructor( @@ -32,7 +32,8 @@ class ServiceAccessor { @IWorkspaceContextService public contextService: TestContextService, @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, - @IElectronService public electronService: TestElectronService + @IElectronService public electronService: TestElectronService, + @IFileDialogService public fileDialogService: TestFileDialogService ) { } } @@ -87,7 +88,7 @@ suite('Files - TextFileService', () => { (accessor.textFileService.models).add(model.resource, model); const service = accessor.textFileService; - service.setConfirmResult(ConfirmResult.CANCEL); + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); await model.load(); model.textEditorModel!.setValue('foo'); @@ -103,7 +104,7 @@ suite('Files - TextFileService', () => { (accessor.textFileService.models).add(model.resource, model); const service = accessor.textFileService; - service.setConfirmResult(ConfirmResult.DONT_SAVE); + accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); await model.load(); @@ -129,7 +130,7 @@ suite('Files - TextFileService', () => { (accessor.textFileService.models).add(model.resource, model); const service = accessor.textFileService; - service.setConfirmResult(ConfirmResult.SAVE); + accessor.fileDialogService.setConfirmResult(ConfirmResult.SAVE); accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); await model.load(); @@ -429,7 +430,7 @@ suite('Files - TextFileService', () => { accessor.electronService.windowCount = Promise.resolve(2); } // Set cancel to force a veto if hot exit does not trigger - service.setConfirmResult(ConfirmResult.CANCEL); + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); await model.load(); model.textEditorModel!.setValue('foo'); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 1e7bd7d147c..1e87e03eeaa 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { ConfirmResult, IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; @@ -49,7 +49,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, IModelDecorationOptions, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; -import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IShowResult } from 'vs/platform/dialogs/common/dialogs'; +import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IShowResult, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -192,7 +192,6 @@ export class TestTextFileService extends NativeTextFileService { public cleanupBackupsBeforeShutdownCalled!: boolean; private promptPath!: URI; - private confirmResult!: ConfirmResult; private resolveTextContentError!: FileOperationError | null; constructor( @@ -241,10 +240,6 @@ export class TestTextFileService extends NativeTextFileService { this.promptPath = path; } - public setConfirmResult(result: ConfirmResult): void { - this.confirmResult = result; - } - public setResolveTextContentErrorOnce(error: FileOperationError): void { this.resolveTextContentError = error; } @@ -275,14 +270,6 @@ export class TestTextFileService extends NativeTextFileService { return Promise.resolve(this.promptPath); } - public confirmSave(_resources?: URI[]): Promise { - return Promise.resolve(this.confirmResult); - } - - public confirmOverwrite(_resource: URI): Promise { - return Promise.resolve(true); - } - protected cleanupBackupsBeforeShutdown(): Promise { this.cleanupBackupsBeforeShutdownCalled = true; return Promise.resolve(); @@ -303,6 +290,8 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); instantiationService.stub(IStorageService, new TestStorageService()); instantiationService.stub(IWorkbenchLayoutService, new TestLayoutService()); + instantiationService.stub(IDialogService, new TestDialogService()); + instantiationService.stub(IFileDialogService, new TestFileDialogService()); instantiationService.stub(IElectronService, new TestElectronService()); instantiationService.stub(IModeService, instantiationService.createInstance(ModeServiceImpl)); instantiationService.stub(IHistoryService, new TestHistoryService()); @@ -420,6 +409,8 @@ export class TestFileDialogService implements IFileDialogService { public _serviceBrand: undefined; + private confirmResult!: ConfirmResult; + public defaultFilePath(_schemeFilter?: string): URI | undefined { return undefined; } @@ -450,6 +441,12 @@ export class TestFileDialogService implements IFileDialogService { public showOpenDialog(_options: IOpenDialogOptions): Promise { return Promise.resolve(undefined); } + public setConfirmResult(result: ConfirmResult): void { + this.confirmResult = result; + } + public showSaveConfirm(resources: string | URI[]): Promise { + return Promise.resolve(this.confirmResult); + } } export class TestLayoutService implements IWorkbenchLayoutService {