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;
}
}