diff --git a/src/vs/workbench/common/editor/untitledTextEditorModel.ts b/src/vs/workbench/common/editor/untitledTextEditorModel.ts index a26aa562d91..8029f739fc9 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorModel.ts @@ -14,9 +14,10 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IResolvedTextEditorModel, ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { withNullAsUndefined } from 'vs/base/common/types'; export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { } @@ -128,14 +129,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return true; } - async backup(): Promise { - if (this.isResolved()) { - return this.backupFileService.backup(this.resource, this.createSnapshot(), this.versionId); - } - } - - hasBackup(): boolean { - return this.backupFileService.hasBackupSync(this.resource, this.versionId); + async backup(): Promise { + return { content: withNullAsUndefined(this.createSnapshot()) }; } async load(): Promise { diff --git a/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts b/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts index e9d79317dd8..50f14d80aea 100644 --- a/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts +++ b/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts @@ -6,7 +6,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { BackupOnShutdown } from 'vs/workbench/contrib/backup/browser/backupOnShutdown'; +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; -// Register Backup On Shutdown -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupOnShutdown, LifecyclePhase.Starting); +// Register Backup Tracker +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BrowserBackupTracker, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/browser/backupOnShutdown.ts b/src/vs/workbench/contrib/backup/browser/backupTracker.ts similarity index 57% rename from src/vs/workbench/contrib/backup/browser/backupOnShutdown.ts rename to src/vs/workbench/contrib/backup/browser/backupTracker.ts index 607607f678c..40e18da8a7a 100644 --- a/src/vs/workbench/contrib/backup/browser/backupOnShutdown.ts +++ b/src/vs/workbench/contrib/backup/browser/backupTracker.ts @@ -3,31 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; -export class BackupOnShutdown extends Disposable implements IWorkbenchContribution { +export class BrowserBackupTracker extends BackupTracker implements IWorkbenchContribution { constructor( - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, + @ILogService logService: ILogService ) { - super(); - - this.registerListeners(); + super(backupFileService, filesConfigurationService, workingCopyService, logService, lifecycleService); } - private registerListeners() { + protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { - // Lifecycle - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown())); - } - - private onBeforeShutdown(): boolean { // Web: we cannot perform long running in the shutdown phase // As such we need to check sync if there are any dirty working @@ -44,7 +41,7 @@ export class BackupOnShutdown extends Disposable implements IWorkbenchContributi } for (const dirtyWorkingCopy of dirtyWorkingCopies) { - if (!dirtyWorkingCopy.hasBackup()) { + if (!this.backupFileService.hasBackupSync(dirtyWorkingCopy.resource, this.getContentVersion(dirtyWorkingCopy))) { console.warn('Unload prevented: pending backups'); return true; // dirty without backup: veto } diff --git a/src/vs/workbench/contrib/backup/common/backup.contribution.ts b/src/vs/workbench/contrib/backup/common/backup.contribution.ts index 9800a1c9472..e51ea2d1aba 100644 --- a/src/vs/workbench/contrib/backup/common/backup.contribution.ts +++ b/src/vs/workbench/contrib/backup/common/backup.contribution.ts @@ -5,12 +5,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -// Register Backup Tracker -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupTracker, LifecyclePhase.Starting); - // Register Backup Restorer Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index 2ce186e7cda..c1bc026348f 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -5,12 +5,12 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; +import { ShutdownReason, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -export class BackupTracker extends Disposable implements IWorkbenchContribution { +export abstract class BackupTracker extends Disposable { // Disable backup for when a short auto-save delay is configured with // the rationale that the auto save will trigger a save periodically @@ -24,15 +24,22 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution // load on the backup service when the user is typing into the editor protected static BACKUP_FROM_CONTENT_CHANGE_DELAY = 1000; + // A map from working copy to a version ID we compute on each content + // change. This version ID allows to e.g. ask if a backup for a specific + // content has been made before closing. + private readonly mapWorkingCopyToContentVersion = new Map(); + private backupsDisabledForAutoSaveables = false; + // A map of scheduled pending backups for working copies private readonly pendingBackups = new Map(); constructor( - @IBackupFileService private readonly backupFileService: IBackupFileService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @ILogService private readonly logService: ILogService + protected readonly backupFileService: IBackupFileService, + protected readonly filesConfigurationService: IFilesConfigurationService, + protected readonly workingCopyService: IWorkingCopyService, + private readonly logService: ILogService, + protected readonly lifecycleService: ILifecycleService ) { super(); @@ -52,6 +59,9 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution // Listen to auto save config changes this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(c => this.onAutoSaveConfigurationChange(c))); + + // Lifecycle (handled in subclasses) + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); } private onDidRegister(workingCopy: IWorkingCopy): void { @@ -59,6 +69,11 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution } private onDidUnregister(workingCopy: IWorkingCopy): void { + + // Remove from content version map + this.mapWorkingCopyToContentVersion.delete(workingCopy); + + // Discard backup this.discardBackup(workingCopy); } @@ -69,6 +84,12 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution } private onDidChangeContent(workingCopy: IWorkingCopy): void { + + // Increment content version ID + const contentVersionId = this.getContentVersion(workingCopy); + this.mapWorkingCopyToContentVersion.set(workingCopy, contentVersionId + 1); + + // Schedule backup if dirty if (workingCopy.isDirty()) { this.scheduleBackup(workingCopy); } @@ -90,7 +111,7 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString()); // Schedule new backup - const handle = setTimeout(() => { + const handle = setTimeout(async () => { // Clear disposable this.pendingBackups.delete(workingCopy); @@ -99,7 +120,8 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution if (workingCopy.isDirty()) { this.logService.trace(`[backup tracker] running backup`, workingCopy.resource.toString()); - workingCopy.backup(); + const backup = await workingCopy.backup(); + this.backupFileService.backup(workingCopy.resource, backup.content, this.getContentVersion(workingCopy), backup.meta); } }, BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY); @@ -111,6 +133,10 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution })); } + protected getContentVersion(workingCopy: IWorkingCopy): number { + return this.mapWorkingCopyToContentVersion.get(workingCopy) || 0; + } + private discardBackup(workingCopy: IWorkingCopy): void { this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString()); @@ -121,4 +147,6 @@ export class BackupTracker extends Disposable implements IWorkbenchContribution // Forward to backup file service this.backupFileService.discardBackup(workingCopy.resource); } + + protected abstract onBeforeShutdown(reason: ShutdownReason): boolean | Promise; } diff --git a/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts b/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts index 26ac4d2a0cb..9868c6285fe 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts @@ -6,7 +6,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { BackupOnShutdown } from 'vs/workbench/contrib/backup/electron-browser/backupOnShutdown'; +import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; -// Register Backup On Shutdown -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupOnShutdown, LifecyclePhase.Starting); +// Register Backup Tracker +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeBackupTracker, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts similarity index 87% rename from src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts rename to src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts index 4f7d2f8e3f8..11a8b3b6a3e 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -18,32 +17,27 @@ import { isMacintosh } from 'vs/base/common/platform'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { ILogService } from 'vs/platform/log/common/log'; -export class BackupOnShutdown extends Disposable implements IWorkbenchContribution { +export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { constructor( - @IBackupFileService private readonly backupFileService: IBackupFileService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @INotificationService private readonly notificationService: INotificationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IElectronService private readonly electronService: IElectronService + @IElectronService private readonly electronService: IElectronService, + @ILogService logService: ILogService ) { - super(); - - this.registerListeners(); + super(backupFileService, filesConfigurationService, workingCopyService, logService, lifecycleService); } - private registerListeners() { - - // Lifecycle - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); - } - - private onBeforeShutdown(reason: ShutdownReason): boolean | Promise { + protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { // Dirty working copies need treatment on shutdown const dirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; @@ -84,7 +78,7 @@ export class BackupOnShutdown extends Disposable implements IWorkbenchContributi // since a backup did not happen, we have to confirm for the dirty working copies now return this.confirmBeforeShutdown(); }, error => { - this.notificationService.error(localize('backupOnShutdown.failSave', "Working copies that are dirty could not be written to the backup location (Error: {0}). Try saving your editors first and then exit.", error.message)); + this.notificationService.error(localize('backupTracker.failSave', "Working copies that are dirty could not be written to the backup location (Error: {0}). Try saving your editors first and then exit.", error.message)); return true; // veto, the backups failed }); @@ -135,7 +129,10 @@ export class BackupOnShutdown extends Disposable implements IWorkbenchContributi } // Backup all working copies - await Promise.all(workingCopies.map(workingCopy => workingCopy.backup())); + await Promise.all(workingCopies.map(async workingCopy => { + const backup = await workingCopy.backup(); + return this.backupFileService.backup(workingCopy.resource, backup.content, this.getContentVersion(workingCopy), backup.meta); + })); return true; } diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupOnShutdown.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupOnShutdown.test.ts deleted file mode 100644 index fcc34430570..00000000000 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupOnShutdown.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; -import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; -import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestContextService, TestFileService, TestElectronService, TestFilesConfigurationService, TestFileDialogService, TestBackupFileService } 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 { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; -import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -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'; -import { BackupOnShutdown } from 'vs/workbench/contrib/backup/electron-browser/backupOnShutdown'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; - -class ServiceAccessor { - constructor( - @ILifecycleService public lifecycleService: TestLifecycleService, - @ITextFileService public textFileService: TestTextFileService, - @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, - @IWorkspaceContextService public contextService: TestContextService, - @IModelService public modelService: ModelServiceImpl, - @IFileService public fileService: TestFileService, - @IElectronService public electronService: TestElectronService, - @IFileDialogService public fileDialogService: TestFileDialogService, - @IBackupFileService public backupFileService: TestBackupFileService, - @IWorkingCopyService public workingCopyService: IWorkingCopyService - ) { - } -} - -class BeforeShutdownEventImpl implements BeforeShutdownEvent { - - value: boolean | Promise | undefined; - reason = ShutdownReason.CLOSE; - - veto(value: boolean | Promise): void { - this.value = value; - } -} - -suite('BackupOnShutdown', () => { - - let instantiationService: IInstantiationService; - let model: TextFileEditorModel; - let accessor: ServiceAccessor; - let backupOnShutdown: BackupOnShutdown; - - setup(() => { - instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); - backupOnShutdown = instantiationService.createInstance(BackupOnShutdown); - }); - - teardown(() => { - if (model) { - model.dispose(); - } - (accessor.textFileService.files).dispose(); - backupOnShutdown.dispose(); - }); - - test('confirm onWillShutdown - no veto', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.files).add(model.resource, model); - - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - const veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(!veto); - } else { - assert.ok(!(await veto)); - } - }); - - test('confirm onWillShutdown - veto if user cancels', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.files).add(model.resource, model); - - accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); - - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - assert.ok(event.value); - }); - - test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.files).add(model.resource, model); - - accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); - accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - let veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(accessor.backupFileService.didDiscardAllWorkspaceBackups); - assert.ok(!veto); - return; - } - - veto = await veto; - assert.ok(accessor.backupFileService.didDiscardAllWorkspaceBackups); - assert.ok(!veto); - }); - - test('confirm onWillShutdown - save (hot.exit: off)', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.files).add(model.resource, model); - - accessor.fileDialogService.setConfirmResult(ConfirmResult.SAVE); - accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - const veto = await (>event.value); - assert.ok(!veto); - assert.ok(!model.isDirty()); - }); - - suite('Hot Exit', () => { - suite('"onExit" setting', () => { - test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!platform.isMacintosh); - }); - test('should hot exit on non-Mac (reason: CLOSE, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); - }); - test('should NOT hot exit (reason: CLOSE, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, true); - }); - test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, false, true); - }); - test('should hot exit (reason: QUIT, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, true, false); - }); - test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, false, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, true, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, false, false); - }); - test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, true, false); - }); - test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, false, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, true, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, false, false); - }); - test('should NOT hot exit (reason: LOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, true, true); - }); - test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, false, true); - }); - test('should NOT hot exit (reason: LOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, true, true); - }); - test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, false, true); - }); - }); - - suite('"onExitAndWindowClose" setting', () => { - test('should hot exit (reason: CLOSE, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, true, false); - }); - test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); - }); - test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, true, false); - }); - test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, false, true); - }); - test('should hot exit (reason: QUIT, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, true, false); - }); - test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, false, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, true, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, false, false); - }); - test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, true, false); - }); - test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, false, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, true, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, false, false); - }); - test('should hot exit (reason: LOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, true, false); - }); - test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, false, true); - }); - test('should hot exit (reason: LOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, true, false); - }); - test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, false, true); - }); - }); - - async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.files).add(model.resource, model); - - // Set hot exit config - accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: setting } }); - - // Set empty workspace if required - if (!workspace) { - accessor.contextService.setWorkspace(new Workspace('empty:1508317022751')); - } - - // Set multiple windows if required - if (multipleWindows) { - accessor.electronService.windowCount = Promise.resolve(2); - } - - // Set cancel to force a veto if hot exit does not trigger - accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); - - const event = new BeforeShutdownEventImpl(); - event.reason = shutdownReason; - accessor.lifecycleService.fireWillShutdown(event); - - const veto = await (>event.value); - assert.ok(!accessor.backupFileService.didDiscardAllWorkspaceBackups); // When hot exit is set, backups should never be cleaned since the confirm result is cancel - assert.equal(veto, shouldVeto); - } - }); -}); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts index 562f6dbeec4..ac8680aea3f 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts @@ -14,7 +14,7 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; -import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; @@ -109,7 +109,7 @@ suite('BackupRestorer', () => { await part.whenRestored; - const tracker = instantiationService.createInstance(BackupTracker); + const tracker = instantiationService.createInstance(NativeBackupTracker); const restorer = instantiationService.createInstance(TestBackupRestorer); // Backup 2 normal files and 2 untitled file diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index 199b8f007d7..2ffb7019f78 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -12,8 +12,8 @@ import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; -import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; -import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; +import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; +import { TestTextFileService, workbenchInstantiationService, TestLifecycleService, TestFilesConfigurationService, TestContextService, TestFileService, TestElectronService, TestFileDialogService } from 'vs/workbench/test/workbenchTestServices'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; @@ -33,6 +33,16 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; import { INewUntitledTextEditorOptions } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; +import { IElectronService } from 'vs/platform/electron/node/electron'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { IModelService } from 'vs/editor/common/services/modelService'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -43,34 +53,60 @@ const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); class ServiceAccessor { constructor( + @ILifecycleService public lifecycleService: TestLifecycleService, @ITextFileService public textFileService: TestTextFileService, - @IEditorService public editorService: IEditorService, - @IBackupFileService public backupFileService: NodeTestBackupFileService + @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, + @IWorkspaceContextService public contextService: TestContextService, + @IModelService public modelService: ModelServiceImpl, + @IFileService public fileService: TestFileService, + @IElectronService public electronService: TestElectronService, + @IFileDialogService public fileDialogService: TestFileDialogService, + @IBackupFileService public backupFileService: NodeTestBackupFileService, + @IWorkingCopyService public workingCopyService: IWorkingCopyService, + @IEditorService public editorService: IEditorService ) { } } -class TestBackupTracker extends BackupTracker { +class TestBackupTracker extends NativeBackupTracker { constructor( @IBackupFileService backupFileService: IBackupFileService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IFileDialogService fileDialogService: IFileDialogService, + @INotificationService notificationService: INotificationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IElectronService electronService: IElectronService, @ILogService logService: ILogService ) { - super(backupFileService, filesConfigurationService, workingCopyService, logService); + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, environmentService, fileDialogService, notificationService, contextService, electronService, logService); // Reduce timeout for tests BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY = 10; } } +class BeforeShutdownEventImpl implements BeforeShutdownEvent { + + value: boolean | Promise | undefined; + reason = ShutdownReason.CLOSE; + + veto(value: boolean | Promise): void { + this.value = value; + } +} + suite('BackupTracker', () => { let accessor: ServiceAccessor; - let disposables: IDisposable[] = []; setup(async () => { + const instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(ServiceAccessor); + disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( TextFileEditor, @@ -181,4 +217,247 @@ suite('BackupTracker', () => { part.dispose(); tracker.dispose(); }); + + test('confirm onWillShutdown - no veto', async function () { + const [accessor, part, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + const veto = event.value; + if (typeof veto === 'boolean') { + assert.ok(!veto); + } else { + assert.ok(!(await veto)); + } + + part.dispose(); + tracker.dispose(); + }); + + test('confirm onWillShutdown - veto if user cancels', async function () { + const [accessor, part, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const model = accessor.textFileService.files.get(resource); + + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); + + await model?.load(); + model?.textEditorModel?.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + assert.ok(event.value); + + part.dispose(); + tracker.dispose(); + }); + + test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { + const [accessor, part, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const model = accessor.textFileService.files.get(resource); + + accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); + + await model?.load(); + model?.textEditorModel?.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + let veto = event.value; + if (typeof veto === 'boolean') { + assert.ok(accessor.backupFileService.didDiscardAllWorkspaceBackups); + assert.ok(!veto); + return; + } + + veto = await veto; + assert.ok(accessor.backupFileService.didDiscardAllWorkspaceBackups); + assert.ok(!veto); + + part.dispose(); + tracker.dispose(); + }); + + test('confirm onWillShutdown - save (hot.exit: off)', async function () { + const [accessor, part, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const model = accessor.textFileService.files.get(resource); + + accessor.fileDialogService.setConfirmResult(ConfirmResult.SAVE); + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); + + await model?.load(); + model?.textEditorModel?.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + const veto = await (>event.value); + assert.ok(!veto); + assert.ok(!model?.isDirty()); + + part.dispose(); + tracker.dispose(); + }); + + suite('Hot Exit', () => { + suite('"onExit" setting', () => { + test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!platform.isMacintosh); + }); + test('should hot exit on non-Mac (reason: CLOSE, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, true); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, false, true); + }); + test('should hot exit (reason: QUIT, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, true, false); + }); + test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, false, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, true, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, false, false); + }); + test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, true, false); + }); + test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, false, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, true, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, false, false); + }); + test('should NOT hot exit (reason: LOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, true, true); + }); + test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, false, true); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, true, true); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, false, true); + }); + }); + + suite('"onExitAndWindowClose" setting', () => { + test('should hot exit (reason: CLOSE, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, true, false); + }); + test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); + }); + test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, true, false); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, false, true); + }); + test('should hot exit (reason: QUIT, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, true, false); + }); + test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, false, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, true, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, false, false); + }); + test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, true, false); + }); + test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, false, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, true, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, false, false); + }); + test('should hot exit (reason: LOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, true, false); + }); + test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, false, true); + }); + test('should hot exit (reason: LOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, true, false); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, false, true); + }); + }); + + async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { + const [accessor, part, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const model = accessor.textFileService.files.get(resource); + + // Set hot exit config + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: setting } }); + + // Set empty workspace if required + if (!workspace) { + accessor.contextService.setWorkspace(new Workspace('empty:1508317022751')); + } + + // Set multiple windows if required + if (multipleWindows) { + accessor.electronService.windowCount = Promise.resolve(2); + } + + // Set cancel to force a veto if hot exit does not trigger + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); + + await model?.load(); + model?.textEditorModel?.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + + const event = new BeforeShutdownEventImpl(); + event.reason = shutdownReason; + accessor.lifecycleService.fireWillShutdown(event); + + const veto = await (>event.value); + assert.ok(!accessor.backupFileService.didDiscardAllWorkspaceBackups); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.equal(veto, shouldVeto); + + part.dispose(); + tracker.dispose(); + } + }); }); diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 239b2b8b878..66e29755f62 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICustomEditorModel, CustomEditorEdit, CustomEditorSaveAsEvent, CustomEditorSaveEvent } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; export class CustomEditorModel extends Disposable implements ICustomEditorModel { @@ -190,11 +190,8 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this.updateContentChanged(); } - public hasBackup(): boolean { - return true; //TODO@matt forward to extension - } - - public async backup(): Promise { - //TODO@matt forward to extension + public async backup(): Promise { + // TODO@matt implement + return {}; } } diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 7183e34f44a..c088f5d9665 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -72,6 +72,7 @@ export class NodeTestBackupFileService extends BackupFileService { this.fileService = fileService; this.backupResourceJoiners = []; this.discardBackupJoiners = []; + this.didDiscardAllWorkspaceBackups = false; } joinBackupResource(): Promise { @@ -97,6 +98,14 @@ export class NodeTestBackupFileService extends BackupFileService { this.discardBackupJoiners.pop()!(); } } + + didDiscardAllWorkspaceBackups: boolean; + + discardBackups(): Promise { + this.didDiscardAllWorkspaceBackups = true; + + return super.discardBackups(); + } } suite('BackupFileService', () => { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 7ce1e571fae..fd3613f8c5c 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { URI } from 'vs/base/common/uri'; -import { assertIsDefined } from 'vs/base/common/types'; +import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { ITextFileService, ModelState, ITextFileEditorModel, ISaveErrorHandler, ISaveParticipant, ITextFileStreamContent, ILoadOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; @@ -19,9 +19,9 @@ import { timeout } from 'vs/base/common/async'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; -import { isEqual, basename } from 'vs/base/common/resources'; +import { basename } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer'; @@ -189,27 +189,21 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Backup - async backup(target = this.resource): Promise { - if (this.isResolved()) { + async backup(): Promise { - // Only fill in model metadata if resource matches - let meta: IBackupMetaData | undefined = undefined; - if (isEqual(target, this.resource) && this.lastResolvedFileStat) { - meta = { - mtime: this.lastResolvedFileStat.mtime, - ctime: this.lastResolvedFileStat.ctime, - size: this.lastResolvedFileStat.size, - etag: this.lastResolvedFileStat.etag, - orphaned: this.inOrphanMode - }; - } - - return this.backupFileService.backup(target, this.createSnapshot(), this.versionId, meta); + // Fill in metadata if we are resolved + let meta: IBackupMetaData | undefined = undefined; + if (this.lastResolvedFileStat) { + meta = { + mtime: this.lastResolvedFileStat.mtime, + ctime: this.lastResolvedFileStat.ctime, + size: this.lastResolvedFileStat.size, + etag: this.lastResolvedFileStat.etag, + orphaned: this.inOrphanMode + }; } - } - hasBackup(): boolean { - return this.backupFileService.hasBackupSync(this.resource, this.versionId); + return { meta, content: withNullAsUndefined(this.createSnapshot()) }; } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 5d7a8a95456..bda1468be03 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { TernarySearchTree, values } from 'vs/base/common/map'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { ITextSnapshot } from 'vs/editor/common/model'; export const enum WorkingCopyCapabilities { @@ -21,6 +22,11 @@ export const enum WorkingCopyCapabilities { Untitled = 1 << 1 } +export interface IWorkingCopyBackup { + meta?: object; + content?: ITextSnapshot; +} + export interface IWorkingCopy { readonly resource: URI; @@ -46,14 +52,12 @@ export interface IWorkingCopy { //#region Save / Backup + backup(): Promise; + save(options?: ISaveOptions): Promise; revert(options?: IRevertOptions): Promise; - hasBackup(): boolean; - - backup(): Promise; - //#endregion } @@ -124,7 +128,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic //#region Registry - private mapResourceToWorkingCopy = TernarySearchTree.forPaths>(); + private readonly mapResourceToWorkingCopy = TernarySearchTree.forPaths>(); get workingCopies(): IWorkingCopy[] { return values(this._workingCopies); } private _workingCopies = new Set(); diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 435a48241f5..38eabd06a6d 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -59,9 +59,9 @@ suite('WorkingCopyService', () => { return true; } - async backup(): Promise { } - - hasBackup(): boolean { return false; } + async backup(): Promise { + return {}; + } dispose(): void { this._onDispose.fire(); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index f4bfd81ea50..c96b3e59f11 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -1198,11 +1198,7 @@ export class TestBackupFileService implements IBackupFileService { return Promise.resolve(); } - didDiscardAllWorkspaceBackups = false; - discardBackups(): Promise { - this.didDiscardAllWorkspaceBackups = true; - return Promise.resolve(); } }