现已发布!阅读关于 11 月新增功能和修复的内容。

树视图 API

树视图 API 允许扩展在 Visual Studio Code 的侧边栏中显示内容。此内容被结构化为树,并符合 VS Code 内置视图的样式。

例如,内置的“查找所有引用”视图扩展将引用搜索结果显示为一个单独的视图。

References Search View

查找所有引用”的结果显示在“引用: 结果”树视图中,该视图位于“引用”视图容器中。

本指南将教您如何编写一个向 Visual Studio Code 贡献树视图和视图容器的扩展。

树视图 API 基础知识

为了解释树视图 API,我们将构建一个名为“节点依赖项”的示例扩展。此扩展将使用树视图来显示当前文件夹中的所有 Node.js 依赖项。添加树视图的步骤是在 package.json 中贡献该树视图,创建 TreeDataProvider,然后注册 TreeDataProvider。您可以在 vscode-extension-samples GitHub 存储库的 tree-view-sample 中找到此示例扩展的完整源代码。

package.json 贡献

首先,您需要通过在 package.json 中使用 contributes.views 贡献点来告知 VS Code 您正在贡献一个视图。

这是我们扩展第一个版本的 package.json

{
  "name": "custom-view-samples",
  "displayName": "Custom view Samples",
  "description": "Samples for VS Code's view API",
  "version": "0.0.1",
  "publisher": "alexr00",
  "engines": {
    "vscode": "^1.74.0"
  },
  "activationEvents": [],
  "main": "./out/extension.js",
  "contributes": {
    "views": {
      "explorer": [
        {
          "id": "nodeDependencies",
          "name": "Node Dependencies"
        }
      ]
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./"
  },
  "devDependencies": {
    "@types/node": "^10.12.21",
    "@types/vscode": "^1.42.0",
    "typescript": "^3.5.1",
    "tslint": "^5.12.1"
  }
}

注意:如果您的扩展针对的是 1.74 之前的 VS Code 版本,您必须在 activationEvents 中明确列出 onView:nodeDependencies

您必须为视图指定一个标识符和名称,并且可以贡献到以下位置:

  • explorer:侧边栏中的“资源管理器”视图。
  • debug:侧边栏中的“运行和调试”视图。
  • scm:侧边栏中的“源代码管理”视图。
  • test:侧边栏中的“测试资源管理器”视图。
  • 自定义视图容器

树数据提供程序

第二步是为您注册的视图提供数据,以便 VS Code 可以显示数据。为此,您应该首先实现 TreeDataProvider。我们的 TreeDataProvider 将提供节点依赖项数据,但您可以拥有一个提供其他类型数据的提供程序。

此 API 中有两个必须实现的方法:

  • getChildren(element?: T): ProviderResult<T[]> - 实现此方法以返回给定 element 或根(如果未传递元素)的子级。
  • getTreeItem(element: T): TreeItem | Thenable<TreeItem> - 实现此方法以返回将在视图中显示的元素的 UI 表示(TreeItem)。

当用户打开树视图时,将调用不带 elementgetChildren 方法。从那里,您的 TreeDataProvider 应该返回您的顶级树项目。在我们的示例中,顶级树项目的 collapsibleStateTreeItemCollapsibleState.Collapsed,这意味着顶级树项目将显示为折叠状态。将 collapsibleState 设置为 TreeItemCollapsibleState.Expanded 将导致树项目显示为展开状态。将 collapsibleState 保留为默认值 TreeItemCollapsibleState.None 表示树项目没有子级。对于 collapsibleStateTreeItemCollapsibleState.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 and devDependencies.
   */
  private getDepsInPackageJson(packageJsonPath: string): Dependency[] {
    if (this.pathExists(packageJsonPath)) {
      const toDep = (moduleName: string, version: string): Dependency => {
        if (this.pathExists(path.join(this.workspaceRoot, 'node_modules', moduleName))) {
          return new Dependency(
            moduleName,
            version,
            vscode.TreeItemCollapsibleState.Collapsed
          );
        } else {
          return new Dependency(moduleName, version, vscode.TreeItemCollapsibleState.None);
        }
      };

      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 - 通过提供注册的视图 ID 和上述数据提供程序来注册树数据提供程序。

    const rootPath =
      vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0
        ? vscode.workspace.workspaceFolders[0].uri.fsPath
        : undefined;
    vscode.window.registerTreeDataProvider(
      'nodeDependencies',
      new NodeDependenciesProvider(rootPath)
    );
    
  • vscode.window.createTreeView - 通过提供注册的视图 ID 和上述数据提供程序来创建树视图。这将提供对 TreeView 的访问,您可以使用它来执行其他视图操作。如果需要 TreeView API,请使用 createTreeView

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

以下是正在运行的扩展:

View

更新树视图内容

我们的节点依赖项视图很简单,一旦数据显示出来就不会再更新。但是,在视图中添加一个刷新按钮并使用当前 package.json 的内容更新节点依赖项视图会很有用。为此,我们可以使用 onDidChangeTreeData 事件。

  • onDidChangeTreeData?: Event<T | undefined | null | void> - 如果您的树数据可能发生变化并且您想更新树视图,请实现此方法。

将以下内容添加到您的 NodeDependenciesProvider

  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.jsoncontributes 部分,添加:

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

并在您的扩展激活时注册该命令:

import * as vscode from 'vscode';
import { NodeDependenciesProvider } from './nodeDependencies';

export function activate(context: vscode.ExtensionContext) {
  const rootPath =
    vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0
      ? vscode.workspace.workspaceFolders[0].uri.fsPath
      : undefined;
  const nodeDependenciesProvider = new NodeDependenciesProvider(rootPath);
  vscode.window.registerTreeDataProvider('nodeDependencies', nodeDependenciesProvider);
  vscode.commands.registerCommand('nodeDependencies.refreshEntry', () =>
    nodeDependenciesProvider.refresh()
  );
}

现在我们有了一个可以刷新节点依赖项视图的命令,但是视图上的按钮会更好。我们已经为命令添加了一个 icon,所以当我们将其添加到视图时,它会显示该图标。

package.jsoncontributes 部分,添加:

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

激活

重要的是,您的扩展应该仅在用户需要您的扩展提供的功能时才激活。在这种情况下,您应该只在用户开始使用该视图时激活您的扩展。当您的扩展声明视图贡献时,VS Code 会自动为您完成此操作。当用户打开视图时,VS Code 会发出一个 onView:${viewId} 激活事件(对于上面的示例,是 onView:nodeDependencies)。

注意:对于 1.74.0 之前的 VS Code 版本,您必须在 package.json 中显式注册此激活事件,以便 VS Code 在此视图上激活您的扩展。

"activationEvents": [
       "onView:nodeDependencies",
],

视图容器

视图容器包含一组显示在活动栏或面板中的视图,以及内置的视图容器。内置视图容器的示例包括源代码管理和资源管理器。

View Container

要贡献视图容器,您应该首先使用 package.json 中的 contributes.viewsContainers 贡献点注册它。

您必须指定以下必需字段:

  • id - 您正在创建的新视图容器的 ID。
  • title - 将显示在视图容器顶部的名称。
  • icon - 在活动栏中显示视图容器的图像。
"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。此属性仅在工作区首次打开具有此视图时被 VS Code 尊重。之后,可见性将设置为用户所选的任何内容。如果您有一个包含许多视图的视图容器,或者您的视图对扩展的每个用户都可能不那么有用,请考虑将视图设置为 collapsedhiddenhidden 视图将出现在视图容器的“视图”菜单中。

Views Menu

视图操作

操作可以作为单个树项目的内联图标、树项目上下文菜单以及视图顶部的视图标题显示。操作是通过在 package.json 中添加贡献来设置的命令,这些命令会显示在这些位置。

要贡献到这三个位置,您可以在 package.json 中使用以下菜单贡献点:

  • view/title - 在视图标题中显示操作的位置。主操作或内联操作使用 "group": "navigation",其余为次要操作,位于 ... 菜单中。
  • view/item/context - 在树项目处显示操作的位置。内联操作使用 "group": "inline",其余为次要操作,位于 ... 菜单中。

您可以使用 when 子句控制这些操作的可见性。

View Actions

示例

"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"
      }
    ]
  }
}

