Improve file explorer to use file system provider and reveal api

This commit is contained in:
Sandeep Somavarapu
2018-02-23 18:33:03 +01:00
parent 4a1d8c37ff
commit d8afd98c65
9 changed files with 1269 additions and 195 deletions

View File

@ -4,14 +4,13 @@ import * as vscode from 'vscode';
import { DepNodeProvider } from './nodeDependencies'
import { JsonOutlineProvider } from './jsonOutline'
import { FtpTreeDataProvider, FtpNode } from './ftpExplorer'
import { FtpTreeDataProvider, FtpNode, FtpExplorer } from './ftpExplorer'
export function activate(context: vscode.ExtensionContext) {
const rootPath = vscode.workspace.rootPath;
const nodeDependenciesProvider = new DepNodeProvider(rootPath);
const jsonOutlineProvider = new JsonOutlineProvider(context);
const ftpExplorerProvider = new FtpTreeDataProvider();
vscode.window.registerTreeDataProvider('nodeDependencies', nodeDependenciesProvider);
vscode.commands.registerCommand('nodeDependencies.refreshEntry', () => nodeDependenciesProvider.refresh());
@ -25,13 +24,5 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('jsonOutline.renameNode', offset => jsonOutlineProvider.rename(offset));
vscode.commands.registerCommand('extension.openJsonSelection', range => jsonOutlineProvider.select(range));
vscode.window.registerTreeDataProvider('ftpExplorer', ftpExplorerProvider);
vscode.commands.registerCommand('ftpExplorer.refresh', () => ftpExplorerProvider.refresh());
vscode.commands.registerCommand('openFtpResource', (node: FtpNode) => {
vscode.workspace.openTextDocument(node.resource).then(document => {
vscode.window.showTextDocument(document);
});
});
new FtpExplorer(context);
}

View File

