diff --git a/tree-explorer-sample/.vscode/launch.json b/tree-explorer-sample/.vscode/launch.json index 858bd1a1..d0a44a90 100644 --- a/tree-explorer-sample/.vscode/launch.json +++ b/tree-explorer-sample/.vscode/launch.json @@ -23,6 +23,16 @@ "sourceMaps": true, "outFiles": ["${workspaceRoot}/out/test/**/*.js"], "preLaunchTask": "npm" + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Extension Host", + "protocol": "legacy", + "port": 5870, + "sourceMaps": true, + "restart": true, + "outDir": "${workspaceRoot}/out/src" } ] } diff --git a/tree-explorer-sample/package.json b/tree-explorer-sample/package.json index 5674e341..7583e773 100644 --- a/tree-explorer-sample/package.json +++ b/tree-explorer-sample/package.json @@ -5,22 +5,30 @@ "version": "0.0.1", "publisher": "octref", "engines": { - "vscode": "^1.7.0" + "vscode": "^1.12.0" }, "enableProposedApi": true, "categories": [ "Other" ], "activationEvents": [ - "*" + "onView:nodeDependencies", + "onView:jsonOutline" ], "main": "./out/src/extension", "icon": "media/dep.png", "contributes": { - "explorer": { - "treeLabel": "Dependencies", - "icon": "media/dep.svg", - "treeExplorerNodeProviderId": "depTree" + "views": { + "explorer": [ + { + "id": "nodeDependencies", + "name": "Node Dependencies" + }, + { + "id": "jsonOutline", + "name": "Json Outline" + } + ] } }, "scripts": { @@ -32,5 +40,8 @@ "typescript": "^2.1.4", "vscode": "^1.0.0", "@types/node": "*" + }, + "dependencies": { + "jsonc-parser": "^0.4.2" } -} +} \ No newline at end of file diff --git a/tree-explorer-sample/resources/light/boolean.svg b/tree-explorer-sample/resources/light/boolean.svg new file mode 100644 index 00000000..d9fd295d --- /dev/null +++ b/tree-explorer-sample/resources/light/boolean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-explorer-sample/resources/light/number.svg b/tree-explorer-sample/resources/light/number.svg new file mode 100644 index 00000000..7b026654 --- /dev/null +++ b/tree-explorer-sample/resources/light/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tree-explorer-sample/resources/light/string.svg b/tree-explorer-sample/resources/light/string.svg new file mode 100644 index 00000000..943e69c4 --- /dev/null +++ b/tree-explorer-sample/resources/light/string.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 ab7a8eba..a7326c6c 100644 --- a/tree-explorer-sample/src/extension.ts +++ b/tree-explorer-sample/src/extension.ts @@ -1,145 +1,23 @@ 'use strict'; import * as vscode from 'vscode'; -import { TreeExplorerNodeProvider } from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; +import { DepNodeProvider } from './nodeDependencies' +import { JsonOutlineProvider } from './jsonOutline' export function activate(context: vscode.ExtensionContext) { const rootPath = vscode.workspace.rootPath; + const jsonOutlineProvider = new JsonOutlineProvider(context); // The `providerId` here must be identical to `contributes.explorer.treeExplorerNodeProviderId` in package.json. - vscode.window.registerTreeExplorerNodeProvider('depTree', new DepNodeProvider(rootPath)); + vscode.window.registerTreeDataProvider('nodeDependencies', new DepNodeProvider(rootPath)); + vscode.window.registerTreeDataProvider('jsonOutline', jsonOutlineProvider); - // This command will be invoked using exactly the node you provided in `resolveChildren`. - vscode.commands.registerCommand('extension.openPackageOnNpm', (node: DepNode) => { - if (node.kind === 'leaf') { - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`https://www.npmjs.com/package/${node.moduleName}`)); - } + vscode.commands.registerCommand('extension.openPackageOnNpm', (node: vscode.TreeItem) => { + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`https://www.npmjs.com/package/${node.label}`)); + }); + + vscode.commands.registerCommand('extension.openJsonSelection', node => { + jsonOutlineProvider.select(node); }); } - -class DepNodeProvider implements TreeExplorerNodeProvider { - constructor(private workspaceRoot: string) { - - } - - /** - * As root node is invisible, its label doesn't matter. - */ - getLabel(node: DepNode): string { - return node.kind === 'root' ? '' : node.moduleName; - } - - /** - * Leaf is unexpandable. - */ - getHasChildren(node: DepNode): boolean { - return node.kind !== 'leaf'; - } - - /** - * Invoke `extension.openPackageOnNpm` command when a Leaf node is clicked. - */ - getClickCommand(node: DepNode): string { - return node.kind === 'leaf' ? 'extension.openPackageOnNpm' : null; - } - - provideRootNode(): DepNode { - return new Root(); - } - - resolveChildren(node: DepNode): Thenable { - if (!this.workspaceRoot) { - vscode.window.showInformationMessage('No dependency in empty workspace'); - return Promise.resolve([]); - } - - return new Promise((resolve) => { - switch (node.kind) { - case 'root': - const packageJsonPath = path.join(this.workspaceRoot, 'package.json'); - if (this.pathExists(packageJsonPath)) { - resolve(this.getDepsInPackageJson(packageJsonPath)); - } else { - vscode.window.showInformationMessage('Workspace has no package.json'); - resolve([]); - } - break; - /** - * npm3 has flat dependencies, so indirect dependencies are still in `node_modules`. - */ - case 'node': - resolve(this.getDepsInPackageJson(path.join(this.workspaceRoot, 'node_modules', node.moduleName, 'package.json'))); - break; - case 'leaf': - resolve([]); - } - }); - } - - /** - * Given the path to package.json, read all its dependencies and devDependencies. - */ - private getDepsInPackageJson(packageJsonPath: string): DepNode[] { - if (this.pathExists(packageJsonPath)) { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - - const toDep = (moduleName: string): DepNode => { - if (this.pathExists(path.join(this.workspaceRoot, 'node_modules', moduleName))) { - return new Node(moduleName); - } else { - return new Leaf(moduleName); - } - } - - const deps = packageJson.dependencies - ? Object.keys(packageJson.dependencies).map(toDep) - : []; - const devDeps = packageJson.devDependencies - ? Object.keys(packageJson.devDependencies).map(toDep) - : []; - return deps.concat(devDeps); - } else { - return []; - } - } - - private pathExists(p: string): boolean { - try { - fs.accessSync(p); - } catch (err) { - return false; - } - - return true; - } -} - -type DepNode = Root // Root node - | Node // A dependency installed to `node_modules` - | Leaf // A dependency not present in `node_modules` - ; - -class Root { - kind: 'root' = 'root'; -} - -class Node { - kind: 'node' = 'node'; - - constructor( - public moduleName: string - ) { - } -} - -class Leaf { - kind: 'leaf' = 'leaf' - - constructor( - public moduleName: string - ) { - } -} diff --git a/tree-explorer-sample/src/jsonOutline.ts b/tree-explorer-sample/src/jsonOutline.ts new file mode 100644 index 00000000..58c9aef3 --- /dev/null +++ b/tree-explorer-sample/src/jsonOutline.ts @@ -0,0 +1,117 @@ +import * as vscode from 'vscode'; +import * as json from 'jsonc-parser'; +import * as path from 'path'; + +export class JsonOutlineProvider implements vscode.TreeDataProvider { + + private _onDidChange : vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChange : vscode.Event = this._onDidChange.event; + + private tree: json.Node; + private editor: vscode.TextEditor; + + constructor(private context: vscode.ExtensionContext) { + vscode.window.onDidChangeActiveTextEditor(editor => { + if (this.parseTree()) { + this._onDidChange.fire(); + } + }); + this.parseTree(); + } + + private parseTree() : boolean { + this.editor = vscode.window.activeTextEditor; + if (this.editor && this.editor.document.languageId === 'json') { + this.tree = json.parseTree(this.editor.document.getText()); + return true; + } + return false; + } + + getChildren(node?: json.Node): Thenable { + if (node) { + return Promise.resolve(node.parent.type === 'array' ? this.toArrayValueNode(node) : (node.type === 'array' ? node.children[0].children : node.children[1].children)); + } else { + return Promise.resolve(this.tree.children); + } + } + + private toArrayValueNode(node: json.Node): json.Node[] { + if (node.type === 'array' || node.type === 'object') { + return node.children; + } + node['arrayValue'] = true; + return [node]; + } + + getTreeItem(node: json.Node): vscode.TreeItem { + let valueNode = node.parent.type === 'array' ? node : node.children[1]; + let hasChildren = (node.parent.type === 'array' && !node['arrayValue']) || valueNode.type === 'object' || valueNode.type === 'array'; + return { + label: this.getLabel(node), + collapsibleState: hasChildren ? vscode.TreeItemCollapsibleState.Collapsed : null, + command: !hasChildren ? { + command: 'extension.openJsonSelection', + title: '', + } : null, + iconPath: this.getIcon(node) + }; + } + + select(node: json.Node) { + this.editor.selection = new vscode.Selection(this.editor.document.positionAt(node.offset), this.editor.document.positionAt(node.offset + node.length)); + } + + private getIcon(node: json.Node): any { + let nodeType = this.getNodeType(node); + if (nodeType === 'boolean') { + return { + light: this.context.asAbsolutePath(path.join('resources', 'light', 'boolean.svg')), + dark: this.context.asAbsolutePath(path.join('resources', 'dark', 'boolean.svg')) + } + } + if (nodeType === 'string') { + return { + light: this.context.asAbsolutePath(path.join('resources', 'light', 'string.svg')), + dark: this.context.asAbsolutePath(path.join('resources', 'dark', 'string.svg')) + } + } + if (nodeType === 'number') { + return { + light: this.context.asAbsolutePath(path.join('resources', 'light', 'number.svg')), + dark: this.context.asAbsolutePath(path.join('resources', 'dark', 'number.svg')) + } + } + return null; + } + + private getNodeType(node: json.Node): json.NodeType { + if (node.parent.type === 'array') { + return node.type; + } + return node.children[1].type; + } + + private getLabel(node: json.Node): string { + if (node.parent.type === 'array') { + if (node['arrayValue']) { + delete node['arrayValue']; + if (!node.children) { + return node.value.toString(); + } + } else { + return node.parent.children.indexOf(node).toString(); + } + } + const property = node.children[0].value.toString(); + if (node.children[1].type === 'object') { + return '{ } ' + property; + } + if (node.children[1].type === 'array') { + return '[ ] ' + property; + } + const value = this.editor.document.getText(new vscode.Range(this.editor.document.positionAt(node.children[1].offset), this.editor.document.positionAt(node.children[1].offset + node.children[1].length))) + return `${property}: ${value}`; + } +} + diff --git a/tree-explorer-sample/src/nodeDependencies.ts b/tree-explorer-sample/src/nodeDependencies.ts new file mode 100644 index 00000000..98e82946 --- /dev/null +++ b/tree-explorer-sample/src/nodeDependencies.ts @@ -0,0 +1,90 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +export class DepNodeProvider implements vscode.TreeDataProvider { + + constructor(private workspaceRoot: string) { + } + + getTreeItem(element: Dependency): vscode.TreeItem { + return element; + } + + getChildren(element?: Dependency): Thenable { + if (!this.workspaceRoot) { + vscode.window.showInformationMessage('No dependency in empty workspace'); + return Promise.resolve([]); + } + + return new Promise(resolve => { + if (element) { + resolve(this.getDepsInPackageJson(path.join(this.workspaceRoot, 'node_modules', element.label, 'package.json'))); + } else { + const packageJsonPath = path.join(this.workspaceRoot, 'package.json'); + if (this.pathExists(packageJsonPath)) { + resolve(this.getDepsInPackageJson(packageJsonPath)); + } else { + vscode.window.showInformationMessage('Workspace has no package.json'); + resolve([]); + } + } + }); + } + + /** + * Given the path to package.json, read all its dependencies and devDependencies. + */ + private getDepsInPackageJson(packageJsonPath: string): Dependency[] { + if (this.pathExists(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + + const toDep = (moduleName: string): Dependency => { + if (this.pathExists(path.join(this.workspaceRoot, 'node_modules', moduleName))) { + return new Node(moduleName); + } else { + return new Dependency(moduleName, { + command: 'extension.openPackageOnNpm', + title: '' + }); + } + } + + const deps = packageJson.dependencies + ? Object.keys(packageJson.dependencies).map(toDep) + : []; + const devDeps = packageJson.devDependencies + ? Object.keys(packageJson.devDependencies).map(toDep) + : []; + return deps.concat(devDeps); + } else { + return []; + } + } + + private pathExists(p: string): boolean { + try { + fs.accessSync(p); + } catch (err) { + return false; + } + + return true; + } +} + +class Dependency implements vscode.TreeItem { + + constructor( + public readonly label: string, + public readonly command?: vscode.Command + ) { + } + +} + +class Node extends Dependency { + + readonly collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + +} \ No newline at end of file diff --git a/tree-explorer-sample/typings/vscode.proposed.d.ts b/tree-explorer-sample/typings/vscode.proposed.d.ts index 421c7c9c..c1cad091 100644 --- a/tree-explorer-sample/typings/vscode.proposed.d.ts +++ b/tree-explorer-sample/typings/vscode.proposed.d.ts @@ -13,76 +13,158 @@ declare module 'vscode' { } export namespace window { - /** - * Register a [TreeExplorerNodeProvider](#TreeExplorerNodeProvider). - * - * @param providerId A unique id that identifies the provider. - * @param provider A [TreeExplorerNodeProvider](#TreeExplorerNodeProvider). - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + * Register a [TreeDataProvider](#TreeDataProvider) for the registered view `id`. + * @param id View id. + * @param treeDataProvider A [TreeDataProvider](#TreeDataProvider) that provides tree data for the view */ - export function registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider): Disposable; + export function registerTreeDataProvider(id: string, treeDataProvider: TreeDataProvider): Disposable; } /** - * A node provider for a tree explorer contribution. - * - * Providers are registered through (#workspace.registerTreeExplorerNodeProvider) with a - * `providerId` that corresponds to the `treeExplorerNodeProviderId` in the extension's - * `contributes.explorer` section. - * - * The contributed tree explorer will ask the corresponding provider to provide the root - * node and resolve children for each node. In addition, the provider could **optionally** - * provide the following information for each node: - * - label: A human-readable label used for rendering the node. - * - hasChildren: Whether the node has children and is expandable. - * - clickCommand: A command to execute when the node is clicked. + * A data provider that provides tree data for a view */ - export interface TreeExplorerNodeProvider { + export interface TreeDataProvider { + /** + * An optional event to signal that an element or root has changed. + */ + onDidChange?: Event; /** - * Provide the root node. This function will be called when the tree explorer is activated - * for the first time. The root node is hidden and its direct children will be displayed on the first level of - * the tree explorer. + * get [TreeItem](#TreeItem) representation of the `element` * - * @return The root node. + * @param element The element for which [TreeItem](#TreeItem) representation is asked for. + * @return [TreeItem](#TreeItem) representation of the element */ - provideRootNode(): T | Thenable; + getTreeItem(element: T): TreeItem; /** - * Resolve the children of `node`. + * get the children of `element` or root. * - * @param node The node from which the provider resolves children. - * @return Children of `node`. + * @param element The element from which the provider gets children for. + * @return Children of `element` or root. */ - resolveChildren(node: T): T[] | Thenable; + getChildren(element?: T): T[] | Thenable; + } + + export interface TreeItem { + /** + * Label of the tree item + */ + label: string; /** - * Provide a human-readable string that will be used for rendering the node. Default to use - * `node.toString()` if not provided. - * - * @param node The node from which the provider computes label. - * @return A human-readable label. + * The icon path for the tree item */ - getLabel?(node: T): string; + iconPath?: string | Uri | { light: string | Uri; dark: string | Uri }; /** - * Determine if `node` has children and is expandable. Default to `true` if not provided. - * - * @param node The node to determine if it has children and is expandable. - * @return A boolean that determines if `node` has children and is expandable. + * The [command](#Command) which should be run when the tree item + * is open in the Source Control viewlet. */ - getHasChildren?(node: T): boolean; + command?: Command; /** - * Get the command to execute when `node` is clicked. - * - * Commands can be registered through [registerCommand](#commands.registerCommand). `node` will be provided - * as the first argument to the command's callback function. - * - * @param node The node that the command is associated with. - * @return The command to execute when `node` is clicked. + * Context value of the tree node */ - getClickCommand?(node: T): string; + contextValue?: string; + + /** + * Collapsible state of the tree item. + * Required only when item has children. + */ + collapsibleState?: TreeItemCollapsibleState; + } + + /** + * Collapsible state of the tree item + */ + export enum TreeItemCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 1, + /** + * Determines an item is expanded + */ + Expanded = 2 + } + + /** + * The contiguous set of modified lines in a diff. + */ + export interface LineChange { + readonly originalStartLineNumber: number; + readonly originalEndLineNumber: number; + readonly modifiedStartLineNumber: number; + readonly modifiedEndLineNumber: number; + } + + export namespace commands { + + /** + * Registers a diff information command that can be invoked via a keyboard shortcut, + * a menu item, an action, or directly. + * + * Diff information commands are different from ordinary [commands](#commands.registerCommand) as + * they only execute when there is an active diff editor when the command is called, and the diff + * information has been computed. Also, the command handler of an editor command has access to + * the diff information. + * + * @param command A unique identifier for the command. + * @param callback A command handler function with access to the [diff information](#LineChange). + * @param thisArg The `this` context used when invoking the handler function. + * @return Disposable which unregisters this command on disposal. + */ + export function registerDiffInformationCommand(command: string, callback: (diff: LineChange[], ...args: any[]) => any, thisArg?: any): Disposable; + } + + export interface Terminal { + + /** + * The name of the terminal. + */ + readonly name: string; + + /** + * The process ID of the shell process. + */ + readonly processId: Thenable; + + /** + * Send text to the terminal. The text is written to the stdin of the underlying pty process + * (shell) of the terminal. + * + * @param text The text to send. + * @param addNewLine Whether to add a new line to the text being sent, this is normally + * required to run a command in the terminal. The character(s) added are \n or \r\n + * depending on the platform. This defaults to `true`. + */ + sendText(text: string, addNewLine?: boolean): void; + + /** + * Show the terminal panel and reveal this terminal in the UI. + * + * @param preserveFocus When `true` the terminal will not take focus. + */ + show(preserveFocus?: boolean): void; + + /** + * Hide the terminal panel if this terminal is currently showing. + */ + hide(): void; + + /** + * Dispose and free associated resources. + */ + dispose(): void; + + /** + * Experimental API that allows listening to the raw data stream coming from the terminal's + * pty process (including ANSI escape sequences). + * + * @param callback The callback that is triggered when data is sent to the terminal. + */ + onData(callback: (data: string) => any): void; } }