From 2aa549d5c24f74e0dccb00e98f54adc32e6b8810 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 10 Jul 2017 15:25:48 +0200 Subject: [PATCH 1/5] starting with remote file service --- .../workbench/electron-browser/workbench.ts | 5 +- .../files/electron-browser/fileService.ts | 4 +- .../electron-browser/remoteFileService.ts | 144 ++++++++++++++++++ 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/services/files/electron-browser/remoteFileService.ts diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index c89a3a7f421de..a56a3bc70e572 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -57,7 +57,8 @@ import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from ' import { IActivityBarService } from 'vs/workbench/services/activity/common/activityBarService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ViewletService } from 'vs/workbench/services/viewlet/browser/viewletService'; -import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; +// import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; +import { RemoteFileService } from "vs/workbench/services/files/electron-browser/remoteFileService"; import { IFileService } from 'vs/platform/files/common/files'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -561,7 +562,7 @@ export class Workbench implements IPartService { serviceCollection.set(ITitleService, this.titlebarPart); // File Service - const fileService = this.instantiationService.createInstance(FileService); + const fileService = this.instantiationService.createInstance(RemoteFileService); serviceCollection.set(IFileService, fileService); this.toDispose.push(fileService.onFileChanges(e => this.configurationService.handleWorkspaceFileEvents(e))); diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 991e116aebc18..cffc61ed957ac 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -41,7 +41,7 @@ export class FileService implements IFileService { private toUnbind: IDisposable[]; private activeOutOfWorkspaceWatchers: ResourceMap; - private _onFileChanges: Emitter; + protected _onFileChanges: Emitter; private _onAfterOperation: Emitter; constructor( @@ -310,4 +310,4 @@ export class FileService implements IFileService { // Dispose service this.raw.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts new file mode 100644 index 0000000000000..1fc46d3d38f81 --- /dev/null +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import URI from 'vs/base/common/uri'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IMessageService } from 'vs/platform/message/common/message'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, FileChangeType } from "vs/platform/files/common/files"; +import { TPromise } from "vs/base/common/winjs.base"; +import Event from "vs/base/common/event"; +import { EventEmitter } from "events"; +import { basename } from "path"; +import { IDisposable } from "vs/base/common/lifecycle"; + +export interface IRemoteFileProvider { + onDidChange: Event; + resolve(resource: URI): TPromise; + update(resource: URI, content: string): TPromise; +} + +export class RemoteFileService extends FileService { + + private readonly _remoteAuthority: string; + private _provider: IRemoteFileProvider; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IEnvironmentService environmentService: IEnvironmentService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @ILifecycleService lifecycleService: ILifecycleService, + @IMessageService messageService: IMessageService, + @IStorageService storageService: IStorageService + ) { + super(configurationService, contextService, editorService, environmentService, editorGroupService, lifecycleService, messageService, storageService); + + this._remoteAuthority = environmentService.args['remote']; + + this.registerProvider(new class implements IRemoteFileProvider { + onDidChange: Event = Event.None; + resolve(resource: URI): TPromise { + return TPromise.as(JSON.stringify(resource, undefined, 4)); + } + update(resource: URI, content: string): TPromise { + return TPromise.as(undefined); + } + }); + } + + private _shouldIntercept(resource: URI): boolean { + return this._provider && resource.authority === this._remoteAuthority; + } + + registerProvider(provider: IRemoteFileProvider): IDisposable { + this._provider = provider; + const reg = this._provider.onDidChange(e => { + // forward change events + this._onFileChanges.fire(new FileChangesEvent([{ resource: e, type: FileChangeType.UPDATED }])); + }); + return { + dispose: () => { + reg.dispose(); + this._provider = undefined; + } + }; + } + + // --- resolve + + resolveContent(resource: URI, options?: IResolveContentOptions): TPromise { + if (this._shouldIntercept(resource)) { + return this._doResolveContent(resource); + } + + return super.resolveContent(resource, options); + } + + resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise { + if (this._shouldIntercept(resource)) { + return this._doResolveContent(resource).then(RemoteFileService._asStreamContent); + } + + return super.resolveStreamContent(resource, options); + } + + private async _doResolveContent(resource: URI): TPromise { + + const stat = RemoteFileService._createFakeStat(resource); + const value = await this._provider.resolve(resource); + return { ...stat, value }; + } + + // --- saving + + updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + if (this._shouldIntercept(resource)) { + return this._doUpdateContent(resource, value).then(RemoteFileService._createFakeStat); + } + + return super.updateContent(resource, value, options); + } + + private async _doUpdateContent(resource: URI, content: string): TPromise { + await this._provider.update(resource, content); + return resource; + } + + // --- util + + private static _createFakeStat(resource: URI): IFileStat { + + return { + resource, + name: basename(resource.path), + encoding: 'utf8', + mtime: Date.now(), + etag: Date.now().toString(16), + isDirectory: false, + hasChildren: false + }; + } + + private static _asStreamContent(content: IContent): IStreamContent { + const emitter = new EventEmitter(); + const { value } = content; + const result = content; + result.value = emitter; + setTimeout(() => { + emitter.emit('data', value); + emitter.emit('end'); + }, 0); + return result; + } +} From 16c00672fad01309cfbb4a6caa7674fc04feba81 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 10 Jul 2017 16:17:27 +0200 Subject: [PATCH 2/5] proposed API and its wiring --- src/vs/vscode.proposed.d.ts | 12 ++++++++ .../electron-browser/mainThreadWorkspace.ts | 30 +++++++++++++++++++ src/vs/workbench/api/node/extHost.api.impl.ts | 5 +++- src/vs/workbench/api/node/extHost.protocol.ts | 4 +++ src/vs/workbench/api/node/extHostWorkspace.ts | 28 +++++++++++++++++ .../electron-browser/remoteFileService.ts | 21 +++++-------- 6 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 65ddf419bb970..a80a3db0b2ab3 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -32,7 +32,19 @@ declare module 'vscode' { } + // todo@joh discover files etc + export interface FileSystemProvider { + // todo@joh -> added, deleted, renamed, changed + onDidChange: Event; + + resolveContents(resource: Uri): string | Thenable; + writeContents(resource: Uri, contents: string): void | Thenable; + } + export namespace workspace { + + export function registerFileSystemProvider(authority: string, provider: FileSystemProvider): Disposable; + /** * Get a configuration object. * diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 3a4fef5dd8972..93cb1b2ce7a6f 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -18,6 +18,8 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IFileService } from 'vs/platform/files/common/files'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { RemoteFileService, IRemoteFileSystemProvider } from 'vs/workbench/services/files/electron-browser/remoteFileService'; +import { Emitter } from 'vs/base/common/event'; export class MainThreadWorkspace extends MainThreadWorkspaceShape { @@ -108,4 +110,32 @@ export class MainThreadWorkspace extends MainThreadWorkspaceShape { return bulkEdit(this._textModelResolverService, codeEditor, edits, this._fileService) .then(() => true); } + + // --- EXPERIMENT: workspace provider + + private _provider = new Map]>(); + + $registerFileSystemProvider(handle: number, authority: string): void { + if (!(this._fileService instanceof RemoteFileService)) { + throw new Error(); + } + const emitter = new Emitter(); + const provider = { + onDidChange: emitter.event, + resolve: (resource) => { + return this._proxy.$resolveFile(handle, resource); + }, + update: (resource, value) => { + return this._proxy.$storeFile(handle, resource, value); + } + }; + this._provider.set(handle, [provider, emitter]); + this._fileService.registerProvider(authority, provider); + } + + $onFileSystemChange(handle: number, resource: URI) { + const [, emitter] = this._provider.get(handle); + emitter.fire(resource); + }; } + diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 2155bc87c3230..e043eaccc5b6f 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -442,7 +442,10 @@ export function createApiFactory( }), registerTaskProvider: (type: string, provider: vscode.TaskProvider) => { return extHostTask.registerTaskProvider(extension, provider); - } + }, + registerFileSystemProvider: proposedApiFunction(extension, (authority, provider) => { + return extHostWorkspace.registerFileSystemProvider(authority, provider); + }) }; // namespace: scm diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 4d0d6607be6b1..9950f8eda23cb 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -295,6 +295,8 @@ export abstract class MainThreadWorkspaceShape { $cancelSearch(requestId: number): Thenable { throw ni(); } $saveAll(includeUntitled?: boolean): Thenable { throw ni(); } $applyWorkspaceEdit(edits: IResourceEdit[]): TPromise { throw ni(); } + $registerFileSystemProvider(handle: number, authority: string): void { throw ni(); } + $onFileSystemChange(handle: number, resource: URI): void { throw ni(); } } export abstract class MainThreadTaskShape { @@ -420,6 +422,8 @@ export abstract class ExtHostTreeViewsShape { export abstract class ExtHostWorkspaceShape { $acceptWorkspaceData(workspace: IWorkspaceData): void { throw ni(); } + $resolveFile(handle: number, resource: URI): TPromise { throw ni(); } + $storeFile(handle: number, resource: URI, content: string): TPromise { throw ni(); } } export abstract class ExtHostExtensionServiceShape { diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 09a239b0c617b..2d9fbd7ab70cc 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -17,6 +17,8 @@ import { fromRange, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverter import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape } from './extHost.protocol'; import * as vscode from 'vscode'; import { compare } from "vs/base/common/strings"; +import { asWinJsPromise } from 'vs/base/common/async'; +import { Disposable } from 'vs/workbench/api/node/extHostTypes'; export class ExtHostWorkspace extends ExtHostWorkspaceShape { @@ -151,4 +153,30 @@ export class ExtHostWorkspace extends ExtHostWorkspaceShape { return this._proxy.$applyWorkspaceEdit(resourceEdits); } + + // --- EXPERIMENT: workspace resolver + + private readonly _provider = new Map(); + + public registerFileSystemProvider(authority: string, provider: vscode.FileSystemProvider): vscode.Disposable { + + const handle = this._provider.size; + this._provider.set(handle, provider); + const reg = provider.onDidChange(e => this._proxy.$onFileSystemChange(handle, e)); + this._proxy.$registerFileSystemProvider(handle, authority); + return new Disposable(() => { + this._provider.delete(handle); + reg.dispose(); + }); + } + + $resolveFile(handle: number, resource: URI): TPromise { + const provider = this._provider.get(handle); + return asWinJsPromise(token => provider.resolveContents(resource)); + } + + $storeFile(handle: number, resource: URI, content: string): TPromise { + const provider = this._provider.get(handle); + return asWinJsPromise(token => provider.writeContents(resource, content)); + } } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 1fc46d3d38f81..ec5433ab58fbb 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -21,7 +21,7 @@ import { EventEmitter } from "events"; import { basename } from "path"; import { IDisposable } from "vs/base/common/lifecycle"; -export interface IRemoteFileProvider { +export interface IRemoteFileSystemProvider { onDidChange: Event; resolve(resource: URI): TPromise; update(resource: URI, content: string): TPromise; @@ -29,8 +29,8 @@ export interface IRemoteFileProvider { export class RemoteFileService extends FileService { + private _provider: IRemoteFileSystemProvider; private readonly _remoteAuthority: string; - private _provider: IRemoteFileProvider; constructor( @IConfigurationService configurationService: IConfigurationService, @@ -43,25 +43,18 @@ export class RemoteFileService extends FileService { @IStorageService storageService: IStorageService ) { super(configurationService, contextService, editorService, environmentService, editorGroupService, lifecycleService, messageService, storageService); - this._remoteAuthority = environmentService.args['remote']; - - this.registerProvider(new class implements IRemoteFileProvider { - onDidChange: Event = Event.None; - resolve(resource: URI): TPromise { - return TPromise.as(JSON.stringify(resource, undefined, 4)); - } - update(resource: URI, content: string): TPromise { - return TPromise.as(undefined); - } - }); } private _shouldIntercept(resource: URI): boolean { return this._provider && resource.authority === this._remoteAuthority; } - registerProvider(provider: IRemoteFileProvider): IDisposable { + registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable { + // todo@joh make this actually work for N provider + if (authority !== this._remoteAuthority) { + throw new Error(); + } this._provider = provider; const reg = this._provider.onDidChange(e => { // forward change events From 0d86cf54a5e6bbd6835e5b55aec77936d6ef1bfd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 11 Jul 2017 17:02:02 +0200 Subject: [PATCH 3/5] remove need for --remote switch just go with proposed api --- .../electron-browser/remoteFileService.ts | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index ec5433ab58fbb..e69adf5a8c6d7 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -6,14 +6,6 @@ import URI from 'vs/base/common/uri'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IMessageService } from 'vs/platform/message/common/message'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, FileChangeType } from "vs/platform/files/common/files"; import { TPromise } from "vs/base/common/winjs.base"; import Event from "vs/base/common/event"; @@ -29,41 +21,26 @@ export interface IRemoteFileSystemProvider { export class RemoteFileService extends FileService { - private _provider: IRemoteFileSystemProvider; - private readonly _remoteAuthority: string; - - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IEnvironmentService environmentService: IEnvironmentService, - @IEditorGroupService editorGroupService: IEditorGroupService, - @ILifecycleService lifecycleService: ILifecycleService, - @IMessageService messageService: IMessageService, - @IStorageService storageService: IStorageService - ) { - super(configurationService, contextService, editorService, environmentService, editorGroupService, lifecycleService, messageService, storageService); - this._remoteAuthority = environmentService.args['remote']; - } + private readonly _provider = new Map(); private _shouldIntercept(resource: URI): boolean { - return this._provider && resource.authority === this._remoteAuthority; + return this._provider.has(resource.authority); } registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable { - // todo@joh make this actually work for N provider - if (authority !== this._remoteAuthority) { + if (this._provider.has(authority)) { throw new Error(); } - this._provider = provider; - const reg = this._provider.onDidChange(e => { + + this._provider.set(authority, provider); + const reg = provider.onDidChange(e => { // forward change events this._onFileChanges.fire(new FileChangesEvent([{ resource: e, type: FileChangeType.UPDATED }])); }); return { dispose: () => { + this._provider.delete(authority); reg.dispose(); - this._provider = undefined; } }; } @@ -89,7 +66,7 @@ export class RemoteFileService extends FileService { private async _doResolveContent(resource: URI): TPromise { const stat = RemoteFileService._createFakeStat(resource); - const value = await this._provider.resolve(resource); + const value = await this._provider.get(resource.authority).resolve(resource); return { ...stat, value }; } @@ -104,7 +81,7 @@ export class RemoteFileService extends FileService { } private async _doUpdateContent(resource: URI, content: string): TPromise { - await this._provider.update(resource, content); + await this._provider.get(resource.authority).update(resource, content); return resource; } From 8b77fb8a49fbfb90e3e1c78698ee3f3d9bd4d87a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 21 Jul 2017 09:59:59 +0200 Subject: [PATCH 4/5] simplify things --- .../files/electron-browser/remoteFileService.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index e69adf5a8c6d7..0b5c7c0a91437 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -23,10 +23,6 @@ export class RemoteFileService extends FileService { private readonly _provider = new Map(); - private _shouldIntercept(resource: URI): boolean { - return this._provider.has(resource.authority); - } - registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable { if (this._provider.has(authority)) { throw new Error(); @@ -48,7 +44,7 @@ export class RemoteFileService extends FileService { // --- resolve resolveContent(resource: URI, options?: IResolveContentOptions): TPromise { - if (this._shouldIntercept(resource)) { + if (this._provider.has(resource.authority)) { return this._doResolveContent(resource); } @@ -56,7 +52,7 @@ export class RemoteFileService extends FileService { } resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise { - if (this._shouldIntercept(resource)) { + if (this._provider.has(resource.authority)) { return this._doResolveContent(resource).then(RemoteFileService._asStreamContent); } @@ -73,7 +69,7 @@ export class RemoteFileService extends FileService { // --- saving updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { - if (this._shouldIntercept(resource)) { + if (this._provider.has(resource.authority)) { return this._doUpdateContent(resource, value).then(RemoteFileService._createFakeStat); } From a04d53f9501151da9b41aff7f8971556e31cf45b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 21 Jul 2017 10:16:24 +0200 Subject: [PATCH 5/5] clarify TaskProvider#resolveTask --- src/vs/vscode.d.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 77b0bf7efda72..ee6ef5dcd0b89 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3857,10 +3857,14 @@ declare module 'vscode' { provideTasks(token?: CancellationToken): ProviderResult; /** - * Resolves a task the has no execution set. + * Resolves a task that has no [`execution`](#Task.execution) set. Tasks are + * often created from information found in the `task.json`-file. Such tasks miss + * the information on how to execute them and a task provider must fill in + * the missing information in the `resolveTask`-method. + * * @param task The task to resolve. * @param token A cancellation token. - * @return the resolved task + * @return The resolved task */ resolveTask(task: Task, token?: CancellationToken): ProviderResult; }