Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / Typescript

Extending Visual Studio Code - The Universally Identifiable Way

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
4 Apr 2023CPOL13 min read 3.5K   2  
Process to create a Visual Studio Code extension that interacts with the editor window

Introduction

I love automating tasks that I regularly do, and something that I do regularly is add UUIDs into documents in Visual Studio Code. Normally, I have a separate UUID generator page open in a web browser, and I generate the items I need there, then copy and paste them into Code. This is less than an ideal way for me to do this task, so I thought I would write an extension that I could host in Code and use directly.

In this article, we are going to cover the process involved in writing and publishing an extension into the Visual Studio marketplace. This assumes no prior knowledge, so sit back, relax, and get coding.

Source

The code for this article can be found here on GitHub. The extension we are building is available in the Visual Studio Marketplace here.

Getting Started

Creating a Visual Studio Code extension relies on two pm packages, yeoman, and the VS Code Extension Generator. Yeoman is an ecosystem designed to scaffold new projects using generators. The extension creation process uses yeoman to generate a new project, in a similar manner to creating an Angular project. In order to install these two items, we use the following command:

BAT
npm install -g yo generator-code

When we run this command, the generator prompts us for some information that will create the final project structure. The following image shows the values I supplied to create the extension for this article.

Image shows the prompts filled in to populate the project. The important prompts are the extension name, which is Goldlight.Extension, and the identifier, which is golglight-extension.

Testing this extension is as easy as opening the code inside vscode, and pressing F5 which opens up an extension development host window. To execute the extension, press Ctrl+Shift+P, to open up the command palette, and type in Hello World to run the extension (don't forget to press Enter to run it). The default extension simply displays a Hello World style message like this:

The default Hello World information bar showing when the extension has been triggered.

Working With the Code Inside, Errr, Code

When the code generation happened, it create a file called extension.ts, which contains the default implementation for our extension. Out of the box, the code looks like this:

TypeScript
// 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';

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

	// Use the console to output diagnostic information (console.log) 
    // and errors (console.error)
	// This line of code will only be executed once when your extension is activated
	console.log('Congratulations, your extension "goldlight-identifier" is now active!');

	// The command has been defined in the package.json file
	// Now provide the implementation of the command with registerCommand
	// The commandId parameter must match the command field in package.json
	let disposable = vscode.commands.registerCommand
                     ('goldlight-identifier.helloWorld', () => {
		// The code you place here will be executed every time your command is executed
		// Display a message box to the user
		vscode.window.showInformationMessage('Hello World from Goldlight.Identifier!');

	});

	context.subscriptions.push(disposable);
}

// This method is called when your extension is deactivated
export function deactivate() {}

The comments in the code are pretty good at explaining what is happening. When the extension activates, it registers the command that we will execute when we choose to invoke the command, using the vscommand.commands.registerCommand function. When Code registers a command, it is available to be called at any time.

What is interesting about the method is that we have an execution context passed in to the activate function. Basically, each extension has a context that it operates under, so the extension context provides a set of utilities for us. With this extension, the code is creating a disposable item that links to our command. When the extension is disabled, or removed altogether, we no longer want Code to have the ability to run it, so it is disposed and removed from the available command list.

If we want to perform some tidy-up operation, we can use the deactivate method to accomplish this. We might want to save state to a database, for instance, so we would trigger this save here. As the extension we are going to write doesn't need to do anything to tidy up, we are going to remove the function; don't worry, it is perfectly safe to do this so we won't destabilise our extension by doing this.

At this stage, it is worth remembering that our extension was called Hello World when we searched for it in the other Code instance. How does Code know what to call our command, and what command to run when it's called? We can't rely on our activate code to tell us what the command is called because we might register several commands inside the function.

The key to linking our commands into Code lies inside package.json, specifically inside a node called contributes. This node has the following command entry listed for the generated code.

JSON
"contributes": {
  "commands": [
    {
      "command": "goldlight-identifier.helloWorld",
      "title": "Hello World"
    }
  ]
},

As mentioned, we could have several commands, so we give each command its own entry. The command part ties back to the name of the command inside the activate function, and the title is the entry that is displayed when people want to use our extension. As we want to be good coders, and give our commands meaningful names, let's rename the command. To make our life easy, we are going to do a global search and replace for all instances of goldlight-identifier.helloWorld, and rename it to goldlight-identifier.createIdentifier. We should see both the package.json and the extension.ts files updated with this new command name.

While we are at it, let's change the title of our extension. We are going to change it from Hello World to Generate UUID.

I'm a fan of keyboard shortcuts, so while I have package.json open, I'm going to add binding support. We add these inside the contributes section, and they look quite similar to the commands entries.

