How to develop a VS Code Extension - Translator Helper

In my last post, I introduced Translator Helper - a VS Code Extension that helps you translate documents to other languages. In this post, I will introduce how to develop it from scratch.

It is not very hard to develop a VS Code extension, but it is quite hard to develop a good one. Let us get started from a “Hello World”. You can find detailed documentation on VS Code site: https://code.visualstudio.com/api.

First, we need to think about which category our extension is:

You should focus on different APIs for each category. For my case, I want to read and insert the text in the file, so it belongs to Extending the Workbench. Next I will introduce how to develop a VS Code extension like Translator Helper.

Creating your first VS Code Extension

You can find the tutorial here: https://code.visualstudio.com/api/get-started/your-first-extension

First, make sure you have Node.js and Git installed on your machine. Use npm to install Yeoman and VS Code Extension Generator:

1
npm install -g yo generator-code

Then navigate to an empty folder, use the command below to create a new project:

1
yo code

The CLI will ask you some questions to complete the project. The first question is like this:

Generator CLI

Here we need to select proper options based on my specified functionalities. For my extension, I use TypeScript because I love strong type. The output is shown below:

1
2
3
4
5
6
7
8
9
10
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

code ./helloworld

So a new VS Code Extension named helloworld will be generated and opened by VS Code. You can just press F5 to run the project and you will see a new VS Code window that has loaded this extension.

Press F1 or Ctrl+Shift+P, then input Hello World, you will see there is a message box shown on the right bottom corner, which shows Hello World. That means the extension works.

The most important code is placed in the extension.ts file. Open the file and have a look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 "helloworld" 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('extension.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!');
});

context.subscriptions.push(disposable);
}

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

It is quite straight forward. In the activate method, it uses vscode.commands.registerCommand() method to register a command, then uses vscode.window.showInformationMessage() method to show the message box.

Requirement Analysis

My requirement is simple: the user can press Keyboard Shortcuts to translate the current paragraph to the specified target language, then insert the translated text after the current paragraph. Because I found that the paragraph is the best unit for translation. No need to translate the whole article because it might lose some key information and we also need to proofread the text. Let check out the supported APIs of VS Code.

You can find all the APIs here: https://code.visualstudio.com/api/references/vscode-api. We need to interact with the current editor interface and implement the functionalities that allow us to automatically select and insert the text, etc. So I found an API named TextEditor: https://code.visualstudio.com/api/references/vscode-api#TextEditor:

It seems that this object can help us operate the text in the editor.

So now we have got an idea: we will register the keyboard shortcuts and the commands, automatically select the current paragraph then send the text to the translation API, at last, insert the result after the current paragraph.

Operating The Text

