Skip to content

Commit

Permalink
Implement file system watcher plugin API
Browse files Browse the repository at this point in the history
Signed-off-by: Mykola Morhun <mmorhun@redhat.com>
  • Loading branch information
mmorhun committed Nov 22, 2018
1 parent 190b034 commit 18d3fe1
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 5 deletions.
15 changes: 15 additions & 0 deletions packages/plugin-ext/src/api/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ export interface SerializedDocumentFilter {
pattern?: theia.GlobPattern;
}

export interface FileWatcherSubscriberOptions {
globPattern: theia.GlobPattern;
ignoreCreateEvents?: boolean;
ignoreChangeEvents?: boolean;
ignoreDeleteEvents?: boolean;
}

export interface FileChangeEvent {
subscriberId: string,
uri: UriComponents,
type: FileChangeEventType
}

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

export enum CompletionTriggerKind {
Invoke = 0,
TriggerCharacter = 1,
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-ext/src/api/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import {
DocumentSymbol,
ReferenceContext,
Location,
FileWatcherSubscriberOptions,
FileChangeEvent,
TextDocumentShowOptions
} from './model';

Expand Down Expand Up @@ -326,11 +328,14 @@ export interface WorkspaceMain {
$registerTextDocumentContentProvider(scheme: string): Promise<void>;
$unregisterTextDocumentContentProvider(scheme: string): void;
$onTextDocumentContentChange(uri: string, content: string): void;
$registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise<string>;
$unregisterFileSystemWatcher(watcherId: string): Promise<void>;
}

export interface WorkspaceExt {
$onWorkspaceFoldersChanged(event: theia.WorkspaceFoldersChangeEvent): void;
$provideTextDocumentContent(uri: string): Promise<string | undefined>;
$fileChanged(event: FileChangeEvent): void;
}

export interface DialogsMain {
Expand Down
14 changes: 13 additions & 1 deletion packages/plugin-ext/src/common/uri-components.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
Expand All @@ -14,6 +13,9 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import URI from '@theia/core/lib/common/uri';

export interface UriComponents {
scheme: string;
authority: string;
Expand All @@ -28,3 +30,13 @@ export namespace Schemes {
export const File = 'file';
export const Untitled = 'untitled';
}

export function theiaUritoUriComponents(uri: URI): UriComponents {
return {
scheme: uri.scheme,
authority: uri.authority,
path: uri.path.toString(),
query: uri.query,
fragment: uri.fragment
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { interfaces } from 'inversify';
import { FileSystemWatcher, FileChangeEvent, FileChangeType, FileChange } from '@theia/filesystem/lib/browser/filesystem-watcher';
import { WorkspaceExt } from '../../api/plugin-api';
import { FileWatcherSubscriberOptions } from '../../api/model';
import { parse, ParsedPattern, IRelativePattern } from '../../common/glob';
import { RelativePattern } from '../../plugin/types-impl';
import { theiaUritoUriComponents } from '../../common/uri-components';

/**
* Actual implementation of file watching system of the plugin API.
* Holds subscriptions (with its settings) from within plugins
* and process all file system events in all workspace roots whether they matches to any subscription.
* Only if event matches it will be sent into plugin side to specific subscriber.
*/
export class InPluginFileSystemWatcherManager {

private proxy: WorkspaceExt;
private subscribers: Map<string, FileWatcherSubscriber>;
private nextSubscriberId: number;

constructor(proxy: WorkspaceExt, container: interfaces.Container) {
this.proxy = proxy;
this.subscribers = new Map<string, FileWatcherSubscriber>();
this.nextSubscriberId = 0;

const fileSystemWatcher = container.get(FileSystemWatcher);
fileSystemWatcher.onFilesChanged(event => this.onFilesChangedEventHandler(event));
}

// Filter file system changes according to subscribers settings here to avoid unneeded traffic.
onFilesChangedEventHandler(changes: FileChangeEvent): void {
for (const change of changes) {
switch (change.type) {
case FileChangeType.UPDATED:
for (const [id, subscriber] of this.subscribers) {
if (!subscriber.ignoreChangeEvents && this.uriMatches(subscriber, change)) {
this.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'updated' });
}
}
break;
case FileChangeType.ADDED:
for (const [id, subscriber] of this.subscribers) {
if (!subscriber.ignoreCreateEvents && this.uriMatches(subscriber, change)) {
this.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'created' });
}
}
break;
case FileChangeType.DELETED:
for (const [id, subscriber] of this.subscribers) {
if (!subscriber.ignoreDeleteEvents && this.uriMatches(subscriber, change)) {
this.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'deleted' });
}
}
break;
}
}
}

