file working copy - implement save participant support
This commit is contained in:
parent
c0f739ea25
commit
28d7c49801
|
@ -167,8 +167,8 @@ export class FileWorkingCopyManager<S extends IStoredFileWorkingCopyModel, U ext
|
|||
this.workingCopyTypeId,
|
||||
this.storedWorkingCopyModelFactory,
|
||||
fileService, lifecycleService, labelService, logService, workingCopyFileService,
|
||||
workingCopyBackupService, uriIdentityService, textFileService, filesConfigurationService,
|
||||
workingCopyService, notificationService, workingCopyEditorService, editorService, elevatedFileService
|
||||
workingCopyBackupService, uriIdentityService, filesConfigurationService, workingCopyService,
|
||||
notificationService, workingCopyEditorService, editorService, elevatedFileService
|
||||
));
|
||||
|
||||
// Untitled file working copies manager
|
||||
|
|
|
@ -146,15 +146,18 @@ export abstract class ResourceWorkingCopy extends Disposable implements IResourc
|
|||
|
||||
//#region Abstract
|
||||
|
||||
abstract typeId: string;
|
||||
abstract name: string;
|
||||
abstract capabilities: WorkingCopyCapabilities;
|
||||
|
||||
abstract onDidChangeDirty: Event<void>;
|
||||
abstract onDidChangeContent: Event<void>;
|
||||
|
||||
abstract isDirty(): boolean;
|
||||
|
||||
abstract backup(token: CancellationToken): Promise<IWorkingCopyBackup>;
|
||||
abstract save(options?: ISaveOptions): Promise<boolean>;
|
||||
abstract revert(options?: IRevertOptions): Promise<void>;
|
||||
abstract typeId: string;
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { IWorkingCopyBackup, IWorkingCopyBackupMeta, WorkingCopyCapabilities } f
|
|||
import { raceCancellation, TaskSequentializer, timeout } from 'vs/base/common/async';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup';
|
||||
|
@ -294,7 +294,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
|||
private readonly modelFactory: IStoredFileWorkingCopyModelFactory<M>,
|
||||
@IFileService fileService: IFileService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService,
|
||||
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
|
||||
@IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService,
|
||||
@IWorkingCopyService workingCopyService: IWorkingCopyService,
|
||||
|
@ -657,7 +657,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
|||
private onModelContentChanged(model: M, isUndoingOrRedoing: boolean): void {
|
||||
this.trace(`[stored file working copy] onModelContentChanged() - enter`);
|
||||
|
||||
// In any case increment the version id because it tracks the textual content state of the model at all times
|
||||
// In any case increment the version id because it tracks the content state of the model at all times
|
||||
this.versionId++;
|
||||
this.trace(`[stored file working copy] onModelContentChanged() - new versionId ${this.versionId}`);
|
||||
|
||||
|
@ -835,7 +835,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
|||
// In addition we update our version right after in case it changed
|
||||
// because of a working copy change
|
||||
// Save participants can also be skipped through API.
|
||||
if (this.isResolved() && !options.skipSaveParticipants && this.isTextFileModel(this.model)) {
|
||||
if (this.isResolved() && !options.skipSaveParticipants && this.workingCopyFileService.hasSaveParticipants) {
|
||||
try {
|
||||
|
||||
// Measure the time it took from the last undo/redo operation to this save. If this
|
||||
|
@ -860,7 +860,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
|||
|
||||
// Run save participants unless save was cancelled meanwhile
|
||||
if (!saveCancellation.token.isCancellationRequested) {
|
||||
await this.textFileService.files.runSaveParticipants(this.model, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token);
|
||||
await this.workingCopyFileService.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(`[stored file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId);
|
||||
|
@ -983,12 +983,8 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
|||
this.inConflictMode = true;
|
||||
}
|
||||
|
||||
// Delegate to save error handler
|
||||
if (this.isTextFileModel(this.model)) {
|
||||
this.textFileService.files.saveErrorHandler.onSaveError(error, this.model);
|
||||
} else {
|
||||
this.doHandleSaveError(error);
|
||||
}
|
||||
// Show save error to user for handling
|
||||
this.doHandleSaveError(error);
|
||||
|
||||
// Emit as event
|
||||
this._onDidSaveError.fire();
|
||||
|
@ -1199,14 +1195,4 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
|
|||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Remainders of text file model world (TODO@bpasero callers have to be handled in a generic way)
|
||||
|
||||
private isTextFileModel(model: unknown): model is ITextFileEditorModel {
|
||||
const textFileModel = this.textFileService.files.get(this.resource);
|
||||
|
||||
return !!(textFileModel && this.model && (textFileModel as unknown) === (this.model as unknown));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
|
|||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService';
|
||||
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
|
||||
|
@ -153,7 +152,6 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel>
|
|||
@IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService,
|
||||
@IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
|
||||
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
|
@ -441,7 +439,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel>
|
|||
resource,
|
||||
this.labelService.getUriBasenameLabel(resource),
|
||||
this.modelFactory,
|
||||
this.fileService, this.logService, this.textFileService, this.filesConfigurationService,
|
||||
this.fileService, this.logService, this.workingCopyFileService, this.filesConfigurationService,
|
||||
this.workingCopyBackupService, this.workingCopyService, this.notificationService, this.workingCopyEditorService,
|
||||
this.editorService, this.elevatedFileService
|
||||
);
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { raceCancellation } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { SaveReason } from 'vs/workbench/common/editor';
|
||||
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { insert } from 'vs/base/common/arrays';
|
||||
import { IStoredFileWorkingCopySaveParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
|
||||
|
||||
export class StoredFileWorkingCopySaveParticipant extends Disposable {
|
||||
|
||||
private readonly saveParticipants: IStoredFileWorkingCopySaveParticipant[] = [];
|
||||
|
||||
get length(): number { return this.saveParticipants.length; }
|
||||
|
||||
constructor(
|
||||
@IProgressService private readonly progressService: IProgressService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
addSaveParticipant(participant: IStoredFileWorkingCopySaveParticipant): IDisposable {
|
||||
const remove = insert(this.saveParticipants, participant);
|
||||
|
||||
return toDisposable(() => remove());
|
||||
}
|
||||
|
||||
participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
|
||||
const cts = new CancellationTokenSource(token);
|
||||
|
||||
return this.progressService.withProgress({
|
||||
title: localize('saveParticipants', "Saving '{0}'", workingCopy.name),
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
delay: workingCopy.isDirty() ? 3000 : 5000
|
||||
}, async progress => {
|
||||
|
||||
// undoStop before participation
|
||||
workingCopy.model?.pushStackElement();
|
||||
|
||||
for (const saveParticipant of this.saveParticipants) {
|
||||
if (cts.token.isCancellationRequested || workingCopy.isDisposed()) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const promise = saveParticipant.participate(workingCopy, context, progress, cts.token);
|
||||
await raceCancellation(promise, cts.token);
|
||||
} catch (err) {
|
||||
this.logService.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
// undoStop after participation
|
||||
workingCopy.model?.pushStackElement();
|
||||
}, () => {
|
||||
// user cancel
|
||||
cts.dispose(true);
|
||||
});
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.saveParticipants.splice(0, this.saveParticipants.length);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,10 @@ import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCo
|
|||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { WorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant';
|
||||
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { SaveReason } from 'vs/workbench/common/editor';
|
||||
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
|
||||
import { StoredFileWorkingCopySaveParticipant } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopySaveParticipant';
|
||||
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
|
||||
|
||||
export const IWorkingCopyFileService = createDecorator<IWorkingCopyFileService>('workingCopyFileService');
|
||||
|
||||
|
@ -80,6 +84,20 @@ export interface IWorkingCopyFileOperationParticipant {
|
|||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IStoredFileWorkingCopySaveParticipant {
|
||||
|
||||
/**
|
||||
* Participate in a save operation of file stored working copies.
|
||||
* Allows to make changes before content is being saved to disk.
|
||||
*/
|
||||
participate(
|
||||
workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>,
|
||||
context: { reason: SaveReason },
|
||||
progress: IProgress<IProgressStep>,
|
||||
token: CancellationToken
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICreateOperation {
|
||||
resource: URI;
|
||||
overwrite?: boolean;
|
||||
|
@ -158,6 +176,26 @@ export interface IWorkingCopyFileService {
|
|||
//#endregion
|
||||
|
||||
|
||||
//#region Stored File Working Copy save participants
|
||||
|
||||
/**
|
||||
* Whether save participants are present for stored file working copies.
|
||||
*/
|
||||
get hasSaveParticipants(): boolean;
|
||||
|
||||
/**
|
||||
* Adds a participant for save operations on stored file working copies.
|
||||
*/
|
||||
addSaveParticipant(participant: IStoredFileWorkingCopySaveParticipant): IDisposable;
|
||||
|
||||
/**
|
||||
* Runs all available save participants for stored file working copies.
|
||||
*/
|
||||
runSaveParticipants(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason; }, token: CancellationToken): Promise<void>;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region File operations
|
||||
|
||||
/**
|
||||
|
@ -444,6 +482,22 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
|||
|
||||
//#endregion
|
||||
|
||||
//#region Save participants (stored file working copies only)
|
||||
|
||||
private readonly saveParticipants = this._register(this.instantiationService.createInstance(StoredFileWorkingCopySaveParticipant));
|
||||
|
||||
get hasSaveParticipants(): boolean { return this.saveParticipants.length > 0; }
|
||||
|
||||
addSaveParticipant(participant: IStoredFileWorkingCopySaveParticipant): IDisposable {
|
||||
return this.saveParticipants.addSaveParticipant(participant);
|
||||
}
|
||||
|
||||
runSaveParticipants(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
|
||||
return this.saveParticipants.participate(workingCopy, context, token);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Path related
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ suite('StoredFileWorkingCopy', function () {
|
|||
let workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>;
|
||||
|
||||
function createWorkingCopy(uri: URI = resource) {
|
||||
return new StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>('testStoredFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService);
|
||||
return new StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>('testStoredFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService);
|
||||
}
|
||||
|
||||
setup(() => {
|
||||
|
@ -601,6 +601,35 @@ suite('StoredFileWorkingCopy', function () {
|
|||
assert.ok(error);
|
||||
});
|
||||
|
||||
test('save participant', async () => {
|
||||
await workingCopy.resolve();
|
||||
|
||||
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, false);
|
||||
|
||||
let participationCounter = 0;
|
||||
const disposable = accessor.workingCopyFileService.addSaveParticipant({
|
||||
participate: async (wc) => {
|
||||
if (workingCopy === wc) {
|
||||
participationCounter++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, true);
|
||||
|
||||
await workingCopy.save({ force: true });
|
||||
assert.strictEqual(participationCounter, 1);
|
||||
|
||||
await workingCopy.save({ force: true, skipSaveParticipants: true });
|
||||
assert.strictEqual(participationCounter, 1);
|
||||
|
||||
disposable.dispose();
|
||||
assert.strictEqual(accessor.workingCopyFileService.hasSaveParticipants, false);
|
||||
|
||||
await workingCopy.save({ force: true });
|
||||
assert.strictEqual(participationCounter, 1);
|
||||
});
|
||||
|
||||
test('revert', async () => {
|
||||
await workingCopy.resolve();
|
||||
workingCopy.model?.updateContents('hello revert');
|
||||
|
|
|
@ -32,7 +32,7 @@ suite('StoredFileWorkingCopyManager', () => {
|
|||
new TestStoredFileWorkingCopyModelFactory(),
|
||||
accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService,
|
||||
accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService,
|
||||
accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService,
|
||||
accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService,
|
||||
accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService
|
||||
);
|
||||
});
|
||||
|
|
|
@ -16,10 +16,10 @@ import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
|||
import { InMemoryStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy';
|
||||
import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent, IDeleteOperation, ICopyOperation, IMoveOperation, IFileOperationUndoRedoInfo, ICreateFileOperation, ICreateOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent, IDeleteOperation, ICopyOperation, IMoveOperation, IFileOperationUndoRedoInfo, ICreateFileOperation, ICreateOperation, IStoredFileWorkingCopySaveParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IActivity, IActivityService } from 'vs/workbench/services/activity/common/activity';
|
||||
|
@ -190,6 +190,10 @@ export class TestWorkingCopyFileService implements IWorkingCopyFileService {
|
|||
|
||||
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable { return Disposable.None; }
|
||||
|
||||
readonly hasSaveParticipants = false;
|
||||
addSaveParticipant(participant: IStoredFileWorkingCopySaveParticipant): IDisposable { return Disposable.None; }
|
||||
async runSaveParticipants(workingCopy: IWorkingCopy, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> { }
|
||||
|
||||
async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<void> { }
|
||||
|
||||
registerWorkingCopyProvider(provider: (resourceOrFolder: URI) => IWorkingCopy[]): IDisposable { return Disposable.None; }
|
||||
|
|
Loading…
Reference in a new issue