VS Code插件開發教程(7) 樹視圖 Tree View

Tree View API容許插件在sidebar中渲染內容,這些內容以樹的形狀來展現node

Tree View API基礎

咱們經過一個示例來介紹Tree View API相關用法,這個示例利用樹視圖來展現當前文件夾中全部的Node.js依賴。你能夠在 tree-view-sample 查閱此示例的完整代碼git

配置package.json

首先你要經過 contributes.viewsVS Code知道你要「貢獻出」一個視圖,下面是package.json的一個初步配置:github

{
    "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"]typescript

生成數據

第二步是利用 TreeDataProvider 生成樹視圖所需的Node.js依賴的數據,其中須要實現兩個方法:npm

  • 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

第三步是將生成的依賴數據提供給視圖,能夠經過兩種方式實現:json

  • vscode.window.registerTreeDataProvider:註冊樹數據的provider,須要提供視圖ID和數據provider對象api

    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)
    });
    複製代碼

至此一個具有基本目標功能的插件就已經完成,能夠看到實際效果以下:markdown

上述代碼的完整示例參見 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

相關文章

相關文章
相關標籤/搜索