默认情况下,操作按字母顺序排序。要指定不同的排序,请在组名后添加 @ 和您想要的顺序。例如,navigation@3 将使操作显示在 navigation 组中的第三位。

您可以通过创建不同的组来进一步分隔 ... 菜单中的项目。这些组名是任意的,并按组名按字母顺序排序。

注意:如果您想为特定的树项目显示操作,可以通过定义树项目的 TreeItem.contextValue 来实现,并在 when 表达式中为 viewItem 键指定上下文值。

示例

"contributes": {
  "menus": {
    "view/item/context": [
      {
        "command": "nodeDependencies.deleteEntry",
        "when": "view == nodeDependencies && viewItem == dependency"
      }
    ]
  }
}

欢迎内容

如果您的视图可能为空,或者您想向其他扩展的空视图添加欢迎内容,您可以贡献 viewsWelcome 内容。空视图是没有 TreeView.message 且树为空的视图。

"contributes": {
  "viewsWelcome": [
    {
      "view": "nodeDependencies",
      "contents": "No node dependencies found [learn more](https://npmjs.net.cn/).\n[Add Dependency](command:nodeDependencies.addEntry)"
    }
  ]
}

Welcome Content

欢迎内容支持链接。按约定,单独一行的链接是按钮。每个欢迎内容也可以包含一个 when 子句。有关更多示例,请参阅 内置 Git 扩展

TreeDataProvider

扩展编写者应通过编程方式注册 TreeDataProvider 来填充视图中的数据。

vscode.window.registerTreeDataProvider('nodeDependencies', new DepNodeProvider());

请参阅 tree-view-sample 中的 nodeDependencies.ts 以获取实现。

TreeView

如果您想通过编程方式在视图上执行一些 UI 操作,可以使用 window.createTreeView 而不是 window.registerTreeDataProvider。这将提供对视图的访问,您可以使用它来执行视图操作。

vscode.window.createTreeView('ftpExplorer', {
  treeDataProvider: new FtpTreeDataProvider()
});

请参阅 tree-view-sample 中的 ftpExplorer.ts 以获取实现。

© . This site is unofficial and not affiliated with Microsoft.