diff --git a/tree-explorer-sample/package.json b/tree-explorer-sample/package.json index 203f4bc8..b3bc4565 100644 --- a/tree-explorer-sample/package.json +++ b/tree-explorer-sample/package.json @@ -27,6 +27,10 @@ { "id": "jsonOutline", "name": "Json Outline" + }, + { + "id": "ftpExplorer", + "name": "FTP Explorer" } ] }, @@ -46,6 +50,10 @@ { "command": "jsonOutline.deleteEntry", "title": "Delete" + }, + { + "command": "openFtpResource", + "title": "Open FTP Resource" } ], "menus": { @@ -83,6 +91,7 @@ "@types/node": "*" }, "dependencies": { - "jsonc-parser": "^0.4.2" + "jsonc-parser": "^0.4.2", + "ftp": "^0.3.10" } } \ No newline at end of file diff --git a/tree-explorer-sample/resources/Document_16x.svg b/tree-explorer-sample/resources/Document_16x.svg new file mode 100644 index 00000000..949a3762 --- /dev/null +++ b/tree-explorer-sample/resources/Document_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-explorer-sample/resources/Document_inverse_16x.svg b/tree-explorer-sample/resources/Document_inverse_16x.svg new file mode 100644 index 00000000..46a9f38c --- /dev/null +++ b/tree-explorer-sample/resources/Document_inverse_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-explorer-sample/resources/Folder_16x.svg b/tree-explorer-sample/resources/Folder_16x.svg new file mode 100644 index 00000000..3d64ae71 --- /dev/null +++ b/tree-explorer-sample/resources/Folder_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-explorer-sample/resources/Folder_inverse_16x.svg b/tree-explorer-sample/resources/Folder_inverse_16x.svg new file mode 100644 index 00000000..13b18d18 --- /dev/null +++ b/tree-explorer-sample/resources/Folder_inverse_16x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-explorer-sample/src/extension.ts b/tree-explorer-sample/src/extension.ts index d966d173..3c17e421 100644 --- a/tree-explorer-sample/src/extension.ts +++ b/tree-explorer-sample/src/extension.ts @@ -4,6 +4,7 @@ import * as vscode from 'vscode'; import { DepNodeProvider } from './nodeDependencies' import { JsonOutlineProvider } from './jsonOutline' +import { FtpTreeDataProvider, FtpNode } from './ftpExplorer' export function activate(context: vscode.ExtensionContext) { const rootPath = vscode.workspace.rootPath; @@ -24,4 +25,13 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('jsonOutline.refreshEntry', () => vscode.window.showInformationMessage('Successfully called refresh')); vscode.commands.registerCommand('jsonOutline.addEntry', node => vscode.window.showInformationMessage('Successfully called add entry')); vscode.commands.registerCommand('jsonOutline.deleteEntry', node => vscode.window.showInformationMessage('Successfully called delete entry')); + + const provider = new FtpTreeDataProvider(); + + vscode.window.registerTreeDataProviderForView('ftpExplorer', provider); + vscode.commands.registerCommand('openFtpResource', (node: FtpNode) => { + vscode.workspace.openTextDocument(node.resource).then(document => { + vscode.window.showTextDocument(document); + }); + }); } diff --git a/tree-explorer-sample/src/ftpExplorer.ts b/tree-explorer-sample/src/ftpExplorer.ts new file mode 100644 index 00000000..6f63884f --- /dev/null +++ b/tree-explorer-sample/src/ftpExplorer.ts @@ -0,0 +1,166 @@ +import { ExtensionContext, TreeDataProvider, EventEmitter, TreeItem, Event, window, TreeItemCollapsibleState, Uri, commands, workspace, TextDocumentContentProvider, CancellationToken, ProviderResult } from 'vscode'; +import * as Client from 'ftp'; +import * as path from 'path'; + +interface IEntry { + name: string; + type: string; +} + +export class FtpNode { + private _resource: Uri; + + constructor(private entry: IEntry, private host: string, private _parent: string) { + this._resource = Uri.parse(`ftp://${host}/${_parent}/${entry.name}`); + } + + public get resource(): Uri { + return this._resource; + } + + public get path(): string { + return path.join(this._parent, this.name); + } + + public get name(): string { + return this.entry.name; + } + + public get isFolder(): boolean { + return this.entry.type === 'd' || this.entry.type === 'l'; + } +} + +export class FtpModel { + private connection: Thenable; + + constructor(private host: string, private user: string, private password: string) { + this.connection = this.connect(); + } + + public connect(): Thenable { + return new Promise((c, e) => { + const client = new Client(); + client.on('ready', () => { + c(client); + }); + + client.connect({ + host: this.host, + username: this.user, + password: this.password + }); + }); + } + + public get roots(): Thenable { + return this.connect().then(client => { + return new Promise((c, e) => { + client.list((err, list) => { + if (err) { + return e(err); + } + + client.end(); + + return c(this.sort(list.map(entry => new FtpNode(entry, this.host, '/')))); + }); + }); + }); + } + + public getChildren(node: FtpNode): Thenable { + return this.connect().then(client => { + return new Promise((c, e) => { + client.list(node.path, (err, list) => { + if (err) { + return e(err); + } + + client.end(); + + return c(this.sort(list.map(entry => new FtpNode(entry, this.host, node.path)))); + }); + }); + }); + } + + private sort(nodes: FtpNode[]): FtpNode[] { + return nodes.sort((n1, n2) => { + if (n1.isFolder && !n2.isFolder) { + return -1; + } + + if (!n1.isFolder && n2.isFolder) { + return 1; + } + + return n1.name.localeCompare(n2.name); + }); + } + + public getContent(resource: Uri): Thenable { + return this.connect().then(client => { + return new Promise((c, e) => { + client.get(resource.path.substr(2), (err, stream) => { + if (err) { + return e(err); + } + + let string = '' + stream.on('data', function (buffer) { + if (buffer) { + var part = buffer.toString(); + string += part; + } + }); + + stream.on('end', function () { + client.end(); + c(string); + }); + }); + }); + }); + } +} + +export class FtpTreeDataProvider implements TreeDataProvider, TextDocumentContentProvider { + + private _onDidChangeTreeData: EventEmitter = new EventEmitter(); + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + private model: FtpModel; + + public getTreeItem(element: FtpNode): TreeItem { + return { + label: element.name, + collapsibleState: element.isFolder ? TreeItemCollapsibleState.Collapsed : void 0, + command: element.isFolder ? void 0 : { + command: 'openFtpResource', + arguments: [element.resource], + title: 'Open FTP Resource' + }, + iconPath: { + light: element.isFolder ? path.join(__filename, '..', '..', '..', 'resources', 'Folder_16x.svg') : path.join(__filename, '..', '..', '..', 'resources', 'Document_16x.svg'), + dark: element.isFolder ? path.join(__filename, '..', '..', '..', 'resources', 'Folder_inverse_16x.svg') : path.join(__filename, '..', '..', '..', 'resources', 'Document_inverse_16x.svg') + } + }; + } + + public getChildren(element?: FtpNode): FtpNode[] | Thenable { + if (!element) { + if (!this.model) { + this.model = new FtpModel('mirror.switch.ch', 'anonymous', 'anonymous@anonymous.de'); + } + + return this.model.roots; + } + + return this.model.getChildren(element); + } + + public provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult { + return this.model.getContent(uri); + } +} \ No newline at end of file