Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { raceTimeout } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperation } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { insert } from 'vs/base/common/arrays';
|
||||
|
||||
export class WorkingCopyFileOperationParticipant extends Disposable {
|
||||
|
||||
private readonly participants: IWorkingCopyFileOperationParticipant[] = [];
|
||||
|
||||
constructor(
|
||||
@IProgressService private readonly progressService: IProgressService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable {
|
||||
const remove = insert(this.participants, participant);
|
||||
|
||||
return toDisposable(() => remove());
|
||||
}
|
||||
|
||||
async participate(files: { source?: URI, target: URI }[], operation: FileOperation): Promise<void> {
|
||||
const timeout = this.configurationService.getValue<number>('files.participants.timeout');
|
||||
if (timeout <= 0) {
|
||||
return; // disabled
|
||||
}
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
|
||||
return this.progressService.withProgress({
|
||||
location: ProgressLocation.Window,
|
||||
title: this.progressLabel(operation)
|
||||
}, async progress => {
|
||||
|
||||
// For each participant
|
||||
for (const participant of this.participants) {
|
||||
if (cts.token.isCancellationRequested) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const promise = participant.participate(files, operation, progress, timeout, cts.token);
|
||||
await raceTimeout(promise, timeout, () => cts.dispose(true /* cancel */));
|
||||
} catch (err) {
|
||||
this.logService.warn(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private progressLabel(operation: FileOperation): string {
|
||||
switch (operation) {
|
||||
case FileOperation.CREATE:
|
||||
return localize('msg-create', "Running 'File Create' participants...");
|
||||
case FileOperation.MOVE:
|
||||
return localize('msg-rename', "Running 'File Rename' participants...");
|
||||
case FileOperation.COPY:
|
||||
return localize('msg-copy', "Running 'File Copy' participants...");
|
||||
case FileOperation.DELETE:
|
||||
return localize('msg-delete', "Running 'File Delete' participants...");
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.participants.splice(0, this.participants.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Event, AsyncEmitter, IWaitUntil } from 'vs/base/common/event';
|
||||
import { insert } from 'vs/base/common/arrays';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, FileOperation, IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
|
||||
import { WorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant';
|
||||
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
|
||||
export const IWorkingCopyFileService = createDecorator<IWorkingCopyFileService>('workingCopyFileService');
|
||||
|
||||
interface SourceTargetPair {
|
||||
|
||||
/**
|
||||
* The source resource that is defined for move operations.
|
||||
*/
|
||||
readonly source?: URI;
|
||||
|
||||
/**
|
||||
* The target resource the event is about.
|
||||
*/
|
||||
readonly target: URI
|
||||
}
|
||||
|
||||
export interface WorkingCopyFileEvent extends IWaitUntil {
|
||||
|
||||
/**
|
||||
* An identifier to correlate the operation through the
|
||||
* different event types (before, after, error).
|
||||
*/
|
||||
readonly correlationId: number;
|
||||
|
||||
/**
|
||||
* The file operation that is taking place.
|
||||
*/
|
||||
readonly operation: FileOperation;
|
||||
|
||||
/**
|
||||
* The array of source/target pair of files involved in given operation.
|
||||
*/
|
||||
readonly files: SourceTargetPair[]
|
||||
}
|
||||
|
||||
export interface IWorkingCopyFileOperationParticipant {
|
||||
|
||||
/**
|
||||
* Participate in a file operation of working copies. Allows to
|
||||
* change the working copies before they are being saved to disk.
|
||||
*/
|
||||
participate(
|
||||
files: SourceTargetPair[],
|
||||
operation: FileOperation,
|
||||
progress: IProgress<IProgressStep>,
|
||||
timeout: number,
|
||||
token: CancellationToken
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the working copies for a given resource.
|
||||
*/
|
||||
type WorkingCopyProvider = (resourceOrFolder: URI) => IWorkingCopy[];
|
||||
|
||||
/**
|
||||
* A service that allows to perform file operations with working copy support.
|
||||
* Any operation that would leave a stale dirty working copy behind will make
|
||||
* sure to revert the working copy first.
|
||||
*
|
||||
* On top of that events are provided to participate in each state of the
|
||||
* operation to perform additional work.
|
||||
*/
|
||||
export interface IWorkingCopyFileService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
//#region Events
|
||||
|
||||
/**
|
||||
* An event that is fired when a certain working copy IO operation is about to run.
|
||||
*
|
||||
* Participants can join this event with a long running operation to keep some state
|
||||
* before the operation is started, but working copies should not be changed at this
|
||||
* point in time. For that purpose, use the `IWorkingCopyFileOperationParticipant` API.
|
||||
*/
|
||||
readonly onWillRunWorkingCopyFileOperation: Event<WorkingCopyFileEvent>;
|
||||
|
||||
/**
|
||||
* An event that is fired after a working copy IO operation has failed.
|
||||
*
|
||||
* Participants can join this event with a long running operation to clean up as needed.
|
||||
*/
|
||||
readonly onDidFailWorkingCopyFileOperation: Event<WorkingCopyFileEvent>;
|
||||
|
||||
/**
|
||||
* An event that is fired after a working copy IO operation has been performed.
|
||||
*
|
||||
* Participants can join this event with a long running operation to make changes
|
||||
* after the operation has finished.
|
||||
*/
|
||||
readonly onDidRunWorkingCopyFileOperation: Event<WorkingCopyFileEvent>;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region File operation participants
|
||||
|
||||
/**
|
||||
* Adds a participant for file operations on working copies.
|
||||
*/
|
||||
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region File operations
|
||||
|
||||
/**
|
||||
* Will create a resource with the provided optional contents, optionally overwriting any target.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
create(resource: URI, contents?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata>;
|
||||
|
||||
/**
|
||||
* Will create a folder and any parent folder that needs to be created.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*
|
||||
* Note: events will only be emitted for the provided resource, but not any
|
||||
* parent folders that are being created as part of the operation.
|
||||
*/
|
||||
createFolder(resource: URI): Promise<IFileStatWithMetadata>;
|
||||
|
||||
/**
|
||||
* Will move working copies matching the provided resources and corresponding children
|
||||
* to the target resources using the associated file service for those resources.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]>;
|
||||
|
||||
/**
|
||||
* Will copy working copies matching the provided resources and corresponding children
|
||||
* to the target resources using the associated file service for those resources.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]>;
|
||||
|
||||
/**
|
||||
* Will delete working copies matching the provided resources and children
|
||||
* using the associated file service for those resources.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
delete(resources: URI[], options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Path related
|
||||
|
||||
/**
|
||||
* Register a new provider for working copies based on a resource.
|
||||
*
|
||||
* @return a disposable that unregisters the provider.
|
||||
*/
|
||||
registerWorkingCopyProvider(provider: WorkingCopyProvider): IDisposable;
|
||||
|
||||
/**
|
||||
* Will return all working copies that are dirty matching the provided resource.
|
||||
* If the resource is a folder and the scheme supports file operations, a working
|
||||
* copy that is dirty and is a child of that folder will also be returned.
|
||||
*/
|
||||
getDirty(resource: URI): IWorkingCopy[];
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export class WorkingCopyFileService extends Disposable implements IWorkingCopyFileService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
//#region Events
|
||||
|
||||
private readonly _onWillRunWorkingCopyFileOperation = this._register(new AsyncEmitter<WorkingCopyFileEvent>());
|
||||
readonly onWillRunWorkingCopyFileOperation = this._onWillRunWorkingCopyFileOperation.event;
|
||||
|
||||
private readonly _onDidFailWorkingCopyFileOperation = this._register(new AsyncEmitter<WorkingCopyFileEvent>());
|
||||
readonly onDidFailWorkingCopyFileOperation = this._onDidFailWorkingCopyFileOperation.event;
|
||||
|
||||
private readonly _onDidRunWorkingCopyFileOperation = this._register(new AsyncEmitter<WorkingCopyFileEvent>());
|
||||
readonly onDidRunWorkingCopyFileOperation = this._onDidRunWorkingCopyFileOperation.event;
|
||||
|
||||
//#endregion
|
||||
|
||||
private correlationIds = 0;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
|
||||
) {
|
||||
super();
|
||||
|
||||
// register a default working copy provider that uses the working copy service
|
||||
this.registerWorkingCopyProvider(resource => {
|
||||
return this.workingCopyService.workingCopies.filter(workingCopy => {
|
||||
if (this.fileService.canHandleResource(resource)) {
|
||||
// only check for parents if the resource can be handled
|
||||
// by the file system where we then assume a folder like
|
||||
// path structure
|
||||
return this.uriIdentityService.extUri.isEqualOrParent(workingCopy.resource, resource);
|
||||
}
|
||||
|
||||
return this.uriIdentityService.extUri.isEqual(workingCopy.resource, resource);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//#region File operations
|
||||
|
||||
create(resource: URI, contents?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata> {
|
||||
return this.doCreateFileOrFolder(resource, true, contents, options);
|
||||
}
|
||||
|
||||
createFolder(resource: URI, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata> {
|
||||
return this.doCreateFileOrFolder(resource, false);
|
||||
}
|
||||
|
||||
async doCreateFileOrFolder(resource: URI, isFile: boolean, contents?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata> {
|
||||
|
||||
// validate create operation before starting
|
||||
if (isFile) {
|
||||
const validateCreate = await this.fileService.canCreateFile(resource, options);
|
||||
if (validateCreate instanceof Error) {
|
||||
throw validateCreate;
|
||||
}
|
||||
}
|
||||
|
||||
// file operation participant
|
||||
await this.runFileOperationParticipants([{ target: resource }], FileOperation.CREATE);
|
||||
|
||||
// before events
|
||||
const event = { correlationId: this.correlationIds++, operation: FileOperation.CREATE, files: [{ target: resource }] };
|
||||
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
// now actually create on disk
|
||||
let stat: IFileStatWithMetadata;
|
||||
try {
|
||||
if (isFile) {
|
||||
stat = await this.fileService.createFile(resource, contents, { overwrite: options?.overwrite });
|
||||
} else {
|
||||
stat = await this.fileService.createFolder(resource);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
// error event
|
||||
await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// after event
|
||||
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
return stat;
|
||||
}
|
||||
|
||||
async move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
|
||||
return this.doMoveOrCopy(files, true, options);
|
||||
}
|
||||
|
||||
async copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
|
||||
return this.doMoveOrCopy(files, false, options);
|
||||
}
|
||||
|
||||
private async doMoveOrCopy(files: Required<SourceTargetPair>[], move: boolean, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
|
||||
const overwrite = options?.overwrite;
|
||||
const stats: IFileStatWithMetadata[] = [];
|
||||
|
||||
// validate move/copy operation before starting
|
||||
for (const { source, target } of files) {
|
||||
const validateMoveOrCopy = await (move ? this.fileService.canMove(source, target, overwrite) : this.fileService.canCopy(source, target, overwrite));
|
||||
if (validateMoveOrCopy instanceof Error) {
|
||||
throw validateMoveOrCopy;
|
||||
}
|
||||
}
|
||||
|
||||
// file operation participant
|
||||
await this.runFileOperationParticipants(files, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
|
||||
// before event
|
||||
const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, files };
|
||||
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
try {
|
||||
for (const { source, target } of files) {
|
||||
|
||||
// if source and target are not equal, handle dirty working copies
|
||||
// depending on the operation:
|
||||
// - move: revert both source and target (if any)
|
||||
// - copy: revert target (if any)
|
||||
if (!this.uriIdentityService.extUri.isEqual(source, target)) {
|
||||
const dirtyWorkingCopies = (move ? [...this.getDirty(source), ...this.getDirty(target)] : this.getDirty(target));
|
||||
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
|
||||
}
|
||||
|
||||
// now we can rename the source to target via file operation
|
||||
if (move) {
|
||||
stats.push(await this.fileService.move(source, target, overwrite));
|
||||
} else {
|
||||
stats.push(await this.fileService.copy(source, target, overwrite));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
// error event
|
||||
await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// after event
|
||||
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
async delete(resources: URI[], options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
|
||||
|
||||
// validate delete operation before starting
|
||||
for (const resource of resources) {
|
||||
const validateDelete = await this.fileService.canDelete(resource, options);
|
||||
if (validateDelete instanceof Error) {
|
||||
throw validateDelete;
|
||||
}
|
||||
}
|
||||
|
||||
// file operation participant
|
||||
const files = resources.map(target => ({ target }));
|
||||
await this.runFileOperationParticipants(files, FileOperation.DELETE);
|
||||
|
||||
// before events
|
||||
const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, files };
|
||||
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
// check for any existing dirty working copies for the resource
|
||||
// and do a soft revert before deleting to be able to close
|
||||
// any opened editor with these working copies
|
||||
for (const resource of resources) {
|
||||
const dirtyWorkingCopies = this.getDirty(resource);
|
||||
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
|
||||
}
|
||||
|
||||
// now actually delete from disk
|
||||
try {
|
||||
for (const resource of resources) {
|
||||
await this.fileService.del(resource, options);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
// error event
|
||||
await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// after event
|
||||
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region File operation participants
|
||||
|
||||
private readonly fileOperationParticipants = this._register(this.instantiationService.createInstance(WorkingCopyFileOperationParticipant));
|
||||
|
||||
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable {
|
||||
return this.fileOperationParticipants.addFileOperationParticipant(participant);
|
||||
}
|
||||
|
||||
private runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation): Promise<void> {
|
||||
return this.fileOperationParticipants.participate(files, operation);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Path related
|
||||
|
||||
private readonly workingCopyProviders: WorkingCopyProvider[] = [];
|
||||
|
||||
registerWorkingCopyProvider(provider: WorkingCopyProvider): IDisposable {
|
||||
const remove = insert(this.workingCopyProviders, provider);
|
||||
return toDisposable(remove);
|
||||
}
|
||||
|
||||
getDirty(resource: URI): IWorkingCopy[] {
|
||||
const dirtyWorkingCopies = new Set<IWorkingCopy>();
|
||||
for (const provider of this.workingCopyProviders) {
|
||||
for (const workingCopy of provider(resource)) {
|
||||
if (workingCopy.isDirty()) {
|
||||
dirtyWorkingCopies.add(workingCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(dirtyWorkingCopies);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
registerSingleton(IWorkingCopyFileService, WorkingCopyFileService, true);
|
||||
@@ -0,0 +1,286 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
export const enum WorkingCopyCapabilities {
|
||||
|
||||
/**
|
||||
* Signals no specific capability for the working copy.
|
||||
*/
|
||||
None = 0,
|
||||
|
||||
/**
|
||||
* Signals that the working copy requires
|
||||
* additional input when saving, e.g. an
|
||||
* associated path to save to.
|
||||
*/
|
||||
Untitled = 1 << 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Data to be associated with working copy backups. Use
|
||||
* `IBackupFileService.resolve(workingCopy.resource)` to
|
||||
* retrieve the backup when loading the working copy.
|
||||
*/
|
||||
export interface IWorkingCopyBackup<MetaType = object> {
|
||||
|
||||
/**
|
||||
* Any serializable metadata to be associated with the backup.
|
||||
*/
|
||||
meta?: MetaType;
|
||||
|
||||
/**
|
||||
* Use this for larger textual content of the backup.
|
||||
*/
|
||||
content?: ITextSnapshot;
|
||||
}
|
||||
|
||||
export interface IWorkingCopy {
|
||||
|
||||
/**
|
||||
* The unique resource of the working copy. There can only be one
|
||||
* working copy in the system with the same URI.
|
||||
*/
|
||||
readonly resource: URI;
|
||||
|
||||
/**
|
||||
* Human readable name of the working copy.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The capabilities of the working copy.
|
||||
*/
|
||||
readonly capabilities: WorkingCopyCapabilities;
|
||||
|
||||
|
||||
//#region Events
|
||||
|
||||
/**
|
||||
* Used by the workbench to signal if the working copy
|
||||
* is dirty or not. Typically a working copy is dirty
|
||||
* once changed until saved or reverted.
|
||||
*/
|
||||
readonly onDidChangeDirty: Event<void>;
|
||||
|
||||
/**
|
||||
* Used by the workbench e.g. to trigger auto-save
|
||||
* (unless this working copy is untitled) and backups.
|
||||
*/
|
||||
readonly onDidChangeContent: Event<void>;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Dirty Tracking
|
||||
|
||||
isDirty(): boolean;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Save / Backup
|
||||
|
||||
/**
|
||||
* The workbench may call this method often after it receives
|
||||
* the `onDidChangeContent` event for the working copy. The motivation
|
||||
* is to allow to quit VSCode with dirty working copies present.
|
||||
*
|
||||
* Providers of working copies should use `IBackupFileService.resolve(workingCopy.resource)`
|
||||
* to retrieve the backup metadata associated when loading the working copy.
|
||||
*
|
||||
* @param token support for cancellation
|
||||
*/
|
||||
backup(token: CancellationToken): Promise<IWorkingCopyBackup>;
|
||||
|
||||
/**
|
||||
* Asks the working copy to save. If the working copy was dirty, it is
|
||||
* expected to be non-dirty after this operation has finished.
|
||||
*
|
||||
* @returns `true` if the operation was successful and `false` otherwise.
|
||||
*/
|
||||
save(options?: ISaveOptions): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Asks the working copy to revert. If the working copy was dirty, it is
|
||||
* expected to be non-dirty after this operation has finished.
|
||||
*/
|
||||
revert(options?: IRevertOptions): Promise<void>;
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export const IWorkingCopyService = createDecorator<IWorkingCopyService>('workingCopyService');
|
||||
|
||||
export interface IWorkingCopyService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
||||
//#region Events
|
||||
|
||||
readonly onDidRegister: Event<IWorkingCopy>;
|
||||
|
||||
readonly onDidUnregister: Event<IWorkingCopy>;
|
||||
|
||||
readonly onDidChangeDirty: Event<IWorkingCopy>;
|
||||
|
||||
readonly onDidChangeContent: Event<IWorkingCopy>;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Dirty Tracking
|
||||
|
||||
readonly dirtyCount: number;
|
||||
|
||||
readonly dirtyWorkingCopies: IWorkingCopy[];
|
||||
|
||||
readonly hasDirty: boolean;
|
||||
|
||||
isDirty(resource: URI): boolean;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Registry
|
||||
|
||||
readonly workingCopies: IWorkingCopy[];
|
||||
|
||||
/**
|
||||
* Register a new working copy with the service. This method will
|
||||
* throw if you try to register a working copy with a resource
|
||||
* that was already registered before. There can only be 1 working
|
||||
* copy per resource registered to the service.
|
||||
*/
|
||||
registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable;
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export class WorkingCopyService extends Disposable implements IWorkingCopyService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
//#region Events
|
||||
|
||||
private readonly _onDidRegister = this._register(new Emitter<IWorkingCopy>());
|
||||
readonly onDidRegister = this._onDidRegister.event;
|
||||
|
||||
private readonly _onDidUnregister = this._register(new Emitter<IWorkingCopy>());
|
||||
readonly onDidUnregister = this._onDidUnregister.event;
|
||||
|
||||
private readonly _onDidChangeDirty = this._register(new Emitter<IWorkingCopy>());
|
||||
readonly onDidChangeDirty = this._onDidChangeDirty.event;
|
||||
|
||||
private readonly _onDidChangeContent = this._register(new Emitter<IWorkingCopy>());
|
||||
readonly onDidChangeContent = this._onDidChangeContent.event;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Registry
|
||||
|
||||
get workingCopies(): IWorkingCopy[] { return Array.from(this._workingCopies.values()); }
|
||||
private _workingCopies = new Set<IWorkingCopy>();
|
||||
|
||||
private readonly mapResourceToWorkingCopy = new ResourceMap<IWorkingCopy>();
|
||||
|
||||
registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable {
|
||||
if (this.mapResourceToWorkingCopy.has(workingCopy.resource)) {
|
||||
throw new Error(`Cannot register more than one working copy with the same resource ${workingCopy.resource.toString(true)}.`);
|
||||
}
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Registry
|
||||
this._workingCopies.add(workingCopy);
|
||||
this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy);
|
||||
|
||||
// Wire in Events
|
||||
disposables.add(workingCopy.onDidChangeContent(() => this._onDidChangeContent.fire(workingCopy)));
|
||||
disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy)));
|
||||
|
||||
// Send some initial events
|
||||
this._onDidRegister.fire(workingCopy);
|
||||
if (workingCopy.isDirty()) {
|
||||
this._onDidChangeDirty.fire(workingCopy);
|
||||
}
|
||||
|
||||
return toDisposable(() => {
|
||||
this.unregisterWorkingCopy(workingCopy);
|
||||
dispose(disposables);
|
||||
|
||||
// Signal as event
|
||||
this._onDidUnregister.fire(workingCopy);
|
||||
});
|
||||
}
|
||||
|
||||
private unregisterWorkingCopy(workingCopy: IWorkingCopy): void {
|
||||
|
||||
// Remove from registry
|
||||
this._workingCopies.delete(workingCopy);
|
||||
this.mapResourceToWorkingCopy.delete(workingCopy.resource);
|
||||
|
||||
// If copy is dirty, ensure to fire an event to signal the dirty change
|
||||
// (a disposed working copy cannot account for being dirty in our model)
|
||||
if (workingCopy.isDirty()) {
|
||||
this._onDidChangeDirty.fire(workingCopy);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Dirty Tracking
|
||||
|
||||
get hasDirty(): boolean {
|
||||
for (const workingCopy of this._workingCopies) {
|
||||
if (workingCopy.isDirty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get dirtyCount(): number {
|
||||
let totalDirtyCount = 0;
|
||||
|
||||
for (const workingCopy of this._workingCopies) {
|
||||
if (workingCopy.isDirty()) {
|
||||
totalDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return totalDirtyCount;
|
||||
}
|
||||
|
||||
get dirtyWorkingCopies(): IWorkingCopy[] {
|
||||
return this.workingCopies.filter(workingCopy => workingCopy.isDirty());
|
||||
}
|
||||
|
||||
isDirty(resource: URI): boolean {
|
||||
const workingCopy = this.mapResourceToWorkingCopy.get(resource);
|
||||
if (workingCopy) {
|
||||
return workingCopy.isDirty();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
registerSingleton(IWorkingCopyService, WorkingCopyService, true);
|
||||
@@ -0,0 +1,471 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { toResource } from 'vs/base/test/common/utils';
|
||||
import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperation } from 'vs/platform/files/common/files';
|
||||
import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
suite('WorkingCopyFileService', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
let accessor: TestServiceAccessor;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = workbenchInstantiationService();
|
||||
accessor = instantiationService.createInstance(TestServiceAccessor);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
(<TextFileEditorModelManager>accessor.textFileService.files).dispose();
|
||||
});
|
||||
|
||||
test('create - dirty file', async function () {
|
||||
await testCreate(toResource.call(this, '/path/file.txt'), VSBuffer.fromString('Hello World'));
|
||||
});
|
||||
|
||||
test('delete - dirty file', async function () {
|
||||
await testDelete([toResource.call(this, '/path/file.txt')]);
|
||||
});
|
||||
|
||||
test('delete multiple - dirty files', async function () {
|
||||
await testDelete([
|
||||
toResource.call(this, '/path/file1.txt'),
|
||||
toResource.call(this, '/path/file2.txt'),
|
||||
toResource.call(this, '/path/file3.txt'),
|
||||
toResource.call(this, '/path/file4.txt')]);
|
||||
});
|
||||
|
||||
test('move - dirty file', async function () {
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], true);
|
||||
});
|
||||
|
||||
test('move - source identical to target', async function () {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([{ source: sourceModel.resource, target: sourceModel.resource }], true);
|
||||
|
||||
sourceModel.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('move - one source == target and another source != target', async function () {
|
||||
let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined);
|
||||
let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined);
|
||||
let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel1.resource, sourceModel1);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel2.resource, sourceModel2);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel2.resource, targetModel2);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([
|
||||
{ source: sourceModel1.resource, target: sourceModel1.resource },
|
||||
{ source: sourceModel2.resource, target: targetModel2.resource }
|
||||
], true);
|
||||
|
||||
sourceModel1.dispose();
|
||||
sourceModel2.dispose();
|
||||
targetModel2.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('move multiple - dirty file', async function () {
|
||||
await testMoveOrCopy([
|
||||
{ source: toResource.call(this, '/path/file1.txt'), target: toResource.call(this, '/path/file1_target.txt') },
|
||||
{ source: toResource.call(this, '/path/file2.txt'), target: toResource.call(this, '/path/file2_target.txt') }],
|
||||
true);
|
||||
});
|
||||
|
||||
test('move - dirty file (target exists and is dirty)', async function () {
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], true, true);
|
||||
});
|
||||
|
||||
test('copy - dirty file', async function () {
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], false);
|
||||
});
|
||||
|
||||
test('copy - source identical to target', async function () {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([{ source: sourceModel.resource, target: sourceModel.resource }]);
|
||||
|
||||
sourceModel.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('copy - one source == target and another source != target', async function () {
|
||||
let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined);
|
||||
let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined);
|
||||
let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel1.resource, sourceModel1);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel2.resource, sourceModel2);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel2.resource, targetModel2);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([
|
||||
{ source: sourceModel1.resource, target: sourceModel1.resource },
|
||||
{ source: sourceModel2.resource, target: targetModel2.resource }
|
||||
]);
|
||||
|
||||
sourceModel1.dispose();
|
||||
sourceModel2.dispose();
|
||||
targetModel2.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('copy multiple - dirty file', async function () {
|
||||
await testMoveOrCopy([
|
||||
{ source: toResource.call(this, '/path/file1.txt'), target: toResource.call(this, '/path/file_target1.txt') },
|
||||
{ source: toResource.call(this, '/path/file2.txt'), target: toResource.call(this, '/path/file_target2.txt') },
|
||||
{ source: toResource.call(this, '/path/file3.txt'), target: toResource.call(this, '/path/file_target3.txt') }],
|
||||
false);
|
||||
});
|
||||
|
||||
test('copy - dirty file (target exists and is dirty)', async function () {
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], false, true);
|
||||
});
|
||||
|
||||
test('getDirty', async function () {
|
||||
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model1.resource, model1);
|
||||
|
||||
const model2 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-2.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model2.resource, model2);
|
||||
|
||||
let dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.equal(dirty.length, 0);
|
||||
|
||||
await model1.load();
|
||||
model1.textEditorModel!.setValue('foo');
|
||||
|
||||
dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.equal(dirty.length, 1);
|
||||
assert.equal(dirty[0], model1);
|
||||
|
||||
dirty = accessor.workingCopyFileService.getDirty(toResource.call(this, '/path'));
|
||||
assert.equal(dirty.length, 1);
|
||||
assert.equal(dirty[0], model1);
|
||||
|
||||
await model2.load();
|
||||
model2.textEditorModel!.setValue('bar');
|
||||
|
||||
dirty = accessor.workingCopyFileService.getDirty(toResource.call(this, '/path'));
|
||||
assert.equal(dirty.length, 2);
|
||||
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
});
|
||||
|
||||
test('registerWorkingCopyProvider', async function () {
|
||||
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model1.resource, model1);
|
||||
await model1.load();
|
||||
model1.textEditorModel!.setValue('foo');
|
||||
|
||||
const testWorkingCopy = new TestWorkingCopy(toResource.call(this, '/path/file-2.txt'), true);
|
||||
const registration = accessor.workingCopyFileService.registerWorkingCopyProvider(() => {
|
||||
return [model1, testWorkingCopy];
|
||||
});
|
||||
|
||||
let dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.strictEqual(dirty.length, 2, 'Should return default working copy + working copy from provider');
|
||||
assert.strictEqual(dirty[0], model1);
|
||||
assert.strictEqual(dirty[1], testWorkingCopy);
|
||||
|
||||
registration.dispose();
|
||||
|
||||
dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.strictEqual(dirty.length, 1, 'Should have unregistered our provider');
|
||||
assert.strictEqual(dirty[0], model1);
|
||||
|
||||
model1.dispose();
|
||||
});
|
||||
|
||||
test('createFolder', async function () {
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const resource = toResource.call(this, '/path/folder');
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
assert.equal(files.length, 1);
|
||||
const file = files[0];
|
||||
assert.equal(file.target.toString(), resource.toString());
|
||||
assert.equal(operation, FileOperation.CREATE);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.files.length, 1);
|
||||
const file = e.files[0];
|
||||
assert.equal(file.target.toString(), resource.toString());
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
correlationId = e.correlationId;
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.files.length, 1);
|
||||
const file = e.files[0];
|
||||
assert.equal(file.target.toString(), resource.toString());
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
await accessor.workingCopyFileService.createFolder(resource);
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
});
|
||||
|
||||
async function testEventsMoveOrCopy(files: { source: URI, target: URI }[], move?: boolean): Promise<number> {
|
||||
let eventCounter = 0;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async files => {
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
if (move) {
|
||||
await accessor.workingCopyFileService.move(files, { overwrite: true });
|
||||
} else {
|
||||
await accessor.workingCopyFileService.copy(files, { overwrite: true });
|
||||
}
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
return eventCounter;
|
||||
}
|
||||
|
||||
async function testMoveOrCopy(files: { source: URI, target: URI }[], move: boolean, targetDirty?: boolean): Promise<void> {
|
||||
|
||||
let eventCounter = 0;
|
||||
const models = await Promise.all(files.map(async ({ source, target }, i) => {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined);
|
||||
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel.resource, targetModel);
|
||||
|
||||
await sourceModel.load();
|
||||
sourceModel.textEditorModel!.setValue('foo' + i);
|
||||
assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
|
||||
if (targetDirty) {
|
||||
await targetModel.load();
|
||||
targetModel.textEditorModel!.setValue('bar' + i);
|
||||
assert.ok(accessor.textFileService.isDirty(targetModel.resource));
|
||||
}
|
||||
|
||||
return { sourceModel, targetModel };
|
||||
}));
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { target, source } = files[i];
|
||||
const { targetModel, sourceModel } = models[i];
|
||||
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
}
|
||||
|
||||
eventCounter++;
|
||||
|
||||
assert.equal(operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
}
|
||||
});
|
||||
|
||||
let correlationId: number;
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < e.files.length; i++) {
|
||||
const { target, source } = files[i];
|
||||
const { targetModel, sourceModel } = models[i];
|
||||
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
}
|
||||
|
||||
eventCounter++;
|
||||
|
||||
correlationId = e.correlationId;
|
||||
assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < e.files.length; i++) {
|
||||
const { target, source } = files[i];
|
||||
const { targetModel, sourceModel } = models[i];
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
}
|
||||
|
||||
eventCounter++;
|
||||
|
||||
assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
});
|
||||
|
||||
if (move) {
|
||||
await accessor.workingCopyFileService.move(models.map(model => ({ source: model.sourceModel.resource, target: model.targetModel.resource })), { overwrite: true });
|
||||
} else {
|
||||
await accessor.workingCopyFileService.copy(models.map(model => ({ source: model.sourceModel.resource, target: model.targetModel.resource })), { overwrite: true });
|
||||
}
|
||||
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const { sourceModel, targetModel } = models[i];
|
||||
|
||||
assert.equal(targetModel.textEditorModel!.getValue(), 'foo' + i);
|
||||
|
||||
if (move) {
|
||||
assert.ok(!accessor.textFileService.isDirty(sourceModel.resource));
|
||||
} else {
|
||||
assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
|
||||
}
|
||||
assert.ok(accessor.textFileService.isDirty(targetModel.resource));
|
||||
|
||||
sourceModel.dispose();
|
||||
targetModel.dispose();
|
||||
}
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
|
||||
async function testDelete(resources: URI[]) {
|
||||
|
||||
const models = await Promise.all(resources.map(async resource => {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
|
||||
await model.load();
|
||||
model!.textEditorModel!.setValue('foo');
|
||||
assert.ok(accessor.workingCopyService.isDirty(model.resource));
|
||||
return model;
|
||||
}));
|
||||
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const file = files[i];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
}
|
||||
assert.equal(operation, FileOperation.DELETE);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const file = e.files[i];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
}
|
||||
assert.equal(e.operation, FileOperation.DELETE);
|
||||
correlationId = e.correlationId;
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const file = e.files[i];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
}
|
||||
assert.equal(e.operation, FileOperation.DELETE);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
await accessor.workingCopyFileService.delete(models.map(m => m.resource));
|
||||
for (const model of models) {
|
||||
assert.ok(!accessor.workingCopyService.isDirty(model.resource));
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
|
||||
async function testCreate(resource: URI, contents: VSBuffer) {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
|
||||
await model.load();
|
||||
model!.textEditorModel!.setValue('foo');
|
||||
assert.ok(accessor.workingCopyService.isDirty(model.resource));
|
||||
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
assert.equal(files.length, 1);
|
||||
const file = files[0];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
assert.equal(operation, FileOperation.CREATE);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.files.length, 1);
|
||||
const file = e.files[0];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
correlationId = e.correlationId;
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.files.length, 1);
|
||||
const file = e.files[0];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
await accessor.workingCopyFileService.create(resource, contents);
|
||||
assert.ok(!accessor.workingCopyService.isDirty(model.resource));
|
||||
model.dispose();
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TestWorkingCopy, TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
|
||||
suite('WorkingCopyService', () => {
|
||||
|
||||
test('registry - basics', () => {
|
||||
const service = new TestWorkingCopyService();
|
||||
|
||||
const onDidChangeDirty: IWorkingCopy[] = [];
|
||||
service.onDidChangeDirty(copy => onDidChangeDirty.push(copy));
|
||||
|
||||
const onDidChangeContent: IWorkingCopy[] = [];
|
||||
service.onDidChangeContent(copy => onDidChangeContent.push(copy));
|
||||
|
||||
const onDidRegister: IWorkingCopy[] = [];
|
||||
service.onDidRegister(copy => onDidRegister.push(copy));
|
||||
|
||||
const onDidUnregister: IWorkingCopy[] = [];
|
||||
service.onDidUnregister(copy => onDidUnregister.push(copy));
|
||||
|
||||
assert.equal(service.hasDirty, false);
|
||||
assert.equal(service.dirtyCount, 0);
|
||||
assert.equal(service.workingCopies.length, 0);
|
||||
assert.equal(service.isDirty(URI.file('/')), false);
|
||||
|
||||
// resource 1
|
||||
const resource1 = URI.file('/some/folder/file.txt');
|
||||
const copy1 = new TestWorkingCopy(resource1);
|
||||
const unregister1 = service.registerWorkingCopy(copy1);
|
||||
|
||||
assert.equal(service.workingCopies.length, 1);
|
||||
assert.equal(service.workingCopies[0], copy1);
|
||||
assert.equal(onDidRegister.length, 1);
|
||||
assert.equal(onDidRegister[0], copy1);
|
||||
assert.equal(service.dirtyCount, 0);
|
||||
assert.equal(service.isDirty(resource1), false);
|
||||
assert.equal(service.hasDirty, false);
|
||||
|
||||
copy1.setDirty(true);
|
||||
|
||||
assert.equal(copy1.isDirty(), true);
|
||||
assert.equal(service.dirtyCount, 1);
|
||||
assert.equal(service.dirtyWorkingCopies.length, 1);
|
||||
assert.equal(service.dirtyWorkingCopies[0], copy1);
|
||||
assert.equal(service.workingCopies.length, 1);
|
||||
assert.equal(service.workingCopies[0], copy1);
|
||||
assert.equal(service.isDirty(resource1), true);
|
||||
assert.equal(service.hasDirty, true);
|
||||
assert.equal(onDidChangeDirty.length, 1);
|
||||
assert.equal(onDidChangeDirty[0], copy1);
|
||||
|
||||
copy1.setContent('foo');
|
||||
|
||||
assert.equal(onDidChangeContent.length, 1);
|
||||
assert.equal(onDidChangeContent[0], copy1);
|
||||
|
||||
copy1.setDirty(false);
|
||||
|
||||
assert.equal(service.dirtyCount, 0);
|
||||
assert.equal(service.isDirty(resource1), false);
|
||||
assert.equal(service.hasDirty, false);
|
||||
assert.equal(onDidChangeDirty.length, 2);
|
||||
assert.equal(onDidChangeDirty[1], copy1);
|
||||
|
||||
unregister1.dispose();
|
||||
|
||||
assert.equal(onDidUnregister.length, 1);
|
||||
assert.equal(onDidUnregister[0], copy1);
|
||||
assert.equal(service.workingCopies.length, 0);
|
||||
|
||||
// resource 2
|
||||
const resource2 = URI.file('/some/folder/file-dirty.txt');
|
||||
const copy2 = new TestWorkingCopy(resource2, true);
|
||||
const unregister2 = service.registerWorkingCopy(copy2);
|
||||
|
||||
assert.equal(onDidRegister.length, 2);
|
||||
assert.equal(onDidRegister[1], copy2);
|
||||
assert.equal(service.dirtyCount, 1);
|
||||
assert.equal(service.isDirty(resource2), true);
|
||||
assert.equal(service.hasDirty, true);
|
||||
|
||||
assert.equal(onDidChangeDirty.length, 3);
|
||||
assert.equal(onDidChangeDirty[2], copy2);
|
||||
|
||||
copy2.setContent('foo');
|
||||
|
||||
assert.equal(onDidChangeContent.length, 2);
|
||||
assert.equal(onDidChangeContent[1], copy2);
|
||||
|
||||
unregister2.dispose();
|
||||
|
||||
assert.equal(onDidUnregister.length, 2);
|
||||
assert.equal(onDidUnregister[1], copy2);
|
||||
assert.equal(service.dirtyCount, 0);
|
||||
assert.equal(service.hasDirty, false);
|
||||
assert.equal(onDidChangeDirty.length, 4);
|
||||
assert.equal(onDidChangeDirty[3], copy2);
|
||||
});
|
||||
|
||||
test('registry - multiple copies on same resource throws', () => {
|
||||
const service = new TestWorkingCopyService();
|
||||
|
||||
const onDidChangeDirty: IWorkingCopy[] = [];
|
||||
service.onDidChangeDirty(copy => onDidChangeDirty.push(copy));
|
||||
|
||||
const resource = URI.parse('custom://some/folder/custom.txt');
|
||||
|
||||
const copy1 = new TestWorkingCopy(resource);
|
||||
service.registerWorkingCopy(copy1);
|
||||
|
||||
const copy2 = new TestWorkingCopy(resource);
|
||||
|
||||
assert.throws(() => service.registerWorkingCopy(copy2));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user