@ -0,0 +1,280 @@
import { ExtensionContext, TreeDataProvider, EventEmitter, TreeItem, Event, window, TreeItemCollapsibleState, Uri, commands, workspace, TextDocumentContentProvider, CancellationToken, ProviderResult, TreeView } from 'vscode';
import * as vscode from 'vscode';
import * as Client from 'ftp';
import { basename, dirname, join } from 'path';
import { Socket } from 'net';
import * as JSFtp from 'jsftp';
class FtpFileSystemProvider implements vscode.FileSystemProvider {
readonly root: vscode.Uri;
private readonly _user: string;
private readonly _pass: string;
private _connection: JSFtp;
private _pending: { resolve: Function, reject: Function, func: keyof JSFtp, args: any[] }[] = [];
constructor(
root: vscode.Uri,
user: string,
pass: string
) {
this.root = root;
this._user = user;
this._pass = pass;
}
private _withConnection<T>(func: keyof JSFtp, ...args: any[]): Promise<T> {
return new Promise<T>((resolve, reject) => {
this._pending.push({ resolve, reject, func, args });
this._nextRequest();
});
}
private _nextRequest(): void {
if (this._pending.length === 0) {
return;
}
if (this._connection === void 0) {
// ensure connection first
const candidate = new JSFtp({
host: this.root.authority
});
candidate.keepAlive(1000 * 5);
candidate.auth(this._user, this._pass, (err) => {
this._connection = err ? null : candidate;
this._nextRequest();
});
return;
}
if (this._connection === null) {
// permanently failed
const request = this._pending.shift();
request.reject(new Error('no connection'))
} else {
// connected
const { func, args, resolve, reject } = this._pending.shift();
(<Function>this._connection[func]).apply(this._connection, args.concat([function (err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
}]));
}
this._nextRequest();
}
dispose(): void {
this._withConnection('raw', 'QUIT');
}
utimes(resource: vscode.Uri, mtime: number): Promise<vscode.FileStat> {
return this._withConnection('raw', 'NOOP')
.then(() => this.stat(resource));
}
stat(resource: vscode.Uri): Promise<vscode.FileStat> {
const { path } = resource;
if (path === '/' || path === '') {
// root directory
return Promise.resolve(<vscode.FileStat>{
type: vscode.FileType.Dir,
id: null,
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 {
id: null,
mtime: entry.time,
size: entry.size,
type: entry.type
};
}
}
return Promise.reject<vscode.FileStat>(new Error(`ENOENT, ${resource.toString(true)}`));
}, err => {
return Promise.reject<vscode.FileStat>(new Error(`ENOENT, ${resource.toString(true)}`));
});
}
readdir(dir: vscode.Uri): Promise<[vscode.Uri, vscode.FileStat][]> {
return this._withConnection<JSFtp.Entry[]>('ls', dir.path).then(entries => {
const result: [vscode.Uri, vscode.FileStat][] = [];
for (let entry of entries) {
const resource = dir.with({ path: join(dir.path, entry.name) });
const stat: vscode.FileStat = {
id: resource.toString(),
mtime: entry.time,
size: entry.size,
type: entry.type
}
result.push([resource, stat]);
}
return result;
});
}
read(resource: vscode.Uri, offset: number = 0, len: number, progress: vscode.Progress<Uint8Array>): Promise<number> {
return this._withConnection<void>('raw', 'REST', [offset]).then(() => {
return this._withConnection<Socket>('get', resource.path)
}).then(socket => {
let bytesRead = 0;
return new Promise<number>((resolve, reject) => {
socket.on('data', buffer => {
progress.report(buffer);
bytesRead += buffer.length;
if (len > 0 && bytesRead > len) {
socket.destroy();
}
});
socket.on('close', hadErr => {
if (hadErr) {
reject(hadErr);
} else {
resolve(bytesRead);
}
});
socket.resume();
});
});
}
write(resource: vscode.Uri, content: Uint8Array): Promise<void> {
return this._withConnection('put', content, resource.path);
}
rmdir(resource: vscode.Uri): Promise<void> {
return this._withConnection('raw', 'RMD', [resource.path]);
}
mkdir(resource: vscode.Uri): Promise<vscode.FileStat> {
return this._withConnection('raw', 'MKD', [resource.path])
.then(() => this.stat(resource));
}
unlink(resource: vscode.Uri): Promise<void> {
return this._withConnection('raw', 'DELE', [resource.path]);
}
move(resource: vscode.Uri, target: vscode.Uri): Promise<vscode.FileStat> {
return this._withConnection<void>('raw', 'RNFR', [resource.path]).then(() => {
return this._withConnection<void>('raw', 'RNTO', [target.path]);
}).then(() => {
return this.stat(target);
});
}
}
export interface FtpNode {
resource: vscode.Uri;
isDirectory: boolean;
}
export class FtpTreeDataProvider implements TreeDataProvider<FtpNode> {
private _onDidChange: vscode.EventEmitter<FtpNode> = new vscode.EventEmitter<FtpNode>();
readonly onDidChangeTreeData = this._onDidChange.event;
constructor(private readonly fileSystemProvider: FtpFileSystemProvider) { }
refresh() {
this._onDidChange.fire();
}
public getTreeItem(element: FtpNode): TreeItem {
return {
id: element.resource.fsPath,
resourceUri: element.resource,
collapsibleState: element.isDirectory ? TreeItemCollapsibleState.Collapsed : void 0,
command: element.isDirectory ? void 0 : {
command: 'ftpExplorer.openFtpResource',
arguments: [element.resource],
title: 'Open FTP Resource'
}
};
}
public getChildren(element?: FtpNode): FtpNode[] | Thenable<FtpNode[]> {
return this.fileSystemProvider.readdir(element ? element.resource : this.fileSystemProvider.root)
.then((result: [vscode.Uri, vscode.FileStat][]) => this.sort(result.map(r => ({ resource: r[0], isDirectory: r[1].type !== vscode.FileType.File }))));
}
public getParent(element: FtpNode): FtpNode {
const parent = vscode.Uri.parse(dirname(element.resource.fsPath));
return parent.fsPath !== this.fileSystemProvider.root.fsPath ? { resource: parent, isDirectory: true } : null;
}
private sort(nodes: FtpNode[]): FtpNode[] {
return nodes.sort((n1, n2) => {
if (n1.isDirectory && !n2.isDirectory) {
return -1;
}
if (!n1.isDirectory && n2.isDirectory) {
return 1;
}
return basename(n1.resource.fsPath).localeCompare(basename(n2.resource.fsPath));
});
}
}
export class FtpExplorer {
private ftpViewer: TreeView<FtpNode>;
constructor(context: vscode.ExtensionContext) {
const fileProvider = new FtpFileSystemProvider(vscode.Uri.parse('ftp://mirror.switch.ch/'), 'anonymous', 'anonymous@anonymous.de')
const treeDataProvider = new FtpTreeDataProvider(fileProvider);
context.subscriptions.push(vscode.workspace.registerFileSystemProvider('ftp', fileProvider));
this.ftpViewer = vscode.window.registerTreeDataProvider('ftpExplorer', treeDataProvider);
vscode.commands.registerCommand('ftpExplorer.refresh', () => treeDataProvider.refresh());
vscode.commands.registerCommand('ftpExplorer.openFtpResource', resource => this.openResource(resource));
vscode.commands.registerCommand('ftpExplorer.revealResource', () => this.reveal());
}
private openResource(resource: vscode.Uri): void {
vscode.workspace.openTextDocument(resource).then(document => vscode.window.showTextDocument(document));
}
private reveal(): void {
const node = this.getNode();
if (node) {
this.ftpViewer.reveal(node);
}
}
private getNode(): FtpNode {
if (vscode.window.activeTextEditor) {
if (vscode.window.activeTextEditor.document.uri.scheme === 'ftp') {
return { resource: vscode.window.activeTextEditor.document.uri, isDirectory: false };
}
}
return null;
}
}

