Skip to content

Commit 16125bc

Browse files
author
Benjamin Pasero
committed
debt - add a test for restoring dirty files from hot exit
1 parent f4fc810 commit 16125bc

11 files changed

Lines changed: 598 additions & 402 deletions

File tree

src/vs/workbench/browser/editor.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
99
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
1010
import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation';
1111
import { find } from 'vs/base/common/arrays';
12+
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
1213

1314
export interface IEditorDescriptor {
1415
instantiate(instantiationService: IInstantiationService): BaseEditor;
@@ -30,7 +31,7 @@ export interface IEditorRegistry {
3031
* @param inputDescriptors A set of constructor functions that return an instance of EditorInput for which the
3132
* registered editor should be used for.
3233
*/
33-
registerEditor(descriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor<EditorInput>[]): void;
34+
registerEditor(descriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor<EditorInput>[]): IDisposable;
3435

3536
/**
3637
* Returns the editor descriptor for the given input or `undefined` if none.
@@ -54,7 +55,7 @@ export interface IEditorRegistry {
5455
*/
5556
export class EditorDescriptor implements IEditorDescriptor {
5657

57-
public static create<Services extends BrandedService[]>(
58+
static create<Services extends BrandedService[]>(
5859
ctor: { new(...services: Services): BaseEditor },
5960
id: string,
6061
name: string
@@ -87,14 +88,22 @@ export class EditorDescriptor implements IEditorDescriptor {
8788

8889
class EditorRegistry implements IEditorRegistry {
8990

90-
private editors: EditorDescriptor[] = [];
91+
private readonly editors: EditorDescriptor[] = [];
9192
private readonly mapEditorToInputs = new Map<EditorDescriptor, readonly SyncDescriptor<EditorInput>[]>();
9293

93-
registerEditor(descriptor: EditorDescriptor, inputDescriptors: readonly SyncDescriptor<EditorInput>[]): void {
94-
// Register (Support multiple Editors per Input)
94+
registerEditor(descriptor: EditorDescriptor, inputDescriptors: readonly SyncDescriptor<EditorInput>[]): IDisposable {
9595
this.mapEditorToInputs.set(descriptor, inputDescriptors);
9696

9797
this.editors.push(descriptor);
98+
99+
return toDisposable(() => {
100+
this.mapEditorToInputs.delete(descriptor);
101+
102+
const index = this.editors.indexOf(descriptor);
103+
if (index !== -1) {
104+
this.editors.splice(index, 1);
105+
}
106+
});
98107
}
99108

100109
getEditor(input: EditorInput): EditorDescriptor | undefined {
@@ -156,10 +165,6 @@ class EditorRegistry implements IEditorRegistry {
156165
return this.editors.slice(0);
157166
}
158167

159-
setEditors(editorsToSet: EditorDescriptor[]): void {
160-
this.editors = editorsToSet;
161-
}
162-
163168
getEditorInputs(): SyncDescriptor<EditorInput>[] {
164169
const inputClasses: SyncDescriptor<EditorInput>[] = [];
165170
for (const editor of this.editors) {

src/vs/workbench/common/editor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Event, Emitter } from 'vs/base/common/event';
77
import { assign } from 'vs/base/common/objects';
88
import { isUndefinedOrNull, withNullAsUndefined, assertIsDefined } from 'vs/base/common/types';
99
import { URI } from 'vs/base/common/uri';
10-
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
10+
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
1111
import { IEditor as ICodeEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon';
1212
import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput, EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor';
1313
import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation';
@@ -181,7 +181,7 @@ export interface IEditorInputFactoryRegistry {
181181
* @param editorInputId the identifier of the editor input
182182
* @param factory the editor input factory for serialization/deserialization
183183
*/
184-
registerEditorInputFactory<Services extends BrandedService[]>(editorInputId: string, ctor: { new(...Services: Services): IEditorInputFactory }): void;
184+
registerEditorInputFactory<Services extends BrandedService[]>(editorInputId: string, ctor: { new(...Services: Services): IEditorInputFactory }): IDisposable;
185185

186186
/**
187187
* Returns the editor input factory for the given editor input.
@@ -1232,6 +1232,7 @@ export interface IEditorMemento<T> {
12321232
class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry {
12331233
private instantiationService: IInstantiationService | undefined;
12341234
private fileInputFactory: IFileInputFactory | undefined;
1235+
12351236
private readonly editorInputFactoryConstructors: Map<string, IConstructorSignature0<IEditorInputFactory>> = new Map();
12361237
private readonly editorInputFactoryInstances: Map<string, IEditorInputFactory> = new Map();
12371238

@@ -1258,12 +1259,18 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry {
12581259
return assertIsDefined(this.fileInputFactory);
12591260
}
12601261

1261-
registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0<IEditorInputFactory>): void {
1262+
registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0<IEditorInputFactory>): IDisposable {
12621263
if (!this.instantiationService) {
12631264
this.editorInputFactoryConstructors.set(editorInputId, ctor);
12641265
} else {
12651266
this.createEditorInputFactory(editorInputId, ctor, this.instantiationService);
1267+
12661268
}
1269+
1270+
return toDisposable(() => {
1271+
this.editorInputFactoryConstructors.delete(editorInputId);
1272+
this.editorInputFactoryInstances.delete(editorInputId);
1273+
});
12671274
}
12681275

12691276
getEditorInputFactory(editorInputId: string): IEditorInputFactory | undefined {

src/vs/workbench/contrib/backup/common/backupRestorer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class BackupRestorer implements IWorkbenchContribution {
3131
this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreBackups());
3232
}
3333

34-
private async doRestoreBackups(): Promise<URI[] | undefined> {
34+
protected async doRestoreBackups(): Promise<URI[] | undefined> {
3535

3636
// Find all files and untitled with backups
3737
const backups = await this.backupFileService.getWorkspaceFileBackups();
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import * as platform from 'vs/base/common/platform';
8+
import * as os from 'os';
9+
import * as path from 'vs/base/common/path';
10+
import * as pfs from 'vs/base/node/pfs';
11+
import { URI } from 'vs/base/common/uri';
12+
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
13+
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
14+
import { DefaultEndOfLine } from 'vs/editor/common/model';
15+
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
16+
import { hashPath } from 'vs/workbench/services/backup/node/backupFileService';
17+
import { BackupModelTracker } from 'vs/workbench/contrib/backup/common/backupModelTracker';
18+
import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
19+
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
20+
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
21+
import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer';
22+
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
23+
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
24+
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
25+
import { EditorService } from 'vs/workbench/services/editor/browser/editorService';
26+
import { Registry } from 'vs/platform/registry/common/platform';
27+
import { EditorInput } from 'vs/workbench/common/editor';
28+
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
29+
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
30+
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
31+
import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor';
32+
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
33+
import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test';
34+
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
35+
import { Schemas } from 'vs/base/common/network';
36+
import { isEqual } from 'vs/base/common/resources';
37+
38+
const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer');
39+
const backupHome = path.join(userdataDir, 'Backups');
40+
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
41+
42+
const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
43+
const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource));
44+
const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo');
45+
const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
46+
const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
47+
const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' });
48+
49+
class TestBackupRestorer extends BackupRestorer {
50+
async doRestoreBackups(): Promise<URI[] | undefined> {
51+
return super.doRestoreBackups();
52+
}
53+
}
54+
55+
class ServiceAccessor {
56+
constructor(
57+
@ITextFileService public textFileService: TestTextFileService,
58+
@IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService
59+
) {
60+
}
61+
}
62+
63+
suite('BackupModelRestorer', () => {
64+
let accessor: ServiceAccessor;
65+
66+
let disposables: IDisposable[] = [];
67+
68+
setup(async () => {
69+
disposables.push(Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
70+
EditorDescriptor.create(
71+
TextFileEditor,
72+
TextFileEditor.ID,
73+
'Text File Editor'
74+
),
75+
[new SyncDescriptor<EditorInput>(FileEditorInput)]
76+
));
77+
78+
// Delete any existing backups completely and then re-create it.
79+
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
80+
await pfs.mkdirp(backupHome);
81+
82+
return pfs.writeFile(workspacesJsonPath, '');
83+
});
84+
85+
teardown(async () => {
86+
dispose(disposables);
87+
disposables = [];
88+
89+
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
90+
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
91+
accessor.untitledTextEditorService.revertAll();
92+
93+
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
94+
});
95+
96+
test('Restore backups', async () => {
97+
const backupFileService = new NodeTestBackupFileService(workspaceBackupPath);
98+
const instantiationService = workbenchInstantiationService();
99+
instantiationService.stub(IBackupFileService, backupFileService);
100+
101+
const part = instantiationService.createInstance(EditorPart);
102+
part.create(document.createElement('div'));
103+
part.layout(400, 300);
104+
105+
instantiationService.stub(IEditorGroupsService, part);
106+
107+
const editorService: EditorService = instantiationService.createInstance(EditorService);
108+
instantiationService.stub(IEditorService, editorService);
109+
110+
accessor = instantiationService.createInstance(ServiceAccessor);
111+
112+
const tracker = instantiationService.createInstance(BackupModelTracker);
113+
const restorer = instantiationService.createInstance(TestBackupRestorer);
114+
115+
// Backup 2 normal files and 2 untitled file
116+
await backupFileService.backupResource(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).createSnapshot(false));
117+
await backupFileService.backupResource(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).createSnapshot(false));
118+
await backupFileService.backupResource(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).createSnapshot(false));
119+
await backupFileService.backupResource(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).createSnapshot(false));
120+
121+
// Verify backups restored and opened as dirty
122+
await restorer.doRestoreBackups();
123+
assert.equal(editorService.editors.length, 4);
124+
assert.ok(editorService.editors.every(editor => editor.isDirty()));
125+
126+
let counter = 0;
127+
for (const editor of editorService.editors) {
128+
const resource = editor.getResource();
129+
if (isEqual(resource, untitledFile1)) {
130+
const model = await accessor.untitledTextEditorService.createOrGet(resource).resolve();
131+
assert.equal(model.textEditorModel.getValue(), 'untitled-1');
132+
counter++;
133+
} else if (isEqual(resource, untitledFile2)) {
134+
const model = await accessor.untitledTextEditorService.createOrGet(resource).resolve();
135+
assert.equal(model.textEditorModel.getValue(), 'untitled-2');
136+
counter++;
137+
} else if (isEqual(resource, fooFile)) {
138+
const model = await accessor.textFileService.models.get(resource!)?.load();
139+
assert.equal(model?.textEditorModel?.getValue(), 'fooFile');
140+
counter++;
141+
} else {
142+
const model = await accessor.textFileService.models.get(resource!)?.load();
143+
assert.equal(model?.textEditorModel?.getValue(), 'barFile');
144+
counter++;
145+
}
146+
}
147+
148+
assert.equal(counter, 4);
149+
150+
part.dispose();
151+
tracker.dispose();
152+
});
153+
});

src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,13 @@ class TestBackupEnvironmentService extends NativeWorkbenchEnvironmentService {
5151
constructor(backupPath: string) {
5252
super({ ...parseArgs(process.argv, OPTIONS), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath, 0);
5353
}
54-
5554
}
5655

57-
class TestBackupFileService extends BackupFileService {
56+
export class NodeTestBackupFileService extends BackupFileService {
5857

5958
readonly fileService: IFileService;
6059

61-
constructor(workspace: URI, backupHome: string, workspacesJsonPath: string) {
60+
constructor(workspaceBackupPath: string) {
6261
const environmentService = new TestBackupEnvironmentService(workspaceBackupPath);
6362
const fileService = new FileService(new NullLogService());
6463
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
@@ -76,10 +75,10 @@ class TestBackupFileService extends BackupFileService {
7675
}
7776

7877
suite('BackupFileService', () => {
79-
let service: TestBackupFileService;
78+
let service: NodeTestBackupFileService;
8079

8180
setup(async () => {
82-
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
81+
service = new NodeTestBackupFileService(workspaceBackupPath);
8382

8483
// Delete any existing backups completely and then re-create it.
8584
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
@@ -141,7 +140,7 @@ suite('BackupFileService', () => {
141140
test('should return whether a backup resource exists', async () => {
142141
await pfs.mkdirp(path.dirname(fooBackupPath));
143142
fs.writeFileSync(fooBackupPath, 'foo');
144-
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
143+
service = new NodeTestBackupFileService(workspaceBackupPath);
145144
const resource = await service.loadBackupResource(fooFile);
146145
assert.ok(resource);
147146
assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath));
@@ -528,10 +527,10 @@ suite('BackupFileService', () => {
528527

529528
suite('BackupFilesModel', () => {
530529

531-
let service: TestBackupFileService;
530+
let service: NodeTestBackupFileService;
532531

533532
setup(async () => {
534-
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
533+
service = new NodeTestBackupFileService(workspaceBackupPath);
535534

536535
// Delete any existing backups completely and then re-create it.
537536
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);

src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil
1919
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
2020
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
2121
import { CancellationToken } from 'vs/base/common/cancellation';
22+
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
23+
24+
const TEST_EDITOR_ID = 'MyFileEditorForEditorGroupService';
25+
const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorGroupService';
2226

2327
class TestEditorControl extends BaseEditor {
2428

25-
constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyFileEditorForEditorGroupService', NullTelemetryService, new TestThemeService(), new TestStorageService()); }
29+
constructor(@ITelemetryService telemetryService: ITelemetryService) { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); }
2630

2731
async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
2832
super.setInput(input, options, token);
2933

3034
await input.resolve();
3135
}
3236

33-
getId(): string { return 'MyFileEditorForEditorGroupService'; }
37+
getId(): string { return TEST_EDITOR_ID; }
3438
layout(): void { }
3539
createEditor(): any { }
3640
}
@@ -39,7 +43,7 @@ class TestEditorInput extends EditorInput implements IFileEditorInput {
3943

4044
constructor(private resource: URI) { super(); }
4145

42-
getTypeId() { return 'testEditorInputForEditorGroupService'; }
46+
getTypeId() { return TEST_EDITOR_INPUT_ID; }
4347
resolve(): Promise<IEditorModel | null> { return Promise.resolve(null); }
4448
matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; }
4549
setEncoding(encoding: string) { }
@@ -53,8 +57,9 @@ class TestEditorInput extends EditorInput implements IFileEditorInput {
5357

5458
suite('EditorGroupsService', () => {
5559

56-
function registerTestEditorInput(): void {
60+
let disposables: IDisposable[] = [];
5761

62+
setup(() => {
5863
interface ISerializedTestEditorInput {
5964
resource: string;
6065
}
@@ -81,11 +86,14 @@ suite('EditorGroupsService', () => {
8186
}
8287
}
8388

84-
(Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories)).registerEditorInputFactory('testEditorInputForGroupsService', TestEditorInputFactory);
85-
(Registry.as<IEditorRegistry>(Extensions.Editors)).registerEditor(EditorDescriptor.create(TestEditorControl, 'MyTestEditorForGroupsService', 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)]);
86-
}
89+
disposables.push((Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories)).registerEditorInputFactory(TEST_EDITOR_INPUT_ID, TestEditorInputFactory));
90+
disposables.push((Registry.as<IEditorRegistry>(Extensions.Editors)).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)]));
91+
});
8792

88-
registerTestEditorInput();
93+
teardown(() => {
94+
dispose(disposables);
95+
disposables = [];
96+
});
8997

9098
function createPart(): EditorPart {
9199
const instantiationService = workbenchInstantiationService();

0 commit comments

Comments
 (0)