Skip to content

Commit

Permalink
plugin: add workspace file api
Browse files Browse the repository at this point in the history
Add `on(will|did)(create|rename|delete)Files` to the plugin's workspace
api. This implementation does not handle the `WorkspaceEdit` apis.

Signed-off-by: Paul Maréchal <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed May 14, 2020
1 parent 8ca3e64 commit 1606e55
Show file tree
Hide file tree
Showing 15 changed files with 517 additions and 143 deletions.
34 changes: 18 additions & 16 deletions packages/core/src/common/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,8 @@ export class Emitter<T = any> {

return result;
}, {
maxListeners: Emitter.LEAK_WARNING_THRESHHOLD
}
);
maxListeners: Emitter.LEAK_WARNING_THRESHHOLD
});
}
return this._event;
}
Expand Down Expand Up @@ -296,7 +295,6 @@ export class Emitter<T = any> {
}

export interface WaitUntilEvent {
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Allows to pause the event loop until the provided thenable resolved.
*
Expand All @@ -305,24 +303,28 @@ export interface WaitUntilEvent {
* @param thenable A thenable that delays execution.
*/
waitUntil(thenable: Promise<any>): void;
/* eslint-enable @typescript-eslint/no-explicit-any */
}
export namespace WaitUntilEvent {
export async function fire<T extends WaitUntilEvent>(
emitter: Emitter<T>,
event: Pick<T, Exclude<keyof T, 'waitUntil'>>,
timeout: number | undefined = undefined
): Promise<void> {
const waitables: Promise<void>[] = [];
/**
* Fires an event with a `waitUntil` field and handles its semantics on your behalf.
*
* @param emitter
* @param event
* @param timeout
* @returns returned values of promises passed to `waitUntil`, `undefined` on timeout.
*/
export async function fire<E extends WaitUntilEvent>(emitter: Emitter<E>, event: Omit<E, 'waitUntil'>): Promise<any[]>;
export async function fire<E extends WaitUntilEvent>(emitter: Emitter<E>, event: Omit<E, 'waitUntil'>, timeout: number): Promise<any[] | undefined>;
export async function fire<E extends WaitUntilEvent>(emitter: Emitter<E>, event: Omit<E, 'waitUntil'>, timeout?: number): Promise<any[] | undefined> {
const waitables: Promise<any>[] = [];
const asyncEvent = Object.assign(event, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
waitUntil: (thenable: Promise<any>) => {
if (Object.isFrozen(waitables)) {
throw new Error('waitUntil cannot be called asynchronously.');
}
waitables.push(thenable);
}
}) as T;
}) as E;
try {
emitter.fire(asyncEvent);
// Asynchronous calls to `waitUntil` should fail.
Expand All @@ -331,12 +333,12 @@ export namespace WaitUntilEvent {
delete asyncEvent['waitUntil'];
}
if (!waitables.length) {
return;
return [];
}
if (timeout !== undefined) {
await Promise.race([Promise.all(waitables), new Promise(resolve => setTimeout(resolve, timeout))]);
return Promise.race([Promise.all(waitables), new Promise<undefined>(resolve => setTimeout(resolve, timeout, undefined))]);
} else {
await Promise.all(waitables);
return Promise.all(waitables);
}
}
}
36 changes: 27 additions & 9 deletions packages/filesystem/src/browser/filesystem-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,27 @@ export class FileOperationEmitter<E extends WaitUntilEvent> implements Disposabl
this.toDispose.dispose();
}

async fireWill(event: Pick<E, Exclude<keyof E, 'waitUntil'>>): Promise<void> {
await WaitUntilEvent.fire(this.onWillEmitter, event);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async fireWill(event: Omit<E, 'waitUntil'>): Promise<any[]> {
return WaitUntilEvent.fire(this.onWillEmitter, event);
}

async fireDid(failed: boolean, event: Pick<E, Exclude<keyof E, 'waitUntil'>>): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fireDid(failed: boolean, event: Omit<E, 'waitUntil'>): Promise<any[]> {
const onDidEmitter = failed ? this.onDidFailEmitter : this.onDidEmitter;
await WaitUntilEvent.fire(onDidEmitter, event);
return WaitUntilEvent.fire(onDidEmitter, event);
}

}

