mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
implement backup on shutdown via working copies (#84672)
This commit is contained in:
@@ -30,6 +30,10 @@
|
||||
"name": "vs/workbench/api/common",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/backup",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/bulkEdit",
|
||||
"project": "vscode-workbench"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
|
||||
// Register Backup On Shutdown
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupOnShutdown, LifecyclePhase.Starting);
|
||||
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
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';
|
||||
|
||||
export class BackupOnShutdown extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
|
||||
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
|
||||
// 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
|
||||
// copies that have not been backed up yet and then prevent the
|
||||
// shutdown if that is the case.
|
||||
|
||||
const dirtyWorkingCopies = this.workingCopyService.workingCopies.filter(workingCopy => workingCopy.isDirty());
|
||||
if (!dirtyWorkingCopies.length) {
|
||||
return false; // no dirty: no veto
|
||||
}
|
||||
|
||||
if (!this.filesConfigurationService.isHotExitEnabled) {
|
||||
return true; // dirty without backup: veto
|
||||
}
|
||||
|
||||
for (const dirtyWorkingCopy of dirtyWorkingCopies) {
|
||||
if (!dirtyWorkingCopy.hasBackup()) {
|
||||
console.warn('Unload prevented: pending backups');
|
||||
return true; // dirty without backup: veto
|
||||
}
|
||||
}
|
||||
|
||||
return false; // dirty with backups: no veto
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
|
||||
// Register Backup On Shutdown
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupOnShutdown, LifecyclePhase.Starting);
|
||||
@@ -0,0 +1,207 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { ConfirmResult, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { HotExitConfiguration } from 'vs/platform/files/common/files';
|
||||
import { IElectronService } from 'vs/platform/electron/node/electron';
|
||||
import type { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
|
||||
export class BackupOnShutdown extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IBackupFileService private readonly backupFileService: IBackupFileService,
|
||||
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
|
||||
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
|
||||
@ILifecycleService private readonly 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
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason)));
|
||||
}
|
||||
|
||||
private onBeforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
|
||||
|
||||
// Dirty working copies need treatment on shutdown
|
||||
const dirtyWorkingCopies = this.workingCopyService.workingCopies.filter(workingCopy => workingCopy.isDirty());
|
||||
if (dirtyWorkingCopies.length) {
|
||||
|
||||
// If auto save is enabled, save all working copies and then check again for dirty copies
|
||||
// We DO NOT run any save participant if we are in the shutdown phase for performance reasons
|
||||
if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) {
|
||||
return this.doSaveAll(dirtyWorkingCopies, false /* not untitled */, { skipSaveParticipants: true }).then(() => {
|
||||
|
||||
// If we still have dirty working copies, we either have untitled ones or working copies that cannot be saved
|
||||
const remainingDirtyWorkingCopies = this.workingCopyService.workingCopies.filter(workingCopy => workingCopy.isDirty());
|
||||
if (remainingDirtyWorkingCopies.length) {
|
||||
return this.handleDirtyBeforeShutdown(remainingDirtyWorkingCopies, reason);
|
||||
}
|
||||
|
||||
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since there are no dirty working copies)
|
||||
});
|
||||
}
|
||||
|
||||
// Auto save is not enabled
|
||||
return this.handleDirtyBeforeShutdown(dirtyWorkingCopies, reason);
|
||||
}
|
||||
|
||||
// No dirty working copies: no veto
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
private handleDirtyBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): boolean | Promise<boolean> {
|
||||
|
||||
// If hot exit is enabled, backup dirty working copies and allow to exit without confirmation
|
||||
if (this.filesConfigurationService.isHotExitEnabled) {
|
||||
return this.backupBeforeShutdown(workingCopies, reason).then(didBackup => {
|
||||
if (didBackup) {
|
||||
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
return true; // veto, the backups failed
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise just confirm from the user what to do with the dirty working copies
|
||||
return this.confirmBeforeShutdown();
|
||||
}
|
||||
|
||||
private async backupBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise<boolean> {
|
||||
|
||||
// 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.
|
||||
|
||||
let doBackup: boolean | undefined;
|
||||
switch (reason) {
|
||||
case ShutdownReason.CLOSE:
|
||||
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
|
||||
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
|
||||
} else if (await this.electronService.getWindowCount() > 1 || isMacintosh) {
|
||||
doBackup = false; // do not backup if a window is closed that does not cause quitting of the application
|
||||
} else {
|
||||
doBackup = true; // backup if last window is closed on win/linux where the application quits right after
|
||||
}
|
||||
break;
|
||||
|
||||
case ShutdownReason.QUIT:
|
||||
doBackup = true; // backup because next start we restore all backups
|
||||
break;
|
||||
|
||||
case ShutdownReason.RELOAD:
|
||||
doBackup = 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) {
|
||||
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
|
||||
} else {
|
||||
doBackup = false; // do not backup because we are switching contexts
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!doBackup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Backup all working copies
|
||||
await Promise.all(workingCopies.map(workingCopy => workingCopy.backup()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async confirmBeforeShutdown(): Promise<boolean> {
|
||||
|
||||
// Show confirm dialog for all dirty working copies
|
||||
const dirtyWorkingCopies = this.workingCopyService.workingCopies.filter(workingCopy => workingCopy.isDirty());
|
||||
const confirm = await this.fileDialogService.showSaveConfirm(dirtyWorkingCopies.map(w => w.resource));
|
||||
|
||||
// Save
|
||||
if (confirm === ConfirmResult.SAVE) {
|
||||
await this.doSaveAll(dirtyWorkingCopies, true /* includeUntitled */, { skipSaveParticipants: true });
|
||||
|
||||
if (this.workingCopyService.hasDirty) {
|
||||
return true; // veto if any save failed
|
||||
}
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
// Don't Save
|
||||
else if (confirm === ConfirmResult.DONT_SAVE) {
|
||||
|
||||
// Make sure to revert working copies so that they do not restore
|
||||
// see https://github.com/Microsoft/vscode/issues/29572
|
||||
await this.doRevertAll(dirtyWorkingCopies, { soft: true } /* soft revert is good enough on shutdown */);
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
// Cancel
|
||||
else if (confirm === ConfirmResult.CANCEL) {
|
||||
return true; // veto
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private doSaveAll(workingCopies: IWorkingCopy[], includeUntitled: boolean, options: ISaveOptions): Promise<boolean[]> {
|
||||
return Promise.all(workingCopies.map(async workingCopy => {
|
||||
if (workingCopy.isDirty() && (includeUntitled || !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled))) {
|
||||
return workingCopy.save(options);
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
private doRevertAll(workingCopies: IWorkingCopy[], options: IRevertOptions): Promise<boolean[]> {
|
||||
return Promise.all(workingCopies.map(workingCopy => workingCopy.revert(options)));
|
||||
}
|
||||
|
||||
private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise<boolean> {
|
||||
if (!options.cleanUpBackups) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.lifecycleService.phase < LifecyclePhase.Restored) {
|
||||
return false; // if editors have not restored, we are not up to speed with backups and thus should not clean them
|
||||
}
|
||||
|
||||
if (this.environmentService.isExtensionDevelopment) {
|
||||
return false; // extension development does not track any backups
|
||||
}
|
||||
|
||||
return this.backupFileService.discardAllWorkspaceBackups().then(() => false, () => false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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';
|
||||
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';
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor(
|
||||
@ILifecycleService public lifecycleService: TestLifecycleService,
|
||||
@ITextFileService public textFileService: TestTextFileService,
|
||||
@IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService,
|
||||
@IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService,
|
||||
@IWorkspaceContextService public contextService: TestContextService,
|
||||
@IModelService public modelService: ModelServiceImpl,
|
||||
@IFileService public fileService: TestFileService,
|
||||
@IElectronService public electronService: TestElectronService,
|
||||
@IFileDialogService public fileDialogService: TestFileDialogService,
|
||||
@IBackupFileService public backupFileService: TestBackupFileService
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class BeforeShutdownEventImpl implements BeforeShutdownEvent {
|
||||
|
||||
value: boolean | Promise<boolean> | undefined;
|
||||
reason = ShutdownReason.CLOSE;
|
||||
|
||||
veto(value: boolean | Promise<boolean>): 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();
|
||||
}
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledTextEditorService.revertAll();
|
||||
backupOnShutdown.dispose();
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - no veto', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).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);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.resource, model);
|
||||
|
||||
accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL);
|
||||
|
||||
await model.load();
|
||||
model.textEditorModel!.setValue('foo');
|
||||
assert.equal(accessor.textFileService.getDirty().length, 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);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).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.textFileService.getDirty().length, 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);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).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.textFileService.getDirty().length, 1);
|
||||
const event = new BeforeShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
const veto = await (<Promise<boolean>>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<void> {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).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.textFileService.getDirty().length, 1);
|
||||
|
||||
const event = new BeforeShutdownEventImpl();
|
||||
event.reason = shutdownReason;
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
const veto = await (<Promise<boolean>>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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -86,7 +86,6 @@ suite('BackupRestorer', () => {
|
||||
dispose(disposables);
|
||||
disposables = [];
|
||||
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledTextEditorService.revertAll();
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ suite('BackupTracker', () => {
|
||||
dispose(disposables);
|
||||
disposables = [];
|
||||
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledTextEditorService.revertAll();
|
||||
|
||||
|
||||
@@ -170,6 +170,10 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel
|
||||
this.updateContentChanged();
|
||||
}
|
||||
|
||||
public hasBackup(): boolean {
|
||||
return true; //TODO@matt forward to extension
|
||||
}
|
||||
|
||||
public async backup(): Promise<void> {
|
||||
//TODO@matt forward to extension
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ suite('EditorAutoSave', () => {
|
||||
|
||||
part.dispose();
|
||||
editorAutoSave.dispose();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
});
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ suite('Files - FileEditorTracker', () => {
|
||||
assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html');
|
||||
|
||||
tracker.dispose();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
});
|
||||
|
||||
@@ -120,7 +119,6 @@ suite('Files - FileEditorTracker', () => {
|
||||
|
||||
part.dispose();
|
||||
tracker.dispose();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/
|
||||
import { ITextFileService, IResourceEncodings, IResourceEncoding, ModelState } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export class BrowserTextFileService extends AbstractTextFileService {
|
||||
|
||||
@@ -17,49 +16,21 @@ export class BrowserTextFileService extends AbstractTextFileService {
|
||||
}
|
||||
};
|
||||
|
||||
protected onBeforeShutdown(reason: ShutdownReason): boolean {
|
||||
// Web: we cannot perform long running in the shutdown phase
|
||||
// As such we need to check sync if there are any dirty files
|
||||
// that have not been backed up yet and then prevent the shutdown
|
||||
// if that is the case.
|
||||
return this.doBeforeShutdownSync();
|
||||
protected registerListeners(): void {
|
||||
super.registerListeners();
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason)));
|
||||
}
|
||||
|
||||
private doBeforeShutdownSync(): boolean {
|
||||
protected onBeforeShutdown(reason: ShutdownReason): boolean {
|
||||
if (this.models.getAll().some(model => model.hasState(ModelState.PENDING_SAVE))) {
|
||||
console.warn('Unload prevented: pending file saves');
|
||||
|
||||
return true; // files are pending to be saved: veto
|
||||
}
|
||||
|
||||
const dirtyResources = this.getDirty();
|
||||
if (!dirtyResources.length) {
|
||||
return false; // no dirty: no veto
|
||||
}
|
||||
|
||||
if (!this.filesConfigurationService.isHotExitEnabled) {
|
||||
return true; // dirty without backup: veto
|
||||
}
|
||||
|
||||
for (const dirtyResource of dirtyResources) {
|
||||
let hasBackup = false;
|
||||
|
||||
if (this.fileService.canHandleResource(dirtyResource)) {
|
||||
const model = this.models.get(dirtyResource);
|
||||
hasBackup = !!(model?.hasBackup());
|
||||
} else if (dirtyResource.scheme === Schemas.untitled) {
|
||||
hasBackup = this.untitledTextEditorService.hasBackup(dirtyResource);
|
||||
}
|
||||
|
||||
if (!hasBackup) {
|
||||
console.warn('Unload prevented: pending backups');
|
||||
return true; // dirty without backup: veto
|
||||
}
|
||||
}
|
||||
|
||||
return false; // dirty with backups: no veto
|
||||
}
|
||||
|
||||
protected async getWindowCount(): Promise<number> {
|
||||
return 1; // web: we only track 1 window, not multiple
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
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, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IRevertOptions, IEncodingSupport } 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';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
|
||||
@@ -24,9 +22,8 @@ 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 } from 'vs/platform/notification/common/notification';
|
||||
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
|
||||
import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } 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';
|
||||
@@ -35,7 +32,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
|
||||
@@ -61,16 +58,13 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
abstract get encoding(): IResourceEncodings;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
@ILifecycleService protected readonly lifecycleService: ILifecycleService,
|
||||
@IInstantiationService protected readonly instantiationService: IInstantiationService,
|
||||
@IModeService private readonly modeService: IModeService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IBackupFileService private readonly backupFileService: IBackupFileService,
|
||||
@IHistoryService private readonly historyService: IHistoryService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IFileDialogService private readonly fileDialogService: IFileDialogService,
|
||||
@@ -84,196 +78,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
protected registerListeners(): void {
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason)));
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
}
|
||||
|
||||
//#region shutdown / backup handling
|
||||
|
||||
protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
|
||||
|
||||
// Dirty files need treatment on shutdown
|
||||
const dirty = this.getDirty();
|
||||
if (dirty.length) {
|
||||
|
||||
// If auto save is enabled, save all files and then check again for dirty files
|
||||
// We DO NOT run any save participant if we are in the shutdown phase for performance reasons
|
||||
if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) {
|
||||
return this.saveAll(false /* files only */, { skipSaveParticipants: true }).then(() => {
|
||||
|
||||
// If we still have dirty files, we either have untitled ones or files that cannot be saved
|
||||
const remainingDirty = this.getDirty();
|
||||
if (remainingDirty.length) {
|
||||
return this.handleDirtyBeforeShutdown(remainingDirty, reason);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto save is not enabled
|
||||
return this.handleDirtyBeforeShutdown(dirty, reason);
|
||||
}
|
||||
|
||||
// No dirty files: no veto
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
private handleDirtyBeforeShutdown(dirty: URI[], reason: ShutdownReason): boolean | Promise<boolean> {
|
||||
|
||||
// If hot exit is enabled, backup dirty files and allow to exit without confirmation
|
||||
if (this.filesConfigurationService.isHotExitEnabled) {
|
||||
return this.backupBeforeShutdown(dirty, reason).then(didBackup => {
|
||||
if (didBackup) {
|
||||
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
|
||||
}
|
||||
|
||||
// since a backup did not happen, we have to confirm for the dirty files now
|
||||
return this.confirmBeforeShutdown();
|
||||
}, error => {
|
||||
this.notificationService.error(nls.localize('files.backup.failSave', "Files that are dirty could not be written to the backup location (Error: {0}). Try saving your files first and then exit.", error.message));
|
||||
|
||||
return true; // veto, the backups failed
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise just confirm from the user what to do with the dirty files
|
||||
return this.confirmBeforeShutdown();
|
||||
}
|
||||
|
||||
private async backupBeforeShutdown(dirtyToBackup: URI[], reason: ShutdownReason): Promise<boolean> {
|
||||
// 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.
|
||||
|
||||
let doBackup: boolean | undefined;
|
||||
switch (reason) {
|
||||
case ShutdownReason.CLOSE:
|
||||
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
|
||||
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
|
||||
} else if (await this.getWindowCount() > 1 || platform.isMacintosh) {
|
||||
doBackup = false; // do not backup if a window is closed that does not cause quitting of the application
|
||||
} else {
|
||||
doBackup = true; // backup if last window is closed on win/linux where the application quits right after
|
||||
}
|
||||
break;
|
||||
|
||||
case ShutdownReason.QUIT:
|
||||
doBackup = true; // backup because next start we restore all backups
|
||||
break;
|
||||
|
||||
case ShutdownReason.RELOAD:
|
||||
doBackup = 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) {
|
||||
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
|
||||
} else {
|
||||
doBackup = false; // do not backup because we are switching contexts
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!doBackup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.backupAll(dirtyToBackup);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract getWindowCount(): Promise<number>;
|
||||
|
||||
private backupAll(dirtyToBackup: URI[]): Promise<void> {
|
||||
|
||||
// split up between files and untitled
|
||||
const filesToBackup: ITextFileEditorModel[] = [];
|
||||
const untitledToBackup: URI[] = [];
|
||||
dirtyToBackup.forEach(dirty => {
|
||||
if (this.fileService.canHandleResource(dirty)) {
|
||||
const model = this.models.get(dirty);
|
||||
if (model) {
|
||||
filesToBackup.push(model);
|
||||
}
|
||||
} else if (dirty.scheme === Schemas.untitled) {
|
||||
untitledToBackup.push(dirty);
|
||||
}
|
||||
});
|
||||
|
||||
return this.doBackupAll(filesToBackup, untitledToBackup);
|
||||
}
|
||||
|
||||
private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise<void> {
|
||||
|
||||
// Handle file resources first
|
||||
await Promise.all(dirtyFileModels.map(model => model.backup()));
|
||||
|
||||
// Handle untitled resources
|
||||
await Promise.all(untitledResources
|
||||
.filter(untitled => this.untitledTextEditorService.exists(untitled))
|
||||
.map(async untitled => (await this.untitledTextEditorService.createOrGet({ resource: untitled }).resolve()).backup()));
|
||||
}
|
||||
|
||||
private async confirmBeforeShutdown(): Promise<boolean> {
|
||||
const confirm = await this.fileDialogService.showSaveConfirm(this.getDirty());
|
||||
|
||||
// Save
|
||||
if (confirm === ConfirmResult.SAVE) {
|
||||
const result = await this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true });
|
||||
|
||||
if (result.results.some(r => r.error)) {
|
||||
return true; // veto if some saves failed
|
||||
}
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
// Don't Save
|
||||
else if (confirm === ConfirmResult.DONT_SAVE) {
|
||||
|
||||
// Make sure to revert untitled so that they do not restore
|
||||
// see https://github.com/Microsoft/vscode/issues/29572
|
||||
this.untitledTextEditorService.revertAll();
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
// Cancel
|
||||
else if (confirm === ConfirmResult.CANCEL) {
|
||||
return true; // veto
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise<boolean> {
|
||||
if (!options.cleanUpBackups) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.lifecycleService.phase < LifecyclePhase.Restored) {
|
||||
return false; // if editors have not restored, we are not up to speed with backups and thus should not clean them
|
||||
}
|
||||
|
||||
return this.cleanupBackupsBeforeShutdown().then(() => false, () => false);
|
||||
}
|
||||
|
||||
protected async cleanupBackupsBeforeShutdown(): Promise<void> {
|
||||
if (this.environmentService.isExtensionDevelopment) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.backupFileService.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region text file IO primitives (read, create, move, delete, update)
|
||||
|
||||
async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
|
||||
@@ -903,6 +713,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region dirty
|
||||
|
||||
getDirty(resources?: URI[]): URI[] {
|
||||
|
||||
// Collect files
|
||||
@@ -924,4 +736,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
// Check for dirty untitled
|
||||
return this.untitledTextEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -27,15 +27,12 @@ import { Readable } from 'stream';
|
||||
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { nodeReadableToString, streamToNodeReadable, nodeStreamToVSBufferReadable } from 'vs/base/node/stream';
|
||||
import { IElectronService } from 'vs/platform/electron/node/electron';
|
||||
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
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';
|
||||
@@ -46,7 +43,6 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
export class NativeTextFileService extends AbstractTextFileService {
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService contextService: IWorkspaceContextService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService,
|
||||
@@ -54,19 +50,16 @@ export class NativeTextFileService extends AbstractTextFileService {
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IBackupFileService backupFileService: IBackupFileService,
|
||||
@IHistoryService historyService: IHistoryService,
|
||||
@IDialogService dialogService: IDialogService,
|
||||
@IFileDialogService fileDialogService: IFileDialogService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IElectronService private readonly electronService: IElectronService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
|
||||
@ITextModelService textModelService: ITextModelService
|
||||
) {
|
||||
super(contextService, fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, notificationService, backupFileService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService, textModelService);
|
||||
super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService, textModelService);
|
||||
}
|
||||
|
||||
private _encoding: EncodingOracle | undefined;
|
||||
@@ -312,10 +305,6 @@ export class NativeTextFileService extends AbstractTextFileService {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getWindowCount(): Promise<number> {
|
||||
return this.electronService.getWindowCount();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IEncodingOverride {
|
||||
|
||||
@@ -47,7 +47,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
TextFileEditorModel.setSaveParticipant(null); // reset any set participant
|
||||
accessor.fileService.setContent(content);
|
||||
});
|
||||
|
||||
@@ -94,7 +94,6 @@ suite('Files - TextFileService i/o', () => {
|
||||
});
|
||||
|
||||
teardown(async () => {
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledTextEditorService.revertAll();
|
||||
|
||||
|
||||
@@ -4,24 +4,23 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
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 { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
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 { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
|
||||
import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files';
|
||||
import { 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
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';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor(
|
||||
@@ -38,16 +37,6 @@ class ServiceAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
class BeforeShutdownEventImpl implements BeforeShutdownEvent {
|
||||
|
||||
value: boolean | Promise<boolean> | undefined;
|
||||
reason = ShutdownReason.CLOSE;
|
||||
|
||||
veto(value: boolean | Promise<boolean>): void {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
suite('Files - TextFileService', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
@@ -63,87 +52,10 @@ suite('Files - TextFileService', () => {
|
||||
if (model) {
|
||||
model.dispose();
|
||||
}
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledTextEditorService.revertAll();
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - no veto', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).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);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.resource, model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL);
|
||||
|
||||
await model.load();
|
||||
model.textEditorModel!.setValue('foo');
|
||||
assert.equal(service.getDirty().length, 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);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.resource, model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE);
|
||||
accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } });
|
||||
|
||||
await model.load();
|
||||
model.textEditorModel!.setValue('foo');
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
const event = new BeforeShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
let veto = event.value;
|
||||
if (typeof veto === 'boolean') {
|
||||
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
|
||||
assert.ok(!veto);
|
||||
return;
|
||||
}
|
||||
|
||||
veto = await veto;
|
||||
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
|
||||
assert.ok(!veto);
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - save (hot.exit: off)', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.resource, model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
accessor.fileDialogService.setConfirmResult(ConfirmResult.SAVE);
|
||||
accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } });
|
||||
|
||||
await model.load();
|
||||
model.textEditorModel!.setValue('foo');
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
const event = new BeforeShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
const veto = await (<Promise<boolean>>event.value);
|
||||
assert.ok(!veto);
|
||||
assert.ok(!model.isDirty());
|
||||
});
|
||||
|
||||
test('isDirty/getDirty - files and untitled', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.resource, model);
|
||||
@@ -310,138 +222,4 @@ suite('Files - TextFileService', () => {
|
||||
sourceModel.dispose();
|
||||
targetModel.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<void> {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.resource, model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
// 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(service.getDirty().length, 1);
|
||||
const event = new BeforeShutdownEventImpl();
|
||||
event.reason = shutdownReason;
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
const veto = await (<Promise<boolean>>event.value);
|
||||
assert.ok(!service.cleanupBackupsBeforeShutdownCalled); // When hot exit is set, backups should never be cleaned since the confirm result is cancel
|
||||
assert.equal(veto, shouldVeto);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +48,6 @@ suite('Workbench - TextModelResolverService', () => {
|
||||
model.dispose();
|
||||
model = (undefined)!;
|
||||
}
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledTextEditorService.revertAll();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
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 } from 'vs/workbench/common/editor';
|
||||
import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
|
||||
export const enum WorkingCopyCapabilities {
|
||||
|
||||
@@ -48,6 +48,10 @@ export interface IWorkingCopy {
|
||||
|
||||
save(options?: ISaveOptions): Promise<boolean>;
|
||||
|
||||
revert(options?: IRevertOptions): Promise<boolean>;
|
||||
|
||||
hasBackup(): boolean;
|
||||
|
||||
backup(): Promise<void>;
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { TestWorkingCopyService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { ISaveOptions } from 'vs/workbench/common/editor';
|
||||
import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
|
||||
suite('WorkingCopyService', () => {
|
||||
|
||||
@@ -53,8 +53,16 @@ suite('WorkingCopyService', () => {
|
||||
return true;
|
||||
}
|
||||
|
||||
async revert(options?: IRevertOptions): Promise<boolean> {
|
||||
this.setDirty(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async backup(): Promise<void> { }
|
||||
|
||||
hasBackup(): boolean { return false; }
|
||||
|
||||
dispose(): void {
|
||||
this._onDispose.fire();
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ suite('MainThreadSaveParticipant', function () {
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
TextFileEditorModel.setSaveParticipant(null); // reset any set participant
|
||||
});
|
||||
|
||||
|
||||
@@ -190,13 +190,10 @@ export class TestContextService implements IWorkspaceContextService {
|
||||
}
|
||||
|
||||
export class TestTextFileService extends NativeTextFileService {
|
||||
cleanupBackupsBeforeShutdownCalled!: boolean;
|
||||
|
||||
private promptPath!: URI;
|
||||
private resolveTextContentError!: FileOperationError | null;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService contextService: IWorkspaceContextService,
|
||||
@IFileService protected fileService: IFileService,
|
||||
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService,
|
||||
@@ -204,20 +201,16 @@ export class TestTextFileService extends NativeTextFileService {
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IBackupFileService backupFileService: IBackupFileService,
|
||||
@IHistoryService historyService: IHistoryService,
|
||||
@IDialogService dialogService: IDialogService,
|
||||
@IFileDialogService fileDialogService: IFileDialogService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IElectronService electronService: IElectronService,
|
||||
@IProductService productService: IProductService,
|
||||
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
|
||||
@ITextModelService textModelService: ITextModelService
|
||||
) {
|
||||
super(
|
||||
contextService,
|
||||
fileService,
|
||||
untitledTextEditorService,
|
||||
lifecycleService,
|
||||
@@ -225,14 +218,11 @@ export class TestTextFileService extends NativeTextFileService {
|
||||
modeService,
|
||||
modelService,
|
||||
environmentService,
|
||||
notificationService,
|
||||
backupFileService,
|
||||
historyService,
|
||||
dialogService,
|
||||
fileDialogService,
|
||||
editorService,
|
||||
textResourceConfigurationService,
|
||||
electronService,
|
||||
productService,
|
||||
filesConfigurationService,
|
||||
textModelService
|
||||
@@ -271,11 +261,6 @@ export class TestTextFileService extends NativeTextFileService {
|
||||
promptForPath(_resource: URI, _defaultPath: URI): Promise<URI> {
|
||||
return Promise.resolve(this.promptPath);
|
||||
}
|
||||
|
||||
protected cleanupBackupsBeforeShutdown(): Promise<void> {
|
||||
this.cleanupBackupsBeforeShutdownCalled = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITestInstantiationService extends IInstantiationService {
|
||||
@@ -1225,7 +1210,11 @@ export class TestBackupFileService implements IBackupFileService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
didDiscardAllWorkspaceBackups = false;
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.didDiscardAllWorkspaceBackups = true;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ import 'vs/workbench/contrib/splash/electron-browser/partsSplash.contribution';
|
||||
import 'vs/workbench/contrib/files/electron-browser/files.contribution';
|
||||
import 'vs/workbench/contrib/files/electron-browser/fileActions.contribution';
|
||||
|
||||
// Backup
|
||||
import 'vs/workbench/contrib/backup/electron-browser/backup.contribution';
|
||||
|
||||
// Debug
|
||||
import 'vs/workbench/contrib/debug/node/debugHelperService';
|
||||
import 'vs/workbench/contrib/debug/electron-browser/extensionHostDebugService';
|
||||
|
||||
@@ -91,6 +91,9 @@ registerSingleton(IUserDataSyncService, UserDataSyncService);
|
||||
// Explorer
|
||||
import 'vs/workbench/contrib/files/browser/files.web.contribution';
|
||||
|
||||
// Backup
|
||||
import 'vs/workbench/contrib/backup/browser/backup.web.contribution';
|
||||
|
||||
// Preferences
|
||||
import 'vs/workbench/contrib/preferences/browser/keyboardLayoutPicker';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user