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