Article
· Dec 19 14m read

Learn to create VSCode extensions with IRIS Global Editor app

VSCode is the most used IDE in the world. It is strategic have more extensions for VSCode for InterSystems technologies to keep increasing the developer community.

My new app IRIS VSCode Global Editor is an excellent sample to learn how to create extensions to IRIS. You can check it on https://openexchange.intersystems.com/package/IRIS-Global-VSCode-Editor.

To be ready to create extensions for VSCode

From https://code.visualstudio.com/api/get-started/your-first-extension you have all steps to get ready, but I will detail here to you.

1. Install Yeoman and start the creation of the extension project:

npm install --global yo generator-code

yo code

2. Answer the questions:

# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? IRISExtension
### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? irisextension
# ? What's the description of your extension? LEAVE BLANK
# ? Initialize a git repository? Yes
# ? Bundle the source code with webpack? No
# ? Which package manager to use? npm

# ? Do you want to open the new folder with Visual Studio Code? Open with `code`

3. On VSCode, go to package.json, section contributes, to define new commands, views, editor and other elements created with your plugin, see this sample:

{
  "name": "iris-global-editor",
  "displayName": "iris-global-editor",
  "description": "IRIS Global Editor",
  "repository": "https://github.com/yurimarx/iris-global-editor",
  "version": "0.0.2",
  "engines": {
    "vscode": "^1.96.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "views": {
      "explorer": [
        {
          "id": "irisGlobals",
          "name": "InterSystems IRIS Globals"
        }
      ]
    },
    "commands": [
      {
        "command": "irisGlobals.addEntry",
        "title": "Add",
        "icon": {
          "light": "resources/light/add.svg",
          "dark": "resources/dark/add.svg"
        }
      },
      {
        "command": "irisGlobals.deleteEntry",
        "title": "Delete",
        "icon": {
          "light": "resources/light/trash.svg",
          "dark": "resources/dark/trash.svg"
        }
      },
      {
        "command": "irisGlobals.editEntry",
        "title": "Edit",
        "icon": {
          "light": "resources/light/edit.svg",
          "dark": "resources/dark/edit.svg"
        }
      },
      {
        "command": "irisGlobals.editTextEntry",
        "title": "Editor",
        "icon": {
          "light": "resources/light/file.svg",
          "dark": "resources/dark/file.svg"
        }
      },
      {
        "command": "irisGlobals.refreshEntry",
        "title": "IRIS Globals: Refresh",
        "icon": {
          "light": "resources/light/refresh.svg",
          "dark": "resources/dark/refresh.svg"
        }
      },
      {
        "command": "irisGlobals.filterGlobals",
        "title": "IRIS Globals: Filter",
        "icon": {
          "light": "resources/light/filter.svg",
          "dark": "resources/dark/filter.svg"
        }
      }
    ],
    "menus": {
      "view/title": [
        {
          "command": "irisGlobals.filterGlobals",
          "when": "view == irisGlobals",
          "group": "navigation"
        },
        {
          "command": "irisGlobals.refreshEntry",
          "when": "view == irisGlobals",
          "group": "navigation"
        },
        {
          "command": "irisGlobals.addEntry",
          "when": "view == irisGlobals",
          "group": "navigation"
        }
      ],
      "view/item/context": [
        {
          "command": "irisGlobals.editEntry",
          "when": "view == irisGlobals",
          "group": "inline"
        },
        {
          "command": "irisGlobals.deleteEntry",
          "when": "view == irisGlobals",
          "group": "inline"
        },
        {
          "command": "irisGlobals.editTextEntry",
          "when": "view == irisGlobals",
          "group": "inline"
        }
      ]
    },
    "configuration": [
      {
        "id": "irisGlobals",
        "title": "Settings InterSystems IRIS Global Editor",
        "order": 1,
        "properties": {
          "conf.irisGlobalEditor.filter": {
            "type": "string",
            "default": "",
            "description": "Filter criteria to filter globals list"
          },
          "conf.irisGlobalEditor.serverconfig": {
            "type": "object",
            "order": 1,
            "description": "Connection settings",
            "properties": {
              "host": {
                "type": "string",
                "description": "InterSystems IRIS Host"
              },
              "namespace": {
                "type": "string",
                "description": "InterSystems IRIS Namespace"
              },
              "username": {
                "type": "string",
                "description": "User name credential"
              },
              "password": {
                "type": "string",
                "description": "Password credential"
              }
            },
            "additionalProperties": false
          }
        }
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run package",
    "compile": "npm run check-types && npm run lint && node esbuild.js",
    "watch": "npm-run-all -p watch:*",
    "watch:esbuild": "node esbuild.js --watch",
    "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
    "package": "npm run check-types && npm run lint && node esbuild.js --production",
    "compile-tests": "tsc -p . --outDir out",
    "watch-tests": "tsc -p . -w --outDir out",
    "pretest": "npm run compile-tests && npm run compile && npm run lint",
    "check-types": "tsc --noEmit",
    "lint": "eslint src",
    "test": "vscode-test"
  },
  "devDependencies": {
    "@types/mocha": "^10.0.9",
    "@types/node": "20.x",
    "@types/vscode": "^1.96.0",
    "@typescript-eslint/eslint-plugin": "^8.10.0",
    "@typescript-eslint/parser": "^8.7.0",
    "@vscode/test-cli": "^0.0.10",
    "@vscode/test-electron": "^2.4.1",
    "axios": "^1.7.9",
    "esbuild": "^0.24.0",
    "eslint": "^9.13.0",
    "npm-run-all": "^4.1.5",
    "typescript": "^5.6.3"
  },
  "dependencies": {
    "axios": "^1.7.9",
    "formdata-node": "^6.0.3",
    "iris-global-editor": "file:"
  }
}

4. With the functionalities declared, it is necessary the implementation. My app implement a TreeView on src folder on file IrisGlobalsProvider. For treeviews it is required implement a vscode.TreeDataProvider, for other type of VSCode functions see the article https://code.visualstudio.com/api/extension-guides/overview. It is my Treeview implementation:

import * as vscode from 'vscode';
import * as path from 'path';
import {fileFromPath} from 'formdata-node/file-from-path';
import { log } from 'console';
import axios, { AxiosRequestConfig, AxiosResponse, RawAxiosRequestHeaders } from 'axios';

export class IrisGlobalsTreeProvider implements vscode.TreeDataProvider<IrisGlobal> {

  constructor(private context: vscode.ExtensionContext) {}

  baseURL: string = "/iris-global-yaml";
  
  private _onDidChangeTreeData: vscode.EventEmitter<IrisGlobal | undefined | null | void> = new vscode.EventEmitter<IrisGlobal | undefined | null | void>();
  readonly onDidChangeTreeData: vscode.Event<IrisGlobal | undefined | null | void> = this._onDidChangeTreeData.event;
  
  public refresh() {
		this._onDidChangeTreeData.fire(undefined);
	}

  public async delete(globalname: string) {
    
    const cfgValues:any = await this.getIrisGlobalsConfig();

    const headers: AxiosRequestConfig = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + btoa(cfgValues.username + ":" + cfgValues.password)
      } as RawAxiosRequestHeaders,
    };
    
    const client = axios.create({
      baseURL: cfgValues.host + this.baseURL,
    });

    const globalParts = globalname.split(":", 2);

    const requestString = "/globals/" + cfgValues.namespace + "/" + globalParts[0].trim();
    
    const globalsYaml: AxiosResponse = await client.delete(requestString, headers);
    
    if (globalsYaml.status === 200) {
      vscode.window.showInformationMessage("Global successfully deleted");
    } else {
      vscode.window.showErrorMessage("Error on try to delete Global. Error: " + globalsYaml.statusText);
    }
  }

  private async getIrisGlobalsConfig() {
    
    try {
      const configuration = await vscode.workspace.getConfiguration('');
      return configuration.get('conf.irisGlobalEditor.serverconfig');
    } catch (error) {
      log(error);
      return null;
    }

  }

  public async add() {

    let selectedText = "";

    const addInput = await vscode.window.showInputBox({
      placeHolder: "GlobalName: Value",
      prompt: "Enter de name of the new Global and its value", 
      value: selectedText
    });
    
    if(addInput === '' || addInput === undefined){
      console.log(addInput);
      vscode.window.showErrorMessage('A Global Name and Value is mandatory to execute this action');
    } else {
      vscode.window.showInformationMessage(addInput!);
    }

    const cfgValues:any = await this.getIrisGlobalsConfig();

    const headers: AxiosRequestConfig = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + btoa(cfgValues.username + ":" + cfgValues.password)
      } as RawAxiosRequestHeaders,
    };
    
    const client = axios.create({
      baseURL: cfgValues.host + this.baseURL,
    });

    const inputParts = addInput === undefined ? [] : addInput.split(':', 2);
    
    const requestString = "/globals/" + cfgValues.namespace + "/" + inputParts[0].trim() + "?globalvalue=" + inputParts[1].trim();
    
    const globalsYaml: AxiosResponse = await client.put(requestString, "{}", headers);
    
    if (globalsYaml.status === 200) {
      vscode.window.showInformationMessage("Global successfully assigned");
    } else {
      vscode.window.showErrorMessage("Error while Global assigned. Error: " + globalsYaml.statusText);
    }

  }

  public async edit(globalvalue: string) {

    let selectedText = globalvalue;

    const addInput = await vscode.window.showInputBox({
      placeHolder: "GlobalName: Value",
      prompt: "Enter de name of the selected Global and its value", 
      value: selectedText
    });
    
    if(addInput === '' || addInput === undefined){
      console.log(addInput);
      vscode.window.showErrorMessage('A Global Name and Value is mandatory to execute this action');
    } else {
      vscode.window.showInformationMessage(addInput!);
    }

    const cfgValues:any = await this.getIrisGlobalsConfig();

    const headers: AxiosRequestConfig = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + btoa(cfgValues.username + ":" + cfgValues.password)
      } as RawAxiosRequestHeaders,
    };
    
    const client = axios.create({
      baseURL: cfgValues.host + this.baseURL,
    });

    const inputParts = addInput === undefined ? [] : addInput.split(':', 2);
    
    const requestString = "/globals/" + cfgValues.namespace + "/" + inputParts[0].trim() + "?globalvalue=" + inputParts[1].trim();
    
    const globalsYaml: AxiosResponse = await client.put(requestString, "{}", headers);
    
    if (globalsYaml.status === 200) {
      vscode.window.showInformationMessage("Global successfully assigned");
    } else {
      vscode.window.showErrorMessage("Error while Global assigned. Error: " + globalsYaml.statusText);
    }

  }

  public async saveGlobalWithYaml(filename: string) {
    
    if(filename.endsWith('.yml')) {
      const cfgValues:any = await this.getIrisGlobalsConfig();
  
      const headers: AxiosRequestConfig = {
        headers: {
          'Content-Type': 'multipart/form-data',
          'Authorization': 'Basic ' + btoa(cfgValues.username + ":" + cfgValues.password)
        } as RawAxiosRequestHeaders,
      };
      
      const client = axios.create({
        baseURL: cfgValues.host + this.baseURL
      });
  
      const requestString = "/globals/" + cfgValues.namespace;
      
      const formData = new FormData();
      formData.append('file', await fileFromPath(filename));
      
      try {
        const globalsYaml: AxiosResponse = await client.post(requestString, formData, headers);
    
        if (globalsYaml.statusText === "OK") {
          vscode.window.showInformationMessage("File " + filename + "sent with success and global saved");
        } else {
          vscode.window.showErrorMessage("Error while send yml file. Error: " + globalsYaml.statusText);
        }
      } catch (error) {
        vscode.window.showErrorMessage("Error while send yml file. Error: " + error);
      }

    }

  }

  public async editText(globalvalue: string) {

    const cfgValues:any = await this.getIrisGlobalsConfig();

    const headers: AxiosRequestConfig = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + btoa(cfgValues.username + ":" + cfgValues.password)
      } as RawAxiosRequestHeaders,
    };
    
    const client = axios.create({
      baseURL: cfgValues.host + this.baseURL
    });

    const inputParts = globalvalue === undefined ? [] : globalvalue.split(':', 2);
    
    const requestString = "/globals/yaml/" + cfgValues.namespace + "/" + inputParts[0].trim();
    
    const globalsYaml: AxiosResponse = await client.get(requestString, headers);
    
    if (globalsYaml.status === 200) {
      vscode.workspace.openTextDocument({
        language: 'yaml',
        content: globalsYaml.data
      });
    } else {
      vscode.window.showErrorMessage("Error while get Global text content. Error: " + globalsYaml.statusText);
    }

  }

  getTreeItem(element: IrisGlobal): vscode.TreeItem {
    return element;
  }

  getChildren(element?: IrisGlobal): Thenable<IrisGlobal[]> {
      
    return Promise.resolve(this.getGlobals());
      
  }

  async filterGlobals() {
    
    const configuration = await vscode.workspace.getConfiguration('');
    var filter: string = configuration.get('conf.irisGlobalEditor.filter')!;
    
    var selectedText = filter;

    const addInput = await vscode.window.showInputBox({
      prompt: "Enter the filter to global name (partial name of global)",
      value: selectedText
    });
    
    if(addInput === undefined){
      console.log(addInput);
      vscode.window.showErrorMessage('A filter value is mandatory to execute this action');
    } else {
      await vscode.workspace.getConfiguration().update('conf.irisGlobalEditor.filter', addInput);
      this.getGlobals();
      this.refresh();
      vscode.window.showInformationMessage('Filtered with success');
    }

  }

  async getGlobals(): Promise<IrisGlobal[]> {

    let response: IrisGlobal[] = [];

    const cfgValues:any = await this.getIrisGlobalsConfig();

    const headers: AxiosRequestConfig = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + btoa(cfgValues.username + ":" + cfgValues.password)
      } as RawAxiosRequestHeaders,
    };
    
    const client = axios.create({
      baseURL: cfgValues.host + this.baseURL,
    });

    const globalsYaml: AxiosResponse = await client.get("/globals/" + cfgValues.namespace, headers);

    var arr = globalsYaml.data.split("\r\n");
    if(arr.length > 0 && arr[0] === "# IRIS-Global-YAML") {
      arr.splice(0, 1);
    }
    const configuration = await vscode.workspace.getConfiguration('');
    var filter: string = configuration.get('conf.irisGlobalEditor.filter')!;
    
    if(filter !== undefined && filter !== null && filter !== "") {
      arr = arr.filter((el: string) => el.toLowerCase().includes(filter!.toLowerCase()));
    } 

    for (var i = 0; i < arr.length; i++) {
      const value = (arr[i] as string).trim();

      if(value !== "") {
        const item: IrisGlobal = {
          label: value,
          version: '1.0',
          collapsibleState: vscode.TreeItemCollapsibleState.None,
          iconPath: {
            light: this.context.asAbsolutePath(path.join('resources', 'light', 'dependency.svg')),
            dark: this.context.asAbsolutePath(path.join('resources', 'dark', 'dependency.svg'))
          }
        };
        response.push(item);
      }
      
    }
    
    return response;
    
  }

}