private uriMatches(subscriber: FileWatcherSubscriber, fileChange: FileChange): boolean {
return subscriber.mather(fileChange.uri.path.toString());
}

/**
* Registers new file system events subscriber.
*
* @param options subscription options
* @returns generated subscriber id
*/
registerFileWatchSubscription(options: FileWatcherSubscriberOptions): string {
const subscriberId = this.getNextId();

let globPatternMatcher: ParsedPattern;
if (typeof options.globPattern === 'string') {
globPatternMatcher = parse(options.globPattern);
} else {
const relativePattern: IRelativePattern = new RelativePattern(options.globPattern.base, options.globPattern.pattern);
globPatternMatcher = parse(relativePattern);
}

const subscriber: FileWatcherSubscriber = {
id: subscriberId,
mather: globPatternMatcher,
ignoreCreateEvents: options.ignoreCreateEvents === true,
ignoreChangeEvents: options.ignoreChangeEvents === true,
ignoreDeleteEvents: options.ignoreDeleteEvents === true
};
this.subscribers.set(subscriberId, subscriber);

return subscriberId;
}

unregisterFileWatchSubscription(subscriptionId: string): void {
this.subscribers.delete(subscriptionId);
}

private getNextId(): string {
return 'ipfsw' + this.nextSubscriberId++;
}

}

interface FileWatcherSubscriber {
id: string;
mather: ParsedPattern;
ignoreCreateEvents: boolean;
ignoreChangeEvents: boolean;
ignoreDeleteEvents: boolean;
}
15 changes: 15 additions & 0 deletions packages/plugin-ext/src/main/browser/workspace-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { FileSearchService } from '@theia/file-search/lib/common/file-search-ser
import URI from '@theia/core/lib/common/uri';
import { Resource } from '@theia/core/lib/common/resource';
import { Emitter, Event, Disposable, ResourceResolver } from '@theia/core';
import { FileWatcherSubscriberOptions } from '../../api/model';
import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager';

