diff --git a/README.md b/README.md
index 18994eb0..12e889cf 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ Sample code illustrating the VS Code extension APIs.
* [Preview Html](/previewhtml-sample/README.md)
* [Vim](/vim-sample/README.md)
* [Integrated Terminal](/terminal-example/README.md)
+* Experimental: [Tree Explorer](/tree-explorer/README.md)
# How to run locally
diff --git a/tree-explorer-sample/.gitignore b/tree-explorer-sample/.gitignore
new file mode 100644
index 00000000..8e5962ee
--- /dev/null
+++ b/tree-explorer-sample/.gitignore
@@ -0,0 +1,2 @@
+out
+node_modules
\ No newline at end of file
diff --git a/tree-explorer-sample/.vscode/launch.json b/tree-explorer-sample/.vscode/launch.json
new file mode 100644
index 00000000..fc26e048
--- /dev/null
+++ b/tree-explorer-sample/.vscode/launch.json
@@ -0,0 +1,28 @@
+// A launch configuration that compiles the extension and then opens it inside a new window
+{
+ "version": "0.1.0",
+ "configurations": [
+ {
+ "name": "Launch Extension",
+ "type": "extensionHost",
+ "request": "launch",
+ "runtimeExecutable": "${execPath}",
+ "args": ["/Users/octref/Code/work/vscode", "--extensionDevelopmentPath=${workspaceRoot}"],
+ "stopOnEntry": false,
+ "sourceMaps": true,
+ "outDir": "${workspaceRoot}/out/src",
+ "preLaunchTask": "npm"
+ },
+ {
+ "name": "Launch Tests",
+ "type": "extensionHost",
+ "request": "launch",
+ "runtimeExecutable": "${execPath}",
+ "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
+ "stopOnEntry": false,
+ "sourceMaps": true,
+ "outDir": "${workspaceRoot}/out/test",
+ "preLaunchTask": "npm"
+ }
+ ]
+}
diff --git a/tree-explorer-sample/.vscode/settings.json b/tree-explorer-sample/.vscode/settings.json
new file mode 100644
index 00000000..7877e3fc
--- /dev/null
+++ b/tree-explorer-sample/.vscode/settings.json
@@ -0,0 +1,10 @@
+// Place your settings in this file to overwrite default and user settings.
+{
+ "files.exclude": {
+ "out": false // set this to true to hide the "out" folder with the compiled JS files
+ },
+ "search.exclude": {
+ "out": true // set this to false to include "out" folder in search results
+ },
+ "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
+}
\ No newline at end of file
diff --git a/tree-explorer-sample/.vscode/tasks.json b/tree-explorer-sample/.vscode/tasks.json
new file mode 100644
index 00000000..fb7f662e
--- /dev/null
+++ b/tree-explorer-sample/.vscode/tasks.json
@@ -0,0 +1,30 @@
+// Available variables which can be used inside of strings.
+// ${workspaceRoot}: the root folder of the team
+// ${file}: the current opened file
+// ${fileBasename}: the current opened file's basename
+// ${fileDirname}: the current opened file's dirname
+// ${fileExtname}: the current opened file's extension
+// ${cwd}: the current working directory of the spawned process
+
+// A task runner that calls a custom npm script that compiles the extension.
+{
+ "version": "0.1.0",
+
+ // we want to run npm
+ "command": "npm",
+
+ // the command is a shell script
+ "isShellCommand": true,
+
+ // show the output window only if unrecognized errors occur.
+ "showOutput": "silent",
+
+ // we run the custom script "compile" as defined in package.json
+ "args": ["run", "compile", "--loglevel", "silent"],
+
+ // The tsc compiler is started in watching mode
+ "isWatching": true,
+
+ // use the standard tsc in watch mode problem matcher to find compile problems in the output.
+ "problemMatcher": "$tsc-watch"
+}
\ No newline at end of file
diff --git a/tree-explorer-sample/.vscodeignore b/tree-explorer-sample/.vscodeignore
new file mode 100644
index 00000000..5ff3c193
--- /dev/null
+++ b/tree-explorer-sample/.vscodeignore
@@ -0,0 +1,9 @@
+.vscode/**
+.vscode-test/**
+out/test/**
+test/**
+src/**
+**/*.map
+.gitignore
+tsconfig.json
+vsc-extension-quickstart.md
diff --git a/tree-explorer-sample/README.md b/tree-explorer-sample/README.md
new file mode 100644
index 00000000..c4d60232
--- /dev/null
+++ b/tree-explorer-sample/README.md
@@ -0,0 +1,14 @@
+# Dependency Tree Explorer
+
+A sample for the Tree Explorer API showing dependencies in a node project.
+
+
+
+## Running the example
+
+Currently, this API is only available in [Insiders](https://code.visualstudio.com/insiders).
+
+- Download Insiders version of VS Code and open this sample
+- `npm install`
+- `npm run compile`
+- `F5` to start debugging
\ No newline at end of file
diff --git a/tree-explorer-sample/media/dep.png b/tree-explorer-sample/media/dep.png
new file mode 100644
index 00000000..9bb43d00
Binary files /dev/null and b/tree-explorer-sample/media/dep.png differ
diff --git a/tree-explorer-sample/media/dep.svg b/tree-explorer-sample/media/dep.svg
new file mode 100644
index 00000000..4ea60619
--- /dev/null
+++ b/tree-explorer-sample/media/dep.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/tree-explorer-sample/package.json b/tree-explorer-sample/package.json
new file mode 100644
index 00000000..c8e95c86
--- /dev/null
+++ b/tree-explorer-sample/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "dep",
+ "displayName": "Dependency Example",
+ "description": "Dependency example for VSCode's explorer API",
+ "version": "0.0.1",
+ "publisher": "octref",
+ "engines": {
+ "vscode": "^1.7.0"
+ },
+ "enableProposedApi": true,
+ "categories": [
+ "Other"
+ ],
+ "activationEvents": [
+ "*"
+ ],
+ "main": "./out/src/extension",
+ "icon": "media/dep.png",
+ "contributes": {
+ "explorer": {
+ "treeLabel": "Dependencies",
+ "icon": "media/dep.svg",
+ "treeExplorerNodeProviderId": "depTree"
+ }
+ },
+ "scripts": {
+ "vscode:prepublish": "tsc -p ./",
+ "compile": "tsc -watch -p ./",
+ "postinstall": "node ./node_modules/vscode/bin/install"
+ },
+ "devDependencies": {
+ "typescript": "^2.0.3",
+ "vscode": "^1.0.0",
+ "@types/node": "^6.0.40"
+ }
+}
diff --git a/tree-explorer-sample/src/extension.ts b/tree-explorer-sample/src/extension.ts
new file mode 100644
index 00000000..b1e613b4
--- /dev/null
+++ b/tree-explorer-sample/src/extension.ts
@@ -0,0 +1,136 @@
+'use strict';
+
+import * as vscode from 'vscode';
+import { TreeExplorerNodeProvider } from 'vscode';
+
+import * as fs from 'fs';
+import * as path from 'path';
+
+export function activate(context: vscode.ExtensionContext) {
+ const rootPath = vscode.workspace.rootPath;
+
+ // The `providerId` here must be identical to `contributes.explorer.treeExplorerNodeProviderId` in package.json.
+ vscode.window.registerTreeExplorerNodeProvider('depTree', new DepNodeProvider(rootPath));
+
+ // 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}`));
+ }
+ });
+}
+
+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 {
+ return new Promise((resolve) => {
+ switch(node.kind) {
+ case 'root':
+ const packageJsonPath = path.join(this.workspaceRoot, 'package.json');
+ if (this.fileExists(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.fileExists(packageJsonPath)) {
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+
+ const toDep = (moduleName: string): DepNode => {
+ if (this.fileExists(path.join(this.workspaceRoot, 'node_modules', moduleName))) {
+ return new Node(moduleName);
+ } else {
+ return new Leaf(moduleName);
+ }
+ }
+
+ const deps = Object.keys(packageJson.dependencies).map(toDep);
+ const devDeps = Object.keys(packageJson.devDependencies).map(toDep);
+ return deps.concat(devDeps);
+ } else {
+ return [];
+ }
+ }
+
+ private fileExists(filePath: string): boolean {
+ try {
+ fs.accessSync(filePath);
+ } 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
+ ) {
+ }
+}
\ No newline at end of file
diff --git a/tree-explorer-sample/tree-explorer.png b/tree-explorer-sample/tree-explorer.png
new file mode 100644
index 00000000..4edf5ff2
Binary files /dev/null and b/tree-explorer-sample/tree-explorer.png differ
diff --git a/tree-explorer-sample/tsconfig.json b/tree-explorer-sample/tsconfig.json
new file mode 100644
index 00000000..11282c9a
--- /dev/null
+++ b/tree-explorer-sample/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es6",
+ "outDir": "out",
+ "lib": [
+ "es6"
+ ],
+ "sourceMap": true,
+ "rootDir": "."
+ },
+ "exclude": [
+ "node_modules",
+ ".vscode-test"
+ ]
+}
\ 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
new file mode 100644
index 00000000..421c7c9c
--- /dev/null
+++ b/tree-explorer-sample/typings/vscode.proposed.d.ts
@@ -0,0 +1,88 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// This is the place for API experiments and proposal.
+
+declare module 'vscode' {
+
+ export namespace window {
+
+ export function sampleFunction(): Thenable;
+ }
+
+ 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.
+ */
+ export function registerTreeExplorerNodeProvider(providerId: string, provider: TreeExplorerNodeProvider): 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.
+ */
+ export interface TreeExplorerNodeProvider {
+
+ /**
+ * 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.
+ *
+ * @return The root node.
+ */
+ provideRootNode(): T | Thenable;
+
+ /**
+ * Resolve the children of `node`.
+ *
+ * @param node The node from which the provider resolves children.
+ * @return Children of `node`.
+ */
+ resolveChildren(node: T): T[] | Thenable;
+
+ /**
+ * 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.
+ */
+ getLabel?(node: T): string;
+
+ /**
+ * 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.
+ */
+ getHasChildren?(node: T): boolean;
+
+ /**
+ * 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.
+ */
+ getClickCommand?(node: T): string;
+ }
+}