/**
* React to file system events, including calls originating from the
* application or event coming from the system's filesystem directly
* (actual file watching).
*
* `on(will|did)(create|rename|delete)` events solely come from application
* usage, not from actual filesystem.
*/
@injectable()
export class FileSystemWatcher implements Disposable {

Expand All @@ -122,6 +132,11 @@ export class FileSystemWatcher implements Disposable {
protected readonly onFileChangedEmitter = new Emitter<FileChangeEvent>();
readonly onFilesChanged = this.onFileChangedEmitter.event;

protected readonly fileCreateEmitter = new FileOperationEmitter<FileEvent>();
readonly onWillCreate = this.fileCreateEmitter.onWill;
readonly onDidFailCreate = this.fileCreateEmitter.onDidFail;
readonly onDidCreate = this.fileCreateEmitter.onDid;

protected readonly fileDeleteEmitter = new FileOperationEmitter<FileEvent>();
readonly onWillDelete = this.fileDeleteEmitter.onWill;
readonly onDidFailDelete = this.fileDeleteEmitter.onDidFail;
Expand Down Expand Up @@ -164,11 +179,15 @@ export class FileSystemWatcher implements Disposable {
}));

this.filesystem.setClient({
/* eslint-disable no-void */
shouldOverwrite: this.shouldOverwrite.bind(this),
willDelete: uri => this.fileDeleteEmitter.fireWill({ uri: new URI(uri) }),
didDelete: (uri, failed) => this.fileDeleteEmitter.fireDid(failed, { uri: new URI(uri) }),
willMove: (source, target) => this.fileMoveEmitter.fireWill({ sourceUri: new URI(source), targetUri: new URI(target) }),
didMove: (source, target, failed) => this.fileMoveEmitter.fireDid(failed, { sourceUri: new URI(source), targetUri: new URI(target) })
willCreate: async uri => void await this.fileCreateEmitter.fireWill({ uri: new URI(uri) }),
didCreate: async (uri, failed) => void await this.fileCreateEmitter.fireDid(failed, { uri: new URI(uri) }),
willDelete: async uri => void await this.fileDeleteEmitter.fireWill({ uri: new URI(uri) }),
didDelete: async (uri, failed) => void await this.fileDeleteEmitter.fireDid(failed, { uri: new URI(uri) }),
willMove: async (sourceUri, targetUri) => void await this.fileMoveEmitter.fireWill({ sourceUri: new URI(sourceUri), targetUri: new URI(targetUri) }),
didMove: async (sourceUri, targetUri, failed) => void await this.fileMoveEmitter.fireDid(failed, { sourceUri: new URI(sourceUri), targetUri: new URI(targetUri) }),
/* eslint-enable no-void */
});
}

Expand Down Expand Up @@ -228,4 +247,3 @@ export class FileSystemWatcher implements Disposable {
}

}

23 changes: 18 additions & 5 deletions packages/filesystem/src/common/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,18 @@ export interface FileSystemClient {
*/
shouldOverwrite: FileShouldOverwrite;

willCreate(uri: string): Promise<void>;

didCreate(uri: string, failed: boolean): Promise<void>;

willDelete(uri: string): Promise<void>;

didDelete(uri: string, failed: boolean): Promise<void>;

willMove(sourceUri: string, targetUri: string): Promise<void>;

didMove(sourceUri: string, targetUri: string, failed: boolean): Promise<void>;

}