JavaScript
"keybindings": [
  {
    "command": "goldlight-identifier.createIdentifier",
    "key": "ctrl+shift+u",
    "mac": "cmd+shift+u"
  }
]

Again, we can have multiple key bindings, so this section allows us to add a binding for each command we register. The command tells us which command our binding is for, while the key and mac entries tell us what the key combinations will be on a PC. Initially, I used CTRL/CMD+SHIFT+U as the key binding but that serves other purposes in Code (it shows the Output window), so I decided to go with a key chord instead. If you don't know what a key chord is, it's where you use a combination of keys to trigger a command (e.g., Ctrl+K+K). To add a key chord to the one we want to use here, Ctrl+U, U, we change the key and map entries so our keybindings looks like this:

JavaScript
"keybindings": [
 {
   "command": "goldlight-identifier.createIdentifier",
   "key": "ctrl+shift+u", 
   "mac": "cmd+shift+u" 
 }
]

Before we finish with our key binding, we have one last thing we want to do. As we want to replace or add text inside a text editor, we are going to say that we want our keybinding to apply when we focus on a read/write editor. To do that, we add a when entry to our binding that specifies that we want the binding to apply when the editor window is writeable, and has text focus. Text focus means that the cursor is blinking in the editor window, and the document open in the editor is not read-only.

JavaScript
"keybindings": [
  {
    "command": "goldlight-identifier. createIdentifier",
    "key": "ctrl+u ctrl+u",
    "mac": "cmd+u cmd+u",
    "when": "editorTextFocus && !editorReadonly"
  }
]

As we have applied a constraint to our control keys to control when the command can be applied, we should have a similar constraint that controls when the command is present in the command palette. Unfortunately, this is not something we can do directly in the command entry, so we need to find another way to do this. The way to do this requires a little bit of understanding about what the command palette is. The command palette is a widget that shows a series of items like a menu. When we think about it like this, we get a hint that the command palette shows up in a menus entry inside package.json. The code that controls the command visibility of the createIdentifier command looks like this:

JavaScript
"menus": {
  "commandPalette": [
    {
      "command": "goldlight-identifier.createIdentifier",
      "when": "editorFocus && !editorReadonly"
    }
  ]
}

Note: Unlike the key binding, the when clause is controlled by editorFocus rather than editorTextFocus. The difference between the two is that editorTextFocus means that the editor has focus, but when the command palette widget appears, we no longer have text focus because the widget is dominant now. This is why we use editorFocus to control the command palette entry because this doesn't require the cursor to be blinking.

Inside the Editor

Now that we have our command hooked up to a keyboard shortcut, and to the command palette, we are ready to add the code to create our UUID. The first thing we are going to do is remove the pre-generated code inside our registerCommand code block. We are going to put our functionality in here, so this area should be empty.

Now that the code block is empty, we have our command get the active text editor. This is the document we want to add our UUIDs in, so we need to get a hold of this.

TypeScript
const editor = vscode.window.activeTextEditor;
if (editor) {

}

Now that we know we have an editor window, the actual implementation is incredibly simple. What we want is the ability to insert a UUID if we haven't selected any text, to replace text with a UUID if text is selected, or to add multiple UUIDs if we have multiple selections (e.g., we have done something like this). The ability to do this lies with the fact that we will always have selections when the editor is focused. The following code sits inside the if (editor) block.

TypeScript
editor.edit((textEditorEdit) => {
    editor.selections.forEach((selection) => {
        textEditorEdit.replace(selection, randomUUID());
    });
});

What we are doing here, is telling Code that we want to edit each selection, and replace it with a randomUUID. The randomUUID call needs the following import in the editor.

TypeScript
import { randomUUID } from 'crypto';

At this stage, it would be tempting to say that we have done everything we need. We have the ability to add UUIDs in the editor, triggered from the command palette, and from keyboard shortcuts. Before we add any additional features, we are going to take a step slightly to the side and look at what we would need to do to publish our extension. After all, it would be great if we could share it with the world and make use of it.

Look ma! I'm an Author. Publishing Our Extension.

There are a couple of ways to publish our extension to the Visual Studio marketplace. Both routes require a first step of installing another commandline tool; this one is needed to package, manage, and publish extensions.

BAT
npm install -g @vscode/vsce

The route we are going to take is to use Azure DevOps to manage the publish process. There's another route that can be used where we package the extension, and upload to the marketplace directly, but we'll assume that we are going to use Azure for the whole process. If you haven't signed up to the DevOps capabilities, don't worry, it's free to use. We aren't going to cover signing up to Azure, as that's pretty straightforward, and you should be able to figure it out easily enough.

In order to publish our extension, we need a Personal Access Token, which we need to create an organisation for first. To do this, click on the New organization button as shown here.

Create a new organisation using Azure DevOps console window.

