diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 622cbc8c48d..0db78001751 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -114,10 +114,10 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp // Trigger backup if configured and enabled for shutdown reason let backups: IWorkingCopy[] = []; let backupError: Error | undefined = undefined; - const backup = await this.shouldBackupBeforeShutdown(reason); - if (backup) { + const modifiedWorkingCopiesToBackup = await this.shouldBackupBeforeShutdown(reason, modifiedWorkingCopies); + if (modifiedWorkingCopiesToBackup.length > 0) { try { - const backupResult = await this.backupBeforeShutdown(modifiedWorkingCopies); + const backupResult = await this.backupBeforeShutdown(modifiedWorkingCopiesToBackup); backups = backupResult.backups; backupError = backupResult.error; @@ -162,49 +162,53 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp } } - private async shouldBackupBeforeShutdown(reason: ShutdownReason): Promise { - let backup: boolean | undefined; + private async shouldBackupBeforeShutdown(reason: ShutdownReason, modifiedWorkingCopies: readonly IWorkingCopy[]): Promise { if (!this.filesConfigurationService.isHotExitEnabled) { - backup = false; // never backup when hot exit is disabled via settings - } else if (this.environmentService.isExtensionDevelopment) { - backup = true; // always backup closing extension development window without asking to speed up debugging - } else { - - // When quit is requested skip the confirm callback and attempt to backup all workspaces. - // When quit is not requested the confirm callback should be shown when the window being - // closed is the only VS Code window open, except for on Mac where hot exit is only - // ever activated when quit is requested. - - switch (reason) { - case ShutdownReason.CLOSE: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - backup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else if (await this.nativeHostService.getWindowCount() > 1 || isMacintosh) { - backup = false; // do not backup if a window is closed that does not cause quitting of the application - } else { - backup = true; // backup if last window is closed on win/linux where the application quits right after - } - break; - - case ShutdownReason.QUIT: - backup = true; // backup because next start we restore all backups - break; - - case ShutdownReason.RELOAD: - backup = true; // backup because after window reload, backups restore - break; - - case ShutdownReason.LOAD: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - backup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else { - backup = false; // do not backup because we are switching contexts - } - break; - } + return []; // never backup when hot exit is disabled via settings } - return backup; + if (this.environmentService.isExtensionDevelopment) { + return modifiedWorkingCopies; // always backup closing extension development window without asking to speed up debugging + } + + switch (reason) { + + // Window Close + case ShutdownReason.CLOSE: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + return modifiedWorkingCopies; // backup if a workspace/folder is open and onExitAndWindowClose is configured + } + + if (isMacintosh || await this.nativeHostService.getWindowCount() > 1) { + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + return modifiedWorkingCopies.filter(modifiedWorkingCopy => modifiedWorkingCopy.capabilities & WorkingCopyCapabilities.Scratchpad); // backup scratchpads automatically to avoid user confirmation + } + + return []; // do not backup if a window is closed that does not cause quitting of the application + } + + return modifiedWorkingCopies; // backup if last window is closed on win/linux where the application quits right after + + // Application Quit + case ShutdownReason.QUIT: + return modifiedWorkingCopies; // backup because next start we restore all backups + + // Window Reload + case ShutdownReason.RELOAD: + return modifiedWorkingCopies; // backup because after window reload, backups restore + + // Workspace Change + case ShutdownReason.LOAD: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + if (this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + return modifiedWorkingCopies; // backup if a workspace/folder is open and onExitAndWindowClose is configured + } + + return modifiedWorkingCopies.filter(modifiedWorkingCopy => modifiedWorkingCopy.capabilities & WorkingCopyCapabilities.Scratchpad); // backup scratchpads automatically to avoid user confirmation + } + + return []; // do not backup because we are switching contexts with no workspace/folder open + } } private showErrorDialog(msg: string, workingCopies: readonly IWorkingCopy[], error?: Error): void { diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts index e2398e4ae67..dedbe3c0f79 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts @@ -565,6 +565,109 @@ suite('WorkingCopyBackupTracker (native)', function () { }); }); + suite('"onExit" setting - scratchpad', () => { + test('should hot exit (reason: CLOSE, windows: single, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, false); + }); + test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, false); + }); + test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, false); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, false, true); + }); + test('should hot exit (reason: QUIT, windows: single, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, true, false); + }); + test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, false, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, true, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, false, false); + }); + test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, true, false); + }); + test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, false, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, true, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, false, false); + }); + test('should hot exit (reason: LOAD, windows: single, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, true, false); + }); + test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, false, true); + }); + test('should hot exit (reason: LOAD, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, true, false); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { + return scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, false, true); + }); + }); + + suite('"onExitAndWindowClose" setting - scratchpad', () => { + test('should hot exit (reason: CLOSE, windows: single, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!isMacintosh); + }); + test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, false, true); + }); + test('should hot exit (reason: QUIT, windows: single, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, false, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, false, false); + }); + test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, false, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, false, false); + }); + test('should hot exit (reason: LOAD, windows: single, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, false, true); + }); + test('should hot exit (reason: LOAD, windows: multiple, workspace)', function () { + return scratchpadHotExitTest.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 scratchpadHotExitTest.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, cleanup } = await createTracker(); @@ -604,5 +707,60 @@ suite('WorkingCopyBackupTracker (native)', function () { await cleanup(); } + + async function scratchpadHotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { + const { accessor, cleanup } = await createTracker(); + + class TestBackupWorkingCopy extends TestWorkingCopy { + + constructor(resource: URI) { + super(resource); + + accessor.workingCopyService.registerWorkingCopy(this); + } + + override capabilities = WorkingCopyCapabilities.Untitled | WorkingCopyCapabilities.Scratchpad; + + override isDirty(): boolean { + return false; + } + + override isModified(): boolean { + return true; + } + } + + // Set hot exit config + accessor.filesConfigurationService.testOnFilesConfigurationChange({ 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.nativeHostService.windowCount = Promise.resolve(2); + } + + // Set cancel to force a veto if hot exit does not trigger + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); + + const resource = toResource.call(this, '/path/custom.txt'); + new TestBackupWorkingCopy(resource); + + const event = new TestBeforeShutdownEvent(); + event.reason = shutdownReason; + accessor.lifecycleService.fireBeforeShutdown(event); + + const veto = await event.value; + assert.ok(typeof event.finalValue === 'function'); // assert the tracker uses the internal finalVeto API + assert.strictEqual(accessor.workingCopyBackupService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.strictEqual(veto, shouldVeto); + + await cleanup(); + + await cleanup(); + } }); });