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; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 53d3d3f82d6f5..2ad31b6544ba6 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -13,7 +13,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 0afe8a0fb65dc..e6bfe3741ac61 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -445,7 +445,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 33fa36a74d607..d0a34021bf919 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -294,6 +294,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 { @@ -426,6 +428,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 b98c5d0b19881..97cc141dc645c 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -16,7 +16,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { fromRange, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverters'; import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape } from './extHost.protocol'; import * as vscode from 'vscode'; -import { compare } from 'vs/base/common/strings'; +import { compare } from "vs/base/common/strings"; +import { asWinJsPromise } from 'vs/base/common/async'; +import { Disposable } from 'vs/workbench/api/node/extHostTypes'; import { TrieMap } from 'vs/base/common/map'; class Workspace2 extends Workspace { @@ -200,4 +202,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/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 725fc79c9bafb..7488609ecccbc 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -58,7 +58,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'; @@ -588,7 +589,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..0b5c7c0a91437 --- /dev/null +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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 IRemoteFileSystemProvider { + onDidChange: Event; + resolve(resource: URI): TPromise; + update(resource: URI, content: string): TPromise; +} + +export class RemoteFileService extends FileService { + + private readonly _provider = new Map(); + + registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable { + if (this._provider.has(authority)) { + throw new Error(); + } + + 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(); + } + }; + } + + // --- resolve + + resolveContent(resource: URI, options?: IResolveContentOptions): TPromise { + if (this._provider.has(resource.authority)) { + return this._doResolveContent(resource); + } + + return super.resolveContent(resource, options); + } + + resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise { + if (this._provider.has(resource.authority)) { + 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.get(resource.authority).resolve(resource); + return { ...stat, value }; + } + + // --- saving + + updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + if (this._provider.has(resource.authority)) { + 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.get(resource.authority).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; + } +}