Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}
});

View File

@@ -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));
});
});