Skip to content

Commit eeb9632

Browse files
authored
model / working copy - go through managers when resolving again (microsoft#146504)
1 parent 7889866 commit eeb9632

File tree

10 files changed

+100
-63
lines changed

10 files changed

+100
-63
lines changed

src/vs/workbench/services/textfile/common/textFileEditorModel.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
247247
const softUndo = options?.soft;
248248
if (!softUndo) {
249249
try {
250-
await this.resolve({ forceReadFromFile: true });
250+
await this.forceResolveFromFile();
251251
} catch (error) {
252252

253253
// FileNotFound means the file got deleted meanwhile, so ignore it
@@ -636,6 +636,23 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
636636
}
637637
}
638638

639+
private async forceResolveFromFile(): Promise<void> {
640+
if (this.isDisposed()) {
641+
return; // return early when the model is invalid
642+
}
643+
644+
// We go through the text file service to make
645+
// sure this kind of `resolve` is properly
646+
// running in sequence with any other running
647+
// `resolve` if any, including subsequent runs
648+
// that are triggered right after.
649+
650+
await this.textFileService.files.resolve(this.resource, {
651+
reload: { async: false },
652+
forceReadFromFile: true
653+
});
654+
}
655+
639656
//#endregion
640657

641658
//#region Dirty
@@ -1004,10 +1021,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
10041021
//
10051022
// (see https://github.com/microsoft/vscode/issues/127936)
10061023

1007-
if (this.hasLanguageSetExplicitly) {
1008-
return; // return early when the language was changed by the user
1009-
}
1010-
10111024
if (!this.configurationService.inspect('files.encoding').overrideIdentifiers?.includes(e.newLanguage)) {
10121025
return; // only when there is a language specific override for the new language
10131026
}
@@ -1021,6 +1034,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
10211034
return; // return early to prevent accident saves in this case
10221035
}
10231036

1037+
this.logService.info(`Adjusting encoding based on configured language override to '${encoding}' for ${this.resource.toString(true)}.`);
1038+
10241039
// Re-open with new encoding
10251040
await this.setEncoding(encoding, EncodingMode.Decode);
10261041
}
@@ -1058,9 +1073,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
10581073

10591074
this.updatePreferredEncoding(encoding);
10601075

1061-
await this.resolve({
1062-
forceReadFromFile: true // because encoding has changed
1063-
});
1076+
await this.forceResolveFromFile();
10641077
}
10651078
}
10661079

src/vs/workbench/services/textfile/common/textfiles.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,7 @@ export interface ITextFileStreamContent extends IBaseTextFileContent {
266266
readonly value: ITextBufferFactory;
267267
}
268268

269-
export interface ITextFileEditorModelResolveOrCreateOptions {
270-
271-
/**
272-
* Context why the model is being resolved or created.
273-
*/
274-
readonly reason?: TextFileResolveReason;
269+
export interface ITextFileEditorModelResolveOrCreateOptions extends ITextFileResolveOptions {
275270

276271
/**
277272
* The language id to use for the model text content.
@@ -283,13 +278,6 @@ export interface ITextFileEditorModelResolveOrCreateOptions {
283278
*/
284279
readonly encoding?: string;
285280

286-
/**
287-
* The contents to use for the model if known. If not
288-
* provided, the contents will be retrieved from the
289-
* underlying resource or backup if present.
290-
*/
291-
readonly contents?: ITextBufferFactory;
292-
293281
/**
294282
* If the model was already resolved before, allows to trigger
295283
* a reload of it to fetch the latest contents.
@@ -302,11 +290,6 @@ export interface ITextFileEditorModelResolveOrCreateOptions {
302290
*/
303291
readonly async: boolean;
304292
};
305-
306-
/**
307-
* Allow to resolve a model even if we think it is a binary file.
308-
*/
309-
readonly allowBinary?: boolean;
310293
}
311294

