[TypeScript] VS-Code API: Let’s Create A Tree-View (Part 1)

You can find the project here on GitHub, I added a tag part-1 for this article.

 

My favorite editor to use is Visual Studio Code. It offers lots of extensions we're about to create our own tree view extendsion.

In my last article Start Using Cucumber I introduced the Cucumber Framework to enable behavior tests in your C++ project. For Visual Studio Code there are already extensions to enable syntax highlighting and auto completion. In this article we'll create a tree view to display all our tests in our project in a vs code sidebar menu: INSERT IMAGE!

This is the result of this article, we parse all *.feature files in our directory and diesplay Features and Scenarios in our own navigation menu

Let's Get Started

First of all, you need npm installed on your machine and (obviously) VS Code. Once you have it, you can install the VS Code Extension Generator and create a TypeScript project. The Generator asks some initial setup questions, see the snippet below.

You can start with the offical getting started guide on: Your First Extension or find a lot of examples on Microsofts GitHub repository.

$ npm install -g yo generator-code
  
$ yo code

     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? cwt-cucumber-support
? What's the identifier of your extension? cwt-cucumber-support
? What's the description of your extension?
? Initialize a git repository? Yes
? Bundle the source code with webpack? No       
? Which package manager to use? npm

Our project is now ready and in ./src/extension.ts you have the entry point of this extension with some example code. To debug our extension, we just need to press F5 (with the example we just created, it's a hello world example).

Adding The View To The Navigation Bar

First of all we create a file ./src/tree_view.ts in which we'll implement the tree view. Second, we need two classes here:

  • tree_item -> represents a item in our tree view
  • tree_view -> holds all items and represents the tree

import * as vscode from 'vscode'

// lets put all in a cwt namespace
export namespace cwt
{
    // this represents an item and it's children (like nested items)
    // we implement the item later
    class tree_item extends vscode.TreeItem 
    {
        children: tree_item[] | undefined;
    }
    
    // tree_view will created in our entry point
    export class tree_view implements vscode.TreeDataProvider<tree_item>
    {
        // will hold our tree view data
        m_data : tree_item [] = [];
        
        
        // in the constructor we register a refresh and item clicked function
        constructor() 
        {
            vscode.commands.registerCommand('cwt_cucumber_view.item_clicked', r => this.item_clicked(r));
            vscode.commands.registerCommand('cwt_cucumber_view.refresh', () => this.refresh());
        }
    
        item_clicked(item: tree_item)
        {
            // this will be executed when we click an item
        }
    
        refresh()
        {
            // this will be clicked when we refresh the view
        }

        getTreeItem(element: tree_item): vscode.TreeItem|Thenable<vscode.TreeItem> 
        {
            // we need to provide getTreeItem
        }
    
        getChildren(element : tree_item | undefined): vscode.ProviderResult<tree_item[]> 
        {
            // same for getChildren
        }
    }
}

Now we can create and register our tree view

import * as vscode from 'vscode';
// import our namespace where we'll get access to the tree_view
import { cwt } from './tree_view';

export function activate(context: vscode.ExtensionContext) 
{
    //create a local tree view and register it in vscode
	let tree = new cwt.tree_view();
	vscode.window.registerTreeDataProvider('cwt-cucumber-view', tree);
}

export function deactivate() {}

And we're almost done, we need to add some properties to the package.json in our root directory. Here we just add a container and a view. I created a logo for the navigation which we can display now.

// for now, we add all activation events
"activationEvents": [
	"*"
],
"contributes": {
        // we add a view container here with the according logo
	"viewsContainers": {
		"activitybar": [
			{
				"id": "cwt-cucumber-view-container",
				"title": "cwt cucumber support",
				"icon": "src/assets/navigation_bar_logo.svg"
			}
		]
	},
	// we create a single view
	"views": {
		"cwt-cucumber-view-container": [
			{
				"id": "cwt_cucumber",
				"name": "cwt cucumber"
			}
		]
	},
	// we add our commands for item clicked and refresh
	"commands": [
		{
			"command": "cwt_cucumber.item_clicked",
			"title": "cwt tree view item"
		},
	        // we add a image to the refresh function which we want to display
		{
			"command": "cwt_cucumber.refresh",
			"title": "refresh",
			"icon": {
				"light": "src/assets/img_light/refresh.svg",
				"dark": "src/assets/img_dark/refresh.svg"
			}
		}
	],
	"menus": {
	        // we link the registered command incl. the image to the view title
	        // we can add more by using navigation@1, etc. 
		"view/title": [
			{
				"command": "cwt_cucumber.refresh",
				"when": "view == cwt_cucumber",
				"group": "navigation@0"
			}
		]
	}
}

There we have it, we have our extension in the navigation bar and the refresh button is on the upper right of our tree view.

 

Let's Implement And Fill The Tree View

As already created, implement the tree items first:

// we need to inherit from vscode.TreeItem
class tree_item extends vscode.TreeItem 
{
    // we'll use the file and line later...
    readonly file: string | undefined;
    readonly line: number | undefined;

    // children represent branches, which are also items 
    public children: tree_item[] = [];
    
    // add all members here, file and line we'll need later
    // the label represent the text which is displayed in the tree
    // and is passed to the base class
    constructor(label: string, file: string, line: number) {
        super(label, vscode.TreeItemCollapsibleState.None);
        this.file = file;
        this.line = line;
        this.collapsibleState = vscode.TreeItemCollapsibleState.None;
    }

    // a public method to add childs, and with additional branches
    // we want to make the item collabsible
    public add_child (child : tree_item) {
        this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
        this.children.push(child);
    }
}

The tree_item was fairly easy and now let's create the tree view class. Let's take a look on the following tree_view. I added all the steps to the comments below.

    // 1. we'll export this class and use it in our extension later
    // 2. we need to implement vscode.TreeDataProvider
    export class tree_view implements vscode.TreeDataProvider<tree_item>
    {
        // m_data holds all tree items 
        private m_data : tree_item [] = [];
        // with the vscode.EventEmitter we can refresh our  tree view
        private m_onDidChangeTreeData: vscode.EventEmitter<tree_item | undefined> = new vscode.EventEmitter<tree_item | undefined>();
        // and vscode will access the event by using a readonly onDidChangeTreeData (this member has to be named like here, otherwise vscode doesnt update our treeview.
        readonly onDidChangeTreeData ? : vscode.Event<tree_item | undefined> = this.m_onDidChangeTreeData.event;

        // we register two commands for vscode, item clicked (we'll implement later) and the refresh button. 
        public constructor()  {
            vscode.commands.registerCommand('cwt_cucumber.item_clicked', r => this.item_clicked(r));
            vscode.commands.registerCommand('cwt_cucumber.refresh', () => this.refresh());
        }
        
        // we need to implement getTreeItem to receive items from our tree view
        public getTreeItem(element: tree_item): vscode.TreeItem|Thenable<vscode.TreeItem> {
            const item = new vscode.TreeItem(element.label!, element.collapsibleState);
            return item;
        }
        
        // and getChildren
        public getChildren(element : tree_item | undefined): vscode.ProviderResult<tree_item[]> {
            if (element === undefined) {
                return this.m_data;
            } else {
                return element.children;
            }
        }
        
        // this is called when we click an item
        public item_clicked(item: tree_item) {
            // we implement this later
        }
        
        // this is called whenever we refresh the tree view
        public refresh() {
            if (vscode.workspace.workspaceFolders) {
                this.m_data = [];
                this.read_directory(vscode.workspace.workspaceFolders[0].uri.fsPath);
                this.m_onDidChangeTreeData.fire(undefined);
            } 
        }
        
        // read the directory recursively over all files
        private read_directory(dir: string) {
            fs.readdirSync(dir).forEach(file => {
                let current = path.join(dir,file);
                if (fs.statSync(current).isFile()) {
                    if(current.endsWith('.feature')) {
                        this.parse_feature_file(current);
                    } 
                } else {
                    this.read_directory(current)
                }
            });
        }
        
        // and if we find a *.feature file parse the content
        private parse_feature_file(file: string) {
            const regex_feature = new RegExp("(?<=Feature:).*");
            const regex_scenario = new RegExp("(?<=Scenario:).*");
            let reader = rd.createInterface(fs.createReadStream(file))
            const line_counter = ((i = 0) => () => ++i)();

            // let's loop over every line
            reader.on("line", (line : string, line_number : number = line_counter()) => {
                let is_feature = line.match(regex_feature);
                if (is_feature) {
                    // we found a feature and add this to our tree view data
                    this.m_data.push(new tree_item(is_feature[0], file, line_number));
                }
                let is_scenario = line.match(regex_scenario);
                if (is_scenario) {
                    // every following scenario will be added to the last added feature with add_children from the tree_item
                    this.m_data.at(-1)?.add_child(new tree_item(is_scenario[0], file, line_number));
                }
            });
        }
    }

And finally, as mentionad above, we need to create our tree_view in our extension and register it in vscode. But we call the refresh function now, to fill our tree:

export function activate(context: vscode.ExtensionContext) 
{
	let tree = new cwt.tree_view();
	// note: we need to provide the same name here as we added in the package.json file
	vscode.window.registerTreeDataProvider('cwt_cucumber', tree);
	tree.refresh();
}

Let's See Our Tree View

And that's it, I added to the project directory some example files from my article about cucumber and two examples from the cucumber-cpp GitHub repo. Unfortunately if you open in debug this exact same folder, vscode doesn't open it again and jumps to the already opened vscode window. Either copy this examples in another directory or navigate into the examples directory.

And there is our tree view which displays all our features and their scenarios

 

What's Next

Now we're only parsing Features and Scenarios, in Cucumber there are for instance also Scenario Outlines which we don't consider right now. However if you want to create a customized tree view for your needs in vscode, this article can help.

So what's next:

  • Jump to the Feature/Scenario by clicking on it
  • Let all tests run with a button on the top
  • Provide a context menu with right mouse click on scenarios
  • Visualize the results with bitmaps on every item

But thats it for now.

Best Thomas

Previous
Previous

[TypeScript] VS Code API: Let’s Create A Tree-View (Part 2)

Next
Next

[C++] Building And Publishing Conan Packages