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. + +![tree-explorer](tree-explorer.png) + +## 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 @@ + + + + Slice + Created with Sketch. + + + + + + + + + \ 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; + } +}