VS Code插件开发教程(7) 树视图 Tree View

Tree View API 允许插件在 sidebar 中渲染内容,这些内容以树的形状来展示

Tree View API基础

我们通过一个示例来介绍 Tree View API 相关用法,这个示例利用树视图来展示当前文件夹中所有的 Node.js 依赖。你可以在 tree-view-sample 查阅此示例的完整代码

配置package.json

首先你要通过 contributes.views VS Code 知道你要“贡献出”一个视图,下面是 package.json 的一个初步配置:

"name" : "helloworld" , "displayName" : "HelloWorld" , "description" : "" , "version" : "0.0.1" , "engines" : { "vscode" : "^1.56.0" "categories" : [ "Other" "activationEvents" : [ "onView:nodeDependencies" ] , "main" : "./extension.js" , "contributes" : { "views" : { "explorer" : [ { "id" : "nodeDependencies" , "name" : "Node Dependencies" "scripts" : { "lint" : "eslint ." , "pretest" : "npm run lint" , "test" : "node ./test/runTest.js" "devDependencies" : { "@types/vscode" : "^1.56.0" , "@types/glob" : "^7.1.3" , "@types/mocha" : "^8.0.4" , "@types/node" : "14.x" , "eslint" : "^7.19.0" , "glob" : "^7.1.6" , "mocha" : "^8.2.1" , "typescript" : "^4.1.3" , "vscode-test" : "^1.5.0"

仅当用户需要时再去激活插件是十分重要的,例如在本文的示例中,我们可以让插件在用户使用插件视图的时候再去激活。 VS Code 提供了 onView:${viewId} 事件来告知程序当前用户打开的视图,我们可以在 package.json 注册一个激活事件 "activationEvents": ["onView:nodeDependencies"]

第二步是利用 TreeDataProvider 生成树视图所需的 Node.js 依赖的数据,其中需要实现两个方法:

  • getChildren(element?: T): ProviderResult<T[]> :返回指定节点(如果没有指定就是根节点)的子节点
  • getTreeItem(element: T): TreeItem | Thenable<TreeItem> :返回用于在视图里展示的UI节点
  • 每当用户打开树视图, getChildren 会被自动调用(没有参数),你可以在这里返回树视图的第一层级内容。在示例中,我们用 TreeItemCollapsibleState.Collapsed (折叠)、 TreeItemCollapsibleState.Expanded (展开)、 TreeItemCollapsibleState.None (无子节点,不会触发 getChildren 方法)控制节点的折叠状态,下面是一个 TreeDataProvider 的实现示例:

    import * as vscode from 'vscode';
    import * as fs from 'fs';
    import * as path from 'path';
    export class NodeDependenciesProvider implements vscode.TreeDataProvider<Dependency> {
        constructor(private workspaceRoot: string) { }
        getTreeItem(element: Dependency): vscode.TreeItem {
            return element;
        getChildren(element?: Dependency): Thenable<Dependency[]> {
            if (!this.workspaceRoot) {
                vscode.window.showInformationMessage('No dependency in empty workspace');
                return Promise.resolve([]);
            if (element) {
                return Promise.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)) {
                    return Promise.resolve(this.getDepsInPackageJson(packageJsonPath));
                } else {
                    vscode.window.showInformationMessage('Workspace has no package.json');
                    return Promise.resolve([]);
         * Given the path to package.json, read all its dependencies
        private getDepsInPackageJson(packageJsonPath: string): Dependency[] {
            if (this.pathExists(packageJsonPath)) {
                const toDep = (moduleName: string, version: string): Dependency => {
                    const depPackageJsonPath = path.join(this.workspaceRoot, 'node_modules', moduleName, 'package.json');
                    let collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
                    if (this.pathExists(depPackageJsonPath)) {
                        const depPackageJson = JSON.parse(fs.readFileSync(depPackageJsonPath, 'utf-8'));
                        // 如果依赖的代码包已经安装(node_modules有内容),且这个安装包本身有dependencies或devDependencies,才设置为可展开的
                        if ((!depPackageJson.dependencies || Object.keys(depPackageJson.dependencies).length === 0) &&
                            (!depPackageJson.devDependencies || Object.keys(depPackageJson.devDependencies).length === 0)) {
                            collapsibleState = vscode.TreeItemCollapsibleState.None;
                    return new Dependency(moduleName, version, collapsibleState);
                const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
                const deps = packageJson.dependencies
                    ? Object.keys(packageJson.dependencies).map(dep =>
                        toDep(dep, packageJson.dependencies[dep])
                    : [];
                const devDeps = packageJson.devDependencies
                    ? Object.keys(packageJson.devDependencies).map(dep =>
                        toDep(dep, packageJson.devDependencies[dep])
                    : [];
                return deps.concat(devDeps);
            } else {
                return [];
        private pathExists(p: string): boolean {
            try {
                fs.accessSync(p);
            } catch (err) {
                return false;
            return true;
    class Dependency extends vscode.TreeItem {
        constructor(
            public readonly label: string,
            private version: string,
            public readonly collapsibleState: vscode.TreeItemCollapsibleState
            super(label, collapsibleState);
            this.tooltip = `${this.label}-${this.version}`;
            this.description = this.version;
        iconPath = {
            light: path.join(__filename, '..', '..', 'resources', 'light', 'dependency.svg'),
            dark: path.join(__filename, '..', '..', 'resources', 'dark', 'dependency.svg')
    

    注册TreeDataProvider

    第三步是将生成的依赖数据提供给视图,可以通过两种方式实现:

    vscode.window.registerTreeDataProvider:注册树数据的provider,需要提供视图ID和数据provider对象

    vscode.window.registerTreeDataProvider(
        'nodeDependencies',
        new NodeDependenciesProvider(vscode.workspace.rootPath)
    

    vscode.window.createTreeView:通过视图ID和数据provider来创建视树视图,这会提供访问 树视图 的能力,如果你需要使用TreeView API,可以使用createTreeView的方式

    vscode.window.createTreeView('nodeDependencies', {
        treeDataProvider: new NodeDependenciesProvider(vscode.workspace.rootPath)
    

    至此一个具备基本目标功能的插件就已经完成,可以看到实际效果如下:

    上述代码的完整示例参见 tree-view-test v1

    更新视图内容

    以命令行方式

    目前完成的这个插件仅具备最基本的功能,数依赖数据一经展示便无法更新。如果在视图中有一个刷新按钮将会是非常方便的,为了实现这个目标,我们需要利用 onDidChangeTreeData 事件:

  • onDidChangeTreeData?: Event<T | undefined | null | void>:当依赖数据变更并且你希望更新树视图的时候执行
  • provider中添加如下代码:

        private _onDidChangeTreeData: vscode.EventEmitter<Dependency | undefined | null | void> = new vscode.EventEmitter<Dependency | undefined | null | void>();
        readonly onDidChangeTreeData: vscode.Event<Dependency | undefined | null | void> = this._onDidChangeTreeData.event;
        refresh(): void {
            this._onDidChangeTreeData.fire();
    

    此时我们有了更新函数,但没有调用它,我们可以在package.json中定义一条更新命令:

        "commands": [
                    "command": "nodeDependencies.refreshEntry",
                    "title": "Refresh Dependence",
                    "icon": {
                        "light": "resources/light/refresh.svg",
                        "dark": "resources/dark/refresh.svg"
    

    然后注册该命令:

      vscode.commands.registerCommand('nodeDependencies.refreshEntry', () =>
          nodeDependenciesProvider.refresh()
    

    此时我们会看到,当执行了Refresh Dependence命令后,Node.js依赖的树视图会被更新:

    以按钮方式

    在前文的基础上,如果在视图中添加一个按钮或许操作的时候有会更加直观、友好。我们在package.json中添加:

    "menus": {
        "view/title": [
                "command": "nodeDependencies.refreshEntry",
                "when": "view == nodeDependencies",
                "group": "navigation"
    

    此时当我们将鼠标浮在视图上时就会看到刷新按钮,点击效果同执行Refresh Dependence命令:

    group属性用于菜单项的排序和分类,其中值为navigationgroup是用来将置顶的,如果不设置,则刷新按钮将会被隐藏在“...”里,效果如下所示:

    上述代码的完整示例参见 tree-view-test v2

    添加到视图容器(View Container)

    创建视图容器

    视图容器包含了一系列展示在Activity BarPanel中的视图,如果希望自己的插件自定义一个视图容器,我们可以用 contributes.viewsContainers package.json中注册:

        "contributes": {
            "viewsContainers": {
                "activitybar": [{
                    "id": "package-explorer",
                    "title": "Package Explorer",
                    "icon": "media/dep.svg"
    

    或者你也可以在panel字段下做配置

        "contributes": {
            "viewsContainers": {
               "panel": [{
                    "id": "package-explorer",
                    "title": "Package Explorer",
                    "icon": "media/dep.svg"
    

    将视图和视图容器绑定

    我们可以在package.json中用 contributes.views 来实现

        "contributes": {
            "views": {
                "package-explorer": [{
                    "id": "nodeDependencies",
                    "name": "Node Dependencies",
                    "icon": "media/dep.svg",
                    "contextualTitle": "Package Explorer"
    

    需要注意的是,一个视图可以设置visibility属性,该属性有三个取值:visiblecollapsedhidden,这三个值仅在首次打开工作台的时候起作用,之后其取值取决于用户的控制。如果你的视图容器里有很多的视图,则可以利用该属性让你的界面更加简洁

    现在我们可以看到左侧的视图容器和树视图了:

    上述代码的完整示例参见 tree-view-test v3

    视图行为解读

    视图的行为附着在视图的内联图标上,这些图标可以在树视图中的每一个节点上、还可以在树视图顶端的标题栏上,我们可以在package.json中对其进行配置:

  • view/title:位置在视图标题栏上,可以用"group": "navigation"来保证其优先级
  • view/item/context:位置在树节点上,可以用"group": "inline"让其内联显示
  • 上述均可用 when clause 控制其生效条件

    如果我们想实现上图的效果,可以用如下代码实现:

    "contributes": { "commands": [{ "command": "nodeDependencies.refreshEntry", "title": "Refresh", "icon": { "light": "resources/light/refresh.svg", "dark": "resources/dark/refresh.svg" "command": "nodeDependencies.addEntry", "title": "Add" "command": "nodeDependencies.editEntry", "title": "Edit", "icon": { "light": "resources/light/edit.svg", "dark": "resources/dark/edit.svg" "command": "nodeDependencies.deleteEntry", "title": "Delete" "menus": { "view/title": [{ "command": "nodeDependencies.refreshEntry", "when": "view == nodeDependencies", "group": "navigation" "command": "nodeDependencies.addEntry", "when": "view == nodeDependencies" "view/item/context": [{ "command": "nodeDependencies.editEntry", "when": "view == nodeDependencies && viewItem == dependency", "group": "inline" "command": "nodeDependencies.deleteEntry", "when": "view == nodeDependencies && viewItem == dependency"

    我们可以在when字段中使用 TreeItem.contextValue 的数据,来控制相应行为的显示

    上述代码的完整示例参见 tree-view-test v4

    视图欢迎内容

    我们可以添加一个欢迎内容,以便当视图内容初始化或为空的时候显示:

        "contributes": {
            "viewsWelcome": [{
                "view": "nodeDependencies",
                "contents": "没有发现依赖内容, [了解更多](https://www.npmjs.com/).\n[添加依赖](command:nodeDependencies.addEntry)"
    

    contributes.viewsWelcome.contents支持链接,如果链接单起一行,会被渲染为按钮。每个viewsWelcome支持 when clause

    上述代码的完整示例参见 tree-view-test v5

    VS Code插件开发教程(1) Overview

    VS Code插件开发教程(2) 起步 Get Started

    VS Code插件开发教程(3) 插件能力一览 Capabilities

    VS Code插件开发教程(4) 插件指南 Extension Guidesd

    VS Code插件开发教程(5) 命令的使用 Command

    VS Code插件开发教程(6) 颜色主题一览 Color Theme

    VS Code插件开发教程(7) 树视图 Tree View

    VS Code插件开发教程(8) Webview

    VS Code插件开发教程(9)构建自定义编辑器 Custom Editor

    VS Code插件开发教程(10)编程式语言特性 Programmatic Language Features

    VS Code插件开发教程(11)语言服务器入门 Language Server Extension Guide

    分类:
    前端
  •