From bde4774ab52e55265a1a4a168f31a9eaafebc028 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 24 Jul 2018 15:04:52 +0200 Subject: [PATCH] new file explorer view --- tree-view-sample/package.json | 32 ++- tree-view-sample/src/extension.ts | 2 + tree-view-sample/src/fileExplorer.ts | 311 +++++++++++++++++++++++++++ 3 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 tree-view-sample/src/fileExplorer.ts diff --git a/tree-view-sample/package.json b/tree-view-sample/package.json index 256702a1..5ec0e818 100644 --- a/tree-view-sample/package.json +++ b/tree-view-sample/package.json @@ -5,7 +5,7 @@ "version": "0.0.1", "publisher": "ms-vscode", "engines": { - "vscode": "^1.23.0" + "vscode": "^1.25.0" }, "enableProposedApi": true, "categories": [ @@ -16,6 +16,7 @@ "onView:ftpExplorer", "onView:jsonOutline", "onLanguage:json", + "onLanguage:fileExplorer", "onLanguage:jsonc" ], "main": "./out/src/extension", @@ -45,6 +46,10 @@ { "id": "ftpExplorer", "name": "FTP Explorer" + }, + { + "id": "fileExplorer", + "name": "File Explorer" } ] }, @@ -100,6 +105,18 @@ { "command": "jsonOutline.renameNode", "title": "Rename" + }, + { + "command": "fileExplorer.refreshFile", + "title": "Refresh", + "icon": { + "light": "resources/light/refresh.svg", + "dark": "resources/dark/refresh.svg" + } + }, + { + "command": "fileExplorer.openFile", + "title": "Open File" } ], "menus": { @@ -142,6 +159,11 @@ "command": "jsonOutline.refreshNode", "when": "view == jsonOutline", "group": "inline" + }, + { + "command": "fileExplorer.refreshFile", + "when": "view == fileExplorer && viewItem == file", + "group": "inline" } ] }, @@ -165,11 +187,15 @@ "devDependencies": { "typescript": "^2.1.4", "vscode": "^1.1.17", - "@types/node": "*" + "@types/node": "*", + "@types/mkdirp": "^0.5.2", + "@types/rimraf": "^2.0.2" }, "dependencies": { "jsonc-parser": "^0.4.2", "ftp": "^0.3.10", - "jsftp": "^2.0.0" + "jsftp": "^2.0.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.2" } } \ No newline at end of file diff --git a/tree-view-sample/src/extension.ts b/tree-view-sample/src/extension.ts index a9dc2b4d..f236fc55 100644 --- a/tree-view-sample/src/extension.ts +++ b/tree-view-sample/src/extension.ts @@ -5,10 +5,12 @@ import * as vscode from 'vscode'; import { DepNodeProvider } from './nodeDependencies' import { JsonOutlineProvider } from './jsonOutline' import { FtpExplorer } from './ftpExplorer.textDocumentContentProvider' +import { FileExplorer } from './fileExplorer'; export function activate(context: vscode.ExtensionContext) { // Complete Tree View Sample new FtpExplorer(context); + new FileExplorer(context); // Following are just data provider samples const rootPath = vscode.workspace.rootPath; diff --git a/tree-view-sample/src/fileExplorer.ts b/tree-view-sample/src/fileExplorer.ts new file mode 100644 index 00000000..f7bfbc50 --- /dev/null +++ b/tree-view-sample/src/fileExplorer.ts @@ -0,0 +1,311 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as mkdirp from 'mkdirp'; +import * as rimraf from 'rimraf'; + +//#region Utilities + +namespace _ { + + function handleResult(resolve: (result: T) => void, reject: (error: Error) => void, error: Error | null | undefined, result: T): void { + if (error) { + reject(massageError(error)); + } else { + resolve(result); + } + } + + function massageError(error: Error & { code?: string }): Error { + if (error.code === 'ENOENT') { + return vscode.FileSystemError.FileNotFound(); + } + + if (error.code === 'EISDIR') { + return vscode.FileSystemError.FileIsADirectory(); + } + + if (error.code === 'EEXIST') { + return vscode.FileSystemError.FileExists(); + } + + if (error.code === 'EPERM' || error.code === 'EACCESS') { + return vscode.FileSystemError.NoPermissions(); + } + + return error; + } + + export function checkCancellation(token: vscode.CancellationToken): void { + if (token.isCancellationRequested) { + throw new Error('Operation cancelled'); + } + } + + export function normalizeNFC(items: string): string; + export function normalizeNFC(items: string[]): string[]; + export function normalizeNFC(items: string | string[]): string | string[] { + if (process.platform !== 'darwin') { + return items; + } + + if (Array.isArray(items)) { + return items.map(item => item.normalize('NFC')); + } + + return items.normalize('NFC'); + } + + export function readdir(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readdir(path, (error, children) => handleResult(resolve, reject, error, normalizeNFC(children))); + }); + } + + export function stat(path: string): Promise { + return new Promise((resolve, reject) => { + fs.stat(path, (error, stat) => handleResult(resolve, reject, error, stat)); + }); + } + + export function readfile(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, (error, buffer) => handleResult(resolve, reject, error, buffer)); + }); + } + + export function writefile(path: string, content: Buffer): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(path, content, error => handleResult(resolve, reject, error, void 0)); + }); + } + + export function exists(path: string): Promise { + return new Promise((resolve, reject) => { + fs.exists(path, exists => handleResult(resolve, reject, null, exists)); + }); + } + + export function rmrf(path: string): Promise { + return new Promise((resolve, reject) => { + rimraf(path, error => handleResult(resolve, reject, error, void 0)); + }); + } + + export function mkdir(path: string): Promise { + return new Promise((resolve, reject) => { + mkdirp(path, error => handleResult(resolve, reject, error, void 0)); + }); + } + + export function rename(oldPath: string, newPath: string): Promise { + return new Promise((resolve, reject) => { + fs.rename(oldPath, newPath, error => handleResult(resolve, reject, error, void 0)); + }); + } + + export function unlink(path: string): Promise { + return new Promise((resolve, reject) => { + fs.unlink(path, error => handleResult(resolve, reject, error, void 0)); + }); + } +} + +export class FileStat implements vscode.FileStat { + + constructor(private fsStat: fs.Stats) { } + + get type(): vscode.FileType { + return this.fsStat.isFile() ? vscode.FileType.File : this.fsStat.isDirectory() ? vscode.FileType.Directory : this.fsStat.isSymbolicLink() ? vscode.FileType.SymbolicLink : vscode.FileType.Unknown; + } + + get isFile(): boolean | undefined { + return this.fsStat.isFile(); + } + + get isDirectory(): boolean | undefined { + return this.fsStat.isDirectory(); + } + + get isSymbolicLink(): boolean | undefined { + return this.fsStat.isSymbolicLink(); + } + + get size(): number { + return this.fsStat.size; + } + + get ctime(): number { + return this.fsStat.ctime.getTime(); + } + + get mtime(): number { + return this.fsStat.mtime.getTime(); + } +} + +interface Entry { + uri: vscode.Uri, + type: vscode.FileType +} + +//#endregion + +export class FileSystemProvider implements vscode.TreeDataProvider, vscode.FileSystemProvider { + + private _onDidChangeFile: vscode.EventEmitter; + + constructor() { + this._onDidChangeFile = new vscode.EventEmitter(); + } + + get onDidChangeFile(): vscode.Event { + return this._onDidChangeFile.event; + } + + watch(uri: vscode.Uri, options: { recursive: boolean; excludes: string[]; }): vscode.Disposable { + const watcher = fs.watch(uri.fsPath, { recursive: options.recursive }, async (event: string, filename: string | Buffer) => { + const filepath = path.join(uri.fsPath, _.normalizeNFC(filename.toString())); + + // TODO support excludes (using minimatch library?) + + this._onDidChangeFile.fire([{ + type: event === 'change' ? vscode.FileChangeType.Changed : await _.exists(filepath) ? vscode.FileChangeType.Created : vscode.FileChangeType.Deleted, + uri: uri.with({ path: filepath }) + } as vscode.FileChangeEvent]); + }); + + return { dispose: () => watcher.close() }; + } + + stat(uri: vscode.Uri): vscode.FileStat | Thenable { + return this._stat(uri.fsPath); + } + + async _stat(path: string): Promise { + return new FileStat(await _.stat(path)); + } + + readDirectory(uri: vscode.Uri): [string, vscode.FileType][] | Thenable<[string, vscode.FileType][]> { + return this._readDirectory(uri); + } + + async _readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + const children = await _.readdir(uri.fsPath); + + const result: [string, vscode.FileType][] = []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const stat = await this._stat(path.join(uri.fsPath, child)); + result.push([child, stat.type]); + } + + return Promise.resolve(result); + } + + createDirectory(uri: vscode.Uri): void | Thenable { + return _.mkdir(uri.fsPath); + } + + readFile(uri: vscode.Uri): Uint8Array | Thenable { + return _.readfile(uri.fsPath); + } + + writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Thenable { + return this._writeFile(uri, content, options); + } + + async _writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): Promise { + const exists = await _.exists(uri.fsPath); + if (!exists) { + if (!options.create) { + throw vscode.FileSystemError.FileNotFound(); + } + + await _.mkdir(path.dirname(uri.fsPath)); + } else { + if (!options.overwrite) { + throw vscode.FileSystemError.FileExists(); + } + } + + return _.writefile(uri.fsPath, content as Buffer); + } + + delete(uri: vscode.Uri, options: { recursive: boolean; }): void | Thenable { + if (options.recursive) { + return _.rmrf(uri.fsPath); + } + + return _.unlink(uri.fsPath); + } + + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): void | Thenable { + return this._rename(oldUri, newUri, options); + } + + async _rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): Promise { + const exists = await _.exists(newUri.fsPath); + if (exists) { + if (!options.overwrite) { + throw vscode.FileSystemError.FileExists(); + } else { + await _.rmrf(newUri.fsPath); + } + } + + const parentExists = await _.exists(path.dirname(newUri.fsPath)); + if (!parentExists) { + await _.mkdir(path.dirname(newUri.fsPath)); + } + + return _.rename(oldUri.fsPath, newUri.fsPath); + } + + // tree data provider + + async getChildren(element?: Entry): Promise { + if (element) { + const children = await this.readDirectory(element.uri); + return children.map(([name, type]) => ({ uri: vscode.Uri.file(path.join(element.uri.fsPath, name)), type })); + } + + const workspaceFolder = vscode.workspace.workspaceFolders.filter(folder => folder.uri.scheme === 'file')[0]; + if (workspaceFolder) { + const children = await this.readDirectory(workspaceFolder.uri); + children.sort((a, b) => { + if (a[1] === b[1]) { + return a[0].localeCompare(b[0]); + } + return a[1] === vscode.FileType.Directory ? -1 : 1; + }) + return children.map(([name, type]) => ({ uri: vscode.Uri.file(path.join(workspaceFolder.uri.fsPath, name)), type })); + } + + return []; + } + + getTreeItem(element: Entry): vscode.TreeItem { + const treeItem = new vscode.TreeItem(element.uri, element.type === vscode.FileType.Directory ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None); + if (element.type === vscode.FileType.File) { + treeItem.command = { command: 'fileExplorer.openFile', title: "Open File", arguments: [element.uri], }; + treeItem.contextValue = 'file'; + } + return treeItem; + } +} + +export class FileExplorer { + + private fileExplorer: vscode.TreeView; + + constructor(context: vscode.ExtensionContext) { + const treeDataProvider = new FileSystemProvider(); + this.fileExplorer = vscode.window.createTreeView('fileExplorer', { treeDataProvider }); + vscode.commands.registerCommand('fileExplorer.openFile', (resource) => this.openResource(resource)); + } + + private openResource(resource: vscode.Uri): void { + vscode.window.showTextDocument(resource); + } +} \ No newline at end of file