[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!
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 viewtree_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"
}
]
}
}
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.
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