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