move ftp provider into an extensions, expose things as proposed api

This commit is contained in:
Johannes Rieken 2017-09-18 16:15:38 +02:00
parent 9bc31f1ca6
commit 19c992bae8
14 changed files with 298 additions and 367 deletions

View file

@ -33,7 +33,6 @@
"https-proxy-agent": "0.3.6",
"iconv-lite": "0.4.15",
"jschardet": "^1.5.1",
"jsftp": "^2.0.0",
"keytar": "^4.0.3",
"minimist": "1.2.0",
"native-keymap": "1.2.5",

View file

@ -14,6 +14,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import Event from 'vs/base/common/event';
import { beginsWithIgnoreCase } from 'vs/base/common/strings';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IDisposable } from 'vs/base/common/lifecycle';
export const IFileService = createDecorator<IFileService>('fileService');
@ -31,6 +32,11 @@ export interface IFileService {
*/
onAfterOperation: Event<FileOperationEvent>;
/**
*
*/
registerProvider?(authority: string, provider: IFileSystemProvider): IDisposable;
/**
* Resolve the properties of a file identified by the resource.
*
@ -166,7 +172,7 @@ export interface IStat {
export interface IFileSystemProvider {
onDidChange?: Event<FileChangesEvent>;
onDidChange?: Event<IFileChange[]>;
// more...
//

View file

@ -25,17 +25,48 @@ declare module 'vscode' {
export function showSaveDialog(options: SaveDialogOptions): Thenable<Uri>;
}
export enum FileChangeType {
Updated = 0,
Added = 1,
Deleted = 2
}
export interface FileChange {
type: FileChangeType;
resource: Uri;
}
export enum FileType {
File = 0,
Dir = 1,
Symlink = 2
}
export interface FileStat {
resource: Uri;
mtime: number;
size: number;
type: FileType;
}
// todo@joh discover files etc
export interface FileSystemProvider {
// todo@joh -> added, deleted, renamed, changed
onDidChange: Event<Uri>;
resolveContents(resource: Uri): string | Thenable<string>;
writeContents(resource: Uri, contents: string): void | Thenable<void>;
onDidChange?: Event<FileChange[]>;
// -- search
// todo@joh - extract into its own provider?
findFiles(query: string, progress: Progress<Uri>, token?: CancellationToken): Thenable<void>;
root: Uri;
// more...
//
utimes(resource: Uri, mtime: number): Thenable<FileStat>;
stat(resource: Uri): Thenable<FileStat>;
read(resource: Uri, progress: Progress<Uint8Array>): Thenable<void>;
write(resource: Uri, content: Uint8Array): Thenable<void>;
unlink(resource: Uri): Thenable<void>;
rename(resource: Uri, target: Uri): Thenable<void>;
mkdir(resource: Uri): Thenable<void>;
readdir(resource: Uri): Thenable<FileStat[]>;
rmdir(resource: Uri): Thenable<void>;
}
export namespace workspace {

View file

@ -28,6 +28,7 @@ import './mainThreadEditor';
import './mainThreadEditors';
import './mainThreadErrors';
import './mainThreadExtensionService';
import './mainThreadFileSystem';
import './mainThreadFileSystemEventService';
import './mainThreadHeapService';
import './mainThreadLanguageFeatures';

View file

@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* 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 { TPromise } from 'vs/base/common/winjs.base';
import { ExtHostContext, MainContext, IExtHostContext, MainThreadFileSystemShape, ExtHostFileSystemShape } from '../node/extHost.protocol';
import { IFileService, IFileSystemProvider, IStat, IFileChange } from 'vs/platform/files/common/files';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@extHostNamedCustomer(MainContext.MainThreadFileSystem)
export class MainThreadFileSystem implements MainThreadFileSystemShape {
private readonly _toDispose: IDisposable[] = [];
private readonly _proxy: ExtHostFileSystemShape;
private readonly _provider = new Map<number, RemoteFileSystemProvider>();
constructor(
extHostContext: IExtHostContext,
@IFileService private readonly _fileService: IFileService,
@IWorkspaceContextService private readonly _workspaceEditService: IWorkspaceContextService
) {
this._proxy = extHostContext.get(ExtHostContext.ExtHostFileSystem);
}
dispose(): void {
dispose(this._toDispose);
}
$registerFileSystemProvider(handle: number, scheme: string): void {
this._provider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, handle, this._proxy));
}
$unregisterFileSystemProvider(handle: number): void {
dispose(this._provider.get(handle));
this._provider.delete(handle);
}
$onDidAddFileSystemRoot(uri: URI): void {
const folders = this._workspaceEditService.getWorkspace().folders.slice(0);
folders.push({
uri,
name: uri.authority,
index: folders.length,
raw: null
});
(<any>this._workspaceEditService.getWorkspace()).folders = folders;
(<any>this._workspaceEditService).onFoldersChanged();
(<any>this._workspaceEditService)._onDidChangeWorkspaceFolders.fire(null);
}
$onFileSystemChange(handle: number, changes: IFileChange[]): void {
this._provider.get(handle).$onFileSystemChange(changes);
}
$reportFileChunk(handle: number, resource: URI, chunk: number[]): void {
this._provider.get(handle).reportFileChunk(resource, chunk);
}
}
class RemoteFileSystemProvider implements IFileSystemProvider {
private readonly _onDidChange = new Emitter<IFileChange[]>();
private readonly _registration: IDisposable;
private readonly _reads = new Map<string, IProgress<Uint8Array>>();
readonly onDidChange: Event<IFileChange[]> = this._onDidChange.event;
constructor(
service: IFileService,
scheme: string,
private readonly _handle: number,
private readonly _proxy: ExtHostFileSystemShape
) {
this._registration = service.registerProvider(scheme, this);
}
dispose(): void {
this._registration.dispose();
this._onDidChange.dispose();
}
$onFileSystemChange(changes: IFileChange[]): void {
this._onDidChange.fire(changes);
}
// --- forwarding calls
utimes(resource: URI, mtime: number): TPromise<IStat, any> {
return this._proxy.$utimes(this._handle, resource, mtime);
}
stat(resource: URI): TPromise<IStat, any> {
return this._proxy.$stat(this._handle, resource);
}
read(resource: URI, progress: IProgress<Uint8Array>): TPromise<void, any> {
this._reads.set(resource.toString(), progress);
return this._proxy.$read(this._handle, resource);
}
reportFileChunk(resource: URI, chunk: number[]): void {
this._reads.get(resource.toString()).report(Buffer.from(chunk));
}
write(resource: URI, content: Uint8Array): TPromise<void, any> {
return this._proxy.$write(this._handle, resource, [].slice.call(content));
}
unlink(resource: URI): TPromise<void, any> {
return this._proxy.$unlink(this._handle, resource);
}
rename(resource: URI, target: URI): TPromise<void, any> {
return this._proxy.$rename(this._handle, resource, target);
}
mkdir(resource: URI): TPromise<void, any> {
return this._proxy.$mkdir(this._handle, resource);
}
readdir(resource: URI): TPromise<IStat[], any> {
return this._proxy.$readdir(this._handle, resource);
}
rmdir(resource: URI): TPromise<void, any> {
return this._proxy.$rmdir(this._handle, resource);
}
}

View file

@ -6,19 +6,17 @@
import { isPromiseCanceledError } from 'vs/base/common/errors';
import URI from 'vs/base/common/uri';
import { ISearchService, QueryType, ISearchQuery, ISearchProgressItem, ISearchComplete } from 'vs/platform/search/common/search';
import { ISearchService, QueryType, ISearchQuery } from 'vs/platform/search/common/search';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ICommonCodeEditor, isCommonCodeEditor } from 'vs/editor/common/editorCommon';
import { bulkEdit, IResourceEdit } from 'vs/editor/common/services/bulkEdit';
import { TPromise, PPromise } from 'vs/base/common/winjs.base';
import { TPromise } from 'vs/base/common/winjs.base';
import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IFileService } from 'vs/platform/files/common/files';
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { RemoteFileService } from 'vs/workbench/services/files/electron-browser/remoteFileService';
import { Emitter } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IExperimentService } from 'vs/platform/telemetry/common/experiments';
@ -36,7 +34,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
@ITextFileService private readonly _textFileService: ITextFileService,
@IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService,
@ITextModelService private readonly _textModelResolverService: ITextModelService,
@IExperimentService private experimentService: IExperimentService,
@IExperimentService private _experimentService: IExperimentService,
@IFileService private readonly _fileService: IFileService
) {
this._proxy = extHostContext.get(ExtHostContext.ExtHostWorkspace);
@ -71,7 +69,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
maxResults,
includePattern: { [include]: true },
excludePattern: { [exclude]: true },
useRipgrep: this.experimentService.getExperiments().ripgrepQuickSearch
useRipgrep: this._experimentService.getExperiments().ripgrepQuickSearch
};
this._searchService.extendQuery(query);
@ -123,83 +121,5 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
return bulkEdit(this._textModelResolverService, codeEditor, edits, this._fileService)
.then(() => true);
}
// --- EXPERIMENT: workspace provider
private _idPool: number = 0;
private readonly _provider = new Map<number, [IDisposable, Emitter<URI>]>();
private readonly _searchSessions = new Map<number, { resolve: (result: ISearchComplete) => void, reject: Function, progress: (item: ISearchProgressItem) => void, matches: URI[] }>();
$registerFileSystemProvider(handle: number, authority: string): void {
if (!(this._fileService instanceof RemoteFileService)) {
throw new Error();
}
const emitter = new Emitter<URI>();
// const provider = {
// onDidChange: emitter.event,
// read: (resource: URI) => {
// return this._proxy.$resolveFile(handle, resource);
// },
// write: (resource: URI, value: string) => {
// return this._proxy.$storeFile(handle, resource, value);
// },
// stat: () => null,
// readdir: () => null
// };
const searchProvider = {
search: (query: ISearchQuery) => {
if (query.type !== QueryType.File) {
return undefined;
}
const session = ++this._idPool;
return new PPromise<any, any>((resolve, reject, progress) => {
this._searchSessions.set(session, { resolve, reject, progress, matches: [] });
this._proxy.$startSearch(handle, session, query.filePattern);
}, () => {
this._proxy.$cancelSearch(handle, session);
});
}
};
const registrations = combinedDisposable([
// this._fileService.registerProvider(authority, provider),
this._searchService.registerSearchResultProvider(searchProvider),
]);
this._provider.set(handle, [registrations, emitter]);
}
$unregisterFileSystemProvider(handle: number): void {
if (this._provider.has(handle)) {
dispose(this._provider.get(handle)[0]);
this._provider.delete(handle);
}
}
$onFileSystemChange(handle: number, resource: URI) {
const [, emitter] = this._provider.get(handle);
emitter.fire(resource);
};
$updateSearchSession(session: number, data: URI): void {
if (this._searchSessions.has(session)) {
this._searchSessions.get(session).progress({ resource: data });
this._searchSessions.get(session).matches.push(data);
}
}
$finishSearchSession(session: number, err?: any): void {
if (this._searchSessions.has(session)) {
const { matches, resolve, reject } = this._searchSessions.get(session);
this._searchSessions.delete(session);
if (err) {
reject(err);
} else {
resolve({
limitHit: false,
stats: undefined,
results: matches.map(resource => ({ resource }))
});
}
}
}
}

View file

@ -53,6 +53,8 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
import { ExtHostThreadService } from 'vs/workbench/services/thread/node/extHostThreadService';
import { ProxyIdentifier } from 'vs/workbench/services/thread/common/threadService';
import { ExtHostDialogs } from 'vs/workbench/api/node/extHostDialogs';
import { ExtHostFileSystem } from 'vs/workbench/api/node/extHostFileSystem';
import { FileChangeType, FileType } from 'vs/platform/files/common/files';
export interface IExtensionApiFactory {
(extension: IExtensionDescription): typeof vscode;
@ -93,6 +95,7 @@ export function createApiFactory(
const extHostConfiguration = threadService.set(ExtHostContext.ExtHostConfiguration, new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration), extHostWorkspace, initData.configuration));
const extHostDiagnostics = threadService.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(threadService));
const languageFeatures = threadService.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(threadService, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics));
const extHostFileSystem = threadService.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(threadService));
const extHostFileSystemEvent = threadService.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService());
const extHostQuickOpen = threadService.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(threadService));
const extHostTerminalService = threadService.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(threadService));
@ -477,7 +480,7 @@ export function createApiFactory(
return extHostTask.registerTaskProvider(extension, provider);
},
registerFileSystemProvider: proposedApiFunction(extension, (authority, provider) => {
return extHostWorkspace.registerFileSystemProvider(authority, provider);
return extHostFileSystem.registerFileSystemProvider(authority, provider);
})
};
@ -601,7 +604,11 @@ export function createApiFactory(
ShellExecution: extHostTypes.ShellExecution,
TaskScope: extHostTypes.TaskScope,
Task: extHostTypes.Task,
ConfigurationTarget: extHostTypes.ConfigurationTarget
ConfigurationTarget: extHostTypes.ConfigurationTarget,
// TODO@JOH
FileChangeType: <any>FileChangeType,
FileType: <any>FileType
};
if (extension.enableProposedApi && extension.isBuiltin) {
api['credentials'] = credentials;

View file

@ -49,6 +49,7 @@ import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { SerializedError } from 'vs/base/common/errors';
import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IStat, IFileChange } from 'vs/platform/files/common/files';
export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
@ -302,12 +303,15 @@ export interface MainThreadWorkspaceShape extends IDisposable {
$cancelSearch(requestId: number): Thenable<boolean>;
$saveAll(includeUntitled?: boolean): Thenable<boolean>;
$applyWorkspaceEdit(edits: IResourceEdit[]): TPromise<boolean>;
}
$registerFileSystemProvider(handle: number, authority: string): void;
export interface MainThreadFileSystemShape extends IDisposable {
$registerFileSystemProvider(handle: number, scheme: string): void;
$unregisterFileSystemProvider(handle: number): void;
$onFileSystemChange(handle: number, resource: URI): void;
$updateSearchSession(session: number, data): void;
$finishSearchSession(session: number, err?: any): void;
$onDidAddFileSystemRoot(root: URI): void;
$onFileSystemChange(handle: number, resource: IFileChange[]): void;
$reportFileChunk(handle: number, resource: URI, chunk: number[] | null): void;
}
export interface MainThreadTaskShape extends IDisposable {
@ -461,11 +465,18 @@ export interface ExtHostTreeViewsShape {
export interface ExtHostWorkspaceShape {
$acceptWorkspaceData(workspace: IWorkspaceData): void;
}
$resolveFile(handle: number, resource: URI): TPromise<string>;
$storeFile(handle: number, resource: URI, content: string): TPromise<any>;
$startSearch(handle: number, session: number, query: string): void;
$cancelSearch(handle: number, session: number): void;
export interface ExtHostFileSystemShape {
$utimes(handle: number, resource: URI, mtime: number): TPromise<IStat>;
$stat(handle: number, resource: URI): TPromise<IStat>;
$read(handle: number, resource: URI): TPromise<void>;
$write(handle: number, resource: URI, content: number[]): TPromise<void>;
$unlink(handle: number, resource: URI): TPromise<void>;
$rename(handle: number, resource: URI, target: URI): TPromise<void>;
$mkdir(handle: number, resource: URI): TPromise<void>;
$readdir(handle: number, resource: URI): TPromise<IStat[]>;
$rmdir(handle: number, resource: URI): TPromise<void>;
}
export interface ExtHostExtensionServiceShape {
@ -602,6 +613,7 @@ export const MainContext = {
MainThreadTelemetry: createMainId<MainThreadTelemetryShape>('MainThreadTelemetry'),
MainThreadTerminalService: createMainId<MainThreadTerminalServiceShape>('MainThreadTerminalService'),
MainThreadWorkspace: createMainId<MainThreadWorkspaceShape>('MainThreadWorkspace'),
MainThreadFileSystem: createMainId<MainThreadFileSystemShape>('MainThreadFileSystem'),
MainThreadExtensionService: createMainId<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
MainThreadSCM: createMainId<MainThreadSCMShape>('MainThreadSCM'),
MainThreadTask: createMainId<MainThreadTaskShape>('MainThreadTask'),
@ -620,6 +632,7 @@ export const ExtHostContext = {
ExtHostDocumentSaveParticipant: createExtId<ExtHostDocumentSaveParticipantShape>('ExtHostDocumentSaveParticipant'),
ExtHostEditors: createExtId<ExtHostEditorsShape>('ExtHostEditors'),
ExtHostTreeViews: createExtId<ExtHostTreeViewsShape>('ExtHostTreeViews'),
ExtHostFileSystem: createExtId<ExtHostFileSystemShape>('ExtHostFileSystem'),
ExtHostFileSystemEventService: createExtId<ExtHostFileSystemEventServiceShape>('ExtHostFileSystemEventService'),
ExtHostHeapService: createExtId<ExtHostHeapServiceShape>('ExtHostHeapMonitor'),
ExtHostLanguageFeatures: createExtId<ExtHostLanguageFeaturesShape>('ExtHostLanguageFeatures'),

View file

@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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 { TPromise } from 'vs/base/common/winjs.base';
import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape } from './extHost.protocol';
import * as vscode from 'vscode';
import { IStat } from 'vs/platform/files/common/files';
import { IDisposable } from 'vs/base/common/lifecycle';
export class ExtHostFileSystem implements ExtHostFileSystemShape {
private readonly _proxy: MainThreadFileSystemShape;
private readonly _provider = new Map<number, vscode.FileSystemProvider>();
private _handlePool: number = 0;
constructor(mainContext: IMainContext) {
this._proxy = mainContext.get(MainContext.MainThreadFileSystem);
}
registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider) {
const handle = this._handlePool++;
this._provider.set(handle, provider);
this._proxy.$registerFileSystemProvider(handle, scheme);
this._proxy.$onDidAddFileSystemRoot(<any>provider.root);
let reg: IDisposable;
if (provider.onDidChange) {
reg = provider.onDidChange(event => this._proxy.$onFileSystemChange(handle, <any>event));
}
return {
dispose: () => {
if (reg) {
reg.dispose();
}
this._provider.delete(handle);
this._proxy.$unregisterFileSystemProvider(handle);
}
};
}
$utimes(handle: number, resource: URI, mtime: number): TPromise<IStat, any> {
return TPromise.as<any>(this._provider.get(handle).utimes(resource, mtime));
}
$stat(handle: number, resource: URI): TPromise<IStat, any> {
return TPromise.as<any>(this._provider.get(handle).stat(resource));
}
$read(handle: number, resource: URI): TPromise<void> {
return TPromise.as<any>(this._provider.get(handle).read(resource, {
report: (chunk) => {
this._proxy.$reportFileChunk(handle, resource, [].slice.call(chunk));
}
}));
}
$write(handle: number, resource: URI, content: number[]): TPromise<void, any> {
return TPromise.as<any>(this._provider.get(handle).write(resource, Buffer.from(content)));
}
$unlink(handle: number, resource: URI): TPromise<void, any> {
return TPromise.as<any>(this._provider.get(handle).unlink(resource));
}
$rename(handle: number, resource: URI, target: URI): TPromise<void, any> {
return TPromise.as<any>(this._provider.get(handle).rename(resource, target));
}
$mkdir(handle: number, resource: URI): TPromise<void, any> {
return TPromise.as<any>(this._provider.get(handle).mkdir(resource));
}
$readdir(handle: number, resource: URI): TPromise<IStat[], any> {
return TPromise.as<any>(this._provider.get(handle).readdir(resource));
}
$rmdir(handle: number, resource: URI): TPromise<void, any> {
return TPromise.as<any>(this._provider.get(handle).rmdir(resource));
}
}

View file

@ -16,11 +16,7 @@ import { fromRange, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverter
import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext } 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';
import { TrieMap } from 'vs/base/common/map';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Progress } from 'vs/platform/progress/common/progress';
class Workspace2 extends Workspace {
@ -203,51 +199,4 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
return this._proxy.$applyWorkspaceEdit(resourceEdits);
}
// --- EXPERIMENT: workspace resolver
private _handlePool = 0;
private readonly _fsProvider = new Map<number, vscode.FileSystemProvider>();
private readonly _searchSession = new Map<number, CancellationTokenSource>();
registerFileSystemProvider(authority: string, provider: vscode.FileSystemProvider): vscode.Disposable {
const handle = ++this._handlePool;
this._fsProvider.set(handle, provider);
const reg = provider.onDidChange(e => this._proxy.$onFileSystemChange(handle, <URI>e));
this._proxy.$registerFileSystemProvider(handle, authority);
return new Disposable(() => {
this._fsProvider.delete(handle);
reg.dispose();
});
}
$resolveFile(handle: number, resource: URI): TPromise<string> {
const provider = this._fsProvider.get(handle);
return asWinJsPromise(token => provider.resolveContents(resource));
}
$storeFile(handle: number, resource: URI, content: string): TPromise<any> {
const provider = this._fsProvider.get(handle);
return asWinJsPromise(token => provider.writeContents(resource, content));
}
$startSearch(handle: number, session: number, query: string): void {
const provider = this._fsProvider.get(handle);
const source = new CancellationTokenSource();
const progress = new Progress<any>(chunk => this._proxy.$updateSearchSession(session, chunk));
this._searchSession.set(session, source);
TPromise.wrap(provider.findFiles(query, progress, source.token)).then(() => {
this._proxy.$finishSearchSession(session);
}, err => {
this._proxy.$finishSearchSession(session, err);
});
}
$cancelSearch(handle: number, session: number): void {
if (this._searchSession.has(session)) {
this._searchSession.get(session).cancel();
this._searchSession.delete(session);
}
}
}

View file

@ -472,12 +472,6 @@ export class WorkspaceServiceImpl extends WorkspaceService {
const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel;
const workspaceFolders = toWorkspaceFolders(workspaceConfigurationModel.folders, URI.file(paths.dirname(this.workspaceConfigPath.fsPath)));
workspaceFolders.push({
uri: URI.parse('ftp://waws-prod-db3-029.ftp.azurewebsites.windows.net/'),
name: 'FTP Sample',
index: workspaceFolders.length,
raw: null
});
if (!workspaceFolders.length) {
return TPromise.wrapError<void>(new Error('Invalid workspace configuraton file ' + this.workspaceConfigPath));
}

View file

@ -1,146 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 Event from 'vs/base/common/event';
import * as JSFtp from 'jsftp';
import { TPromise } from 'vs/base/common/winjs.base';
import { Readable } from 'stream';
import { join, dirname, basename } from 'path';
import { IStat, FileType, IFileSystemProvider } from 'vs/platform/files/common/files';
import { IProgress } from 'vs/platform/progress/common/progress';
export class FtpFileSystemProvider implements IFileSystemProvider {
private _connection: TPromise<JSFtp>;
readonly onDidChange = Event.None;
constructor() {
this._connection = new TPromise<JSFtp>((resolve, reject) => {
const connection = new JSFtp({
host: 'waws-prod-db3-029.ftp.azurewebsites.windows.net'
});
connection.keepAlive(1000 * 5);
connection.auth('USER', 'PASS', (err) => {
if (err) {
reject(err);
} else {
resolve(connection);
}
});
});
}
private _withConnection<T>(func: keyof JSFtp, ...args: any[]): TPromise<T> {
return this._connection.then(connection => {
return new TPromise<T>((resolve, reject) => {
(<Function>connection[func]).apply(connection, args.concat([function (err, result) {
if (err) {
reject(err);
} else {
resolve(result);
}
}]));
});
});
}
dispose(): void {
this._withConnection('raw', 'QUIT');
}
utimes(resource: URI, mtime: number): TPromise<IStat> {
return this._withConnection('raw', 'NOOP')
.then(() => this.stat(resource));
}
stat(resource: URI): TPromise<IStat> {
const { path } = resource;
if (path === '/') {
// root directory
return TPromise.as(<IStat>{
type: FileType.Dir,
resource,
mtime: 0,
size: 0
});
}
const name = basename(path);
const dir = dirname(path);
return this._withConnection<JSFtp.Entry[]>('ls', dir).then(entries => {
for (const entry of entries) {
if (entry.name === name) {
return {
resource,
mtime: entry.time,
size: entry.size,
type: entry.type
};
}
}
// console.log(entries, name, resource);
throw new Error(`ENO: ${resource.path}`);
});
}
readdir(resource: URI): TPromise<IStat[]> {
return this._withConnection<JSFtp.Entry[]>('ls', resource.path).then(ret => {
const result: IStat[] = [];
for (let entry of ret) {
result.push({
resource: resource.with({ path: join(resource.path, entry.name) }),
mtime: entry.time,
size: entry.size,
type: entry.type
});
}
return result;
});
}
read(resource: URI, progress: IProgress<Uint8Array>): TPromise<void> {
return this._withConnection<Readable>('get', resource.path).then(stream => {
return new TPromise<void>((resolve, reject) => {
stream.on('data', d => progress.report(<any>d));
stream.on('close', hadErr => {
if (hadErr) {
reject(hadErr);
} else {
resolve(undefined);
}
});
stream.resume();
});
});
}
write(resource: URI, content: Uint8Array): TPromise<void> {
return this._withConnection('put', content, resource.path);
}
rmdir(resource: URI): TPromise<void> {
return this._withConnection('raw', 'RMD', [resource.path]);
}
mkdir(resource: URI): TPromise<void> {
return this._withConnection('raw', 'MKD', [resource.path]);
}
unlink(resource: URI): TPromise<void> {
return this._withConnection('raw', 'DELE', [resource.path]);
}
rename(resource: URI, target: URI): TPromise<void> {
return this._withConnection<void>('raw', 'RNFR', [resource.path]).then(() => {
return this._withConnection<void>('raw', 'RNTO', [target.path]);
});
}
}

View file

@ -1,47 +0,0 @@
import { Readable } from 'stream';
import { EventEmitter } from 'events';
declare namespace JSFtp {
interface JSFtpOptions {
host: string;
port?: number | 21;
user?: string | 'anonymous';
pass?: string | '@anonymous';
useList?: boolean
}
interface Callback<T> {
(err: any, result: T): void;
}
interface Entry {
name: string;
size: number;
time: number;
type: 0 | 1;
}
}
interface JSFtp extends EventEmitter {
auth(user: string, password: string, callback: JSFtp.Callback<void>): void
keepAlive(wait?: number): void;
ls(path: string, callback: JSFtp.Callback<JSFtp.Entry[]>): void;
list(path: string, callback: JSFtp.Callback<any>): void;
put(buffer: Buffer, path: string, callback: JSFtp.Callback<void>): void;
get(path: string, callback: JSFtp.Callback<Readable>): void;
setType(type: 'A' | 'AN' | 'AT' | 'AC' | 'E' | 'I' | 'L', callback: JSFtp.Callback<any>): void;
raw<T = any>(command: string, args: any[], callback: JSFtp.Callback<T>): void
}
interface JSFtpConstructor {
new(options: JSFtp.JSFtpOptions): JSFtp;
}
declare const JSFtp: JSFtpConstructor;
export = JSFtp;

View file

@ -6,11 +6,10 @@
import URI from 'vs/base/common/uri';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult } from 'vs/platform/files/common/files';
import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
import { basename, join } from 'path';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as Ftp from './ftpFileSystemProvider';
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';
@ -60,7 +59,6 @@ function toIFileStat(provider: IFileSystemProvider, stat: IStat, recurse?: (stat
}
}
export function toDeepIFileStat(provider: IFileSystemProvider, stat: IStat, to: URI[]): TPromise<IFileStat> {
const trie = new TrieMap<true>();
@ -102,7 +100,6 @@ export class RemoteFileService extends FileService {
storageService,
textResourceConfigurationService,
);
this.registerProvider('ftp', new Ftp.FtpFileSystemProvider());
}
registerProvider(authority: string, provider: IFileSystemProvider): IDisposable {
@ -111,9 +108,9 @@ export class RemoteFileService extends FileService {
}
this._provider.set(authority, provider);
const reg = provider.onDidChange(e => {
const reg = provider.onDidChange(changes => {
// forward change events
this._onFileChanges.fire(e);
this._onFileChanges.fire(new FileChangesEvent(changes));
});
return {
dispose: () => {
@ -201,7 +198,7 @@ export class RemoteFileService extends FileService {
const encoding = this.getEncoding(resource);
const stream = decodeStream(encoding);
await provider.read(resource, new Progress<Uint8Array>(chunk => stream.write(<Buffer>chunk)));
await provider.read(resource, new Progress<Buffer>(chunk => stream.write(chunk)));
stream.end();
return {
@ -391,10 +388,15 @@ export class RemoteFileService extends FileService {
return toIFileStat(provider, stat);
}
// public watchFileChanges(resource: URI): void {
// throw new Error("Method not implemented.");
// }
// public unwatchFileChanges(resource: URI): void {
// throw new Error("Method not implemented.");
// }
// TODO@Joh - file watching on demand!
public watchFileChanges(resource: URI): void {
if (!this._provider.has(resource.scheme)) {
super.watchFileChanges(resource);
}
}
public unwatchFileChanges(resource: URI): void {
if (!this._provider.has(resource.scheme)) {
super.unwatchFileChanges(resource);
}
}
}