Visual Studio Code is a lightweight, yet powerful text editor that runs on Windows, Mac, and Linux.
VS Code supports custom colorization, snippets, commands, and more through a flexible extension API.
In this talk you'll learn some basics about writing extensions for VS Code, how to publish those extensions into the Visual Studio Marketplace, and get some resources and guidance on considerations for extension development.
The target audience for the talk is VS Code users at an intermediate level with experience in TypeScript or JavaScript. There is an accompanying PowerPoint presentation in the session's repository
No previous VS Code or Visual Studio extension development is required. By all meausre this is a general 200-level session, or a 100-level session for those who have extended IDEs in the past. You wouldn't want to attend if you've already developed an arsenal of VS Code extension development experience, but if you're new to it you're in the right spot.
The demo story will be the creation of a simple extension that performs some simple tricks, but that encorporates numerous facets of VS Code extension development in a continuous experience.
- Visual Studio Code
- Node.js - VS Code depends on Node.js
- Yeoman - The template engine used by the scaffolder
- Yeoman VS Code Generator - scaffolds extension project structure
- Visual Studio Code Extension Manager (VSCE) - for publishing to the marketplace
- Set up an account in the Marketplace.
- Create a bookmark to your Marketplace account at https://marketplace.visualstudio.com/manage/publishers/{your-publisher}
- Create a new extension using the Yeoman scaffolder named target-workspace before continuing with the demos. Open it up whenever debugging the extension as it is built throughout the demos.
- Clone this repository.
(Optional setup if you truly want a fresh start)
- Create a local branch to practice in, but keeping the master branch in case you bone something up.
git branch practice01 git checkout practice01
- Delete the
demo-extension-workspace
andtarget-workspace
When you're developing extensions you need to see what's happening in your development instance of VS Code. Let's make a snippet that makes it easy to insert the console.log
call, since we'll use it a lot during extension debugging.
- Create a new empty JS extension named
demo-extension-workspace
- Open the
demo-extension-workspace
folder in VS Code - Add the file
snippets/console.json
to the workspace with this JSON
{
"Print to console": {
"prefix": "logToConsole",
"body": [
"console.log('$1');"
],
"description": "Log output to the VS Code console window."
}
}
- Add this JSON to the
contributes
section of thepackage.json
file to load the snippet when the extension is loaded
"contributes": {
"snippets": [
{
"language": "javascript",
"path": "./snippets/console.json"
}
]
}
- Debug the extension
- Open the target-workspace target project in the debugging instance
- Use the two snippets to extend the target-workspace project
- Debug the target-workspace to show the message appearing in the development instance
The command palette gives customers a convenient way to execute commands contributed by extensions.
- Note the
contributes
section and how there is a single command namedsayHello
"commands": [
{
"command": "extension.sayHello",
"title": "Hello World"
}
],
- Note the handler for the
sayHello
command inextension.js
var disposable = vscode.commands.registerCommand('extension.sayHello', function () {
// 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!');
});
- Put a breakpoint on the handler and run the extension
- Use
Ctrl-Shift-P
orCmd-Shift-P
to open the command palette - Type Hello World to see the command in the palette
- Select it and note execution stops on the breakpoint
Commands are useful for componentizing and isolating parts of the functionality your extension offers so that it can be called via the Command Palette or when users perform certain actions, like clicking on a button in the VS Code toolbar.
- Note the
activationEvents
section inpackage.json
"activationEvents": [
"onCommand:extension.sayHello"
]
- With this setting the extension won't activate until the
sayHello
command is called, so it needs to be changed so that the extension activates right away. Optionally, we could just add the various commands that could activate the extension.
"activationEvents": [
"*"
],
- Add a new command named
meta.slowProcess
to thepackage.json
file
{
"command": "meta.slowProcess",
"title": "Run Slow Process"
}
-
Create a new directory named
commands
in the project and add a file to it namedcommands\slowProcess.js
Rather than register all the command handlers manually in the
extension.js
file, the command handler logic can be separated into multiple files.Then add the following code to the file to export a method to wire up the command handler to the VS Code context.
var vscode = require('vscode');
module.exports = exports = function (context) {
var disposable = vscode.commands.registerCommand('meta.slowProcess', function () {
vscode.window.showInformationMessage('Starting the slow process...');
});
context.subscriptions.push(disposable);
}
- In the
extension.js
file, wire up the command handler after thesayHello
command has been registered.
var slowProcess = require('./commands/slowProcess.js');
slowProcess(context);
- Debug the extension and use
Ctrl-Shift-P
orCmd-Shift-P
to run command titledRun Slow Process
Customers who use your extensions need to know what's happening so they typically look at the output window.
- Replace the code in the
commands/slowProcess.js
file with this code
var vscode = require('vscode');
const outputChannelName = 'META';
module.exports = exports = function (context) {
var disposable = vscode.commands.registerCommand('meta.slowProcess', function () {
var outputChannel = vscode.window.createOutputChannel(outputChannelName);
outputChannel.show(false);
outputChannel.appendLine('Starting the slow process...');
setTimeout(() => {
outputChannel.appendLine('Process complete');
}, 3000);
});
context.subscriptions.push(disposable);
}
- Debug the extension and run the
slowProcess
command a few times. Once with the output window entirely closed, once with a different channel selected. Note how theoutputChannel.show()
method could be used to make for a less-intrusive experience when providing the user feedback.
Once commands are working in an extension a good way to trigger them is with a button in the toolbar.
-
Show the list of icons in the octicon. Point out this icon library ships with VS Code and the icon names are used to specify which button icon should be shown.
-
Create a new file
utils\toolbar.js
in the workspace and add this code to it. This will componentize the toolbar so it can be used throughout the extension's codebase.
var vscode = require('vscode');
module.exports = {
addButton: function (command, text, tooltip) {
var customStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
customStatusBarItem.color = 'white';
customStatusBarItem.command = command;
customStatusBarItem.text = text;
customStatusBarItem.tooltip = tooltip;
customStatusBarItem.show();
}
}
- A command should be able to add itself to the toolbar rather than be centralized, so the
toolbar
will be referenced from theslowProcess.js
.
var toolbar = require('../utils/toolbar.js');
- The command name needs to be passed to the
addButton
method, so it can be extracted to a variable. This updated code forslowProcess.js
is below.
module.exports = exports = function (context) {
var commandName = 'meta.slowProcess';
var disposable = vscode.commands.registerCommand(commandName, function () {
var outputChannel = vscode.window.createOutputChannel(outputChannelName);
outputChannel.show(false);
outputChannel.appendLine('Starting the slow process...');
setTimeout(() => {
outputChannel.appendLine('Process complete');
}, 3000);
});
context.subscriptions.push(disposable);
toolbar.addButton(commandName, '$(bug)', 'Submit a bug');
}
- Debug the extension and show how the button is present and how clicking it causes the
slowProcess
command to fire.
Intrinsic validation in the form of drop-down lists is a great way to enable users to interface with your extension.
- Change the code in the
slowProcess
so that the user needs to confirm the command's execution.
var vscode = require('vscode');
var toolbar = require('../utils/toolbar.js');
const outputChannelName = 'META';
const startConfirmYes = 'Yes, start the process';
const startConfirmNo = 'Actually, nevermind';
module.exports = exports = function (context) {
var commandName = 'meta.slowProcess';
var disposable = vscode.commands.registerCommand(commandName, function () {
vscode.window
.showQuickPick([startConfirmYes, startConfirmNo])
.then((selected) => {
if (selected == startConfirmYes) {
var outputChannel = vscode.window.createOutputChannel(outputChannelName);
outputChannel.show(false);
outputChannel.appendLine('Starting the slow process...');
setTimeout(() => {
outputChannel.appendLine('Process complete');
}, 3000);
}
});
});
context.subscriptions.push(disposable);
toolbar.addButton(commandName, '$(bug)', 'Submit a bug');
}
- The
showQuickPick
method is called, by passing an array of strings as a parameter. - Once the user makes a selection (or hits ESC), the method passed to
then
is called.
Data can be collected from the user, and dialogs to inform the user of events or of error states can be shown.
- Replace the code from
slowProcess.js
to match the code below.
var vscode = require('vscode');
var toolbar = require('../utils/toolbar.js');
const outputChannelName = 'META';
const startConfirmYes = 'Yes, start the process';
const startConfirmNo = 'Actually, nevermind';
const password = 'secret';
module.exports = exports = function (context) {
var commandName = 'meta.slowProcess';
var disposable = vscode.commands.registerCommand(commandName, function () {
vscode.window
.showInputBox({
password: true,
prompt: 'Enter the password'
})
.then((value) => {
if (value == password) {
vscode.window
.showQuickPick([startConfirmYes, startConfirmNo])
.then((selected) => {
if (selected == startConfirmYes) {
var outputChannel = vscode.window.createOutputChannel(outputChannelName);
outputChannel.show(false);
outputChannel.appendLine('Starting the slow process...');
setTimeout(() => {
vscode.window.showInformationMessage('Process complete!')
}, 3000);
}
});
}
else {
vscode.window.showErrorMessage('Incorrect password');
}
});
});
context.subscriptions.push(disposable);
toolbar.addButton(commandName, '$(bug)', 'Submit a bug');
}
- Run the demo and show how data can be collected and used in the extension's logic flow.
The text in the active editor document can be used as an object that you can parse and decorate. Colorizers, decorators, and code lens providers are a few of the various ways you can use the text editor in creative ways.
- Add a new file to the workspace named
utils\decoration.js
and insert the following code into it. ThecreateTextEditorDecorationType
creates a decoration object representing how the specified text should appear in the editor.
var vscode = require('vscode');
exports.activate = (context, searchTerms) => {
var termDecorationType = vscode.window.createTextEditorDecorationType({
borderWidth: '1px',
borderSpacing: '2px',
borderStyle: 'solid',
backgroundColor: '#2572E5',
color: 'white',
cursor: 'pointer'
});
}
- Add this code to the
utils\decoration.js
file'sactivate
method to provide a method to update the active editor's text's display with a decoration, and another method that can be used within event handlers later to update when users type or open new documents.
var timeout = null;
var activeEditor = vscode.window.activeTextEditor;
function triggerUpdateDecorations() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(updateDecorations, 500);
}
function updateDecorations() {
if (!activeEditor) {
return;
}
var documentText = activeEditor.document.getText();
var regExpFromArray = new RegExp(searchTerms.join('|'), 'gi');
var matchedStrings = [];
var match;
while (match = regExpFromArray.exec(documentText)) {
const startPos = activeEditor.document.positionAt(match.index);
const endPos = activeEditor.document.positionAt(match.index + match[0].length);
const decoration = {
range: new vscode.Range(startPos, endPos),
color: '#FFFFFF'
};
matchedStrings.push(decoration);
}
activeEditor.setDecorations(termDecorationType, matchedStrings);
}
if (activeEditor) {
triggerUpdateDecorations();
}
- Next, add handlers for the editor window's
onDidChangeActiveTextEditor
andonDidChangeTextDocument
events, during which the document's decorations will ne updated.
vscode.window.onDidChangeActiveTextEditor(editor => {
activeEditor = editor;
if (editor) {
triggerUpdateDecorations();
}
}, null, context.subscriptions);
vscode.workspace.onDidChangeTextDocument(event => {
if (activeEditor && event.document === activeEditor.document) {
triggerUpdateDecorations();
}
}, null, context.subscriptions);
- The last step in getting the decorations lit up is to wire it up in the
extension.js
file. First, pull in thedecoration.js
functionality:
var decoration = require('./utils/decoration.js');
- Then, call the
decoration.activate
method in theextension.activate
method so that the extension begins watching the editor for document changes or edits.
// decorate the important words in the doc
var searchTerms = ['Slovenia', 'NTK', 'Microsoft'];
decoration.activate(context, searchTerms);
- Once these changes are made, debug the extension. During the debugging session, create a new text file and enter a phrase like "NTK Slovenia is a great Microsoft conference" and note how the desired words are decorated.
Publishing extensions, and updating them, is as easy as running the VSCE command included with the Visual Studio Code Extension Manager npm package.
-
Hit Ctrl-` to open the integrated terminal window.
-
Check the
version
property in the extension folder'spackage.json
file to make sure the version is desirable. -
Enter the command
vsce package
in the root of the extension project's workspace. Note that a file with the filename format{extension-name}-{version}.vsix
will be created.Note: If you haven't customized the README.md file in the extension root you'll see an error message. The README.md file will be used to inform your potential users what value your extension offers them, so take time to customize it.
-
Login to the marketplace using your Microsoft account. The URL format should be
https://marketplace.visualstudio.com/manage/publishers/{your-publisher-name}
-
Show the slides on publishing an extension (or updating it to a new version) in the PowerPoint deck in this repository, then walk through the process with the extension you build during the session.
Note: If you chose a name for your demo extension that's unavailable, rename it in the
package.json
file, re-tunvsce package
, and re-publish it.
-
Open the Extensions palette in VS Code.
-
Search for the name of your extension.
Note how the
changelog.md
file is reflected on theCHANGELOG
tab in the Extensions palette. Point out that every version update should have an entry and stress that potential customers will use this to determine how confident they are that you'll keep them informed. -
Install it!