export class WorkspaceMainImpl implements WorkspaceMain {

Expand All @@ -38,6 +40,8 @@ export class WorkspaceMainImpl implements WorkspaceMain {

private fileSearchService: FileSearchService;

private inPluginFileSystemWatcherManager: InPluginFileSystemWatcherManager;

private roots: FileStat[];

private resourceResolver: TextContentResourceResolver;
Expand All @@ -49,6 +53,8 @@ export class WorkspaceMainImpl implements WorkspaceMain {
this.fileSearchService = container.get(FileSearchService);
this.resourceResolver = container.get(TextContentResourceResolver);

this.inPluginFileSystemWatcherManager = new InPluginFileSystemWatcherManager(this.proxy, container);

workspaceService.roots.then(roots => {
this.roots = roots;
this.notifyWorkspaceFoldersChanged();
Expand Down Expand Up @@ -163,6 +169,15 @@ export class WorkspaceMainImpl implements WorkspaceMain {
});
}

$registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise<string> {
return Promise.resolve(this.inPluginFileSystemWatcherManager.registerFileWatchSubscription(options));
}

$unregisterFileSystemWatcher(watcherId: string): Promise<void> {
this.inPluginFileSystemWatcherManager.unregisterFileWatchSubscription(watcherId);
return Promise.resolve();
}

async $registerTextDocumentContentProvider(scheme: string): Promise<void> {
return this.resourceResolver.registerContentProvider(scheme, this.proxy);
}
Expand Down
150 changes: 150 additions & 0 deletions packages/plugin-ext/src/plugin/in-plugin-filesystem-watcher-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as theia from '@theia/plugin';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { WorkspaceMain } from '../api/plugin-api';
import { FileWatcherSubscriberOptions, FileChangeEventType } from '../api/model';
import URI from 'vscode-uri';

/**
* This class is responsible for file watchers subscription registering and file system events proxying.
* It contains no logic, only communicates with main side to add / remove subscription and
* delivers file system events to corresponding subscribers.
*/
export class InPluginFileSystemWatcherProxy {

private proxy: WorkspaceMain;
private subscribers: Map<string, Emitter<FileSystemEvent>>;

constructor(proxy: WorkspaceMain) {
this.proxy = proxy;
this.subscribers = new Map<string, Emitter<FileSystemEvent>>();
}

createFileSystemWatcher(
globPattern: theia.GlobPattern,
ignoreCreateEvents?: boolean,
ignoreChangeEvents?: boolean,
ignoreDeleteEvents?: boolean): theia.FileSystemWatcher {

const perSubscriberEventEmitter = new Emitter<FileSystemEvent>();
const subscriberPrivateData: SubscriberData = {
event: perSubscriberEventEmitter.event
};
const fileWatcherSubscriberOptions: FileWatcherSubscriberOptions = { globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents };
// ids are generated by server side to be able handle several subscribers.
this.proxy.$registerFileSystemWatcher(fileWatcherSubscriberOptions).then((id: string) => {
// this is safe, because actual subscription happens on server side and response is
// sent right after actual subscription, so no events are possible in between.
this.subscribers.set(id, perSubscriberEventEmitter);
subscriberPrivateData.unsubscribe = () => this.proxy.$unregisterFileSystemWatcher(id);
});
return new FileSystemWatcher(subscriberPrivateData, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents);
}

onFileSystemEvent(id: string, uri: URI, type: FileChangeEventType) {
const perSubscriberEventEmitter: Emitter<FileSystemEvent> | undefined = this.subscribers.get(id);
if (perSubscriberEventEmitter) {
perSubscriberEventEmitter.fire({ uri, type });
} else {
// shouldn't happen
// if it happens then a message was lost, unsubscribe to make state consistent
this.proxy.$unregisterFileSystemWatcher(id);
}
}
}

class FileSystemWatcher implements theia.FileSystemWatcher {
private subscriberData: SubscriberData;

private onDidCreateEmitter: Emitter<theia.Uri>;
private onDidChangeEmitter: Emitter<theia.Uri>;
private onDidDeleteEmitter: Emitter<theia.Uri>;

constructor(
subscriberData: SubscriberData,
private isIgnoreCreateEvents: boolean = false,
private isIgnoreChangeEvents: boolean = false,
private isIgnoreDeleteEvents: boolean = false
) {
this.onDidCreateEmitter = new Emitter<theia.Uri>();
this.onDidChangeEmitter = new Emitter<theia.Uri>();
this.onDidDeleteEmitter = new Emitter<theia.Uri>();

this.subscriberData = subscriberData;
subscriberData.event((event: FileSystemEvent) => {
// Here ignore event flags are not analyzed because all the logic is
// moved to server side to avoid unneded data transfer via network.
// The flags are present just to be read only accesible for user.
switch (event.type) {
case 'updated':
this.onDidChangeEmitter.fire(event.uri);
break;
case 'created':
this.onDidCreateEmitter.fire(event.uri);
break;
case 'deleted':
this.onDidDeleteEmitter.fire(event.uri);
break;
}
});
}

get ignoreCreateEvents(): boolean {
return this.isIgnoreCreateEvents;
}

get ignoreChangeEvents(): boolean {
return this.isIgnoreChangeEvents;
}

get ignoreDeleteEvents(): boolean {
return this.isIgnoreDeleteEvents;
}

get onDidCreate(): Event<theia.Uri> {
return this.onDidCreateEmitter.event;
}

get onDidChange(): Event<theia.Uri> {
return this.onDidChangeEmitter.event;
}

get onDidDelete(): Event<theia.Uri> {
return this.onDidDeleteEmitter.event;
}

dispose(): void {
this.onDidCreateEmitter.dispose();
this.onDidChangeEmitter.dispose();
this.onDidDeleteEmitter.dispose();
if (this.subscriberData.unsubscribe) {
this.subscriberData.unsubscribe();
}
}

}

interface FileSystemEvent {
uri: URI,
type: FileChangeEventType
}

interface SubscriberData {
event: Event<FileSystemEvent>
unsubscribe?: () => void;
}
Loading

0 comments on commit 18d3fe1

Please sign in to comment.