View File

@ -0,0 +1,193 @@
import { ExtensionContext, EventEmitter, TreeItem, Event, window, TreeItemCollapsibleState, Uri, commands, workspace, TextDocumentContentProvider, CancellationToken, ProviderResult, TreeView } from 'vscode';
import * as vscode from 'vscode';
import * as Client from 'ftp';
import { basename, dirname, join } from 'path';
import { Socket } from 'net';
import * as JSFtp from 'jsftp';
import { TreeDataProvider } from 'vscode';
interface IEntry {
name: string;
type: string;
}
export interface FtpNode {
resource: vscode.Uri;
isDirectory: boolean;
}
export class FtpModel {
private nodes: Map<string, FtpNode> = new Map<string, FtpNode>();
constructor(readonly host: string, private user: string, private password: string) {
}
public connect(): Thenable<Client> {
return new Promise((c, e) => {
const client = new Client();
client.on('ready', () => {
c(client);
});
client.on('error', error => {
e('Error while connecting: ' + error.message);
})
client.connect({
host: this.host,
username: this.user,
password: this.password
});
});
}
public get roots(): Thenable<FtpNode[]> {
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 => ({ resource: Uri.parse(`ftp://${this.host}///${entry.name}`), isDirectory: entry.type === 'd' }))));
});
});
});
}
public getChildren(node: FtpNode): Thenable<FtpNode[]> {
return this.connect().then(client => {
return new Promise((c, e) => {
client.list(node.resource.fsPath, (err, list) => {
if (err) {
return e(err);
}
client.end();
return c(this.sort(list.map(entry => ({ resource: Uri.parse(`${node.resource.fsPath}/${entry.name}`), isDirectory: entry.type === 'd' }))));
});
});
});
}
private sort(nodes: FtpNode[]): FtpNode[] {
return nodes.sort((n1, n2) => {
if (n1.isDirectory && !n2.isDirectory) {
return -1;
}
if (!n1.isDirectory && n2.isDirectory) {
return 1;
}
return basename(n1.resource.fsPath).localeCompare(basename(n2.resource.fsPath));
});
}
public getContent(resource: Uri): Thenable<string> {
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<FtpNode>, TextDocumentContentProvider {
private _onDidChangeTreeData: EventEmitter<any> = new EventEmitter<any>();
readonly onDidChangeTreeData: Event<any> = this._onDidChangeTreeData.event;
constructor(private readonly model: FtpModel) { }
public refresh(): any {
this._onDidChangeTreeData.fire();
}
public getTreeItem(element: FtpNode): TreeItem {
return {
id: element.resource.fsPath,
resourceUri: element.resource,
collapsibleState: element.isDirectory ? TreeItemCollapsibleState.Collapsed : void 0,
command: element.isDirectory ? void 0 : {
command: 'ftpExplorer.openFtpResource',
arguments: [element.resource],
title: 'Open FTP Resource'
}
};
}
public getChildren(element?: FtpNode): FtpNode[] | Thenable<FtpNode[]> {
return element ? this.model.getChildren(element) : this.model.roots;
}
public getParent(element: FtpNode): FtpNode {
const parent = vscode.Uri.parse(dirname(element.resource.fsPath));
return parent.fsPath !== this.model.host ? { resource: parent, isDirectory: true } : null;
}
public provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult<string> {
return this.model.getContent(uri).then(content => content);
}
}
export class FtpExplorer {
private ftpViewer: TreeView<FtpNode>;
constructor(context: vscode.ExtensionContext) {
const ftpModel = new FtpModel('mirror.switch.ch', 'anonymous', 'anonymous@anonymous.de');
const treeDataProvider = new FtpTreeDataProvider(ftpModel);
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('ftp', treeDataProvider));
this.ftpViewer = vscode.window.registerTreeDataProvider('ftpExplorer', treeDataProvider);
vscode.commands.registerCommand('ftpExplorer.refresh', () => treeDataProvider.refresh());
vscode.commands.registerCommand('ftpExplorer.openFtpResource', resource => this.openResource(resource));
vscode.commands.registerCommand('ftpExplorer.revealResource', () => this.reveal());
}
private openResource(resource: vscode.Uri): void {
vscode.window.showTextDocument(resource);
}
private reveal(): void {
const node = this.getNode();
if (node) {
this.ftpViewer.reveal(node);
}
}
private getNode(): FtpNode {
if (vscode.window.activeTextEditor) {
if (vscode.window.activeTextEditor.document.uri.scheme === 'ftp') {
return { resource: vscode.window.activeTextEditor.document.uri, isDirectory: false };
}
}
return null;
}
}