Again, this is a simple process, so I'll leave it to you to complete signing your organisation up. Just choose a unique organisation name, choose where in the world you want it hosting, and fill in the Captcha.

Once the organisation is available, we can create our Personal Access Token (PAT). In your organisation, select the User settings option and, in the menu that is shown, select Personal access tokens.

User settings tab selected, displaying the Personal access tokens menu option.

In the user settings page, click the New Token button to bring up the dialog we create the PAT in. Obviously, we want to give the token a name, so we choose something meaningful. In this case, we are going to call it goldlight.identity. Change the Organization field be All accessible organizations if it isn't already selected. By default, the scope should be set to Custom defined, so leave it at that.

In order to set the scope for the marketplace, we have to click the Show all scopes link to bring up the missing ones. Once we have done this, we search for Marketplace, and select the Manage option.

We have left the personal convenience part for last. If we create a short lived token, we are going to have to regenerate it fairly regularly. We can change the duration for the token by changing the Expiration to Custom defined. We can now choose a date up to a year in advance.

In the following image, I show everything we need to set. Once we have everything we need, click the Create button.

Personal access token generation with all the fields filled in.

We need to copy the key that is created. Once it's generated, it can't be recovered, so make sure to copy it somewhere safe. This is the token we're going to use later on.

A Brief Diversion

Sometimes, we want to import a GitHub repository into our DevOps installation. If we are still updating the GitHub code, rather than the code that now sits inside Azure DevOps, we soon notice that the code does not change in Azure when we update the GitHub repository, and there's no direct way to synchronise the changes between the two.

Now, there are ways in which we could force the synchronisation to happen using build pipelines, or GitHub actions, but I find the easiest way to sync the two is a simple command line operation that relies on the ability of git commands. Simply set up a remote mirror that points to our Azure code using the command (this only needs to be set up once).

BAT
git remote add --mirror=fetch secondary <<our Azure repository>>

Now, when we have pushed a commit into GitHub, simply run the following command:

BAT
git push secondary --all

It's quick, easy to use, and is my gift to you as it's been a major frustration of mine, for quite some time, that Azure doesn't support this "out the box".

Visual Studio Marketplace

We are almost ready to publish our extension. Before we do that, we must create a publisher in the Visual Studio Marketplace. Our id should be unique as we are just about to use this. For the purposes of our extension, I created a publisher with an id called Goldlight-Extensions.

Note: We must use the same account to log into the marketplace as we did to create our PAT.

Back to Publishing

We now have everything we need to log into our extension. We are going to log into the marketplace using the following command:

BAT
vsce login Goldlight-Extensions

This logs us into the marketplace with the extension we specified when we created our marketplace entry. We will be prompted for our PAT, so let's paste that in now. When we press enter, our token is verified against our login, and we are ready to go.

Before we publish our extension, we have one thing that we want to do. We want to add an image, and a publisher for our extension. Open up the package.json file and add an img entry just below the version. The image must point to a fixed internet location, so we set it to https://github.com/ohanlon/goldlight-identifier/blob/main/src/goldlight.png in our file. The publisher uses our marketplace publisher id (Goldlight-Extensions in this example).

We are finally ready to publish the extension, which can be done with the following command:

BAT
vsce publish

If we hadn't specified the token when we logged in, we would be prompted to do so now.

The publish process runs through various steps now, compiling and packaging the code. As we haven't specified a repository, we are going to be prompted to choose whether or not we want to proceed with the process. At the moment, we aren't too worried about this because the repository just lets people know where they can contribute code if they want to. We have a similar prompt to let us know that we haven't added a license file. Again, we aren't too bothered about this at the moment.

Once the publish process is complete, our extension is now available in the marketplace. Let's see what this looks like in Code.

Our Goldlight Identity extension showing in the extensions marketplace window in vscode.

I did mention that there was another way to publish our extension. What we need to do there is to run the following command.

BAT
vsce package

This packages up our extension, then we manually upload it inside the marketplace itself. My preference is to run the login/publish process described above because this saves me time.

Conclusion

We have successfully created a vscode extension from scratch. In doing this, we have seen how to add command palette support, and link it to key chord menus. Once we finished the code, we have taken the next steps to create a Personal Access Token in Azure DevOps, and used that to publish to the Visual Studio Marketplace.

I hope you have reached the point where you are excited at the thought of building your own Code extensions, and sharing them with the world.

History

  • 4th April, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO
United Kingdom United Kingdom
A developer for over 30 years, I've been lucky enough to write articles and applications for Code Project as well as the Intel Ultimate Coder - Going Perceptual challenge. I live in the North East of England with 2 wonderful daughters and a wonderful wife.

I am not the Stig, but I do wish I had Lotus Tuned Suspension.

Comments and Discussions

 
-- There are no messages in this forum --