Skip to content

Commit 675314d

Browse files
authored
working copy - allow to custom save method on model (microsoft#172345) (microsoft#185963)
1 parent 03d7160 commit 675314d

File tree

2 files changed

+147
-22
lines changed

2 files changed

+147
-22
lines changed

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

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ export interface IStoredFileWorkingCopyModel extends IFileWorkingCopyModel {
6969
* to the state before saving.
7070
*/
7171
pushStackElement(): void;
72+
73+
/**
74+
* Optionally allows a stored file working copy model to
75+
* implement the `save` method. This allows to implement
76+
* a more efficient save logic compared to the default
77+
* which is to ask the model for a `snapshot` and then
78+
* writing that to the model's resource.
79+
*/
80+
save?(options: IWriteFileOptions, token: CancellationToken): Promise<IFileStatWithMetadata>;
7281
}
7382

7483
export interface IStoredFileWorkingCopyModelContentChangedEvent {
@@ -974,34 +983,43 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
974983
const resolvedFileWorkingCopy = this;
975984
return this.saveSequentializer.setPending(versionId, (async () => {
976985
try {
977-
978-
// Snapshot working copy model contents
979-
const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token);
980-
981-
// It is possible that a subsequent save is cancelling this
982-
// running save. As such we return early when we detect that
983-
// However, we do not pass the token into the file service
984-
// because that is an atomic operation currently without
985-
// cancellation support, so we dispose the cancellation if
986-
// it was not cancelled yet.
987-
if (saveCancellation.token.isCancellationRequested) {
988-
return;
989-
} else {
990-
saveCancellation.dispose();
991-
}
992-
993986
const writeFileOptions: IWriteFileOptions = {
994987
mtime: lastResolvedFileStat.mtime,
995988
etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag,
996989
unlock: options.writeUnlock
997990
};
998991

999-
// Write them to disk
1000992
let stat: IFileStatWithMetadata;
1001-
if (options?.writeElevated && this.elevatedFileService.isSupported(lastResolvedFileStat.resource)) {
1002-
stat = await this.elevatedFileService.writeFileElevated(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
1003-
} else {
1004-
stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
993+
994+
// Delegate to working copy model save method if any
995+
if (typeof resolvedFileWorkingCopy.model.save === 'function') {
996+
stat = await resolvedFileWorkingCopy.model.save(writeFileOptions, saveCancellation.token);
997+
}
998+
999+
// Otherwise ask for a snapshot and save via file services
1000+
else {
1001+
1002+
// Snapshot working copy model contents
1003+
const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token);
1004+
1005+
// It is possible that a subsequent save is cancelling this
1006+
// running save. As such we return early when we detect that
1007+
// However, we do not pass the token into the file service
1008+
// because that is an atomic operation currently without
1009+
// cancellation support, so we dispose the cancellation if
1010+
// it was not cancelled yet.
1011+
if (saveCancellation.token.isCancellationRequested) {
1012+
return;
1013+
} else {
1014+
saveCancellation.dispose();
1015+
}
1016+
1017+
// Write them to disk
1018+
if (options?.writeElevated && this.elevatedFileService.isSupported(lastResolvedFileStat.resource)) {
1019+
stat = await this.elevatedFileService.writeFileElevated(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
1020+
} else {
1021+
stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
1022+
}
10051023
}
10061024

10071025
this.handleSaveSuccess(stat, versionId, options);

src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
1313
import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
1414
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1515
import { basename } from 'vs/base/common/resources';
16-
import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files';
16+
import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, IFileStatWithMetadata, IWriteFileOptions, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files';
1717
import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor';
1818
import { Promises, timeout } from 'vs/base/common/async';
1919
import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream';
@@ -82,13 +82,120 @@ export class TestStoredFileWorkingCopyModel extends Disposable implements IStore
8282
}
8383
}
8484

85+
export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFileWorkingCopyModel {
86+
87+
saveCounter = 0;
88+
throwOnSave = false;
89+
90+
async save(options: IWriteFileOptions, token: CancellationToken): Promise<IFileStatWithMetadata> {
91+
if (this.throwOnSave) {
92+
throw new Error('Fail');
93+
}
94+
95+
this.saveCounter++;
96+
97+
return {
98+
resource: this.resource,
99+
ctime: 0,
100+
etag: '',
101+
isDirectory: false,
102+
isFile: true,
103+
mtime: 0,
104+
name: 'resource2',
105+
size: 0,
106+
isSymbolicLink: false,
107+
readonly: false,
108+
locked: false,
109+
children: undefined
110+
};
111+
}
112+
}
113+
85114
export class TestStoredFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory<TestStoredFileWorkingCopyModel> {
86115

87116
async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise<TestStoredFileWorkingCopyModel> {
88117
return new TestStoredFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString());
89118
}
90119
}
91120

121+
export class TestStoredFileWorkingCopyModelWithCustomSaveFactory implements IStoredFileWorkingCopyModelFactory<TestStoredFileWorkingCopyModelWithCustomSave> {
122+
123+
async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise<TestStoredFileWorkingCopyModelWithCustomSave> {
124+
return new TestStoredFileWorkingCopyModelWithCustomSave(resource, (await streamToBuffer(contents)).toString());
125+
}
126+
}
127+
128+
suite('StoredFileWorkingCopy (with custom save)', function () {
129+
130+
const factory = new TestStoredFileWorkingCopyModelWithCustomSaveFactory();
131+
132+
let disposables: DisposableStore;
133+
const resource = URI.file('test/resource');
134+
let instantiationService: IInstantiationService;
135+
let accessor: TestServiceAccessor;
136+
let workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModelWithCustomSave>;
137+
138+
function createWorkingCopy(uri: URI = resource) {
139+
const workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModelWithCustomSave> = new StoredFileWorkingCopy<TestStoredFileWorkingCopyModelWithCustomSave>('testStoredFileWorkingCopyType', uri, basename(uri), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService);
140+
141+
return workingCopy;
142+
}
143+
144+
setup(() => {
145+
disposables = new DisposableStore();
146+
instantiationService = workbenchInstantiationService(undefined, disposables);
147+
accessor = instantiationService.createInstance(TestServiceAccessor);
148+
149+
workingCopy = createWorkingCopy();
150+
});
151+
152+
teardown(() => {
153+
workingCopy.dispose();
154+
disposables.dispose();
155+
});
156+
157+
test('save (custom implemented)', async () => {
158+
let savedCounter = 0;
159+
let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined;
160+
workingCopy.onDidSave(e => {
161+
savedCounter++;
162+
lastSaveEvent = e;
163+
});
164+
165+
let saveErrorCounter = 0;
166+
workingCopy.onDidSaveError(() => {
167+
saveErrorCounter++;
168+
});
169+
170+
// unresolved
171+
await workingCopy.save();
172+
assert.strictEqual(savedCounter, 0);
173+
assert.strictEqual(saveErrorCounter, 0);
174+
175+
// simple
176+
await workingCopy.resolve();
177+
workingCopy.model?.updateContents('hello save');
178+
await workingCopy.save();
179+
180+
assert.strictEqual(savedCounter, 1);
181+
assert.strictEqual(saveErrorCounter, 0);
182+
assert.strictEqual(workingCopy.isDirty(), false);
183+
assert.strictEqual(lastSaveEvent!.reason, SaveReason.EXPLICIT);
184+
assert.ok(lastSaveEvent!.stat);
185+
assert.ok(isStoredFileWorkingCopySaveEvent(lastSaveEvent!));
186+
assert.strictEqual(workingCopy.model?.pushedStackElement, true);
187+
assert.strictEqual((workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).saveCounter, 1);
188+
189+
// error
190+
workingCopy.model?.updateContents('hello save error');
191+
(workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).throwOnSave = true;
192+
await workingCopy.save();
193+
194+
assert.strictEqual(saveErrorCounter, 1);
195+
assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true);
196+
});
197+
});
198+
92199
suite('StoredFileWorkingCopy', function () {
93200

94201
const factory = new TestStoredFileWorkingCopyModelFactory();

0 commit comments

Comments
 (0)