I created a class called DocService to operate the text in the editor, such as selecting a paragraph, inserting a paragraph, etc. The code is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class DocService {
editor: vscode.TextEditor | undefined;

setCurrentEditor(): void {
this.editor = vscode.window.activeTextEditor;
}

getParagraph(): string {
if (this.editor !== undefined) {
let startLine = this.editor.selection.start.line;
let endLine = this.editor.selection.end.line;
const endCharacter = this.editor.document.lineAt(endLine).text.length;
this.editor.selection = new vscode.Selection(startLine, 0, startLine, endCharacter);
var paragraph = this.editor.selection;
let result = this.editor.document.getText(paragraph);
if (result !== undefined) {
return result;
}
else {
return '';
}
} else {
return '';
}
}

getSelectionText(): string {
if (this.editor !== undefined) {
return this.editor.document.getText(this.editor.selection);
} else {
return '';
}
}

insertText(text: string): void {
if (this.editor !== undefined) {
let end = this.editor.selection.end;
this.editor.edit(editBuilder => {
editBuilder.insert(end, '\n');
editBuilder.insert(end, text);
}).then(success => {
if (success && this.editor !== undefined) {
let end = this.editor.selection.end;
this.editor.selection = new vscode.Selection(end, end);
let startLine = this.editor.selection.start.line;
let endLine = this.editor.selection.end.line;
const endCharacter = this.editor.document.lineAt(endLine).text.length;
this.editor.selection = new vscode.Selection(startLine, 0, startLine, endCharacter);
}
});
}
}

Calling Google Translation API

We use Google Translate API to translate the text. It is just a simple HTTP request. For simplicity, we can use an npm package named @vitalets/google-translate-api to do it. The code is like this:

1
2
3
4
5
6
7
class GoogleTranslationService implements ITranslatorService {
async translate(text: string, source: string, target: string): Promise<string> {
const service = googleTranslate;
let result = await service(text, { from: source, to: target });
return result.text;
}
}

Implementing The Core Function

Now we can combine these code snippets together. Create the function by following the “hello world” sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
let translateInsert = vscode.commands.registerCommand('translatorHelper.translateInsert', async () => {
// The code you place here will be executed every time your command is executed
docService.setCurrentEditor();
const text = docService.getParagraph();
try {
if (text.trim() !== '') {
let result = await servie.translate(text, source, target);
docService.insertText(result);
}
} catch (error) {
vscode.window.showErrorMessage(`Error occurs. ${error.message}`);
}
});

It is easy to understand. We call getParagraph() method in DocService class to automatically get the text of the current paragraph, send the orignial text to Google Translate API then insert the returned text after the current paragraph.

Configurations & Contribution Points

It will not work if you just register the commands in the code. We also need to update package.json to configure the new commands. There is an important concept called Contribution Points in VS Code extension development. You can find the documentation here: https://code.visualstudio.com/api/references/contribution-points. Contribution Points are a set of configurations in the package.json file, which is used to declare some properties of the extension, such as short cuts, commands and settings, etc. Here is an example:

1
2
3
"activationEvents": [
"onCommand:translatorHelper.translateInsert"
]

The code above shows that it registers an event which will be invoked by the command namedtranslatorHelper.translateInsert . Another example is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
"contributes": {
"commands": [{
"command": "translatorHelper.translateInsert",
"title": "Translate & Insert"
}
],
"keybindings": [{
"command": "translatorHelper.translateInsert",
"key": "alt+t",
"when": "editorTextFocus"
}
],
...

I added a command named translatorHelper.translateInsert, which is exact same with the one in the last code snippet. Also, I added a keybindings, so that when the user presses Alt+T, the command will be invoked.

Extension Configuration

We should provide users with the capability to set up the translate languages. The API we need to use for this purpose is contributes.configuration. VS Code provides a unified API to manage the configurations for all the extensions in one place. To enable configurations for my extension, I need to add the below code to the contributes section in package.json :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"configuration": {
"title": "Translator Helper",
"properties": {
"translatorHelper.api": {
"type": "string",
"default": "google",
"enum": [
"google",
"google-cn"
],
"enumDescriptions": [
"Google Translation API.",
"Google Translation API for Chinese users."
],
"description": "Specify the api to translate the text."
},
"translatorHelper.sourceLanguage": {
"type": "string",
"default": "en",
"description": "The source language to be translated."
},
"translatorHelper.targetLanguage": {
"type": "string",
"default": "zh-CN",
"description": "The target language."
}
}
}

The configuration could be text, enum or other types. You can also set the default value and add descriptions, etc. After that, you can see the configuration options in the dropdownlist on the configuration page. The detailed API is here: https://code.visualstudio.com/api/references/contribution-points#contributes.configuration.

For my case, I can read the configurations by the below code:

1
2
3
4
let config = vscode.workspace.getConfiguration("translatorHelper");
const api = config.api;
const source = config.sourceLanguage;
const target = config.targetLanguage;

Now the basic functionalities of the translator extension are ready to go!

Testing

Testing is very important in the software development process, especially in the DevOps process. Good testing helps us improve the quality of the software. We can follow the instruction to create tests for the VS Code extension: https://code.visualstudio.com/api/working-with-extensions/testing-extension.

The default project template has a full test case integrated already, which is in the src/test folder. We can easily update the test cases. I added a default markdown file named test.md in the src/test/suite folder, which contains a “Hello World”. When the test runs, VS Code will load this file automatically and execute the command to translate it. Some code snippets are shown below:

1
2
3
4
5
6
7
8
9
10
11
12
test("Should get the correct translation then insert it.", async () => {
const uri = vscode.Uri.file(
path.join(__dirname + testFileLocation)
);
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document);
// Make sure the file is fully loaded before interacting with it.
await sleep(200);
vscode.commands.executeCommand('extension.translateInsert').then(result => {
assert.equal(editor.document.getText(editor.selection).indexOf('你好') > -1, true);
});
});

This piece of code will automatically load the text file and call 'extension.translateInsert' command, then compare the translated text with expected text. If it contains the expected text, that means the extension works as expected.

Bundling

You might be aware that the VS Code extension is similar to the front-end app which contains JavaScript/TypeScript. For apps developed by JavaScript or TypeScript, loading lots of small files is slower than loading one big file. So usually, we need to use some bundlers to bundle the code and reduce the size of the app. There are quite a few different bundlers available, such as rollup.js, webpack, etc. For our case, we use webpack as the bundler. That is also the recommended way of VS Code: https://code.visualstudio.com/api/working-with-extensions/bundling-extension

Install webpack by typing this command:

1
npm i --save-dev webpack webpack-cli

Then install ts-loader to add supports to TypeScript for webpack:

1
npm i --save-dev ts-loader

After that, a webpack.config.js will be added to the project. In addition, we need to update the package.json file to use webpack for bundling:

1
2
3
4
5
6
7
8
9
10
11
"scripts": {
"vscode:prepublish": "webpack --mode production",
"webpack": "webpack --mode development",
"webpack-dev": "webpack --mode development --watch",
"test-compile": "tsc -p ./",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile",
"test": "node ./out/test/runTest.js",
"deploy": "vsce publish --yarn"
},

If you have experience with front-end development, you may find that is easy to config.

Publishing

It is time to publish our extension. Before doing it, we need to install vsce which is a command-line tool for packaging, publishing and managing VS code extensions. Run this command to install it:

1
npm install -g vsce

We also need to register an account in Marketplace to get the Access Token. Here is the instruction for publishing:https://code.visualstudio.com/api/working-with-extensions/publishing-extension

I do not want to copy & paste the documents here so you can find detailed steps in the document above. Just keep in mind that if you do not create a Publisher, you will not be able to use the command to publish the extension.

CI/CD

It is tedious to publish the extension manually for each update. The best way is to integrate CI/CD by Azure DevOps. Here is the documentation to describe how to build a Pipeline:https://code.visualstudio.com/api/working-with-extensions/continuous-integration.

Note that we need to use Access Token to publish the extension, but this token is sensitive information so we need to create a variable to store the token in the Pipeline, then use is in YAML file.

Variables in Pipeline

The code of the YAML file is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
trigger:
branches:
include: ['master']
tags:
include: ['*']

strategy:
matrix:
linux:
imageName: 'ubuntu-16.04'
mac:
imageName: 'macos-10.13'
windows:
imageName: 'vs2017-win2016'

pool:
vmImage: $(imageName)

steps:

- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'

- bash: |
/usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
echo ">>> Started xvfb"
displayName: Start xvfb
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))