312295
export interface ITextFileSaveEvent extends ITextFileEditorModelSaveEvent {

src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ suite('Files - TextFileEditorModel', () => {
4949

5050
test('basic events', async function () {
5151
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
52+
accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise
5253

5354
let onDidResolveCounter = 0;
5455
model.onDidResolve(() => onDidResolveCounter++);
@@ -300,16 +301,24 @@ suite('Files - TextFileEditorModel', () => {
300301
});
301302

302303
test('setEncoding - decode', async function () {
303-
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
304+
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
305+
accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise
304306

305307
await model.setEncoding('utf16', EncodingMode.Decode);
306308

309+
// we have to get the model again from working copy service
310+
// because `setEncoding` will resolve it again through the
311+
// text file service which is outside our scope
312+
model = accessor.workingCopyService.get(model) as TextFileEditorModel;
313+
307314
assert.ok(model.isResolved()); // model got resolved due to decoding
308315
model.dispose();
309316
});
310317

311318
test('setEncoding - decode dirty file saves first', async function () {
312319
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
320+
accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise
321+
313322
await model.resolve();
314323

315324
model.updateTextEditorModel(createTextBufferFactory('bar'));
@@ -397,7 +406,7 @@ suite('Files - TextFileEditorModel', () => {
397406
test('Revert', async function () {
398407
let eventCounter = 0;
399408

400-
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
409+
let model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
401410

402411
model.onDidRevert(() => eventCounter++);
403412

@@ -415,9 +424,16 @@ suite('Files - TextFileEditorModel', () => {
415424
assert.strictEqual(accessor.workingCopyService.dirtyCount, 1);
416425
assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true);
417426

427+
accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise
428+
418429
await model.revert();
430+
431+
// we have to get the model again from working copy service
432+
// because `setEncoding` will resolve it again through the
433+
// text file service which is outside our scope
434+
model = accessor.workingCopyService.get(model) as TextFileEditorModel;
435+
419436
assert.strictEqual(model.isDirty(), false);
420-
assert.strictEqual(model.textEditorModel!.getValue(), 'Hello Html');
421437
assert.strictEqual(eventCounter, 1);
422438

423439
assert.ok(workingCopyEvent);

src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import * as assert from 'assert';
77
import { URI } from 'vs/base/common/uri';
88
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
9-
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
109
import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices';
1110
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
1211
import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
@@ -34,7 +33,7 @@ suite('Files - TextFileEditorModelManager', () => {
3433
});
3534

3635
test('add, remove, clear, get, getAll', function () {
37-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
36+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
3837

3938
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined);
4039
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined);
@@ -91,7 +90,7 @@ suite('Files - TextFileEditorModelManager', () => {
9190
});
9291

9392
test('resolve', async () => {
94-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
93+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
9594
const resource = URI.file('/test.html');
9695
const encoding = 'utf8';
9796

@@ -131,7 +130,7 @@ suite('Files - TextFileEditorModelManager', () => {
131130
});
132131

133132
test('resolve (async)', async () => {
134-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
133+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
135134
const resource = URI.file('/path/index.txt');
136135

137136
await manager.resolve(resource);
@@ -154,7 +153,7 @@ suite('Files - TextFileEditorModelManager', () => {
154153
});
155154

156155
test('resolve (sync)', async () => {
157-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
156+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
158157
const resource = URI.file('/path/index.txt');
159158

160159
await manager.resolve(resource);
@@ -171,7 +170,7 @@ suite('Files - TextFileEditorModelManager', () => {
171170
});
172171

173172
test('resolve (sync) - model disposed when error and first call to resolve', async () => {
174-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
173+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
175174
const resource = URI.file('/path/index.txt');
176175

177176
accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('fail', FileOperationResult.FILE_OTHER_ERROR));
@@ -188,7 +187,7 @@ suite('Files - TextFileEditorModelManager', () => {
188187
});
189188

190189
test('resolve (sync) - model not disposed when error and model existed before', async () => {
191-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
190+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
192191
const resource = URI.file('/path/index.txt');
193192

194193
await manager.resolve(resource);
@@ -207,7 +206,7 @@ suite('Files - TextFileEditorModelManager', () => {
207206
});
208207

209208
test('resolve with initial contents', async () => {
210-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
209+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
211210
const resource = URI.file('/test.html');
212211

213212
const model = await manager.resolve(resource, { contents: createTextBufferFactory('Hello World') });
@@ -223,7 +222,7 @@ suite('Files - TextFileEditorModelManager', () => {
223222
});
224223

225224
test('multiple resolves execute in sequence', async () => {
226-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
225+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
227226
const resource = URI.file('/test.html');
228227

229228
let resolvedModel: unknown;
@@ -257,7 +256,7 @@ suite('Files - TextFileEditorModelManager', () => {
257256
});
258257

259258
test('removed from cache when model disposed', function () {
260-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
259+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
261260

262261
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined);
263262
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined);
@@ -279,7 +278,7 @@ suite('Files - TextFileEditorModelManager', () => {
279278
});
280279

281280
test('events', async function () {
282-
const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager);
281+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
283282

284283
const resource1 = toResource.call(this, '/path/index.txt');
285284
const resource2 = toResource.call(this, '/path/other.txt');
@@ -368,7 +367,7 @@ suite('Files - TextFileEditorModelManager', () => {
368367
});
369368

370369
test('disposing model takes it out of the manager', async function () {
371-
const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager);
370+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
372371

373372
const resource = toResource.call(this, '/path/index_something.txt');
374373

@@ -380,7 +379,7 @@ suite('Files - TextFileEditorModelManager', () => {
380379
});
381380

382381
test('canDispose with dirty model', async function () {
383-
const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager);
382+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
384383

385384
const resource = toResource.call(this, '/path/index_something.txt');
386385

@@ -414,7 +413,7 @@ suite('Files - TextFileEditorModelManager', () => {
414413
id: languageId,
415414
});
416415

417-
const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager);
416+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
418417

419418
const resource = toResource.call(this, '/path/index_something.txt');
420419

@@ -429,7 +428,7 @@ suite('Files - TextFileEditorModelManager', () => {
429428
});
430429

431430
test('file change events trigger reload (on a resolved model)', async () => {
432-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
431+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
433432
const resource = URI.file('/path/index.txt');
434433

435434
await manager.resolve(resource);
@@ -451,7 +450,7 @@ suite('Files - TextFileEditorModelManager', () => {
451450
});
452451

453452
test('file change events trigger reload (after a model is resolved: https://github.com/microsoft/vscode/issues/132765)', async () => {
454-
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
453+
const manager = accessor.textFileService.files as TestTextFileEditorModelManager;
455454
const resource = URI.file('/path/index.txt');
456455

457456
manager.resolve(resource);

src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,16 @@ export interface IStoredFileWorkingCopySaveOptions extends ISaveOptions {
226226
readonly ignoreErrorHandler?: boolean;
227227
}
228228

229+
export interface IStoredFileWorkingCopyResolver {
230+
231+
/**
232+
* Resolves the working copy in a safe way from an external
233+
* working copy manager that can make sure multiple parallel
234+
* resolves execute properly.
235+
*/
236+
(options?: IStoredFileWorkingCopyResolveOptions): Promise<void>;
237+
}
238+
229239
export interface IStoredFileWorkingCopyResolveOptions {
230240

231241
/**
@@ -306,6 +316,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
306316
resource: URI,
307317
readonly name: string,
308318
private readonly modelFactory: IStoredFileWorkingCopyModelFactory<M>,
319+
private readonly externalResolver: IStoredFileWorkingCopyResolver,
309320
@IFileService fileService: IFileService,
310321
@ILogService private readonly logService: ILogService,
311322
@IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService,
@@ -716,6 +727,22 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
716727
this._onDidChangeContent.fire();
717728
}
718729

730+
private async forceResolveFromFile(): Promise<void> {
731+
if (this.isDisposed()) {
732+
return; // return early when the working copy is invalid
733+
}
734+
735+
// We go through the resolver to make
736+
// sure this kind of `resolve` is properly
737+
// running in sequence with any other running
738+
// `resolve` if any, including subsequent runs
739+
// that are triggered right after.
740+
741+
await this.externalResolver({
742+
forceReadFromFile: true
743+
});
744+
}
745+
719746
//#endregion
720747

721748
//#region Backup
@@ -1132,7 +1159,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
11321159
const softUndo = options?.soft;
11331160
if (!softUndo) {
11341161
try {
1135-
await this.resolve({ forceReadFromFile: true });
1162+
await this.forceResolveFromFile();
11361163
} catch (error) {
11371164

11381165
// FileNotFound means the file got deleted meanwhile, so ignore it

src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,7 @@ export interface IStoredFileWorkingCopySaveEvent<M extends IStoredFileWorkingCop
112112
readonly workingCopy: IStoredFileWorkingCopy<M>;
113113
}
114114

115-
export interface IStoredFileWorkingCopyManagerResolveOptions {
116-
117-
/**
118-
* The contents to use for the stored file working copy if known. If not
119-
* provided, the contents will be retrieved from the underlying
120-
* resource or backup if present.
121-
*
122-
* If contents are provided, the stored file working copy will be marked
123-
* as dirty right from the beginning.
124-
*/
125-
readonly contents?: VSBufferReadableStream;
115+
export interface IStoredFileWorkingCopyManagerResolveOptions extends IStoredFileWorkingCopyResolveOptions {
126116

127117
/**
128118
* If the stored file working copy was already resolved before,
@@ -529,6 +519,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel>
529519
resource,
530520
this.labelService.getUriBasenameLabel(resource),
531521
this.modelFactory,
522+
async options => { await this.resolve(resource, { ...options, reload: { async: false } }); },
532523
this.fileService, this.logService, this.workingCopyFileService, this.filesConfigurationService,
533524
this.workingCopyBackupService, this.workingCopyService, this.notificationService, this.workingCopyEditorService,
534525
this.editorService, this.elevatedFileService

0 commit comments

Comments
 (0)