diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts index a8f495b38f4dc87ce4066a024987d36efd03f15f..8dad613683cb1d6174cd07aba1d0451eff0b2ac6 100644 --- a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -57,7 +57,8 @@ class UpdateImportsOnFileRenameHandler extends Disposable { ) { super(); - this._register(vscode.workspace.onDidRenameFile(async ({ newUri, oldUri }) => { + this._register(vscode.workspace.onDidRenameFiles(async (e) => { + const [{ newUri, oldUri }] = e.renamed; const newFilePath = this.client.toPath(newUri); if (!newFilePath) { return; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 787b40ec47700c3415a8fccbe9fc37a496c05602..b85073b6786e1a02dd96ad57a075cf91c425dffc 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -688,7 +688,7 @@ export class AsyncEmitter extends Emitter { // freeze thenables-collection to enforce sync-calls to // wait until and then wait for all thenables to resolve Object.freeze(thenables); - await Promise.all(thenables); + await Promise.all(thenables).catch(e => onUnexpectedError(e)); } } } diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 09f3f6eec81c0cba8868d9e3a0448217a59046b1..25d47dc6e00a899702717e91b194812894a75ff6 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -350,6 +350,42 @@ suite('AsyncEmitter', function () { })); assert.ok(done); }); + + test('catch errors', async function () { + const origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); + Errors.setUnexpectedErrorHandler(() => null); + + interface E extends IWaitUntil { + foo: boolean; + } + + let globalState = 0; + let emitter = new AsyncEmitter(); + + emitter.event(e => { + globalState += 1; + e.waitUntil(new Promise((_r, reject) => reject(new Error()))); + }); + + emitter.event(e => { + globalState += 1; + e.waitUntil(timeout(10)); + }); + + await emitter.fireAsync(thenables => ({ + foo: true, + waitUntil(t) { + thenables.push(t); + } + })).then(() => { + assert.equal(globalState, 2); + }).catch(e => { + console.log(e); + assert.ok(false); + }); + + Errors.setUnexpectedErrorHandler(origErrorHandler); + }); }); suite('PausableEmitter', function () { diff --git a/src/vs/platform/files/test/node/diskFileService.test.ts b/src/vs/platform/files/test/node/diskFileService.test.ts index f322bf90872f4da403f90b72196da410f539784b..32cbb18e545820840c06aad26778a657bc11b76d 100644 --- a/src/vs/platform/files/test/node/diskFileService.test.ts +++ b/src/vs/platform/files/test/node/diskFileService.test.ts @@ -428,6 +428,7 @@ suite('Disk File Service', function () { await service.del(source.resource); assert.equal(existsSync(source.resource.fsPath), false); + assert.ok(event!); assert.equal(event!.resource.fsPath, resource.fsPath); assert.equal(event!.operation, FileOperation.DELETE); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 22067687355fd0902d151ca14b65cd98345b3121..0000ccf457c5dc805b59b71a96d9546fd09832fe 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -708,20 +708,45 @@ declare module 'vscode' { //#endregion //#region mjbvz,joh: https://github.com/Microsoft/vscode/issues/43768 + + export interface FileCreateEvent { + readonly created: ReadonlyArray; + } + + export interface FileWillCreateEvent { + readonly creating: ReadonlyArray; + waitUntil(thenable: Thenable): void; + } + + export interface FileDeleteEvent { + readonly deleted: ReadonlyArray; + } + + export interface FileWillDeleteEvent { + readonly deleting: ReadonlyArray; + waitUntil(thenable: Thenable): void; + } + export interface FileRenameEvent { - readonly oldUri: Uri; - readonly newUri: Uri; + readonly renamed: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; } export interface FileWillRenameEvent { - readonly oldUri: Uri; - readonly newUri: Uri; + readonly renaming: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; waitUntil(thenable: Thenable): void; } export namespace workspace { - export const onWillRenameFile: Event; - export const onDidRenameFile: Event; + + export const onWillCreateFiles: Event; + export const onDidCreateFiles: Event; + + export const onWillDeleteFiles: Event; + export const onDidDeleteFiles: Event; + + export const onWillRenameFiles: Event; + export const onDidRenameFiles: Event; + } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index e35329a9dbab9f59961ae6e5c00dbafc2f5b886e..295bd0d8c9ea957f6fea93888fa19aa46e2afa31 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -3,21 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileChangeType, IFileService, FileOperation } from 'vs/platform/files/common/files'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { localize } from 'vs/nls'; @extHostCustomer export class MainThreadFileSystemEventService { - private readonly _listener = new Array(); + private readonly _listener = new DisposableStore(); constructor( extHostContext: IExtHostContext, @IFileService fileService: IFileService, - @ITextFileService textfileService: ITextFileService, + @ITextFileService textFileService: ITextFileService, + @IProgressService progressService: IProgressService ) { const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); @@ -28,7 +31,7 @@ export class MainThreadFileSystemEventService { changed: [], deleted: [] }; - fileService.onFileChanges(event => { + this._listener.add(fileService.onFileChanges(event => { for (let change of event.changes) { switch (change.type) { case FileChangeType.ADDED: @@ -47,22 +50,35 @@ export class MainThreadFileSystemEventService { events.created.length = 0; events.changed.length = 0; events.deleted.length = 0; - }, undefined, this._listener); + })); - // file operation events - (changes the editor makes) - fileService.onAfterOperation(e => { - if (e.isOperation(FileOperation.MOVE)) { - proxy.$onFileRename(e.resource, e.target.resource); - } - }, undefined, this._listener); - textfileService.onWillMove(e => { - const promise = proxy.$onWillRename(e.oldResource, e.newResource); - e.waitUntil(promise); - }, undefined, this._listener); + // BEFORE file operation + const messages = new Map(); + messages.set(FileOperation.CREATE, localize('msg-create', "Running 'File Create' participants...")); + messages.set(FileOperation.DELETE, localize('msg-delete', "Running 'File Delete' participants...")); + messages.set(FileOperation.MOVE, localize('msg-rename', "Running 'File Rename' participants...")); + + this._listener.add(textFileService.onWillRunOperation(e => { + const p = progressService.withProgress({ location: ProgressLocation.Window }, progress => { + + progress.report({ message: messages.get(e.operation) }); + + const p1 = proxy.$onWillRunFileOperation(e.operation, e.target, e.source); + const p2 = new Promise((_resolve, reject) => { + setTimeout(() => reject(new Error('timeout')), 5000); + }); + return Promise.race([p1, p2]); + }); + + e.waitUntil(p); + })); + + // AFTER file operation + this._listener.add(textFileService.onDidRunOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target, e.source))); } dispose(): void { - dispose(this._listener); + this._listener.dispose(); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5b5a76c6fc0fc0f6d47ae9248572b767e7d90d6a..2bb2f6ef38ca1daf03073c638ee96d79c4662b8b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -695,11 +695,27 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostLabelService.$registerResourceLabelFormatter(formatter); }, - onDidRenameFile: (listener: (e: vscode.FileRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + onDidCreateFiles: (listener, thisArg, disposables) => { + checkProposedApiEnabled(extension); + return extHostFileSystemEvent.onDidCreateFile(listener, thisArg, disposables); + }, + onDidDeleteFiles: (listener, thisArg, disposables) => { + checkProposedApiEnabled(extension); + return extHostFileSystemEvent.onDidDeleteFile(listener, thisArg, disposables); + }, + onDidRenameFiles: (listener, thisArg, disposables) => { checkProposedApiEnabled(extension); return extHostFileSystemEvent.onDidRenameFile(listener, thisArg, disposables); }, - onWillRenameFile: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + onWillCreateFiles: (listener: (e: vscode.FileWillCreateEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + checkProposedApiEnabled(extension); + return extHostFileSystemEvent.getOnWillCreateFileEvent(extension)(listener, thisArg, disposables); + }, + onWillDeleteFiles: (listener: (e: vscode.FileWillDeleteEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { + checkProposedApiEnabled(extension); + return extHostFileSystemEvent.getOnWillDeleteFileEvent(extension)(listener, thisArg, disposables); + }, + onWillRenameFiles: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { checkProposedApiEnabled(extension); return extHostFileSystemEvent.getOnWillRenameFileEvent(extension)(listener, thisArg, disposables); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index acb3246bef7ca1e66736da8b6a1d67f91ae6b8ff..6d218ee0395115a74e8c2bc89b93f31050dc62ff 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -914,10 +914,11 @@ export interface FileSystemEvents { changed: UriComponents[]; deleted: UriComponents[]; } + export interface ExtHostFileSystemEventServiceShape { $onFileEvent(events: FileSystemEvents): void; - $onFileRename(oldUri: UriComponents, newUri: UriComponents): void; - $onWillRename(oldUri: UriComponents, newUri: UriComponents): Promise; + $onWillRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): Promise; + $onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void; } export interface ObjectIdentifier { diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 8658c24bb14278da13d96f073821ff3ae7d656b5..bb0e51045a3f92ed6d155e0443151c0fd3370ade 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { flatten } from 'vs/base/common/arrays'; -import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; +import { AsyncEmitter, Emitter, Event, IWaitUntil } from 'vs/base/common/event'; import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -13,6 +13,7 @@ import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, Mai import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { FileOperation } from 'vs/platform/files/common/files'; class FileSystemWatcher implements vscode.FileSystemWatcher { @@ -96,18 +97,26 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } } -interface WillRenameListener { +interface IExtensionListener { extension: IExtensionDescription; - (e: vscode.FileWillRenameEvent): any; + (e: E): any; } export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape { - private readonly _onFileEvent = new Emitter(); + private readonly _onFileSystemEvent = new Emitter(); + private readonly _onDidRenameFile = new Emitter(); + private readonly _onDidCreateFile = new Emitter(); + private readonly _onDidDeleteFile = new Emitter(); private readonly _onWillRenameFile = new AsyncEmitter(); + private readonly _onWillCreateFile = new AsyncEmitter(); + private readonly _onWillDeleteFile = new AsyncEmitter(); readonly onDidRenameFile: Event = this._onDidRenameFile.event; + readonly onDidCreateFile: Event = this._onDidCreateFile.event; + readonly onDidDeleteFile: Event = this._onDidDeleteFile.event; + constructor( mainContext: IMainContext, @@ -117,37 +126,78 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ // } - public createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { - return new FileSystemWatcher(this._onFileEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); + //--- file events + + createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { + return new FileSystemWatcher(this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); } $onFileEvent(events: FileSystemEvents) { - this._onFileEvent.fire(events); + this._onFileSystemEvent.fire(events); } - $onFileRename(oldUri: UriComponents, newUri: UriComponents) { - this._onDidRenameFile.fire(Object.freeze({ oldUri: URI.revive(oldUri), newUri: URI.revive(newUri) })); + + //--- file operations + + $onDidRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): void { + switch (operation) { + case FileOperation.MOVE: + this._onDidRenameFile.fire(Object.freeze({ renamed: [{ oldUri: URI.revive(target), newUri: URI.revive(source!) }] })); + break; + case FileOperation.DELETE: + this._onDidDeleteFile.fire(Object.freeze({ deleted: [URI.revive(target)] })); + break; + case FileOperation.CREATE: + this._onDidCreateFile.fire(Object.freeze({ created: [URI.revive(target)] })); + break; + default: + //ignore, dont send + } } + getOnWillRenameFileEvent(extension: IExtensionDescription): Event { + return this._createWillExecuteEvent(extension, this._onWillRenameFile); + } + + getOnWillCreateFileEvent(extension: IExtensionDescription): Event { + return this._createWillExecuteEvent(extension, this._onWillCreateFile); + } + + getOnWillDeleteFileEvent(extension: IExtensionDescription): Event { + return this._createWillExecuteEvent(extension, this._onWillDeleteFile); + } + + private _createWillExecuteEvent(extension: IExtensionDescription, emitter: AsyncEmitter): Event { return (listener, thisArg, disposables) => { - const wrappedListener: WillRenameListener = ((e: vscode.FileWillRenameEvent) => { - listener.call(thisArg, e); - }); + const wrappedListener: IExtensionListener = function wrapped(e: E) { listener.call(thisArg, e); }; wrappedListener.extension = extension; - return this._onWillRenameFile.event(wrappedListener, undefined, disposables); + return emitter.event(wrappedListener, undefined, disposables); }; } - $onWillRename(oldUriDto: UriComponents, newUriDto: UriComponents): Promise { - const oldUri = URI.revive(oldUriDto); - const newUri = URI.revive(newUriDto); + async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): Promise { + switch (operation) { + case FileOperation.MOVE: + await this._fireWillRename(URI.revive(target), URI.revive(source!)); + break; + case FileOperation.DELETE: + this._onWillDeleteFile.fireAsync(thenables => ({ deleting: [URI.revive(target)], waitUntil: p => thenables.push(Promise.resolve(p)) })); + break; + case FileOperation.CREATE: + this._onWillCreateFile.fireAsync(thenables => ({ creating: [URI.revive(target)], waitUntil: p => thenables.push(Promise.resolve(p)) })); + break; + default: + //ignore, dont send + } + } + + private async _fireWillRename(oldUri: URI, newUri: URI): Promise { const edits: WorkspaceEdit[] = []; - return Promise.resolve(this._onWillRenameFile.fireAsync((bucket, _listener) => { + await Promise.resolve(this._onWillRenameFile.fireAsync(bucket => { return { - oldUri, - newUri, + renaming: [{ oldUri, newUri }], waitUntil: (thenable: Promise): void => { if (Object.isFrozen(bucket)) { throw new TypeError('waitUntil cannot be called async'); @@ -163,20 +213,23 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ bucket.push(wrappedThenable); } }; - }).then((): any => { - if (edits.length === 0) { - return undefined; - } - // flatten all WorkspaceEdits collected via waitUntil-call - // and apply them in one go. - const allEdits = new Array>(); - for (let edit of edits) { - if (edit) { // sparse array - let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - allEdits.push(edits); - } - } - return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); })); + + if (edits.length === 0) { + return undefined; + } + + // flatten all WorkspaceEdits collected via waitUntil-call + // and apply them in one go. + const allEdits = new Array>(); + for (let edit of edits) { + if (edit) { // sparse array + let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + allEdits.push(edits); + } + } + return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); } + + } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 5c49277fda4485a00f35e7ea2c4a1aa979fa8ae6..dd23e60c1c50248797643a99d73a9086547341b5 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -5,16 +5,15 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import * as errors from 'vs/base/common/errors'; import * as objects from 'vs/base/common/objects'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event, Emitter, AsyncEmitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; +import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IFileService, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions } from 'vs/platform/files/common/files'; +import { IFileService, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -53,8 +52,13 @@ export abstract class AbstractTextFileService extends Disposable implements ITex private readonly _onFilesAssociationChange: Emitter = this._register(new Emitter()); readonly onFilesAssociationChange: Event = this._onFilesAssociationChange.event; - private readonly _onWillMove = this._register(new Emitter()); - readonly onWillMove: Event = this._onWillMove.event; + + private _onWillRunOperation = this._register(new AsyncEmitter()); + readonly onWillRunOperation = this._onWillRunOperation.event; + + private _onDidRunOperation = this._register(new Emitter()); + readonly onDidRunOperation = this._onDidRunOperation.event; + private _models: TextFileEditorModelManager; get models(): ITextFileEditorModelManager { return this._models; } @@ -409,6 +413,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise { + + // before event + await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.CREATE, resource)); + const stat = await this.doCreate(resource, value, options); // If we had an existing model for the given resource, load @@ -420,6 +428,9 @@ export abstract class AbstractTextFileService extends Disposable implements ITex await existingModel.revert(); } + // after event + this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.CREATE, resource)); + return stat; } @@ -432,17 +443,20 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { - const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource)); + await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.DELETE, resource)); + + const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource)); await this.revertAll(dirtyFiles, { soft: true }); - return this.fileService.del(resource, options); + await this.fileService.del(resource, options); + this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.DELETE, resource)); } async move(source: URI, target: URI, overwrite?: boolean): Promise { - // await onWillMove event joiners - await this.notifyOnWillMove(source, target); + // before events + await this._onWillRunOperation.fireAsync(promises => new FileOperationWillRunEvent(promises, FileOperation.MOVE, target, source)); // find all models that related to either source or target (can be many if resource is a folder) const sourceModels: ITextFileEditorModel[] = []; @@ -521,25 +535,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } })); - return stat; - } - - private async notifyOnWillMove(source: URI, target: URI): Promise { - const waitForPromises: Promise[] = []; - - // fire event - this._onWillMove.fire({ - oldResource: source, - newResource: target, - waitUntil(promise: Promise) { - waitForPromises.push(promise.then(undefined, errors.onUnexpectedError)); - } - }); + // after event + this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.MOVE, target, source)); - // prevent async waitUntil-calls - Object.freeze(waitForPromises); - - await Promise.all(waitForPromises); + return stat; } //#endregion diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 8d6aef56f94466451460b2e66d4989fa201feb5d..5a752b201286bba6442fd474f272c772daeeaede 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Event } from 'vs/base/common/event'; +import { Event, IWaitUntil } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, ConfirmResult, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor'; -import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult, FileOperation } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; @@ -22,14 +22,22 @@ export interface ITextFileService extends IDisposable { _serviceBrand: undefined; - readonly onWillMove: Event; - readonly onAutoSaveConfigurationChange: Event; readonly onFilesAssociationChange: Event; readonly isHotExitEnabled: boolean; + /** + * An event that is fired before attempting a certain file operation. + */ + readonly onWillRunOperation: Event; + + /** + * An event that is fired after a file operation has been performed. + */ + readonly onDidRunOperation: Event; + /** * Access to the manager of text file editor models providing further methods to work with them. */ @@ -147,6 +155,35 @@ export interface ITextFileService extends IDisposable { getAutoSaveConfiguration(): IAutoSaveConfiguration; } + +export class FileOperationWillRunEvent implements IWaitUntil { + + constructor( + private _thenables: Promise[], + readonly operation: FileOperation, + readonly target: URI, + readonly source?: URI | undefined + ) { } + + waitUntil(thenable: Promise): void { + if (Object.isFrozen(this._thenables)) { + throw new Error('waitUntil cannot be used aync'); + } + this._thenables.push(thenable); + } +} + + +export class FileOperationDidRunEvent { + + constructor( + readonly operation: FileOperation, + readonly target: URI, + readonly source?: URI | undefined + ) { } +} + + export interface IReadTextFileOptions extends IReadFileOptions { /** @@ -492,13 +529,6 @@ export interface IResolvedTextFileEditorModel extends ITextFileEditorModel { createSnapshot(): ITextSnapshot; } -export interface IWillMoveEvent { - oldResource: URI; - newResource: URI; - - waitUntil(p: Promise): void; -} - /** * Helper method to convert a snapshot into its full string form. */