export class IrisGlobal extends vscode.TreeItem {
  
  constructor(
		public readonly label: string,
		public readonly version: string,
		public readonly collapsibleState: vscode.TreeItemCollapsibleState,
    public readonly iconPath: any
	) {
		super(label, collapsibleState);
    this.tooltip = `${this.label}-${this.version}`;
		this.description = this.description;
    this.iconPath = iconPath;
    this.contextValue = 'irisGlobals';
	}

  

}
  • I used axios to communicate with a REST API created for me to manage globals.
  • All methods for each function are created here.
  • It is necessary implement the interface vscode.TreeItem also, to define the data of treeview items.

5. On folder src, it is necessary register your implementation to be executed on the VSCode processes on the file extension.ts, see the sample:

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import { IrisGlobal, IrisGlobalsTreeProvider } from './IrisGlobalsProvider';

export function activate(context: vscode.ExtensionContext) {

	const irisGlobalsProvider = new IrisGlobalsTreeProvider(context);
	vscode.window.registerTreeDataProvider('irisGlobals', irisGlobalsProvider);
	const disposableRefresh = vscode.commands.registerCommand('irisGlobals.refreshEntry', () => irisGlobalsProvider.refresh());
	const disposableAdd = vscode.commands.registerCommand('irisGlobals.addEntry', () => irisGlobalsProvider.add());
	const filterGlobals = vscode.commands.registerCommand('irisGlobals.filterGlobals', () => irisGlobalsProvider.filterGlobals());
	const disposableDelete = vscode.commands.registerCommand('irisGlobals.deleteEntry', (node: IrisGlobal) => irisGlobalsProvider.delete(node.label));
	const disposableEdit = vscode.commands.registerCommand('irisGlobals.editEntry', (node: IrisGlobal) => irisGlobalsProvider.edit(node.label));
	const disposableTextEdit = vscode.commands.registerCommand('irisGlobals.editTextEntry', (node: IrisGlobal) => irisGlobalsProvider.editText(node.label));


	context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(editorsaveevent => {      

        if (editorsaveevent) {
			irisGlobalsProvider.saveGlobalWithYaml(editorsaveevent.fileName);
        }

    }));
	
	context.subscriptions.push(disposableRefresh);
	context.subscriptions.push(disposableAdd);
	context.subscriptions.push(disposableDelete);
	context.subscriptions.push(disposableEdit);
	context.subscriptions.push(disposableTextEdit);
	context.subscriptions.push(filterGlobals);
}

// This method is called when your extension is deactivated
export function deactivate() {}
  • I registered my TreeView Provider class and all commands implementations.
  • I subscribed to the event saveTextDocument, to call the saveGlobalWithYaml always a yaml file are saved on VSCode.

It is simple, no? Enjoy!

Discussion (0)1
Log in or sign up to continue