- bash: |
echo ">>> Compile vscode-test"
yarn && yarn compile
echo ">>> Compiled vscode-test"
cd sample
echo ">>> Run sample integration test"
yarn && yarn compile && yarn test
displayName: Run Tests
env:
DISPLAY: ':99.0'

- bash: |
echo ">>> Publish"
yarn deploy -p $(VSCODE_MARKETPLACE_TOKEN)
displayName: Publish
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))

It uses $(VSCODE_MARKETPLACE_TOKEN) to retrieve the Access Token. In the trigger section, we set up the trigger to specify the trigger branch and the conditions. So that every time I commit a Pull Request, the Pipeline will be automatically triggered then publish the extension to the Marketplace.

Icon & README

Another important thing is the manifest information of the extension, including the icon and README, etc. To make the icon, I recommend using Lunacy, which you can download from Windows 10 Store for free. This tool has a bunch of beautiful free icons. Also, you can download the extension icon packs that include more than 100,000 icons.

Lunacy

Place the icon file in the project folder, and config it in the package.json file:

1
"icon": "images/icon.png",

In addition, you need to define the attributes of the extension, such as id, display name, description, version, publisher, repo link, VS Code version, etc. You can also add some badges to show the status like build status, current version, downloads, reviews. etc. For more details, please read this documentation: https://code.visualstudio.com/api/references/extension-manifest

The README file is shown on the extension page in Marketplace. You better make a couple of GIF files to show what the extension can do. There are lots of tools to make GIF files. I use SNAGIT, which is a good software to capture your screens.

SNAGIT

Summary

This is a very simple VS Code extension - you might find that the core method is just less than 50 lines. But it definitely improves my work efficiency. What I learned from it is that when you cannot find a good tool, why not make if yourself? The progress is fun! The project is open-sourced on GitHub: https://github.com/yanxiaodi/vscode-translator-helper. All feedback and advice are welcomed.

Happy Coding!