@injectable()
Expand All @@ -227,25 +232,33 @@ export class DispatchingFileSystemClient implements FileSystemClient {
readonly clients = new Set<FileSystemClient>();

shouldOverwrite(originalStat: FileStat, currentStat: FileStat): Promise<boolean> {
return Promise.race([...this.clients].map(client =>
return Promise.race(Array.from(this.clients, client =>
client.shouldOverwrite(originalStat, currentStat))
);
}

async willCreate(uri: string): Promise<void> {
await Promise.all(Array.from(this.clients, client => client.willCreate(uri)));
}

async didCreate(uri: string, failed: boolean): Promise<void> {
await Promise.all(Array.from(this.clients, client => client.didCreate(uri, failed)));
}

async willDelete(uri: string): Promise<void> {
await Promise.all([...this.clients].map(client => client.willDelete(uri)));
await Promise.all(Array.from(this.clients, client => client.willDelete(uri)));
}

async didDelete(uri: string, failed: boolean): Promise<void> {
await Promise.all([...this.clients].map(client => client.didDelete(uri, failed)));
await Promise.all(Array.from(this.clients, client => client.didDelete(uri, failed)));
}

async willMove(sourceUri: string, targetUri: string): Promise<void> {
await Promise.all([...this.clients].map(client => client.willMove(sourceUri, targetUri)));
await Promise.all(Array.from(this.clients, client => client.willMove(sourceUri, targetUri)));
}

async didMove(sourceUri: string, targetUri: string, failed: boolean): Promise<void> {
await Promise.all([...this.clients].map(client => client.didMove(sourceUri, targetUri, failed)));
await Promise.all(Array.from(this.clients, client => client.didMove(sourceUri, targetUri, failed)));
}

}
Expand Down
38 changes: 38 additions & 0 deletions packages/filesystem/src/node/node-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,25 @@ export class FileSystemNode implements FileSystem {
}

async createFile(uri: string, options?: { content?: string, encoding?: string }): Promise<FileStat> {
if (this.client) {
await this.client.willCreate(uri);
}
let result: FileStat;
let failed = false;
try {
result = await this.doCreateFile(uri, options);
} catch (e) {
failed = true;
throw e;
} finally {
if (this.client) {
await this.client.didCreate(uri, failed);
}
}
return result;
}

protected async doCreateFile(uri: string, options?: { content?: string, encoding?: string }): Promise<FileStat> {
const _uri = new URI(uri);
const parentUri = _uri.parent;
const [stat, parentStat] = await Promise.all([this.doGetStat(_uri, 0), this.doGetStat(parentUri, 0)]);
Expand All @@ -284,6 +303,25 @@ export class FileSystemNode implements FileSystem {
}

async createFolder(uri: string): Promise<FileStat> {
if (this.client) {
await this.client.willCreate(uri);
}
let result: FileStat;
let failed = false;
try {
result = await this.doCreateFolder(uri);
} catch (e) {
failed = true;
throw e;
} finally {
if (this.client) {
await this.client.didCreate(uri, failed);
}
}
return result;
}

async doCreateFolder(uri: string): Promise<FileStat> {
const _uri = new URI(uri);
const stat = await this.doGetStat(_uri, 0);
if (stat) {
Expand Down
24 changes: 12 additions & 12 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,6 @@ export interface FileChangeEvent {
type: FileChangeEventType
}

export interface FileMoveEvent {
subscriberId: string,
oldUri: UriComponents,
newUri: UriComponents
}

export interface FileWillMoveEvent {
subscriberId: string,
oldUri: UriComponents,
newUri: UriComponents
}

export type FileChangeEventType = 'created' | 'updated' | 'deleted';

export enum CompletionTriggerKind {
Expand Down Expand Up @@ -547,3 +535,15 @@ export interface CallHierarchyOutgoingCall {
to: CallHierarchyItem;
fromRanges: Range[];
}

export interface CreateFilesEventDTO {
files: UriComponents[]
}

export interface RenameFilesEventDTO {
files: { oldUri: UriComponents, newUri: UriComponents }[]
}

export interface DeleteFilesEventDTO {
files: UriComponents[]
}
16 changes: 11 additions & 5 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,17 @@ import {
Breakpoint,
ColorPresentation,
RenameLocation,
FileMoveEvent,
FileWillMoveEvent,
SignatureHelpContext,
CodeAction,
CodeActionContext,
FoldingContext,
FoldingRange,
SelectionRange,
CallHierarchyDefinition,
CallHierarchyReference
CallHierarchyReference,
CreateFilesEventDTO,
RenameFilesEventDTO,
DeleteFilesEventDTO,
} from './plugin-api-rpc-model';
import { ExtPluginApi } from './plugin-ext-api-contribution';
import { KeysToAnyValues, KeysToKeysToAnyValue } from './types';
Expand Down Expand Up @@ -506,8 +507,13 @@ export interface WorkspaceExt {
$onWorkspaceFoldersChanged(event: WorkspaceRootsChangeEvent): void;
$provideTextDocumentContent(uri: string): Promise<string | undefined>;
$fileChanged(event: FileChangeEvent): void;
$onFileRename(event: FileMoveEvent): void;
$onWillRename(event: FileWillMoveEvent): Promise<any>;

$onWillCreateFiles(event: CreateFilesEventDTO): Promise<any[]>;
$onDidCreateFiles(event: CreateFilesEventDTO): void;
$onWillRenameFiles(event: RenameFilesEventDTO): Promise<any[]>;
$onDidRenameFiles(event: RenameFilesEventDTO): void;
$onWillDeleteFiles(event: DeleteFilesEventDTO): Promise<any[]>;
$onDidDeleteFiles(event: DeleteFilesEventDTO): void;
}

export interface DialogsMain {
Expand Down
Loading

0 comments on commit 1606e55

Please sign in to comment.