Skip to content

Commit

Permalink
working copy - allow to custom save method on model (#172345) (#185963
Browse files Browse the repository at this point in the history
)
  • Loading branch information
bpasero authored Jun 23, 2023
1 parent 03d7160 commit 675314d
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ export interface IStoredFileWorkingCopyModel extends IFileWorkingCopyModel {
* to the state before saving.
*/
pushStackElement(): void;

/**
* Optionally allows a stored file working copy model to
* implement the `save` method. This allows to implement
* a more efficient save logic compared to the default
* which is to ask the model for a `snapshot` and then
* writing that to the model's resource.
*/
save?(options: IWriteFileOptions, token: CancellationToken): Promise<IFileStatWithMetadata>;
}

export interface IStoredFileWorkingCopyModelContentChangedEvent {
Expand Down Expand Up @@ -974,34 +983,43 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
const resolvedFileWorkingCopy = this;
return this.saveSequentializer.setPending(versionId, (async () => {
try {

// Snapshot working copy model contents
const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token);

// It is possible that a subsequent save is cancelling this
// running save. As such we return early when we detect that
// However, we do not pass the token into the file service
// because that is an atomic operation currently without
// cancellation support, so we dispose the cancellation if
// it was not cancelled yet.
if (saveCancellation.token.isCancellationRequested) {
return;
} else {
saveCancellation.dispose();
}

const writeFileOptions: IWriteFileOptions = {
mtime: lastResolvedFileStat.mtime,
etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag,
unlock: options.writeUnlock
};

// Write them to disk
let stat: IFileStatWithMetadata;
if (options?.writeElevated && this.elevatedFileService.isSupported(lastResolvedFileStat.resource)) {
stat = await this.elevatedFileService.writeFileElevated(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
} else {
stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);

// Delegate to working copy model save method if any
if (typeof resolvedFileWorkingCopy.model.save === 'function') {
stat = await resolvedFileWorkingCopy.model.save(writeFileOptions, saveCancellation.token);
}

// Otherwise ask for a snapshot and save via file services
else {

// Snapshot working copy model contents
const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token);

// It is possible that a subsequent save is cancelling this
// running save. As such we return early when we detect that
// However, we do not pass the token into the file service
// because that is an atomic operation currently without
// cancellation support, so we dispose the cancellation if
// it was not cancelled yet.
if (saveCancellation.token.isCancellationRequested) {
return;
} else {
saveCancellation.dispose();
}

// Write them to disk
if (options?.writeElevated && this.elevatedFileService.isSupported(lastResolvedFileStat.resource)) {
stat = await this.elevatedFileService.writeFileElevated(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
} else {
stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), writeFileOptions);
}
}

this.handleSaveSuccess(stat, versionId, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { basename } from 'vs/base/common/resources';
import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files';
import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, IFileStatWithMetadata, IWriteFileOptions, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files';
import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor';
import { Promises, timeout } from 'vs/base/common/async';
import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream';
Expand Down Expand Up @@ -82,13 +82,120 @@ export class TestStoredFileWorkingCopyModel extends Disposable implements IStore
}
}

export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFileWorkingCopyModel {

saveCounter = 0;
throwOnSave = false;

async save(options: IWriteFileOptions, token: CancellationToken): Promise<IFileStatWithMetadata> {
if (this.throwOnSave) {
throw new Error('Fail');
}

this.saveCounter++;

return {
resource: this.resource,
ctime: 0,
etag: '',
isDirectory: false,
isFile: true,
mtime: 0,
name: 'resource2',
size: 0,
isSymbolicLink: false,
readonly: false,
locked: false,
children: undefined
};
}
}

export class TestStoredFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory<TestStoredFileWorkingCopyModel> {

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

export class TestStoredFileWorkingCopyModelWithCustomSaveFactory implements IStoredFileWorkingCopyModelFactory<TestStoredFileWorkingCopyModelWithCustomSave> {

async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise<TestStoredFileWorkingCopyModelWithCustomSave> {
return new TestStoredFileWorkingCopyModelWithCustomSave(resource, (await streamToBuffer(contents)).toString());
}
}

suite('StoredFileWorkingCopy (with custom save)', function () {

const factory = new TestStoredFileWorkingCopyModelWithCustomSaveFactory();

let disposables: DisposableStore;
const resource = URI.file('test/resource');
let instantiationService: IInstantiationService;
let accessor: TestServiceAccessor;
let workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModelWithCustomSave>;

function createWorkingCopy(uri: URI = resource) {
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);

return workingCopy;
}

setup(() => {
disposables = new DisposableStore();
instantiationService = workbenchInstantiationService(undefined, disposables);
accessor = instantiationService.createInstance(TestServiceAccessor);

workingCopy = createWorkingCopy();
});

teardown(() => {
workingCopy.dispose();
disposables.dispose();
});

test('save (custom implemented)', async () => {
let savedCounter = 0;
let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined;
workingCopy.onDidSave(e => {
savedCounter++;
lastSaveEvent = e;
});

let saveErrorCounter = 0;
workingCopy.onDidSaveError(() => {
saveErrorCounter++;
});

// unresolved
await workingCopy.save();
assert.strictEqual(savedCounter, 0);
assert.strictEqual(saveErrorCounter, 0);

// simple
await workingCopy.resolve();
workingCopy.model?.updateContents('hello save');
await workingCopy.save();

assert.strictEqual(savedCounter, 1);
assert.strictEqual(saveErrorCounter, 0);
assert.strictEqual(workingCopy.isDirty(), false);
assert.strictEqual(lastSaveEvent!.reason, SaveReason.EXPLICIT);
assert.ok(lastSaveEvent!.stat);
assert.ok(isStoredFileWorkingCopySaveEvent(lastSaveEvent!));
assert.strictEqual(workingCopy.model?.pushedStackElement, true);
assert.strictEqual((workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).saveCounter, 1);

// error
workingCopy.model?.updateContents('hello save error');
(workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).throwOnSave = true;
await workingCopy.save();

assert.strictEqual(saveErrorCounter, 1);
assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true);
});
});

suite('StoredFileWorkingCopy', function () {

const factory = new TestStoredFileWorkingCopyModelFactory();
Expand Down

0 comments on commit 675314d

Please sign in to comment.