View File

@ -1,168 +0,0 @@
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 {
constructor(private host: string, private user: string, private password: string) {
}
public connect(): Thenable<Client> {
return new Promise((c, e) => {
const client = new Client();
client.on('ready', () => {
c(client);
});
client.on('error', error => {
e('Error while connecting: ' + error.message);
})
client.connect({
host: this.host,
username: this.user,
password: this.password
});
});
}
public get roots(): Thenable<FtpNode[]> {
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<FtpNode[]> {
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<string> {
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<FtpNode>, TextDocumentContentProvider {
private _onDidChangeTreeData: EventEmitter<any> = new EventEmitter<any>();
readonly onDidChangeTreeData: Event<any> = this._onDidChangeTreeData.event;
private model: FtpModel;
refresh(): any {
this._onDidChangeTreeData.fire();
}
public getTreeItem(element: FtpNode): TreeItem {
return {
resourceUri: element.resource,
collapsibleState: element.isFolder ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None,
command: element.isFolder ? void 0 : {
command: 'openFtpResource',
arguments: [element.resource],
title: 'Open FTP Resource'
}
};
}
public getChildren(element?: FtpNode): FtpNode[] | Thenable<FtpNode[]> {
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<string> {
return this.model.getContent(uri);
}
}

48
tree-view-sample/src/jsftp.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
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(command: string, args: any[], callback: JSFtp.Callback<void>): void;
raw<T>(command: string, args: any[], callback: JSFtp.Callback<T>): void;
}
interface JSFtpConstructor {
new(options: JSFtp.JSFtpOptions): JSFtp;
}
declare const JSFtp: JSFtpConstructor;
export = JSFtp;

View File

@ -46,23 +46,23 @@ export class DepNodeProvider implements vscode.TreeDataProvider<Dependency> {
if (this.pathExists(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const toDep = (moduleName: string): Dependency => {
const toDep = (moduleName: string, version: string): Dependency => {
if (this.pathExists(path.join(this.workspaceRoot, 'node_modules', moduleName))) {
return new Dependency(moduleName, vscode.TreeItemCollapsibleState.Collapsed);
return new Dependency(moduleName, version, vscode.TreeItemCollapsibleState.Collapsed);
} else {
return new Dependency(moduleName, vscode.TreeItemCollapsibleState.None, {
return new Dependency(moduleName, version, vscode.TreeItemCollapsibleState.None, {
command: 'extension.openPackageOnNpm',
title: '',
arguments: [moduleName],
arguments: [moduleName]
});
}
}
const deps = packageJson.dependencies
? Object.keys(packageJson.dependencies).map(toDep)
? Object.keys(packageJson.dependencies).map(dep => toDep(dep, packageJson.dependencies[dep]))
: [];
const devDeps = packageJson.devDependencies
? Object.keys(packageJson.devDependencies).map(toDep)
? Object.keys(packageJson.devDependencies).map(dep => toDep(dep, packageJson.devDependencies[dep]))
: [];
return deps.concat(devDeps);
} else {
@ -85,12 +85,17 @@ class Dependency extends vscode.TreeItem {
constructor(
public readonly label: string,
private version: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly command?: vscode.Command
) {
super(label, collapsibleState);
}
get tooltip(): string {
return `${this.label}-${this.version}`
}
iconPath = {
light: path.join(__filename, '..', '..', '..', 'resources', 'light', 'dependency.svg'),
dark: path.join(__filename, '..', '..', '..', 'resources', 'dark', 'dependency.svg')