Home > Software design >  VSCode enable/disable commands in custom TreeView based on properties of node/item
VSCode enable/disable commands in custom TreeView based on properties of node/item

Time:04-30

I'm currently working on a custom VSCode extension that fetches data from a remote API and presents the data within a custom tree-view. The treeview itself allows to interact with the API and i.e. filter results based on some predefined criteria such as a search term or whether or not the response has some files attached to it, adding new nodes to or removing or updating existing nodes in the tree dynamically.

For simplicity reasons I created an own enter image description here

GIF was resized to fit the 2MB limit and thus produces ugly artifacts

While showing action-icons for commands on a TreeView, defined by a custom TreeDataProvider, and on a TreeItem are fairly straight forward by defining something like:

...
{
  "contributes":
      "commands": [
          {
              "command": "extension.myTreeView.filter",
              "title": "Filter",
              "icon": "$(filter)",
              "when": "view == extension.myTreeView"
          },
          {
              "command": "extension.myTreeView.resetFilter",
              "title": "Reset Filter",
              "shortTitle": "Reset",
              "icon": "$(close)",
              "enablement": "view == extension.myTreeView && extension.myTreeView.hasFilter",
              "when": "view == extension.myTreeView && extension.myTreeView.hasFilter"
          },
          {
              "command": "extension.myTreeView.delete",
              "title": "Delete Item",
              "shortTitle": "Delete",
              "icon": "$(trash)",
              "when": "view == extension.myTreeView && viewItem == archive"
          }
          ...
      ],
      "menus": {
          "view/title": [
              {
                  "command": "extension.myTreeView.filter",
                  "when": "view == extension.myTreeView"
                  "group": "navigation@1"
              },
              {
                  "command": "extension.myTreeView.resetFilter",
                  "when": "view == extension.myTreeView",
                  "group": "navigation@2"
              },
              ...   
          ],
          "view/item/context": [
              ...
              {
                  "command": "extension.myTreeView.delete",
                  "when": "view == extension.myTreeView && viewItem == archive",
                  "group": "inline@3"
              }
          ]
      }
},
...

in the package.json file, I somehow struggle to add enabling conditions to commands targeting a particular item itself. I.e. the tree allows the download of some files contained in (or attached to) the item represented by the node. Such nodes may or may not have files attached to them and thus enabling the download action only makes sense for nodes that at least have a file to download. While generally downloading that content or rendering the button for triggering the download action isn't the issue, the enabling option for that command is where I am a bit clueless.

I temporarily "solved" that issue by defining different kinds of contextValue's for such tree nodes, one for an archive with and one without files to download. While this in general seems to solve that issue, it a) doesn't feel proper and b) leads to downstream problems. Imagine that archives can be locked preventing users who do not belong to the group the archive was locked for the actual download. This is just a fictional case here but should illustrate that you might end up with a combination of property fields within that contextValue variable that you more or less need to parse in that enable statement. And each further possible context value option must be carried around through the whole commands and menus definitions like this:

...
{
    "command": "extension.myTreeView.delete",
    "when": "view == extension.myTreeView && viewItem == archive || viewItem == archiveWithFiles || viewItem == ...",
    "group": "inline@2"
},
...

In general, custom context variables, such as hasFilter in the sample command configuration shown above, which can be used in the enablement and when clauses, can be used to show/hide or enable/disable certain commands but these seem to work only on the whole tree itself and not a particular tree item. I.e. the sample application uses the following code to enable/disable the resetFilter command based on whether a filter is defined or not:

    ...

    public async updateFilter(filter: Filter): Promise<void> {
        this.filter = filter;

        await commands.executeCommand("setContext", TestTreeDataProvider.CONTEXT_HAS_FILTER, true);
        this.items = [];
        this._onDidChangeTreeData.fire();
    }

    private async resetFilter(): Promise<void> {
        this.filter = undefined;
        await commands.executeCommand("setContext", TestTreeDataProvider.CONTEXT_HAS_FILTER, false);
        this.items = [];
        this._onDidChangeTreeData.fire();
    }

This works as this is more or less a global action on the tree itself and has a single source of property it reacts to. But such context values may only have base types assigned to them or objects where only the keys (a.k.a. property names) of that objects are checked for their availability in in clauses. I could potentially create custom context variables per tree item generated but then I need some form of addressing mechanism as each of the context value names must be different or else they are overwritten by the context value of the next node. And as the tree can hold plenty of nodes I feel adding a context value per node is also not very performant then. In the end what seems to be missing here is a way to define a function as context value that allows the current node (either the TreeItem or the actual object the node is referring to) as input and needs to return one of the base types as output, i.e. true or false to determine whether that item should be enabled or not.

While this SO question here may sound similar, it actually asks for disabling the whole tree item node when I just want the command that can be performed on that tree item to enable/disabled based on some tree item's current property value. Some styling of TreeItems based on their context can be done via FileDecorationProvider's. That question also is a result of a bug inside VSCode that doesn't seem to force a rerender on any changes done to the enablement guards.

I feel that there must be an easier solution on this matter and that's why I ask here. In short, how can a command be enabled/disabled in Visual Studio Code based on the current tree items' context (either properties held by or functions executed on that node)?

CodePudding user response:

You now put a type description as the context value of the TreeItem.

Why not put the allowed operations also in the context value:

archive_Del
archive_Read_Update

Then use this in the when clause

{
    "command": "extension.myTreeView.delete",
    "when": "view == extension.myTreeView && viewItem =~ /^.*_Del.*$/",
    "group": "inline@2"
}

{
    "command": "extension.myTreeView.read",
    "when": "view == extension.myTreeView && viewItem =~ /^.*_Read.*$/",
    "group": "inline@2"
}

  • Related