Skip to content

Latest commit

 

History

History
790 lines (522 loc) · 59.7 KB

File metadata and controls

790 lines (522 loc) · 59.7 KB

AngularConnect 2019 Mini-workshop:
Beyond VS Code - Extending your favorite code editor

George Kalpakas | Twitter: https://twitter.com/gkalpakas

VS Code has been taking the dev community by storm, quickly climbing the ranks of popular code editors - especially among front-end developers. One of its most powerful (and often overlooked) features is its extensibility, which allows it to be tailored to the diverse needs and preferences of thousands of developers.

But what if none of the 10000+ freely available extensions does exactly what you want?

In this workshop, you will learn how to take advantage of VS Code's public extensibility model - using the same tools you use for your day-to-day work - and make your (and your team's) dev experience truly yours.

In particular, you will learn (by doing) how to:

  • Set up, run and debug a VS Code extension.
  • Write unit and end-to-end tests for your extension.
  • Package and share your extension with others.

Remember...you are only as good as your tools ;)

Table of contents

  1. Prerequisites
  2. Preparation steps
  3. Introduction
  4. Labs
    1. Lab 1: Scaffold and run a basic extension
    2. Lab 2: Understand the extension layout
    3. Lab 3: Develop and debug the extension
    4. Lab 4: Feature: Show Angular template on hover
    5. Lab 5: Feature: Peek/Go to Angular template definition
    6. Lab 6: End-to-end (E2E) testing
    7. Lab 7: Unit testing
    8. Lab 8: Package and share the extension

Prerequisites

A basic knowledge of the following languages/tools will be assumed during the workshop. You might find it hard to follow along, if you have never heard of/used them.

Back to top

Preparation steps

If you intend to code along on your laptop (which will help you get the most out of the workshop and is the recommended way to attend), having completed the following steps beforehand will help ensure your environment is set up correctly and you will be able to keep up (even, for example, in the event of a bad/congested internet connection during the workshop).

Back to top

Introduction

About VS Code and extensions

VS Code is a cross platform desktop application. It is based on Electron and written in TypeScript. What this means, is that the whole editor is basically a big JavaScript application.

It has been built with extensibility in mind. It exposes a rich API to extensions, allowing almost every part of it to be customized and enhanced. At the same time, care has been taken to ensure that extensions will not negatively affect the editor's performance (e.g. they run in a different process).

VS Code extensions can be written in TypeScript or JavaScript. There are several different types of extensions. Here are some of the things extensions can do:

  • Theming: Change the look of VS Code with a color or icon theme.
  • Extending workbench: Add custom components & views in the UI or create Webviews to display custom webpages built with HTML/CSS/JS.
  • Declarative language features: Support a new programming language (by providing features such as syntax highlighting, comment toggling and more).
  • Programmatic language features: Add rich programming language support (by providing features such as intellisense, diagnostic errors, Go to Definition and more).
  • Debugging: Support debugging a specific runtime.

The documentation provided by the VS Code team covers everything you need to know. The Extension overview page is a good place to start, if you are looking to learn more.

Another great way to learn is looking at the source code of existing extensions, most of which are hosted publicly on GitHub. Head over to the VS Code Extension Marketplace, find an extension that seems to be doing something similar to what you are looking to implement and go to their repository.

The VS Code team also maintains a great collection of extension samples, featuring all the different types of extensions. Worth checking out here.

About the workshop

In this workshop, we will start with a very basic extension and then enhance it with some simple features that fall under the "programmatic language features" category (see above). We are going to use TypeScript, because it provides a better developer experience and allows exploring unfamiliar APIs more easily.

The workshop is separated in a few self-contained "labs", each tackling a specific topic related to VS Code extension development. The entire source code can be found in the my-first-vscode-extension GitHub repository. The code for each lab is spread across one or more commits.

The code itself is not the main focus of the workshop. What's more important is to get a solid understanding of the various tools and processes involved and how to efficiently use them to develop your own VS Code extensions.

If you intend to code along during the workshop, it is recommended that you clone the repository and open it in VS Code. There are Git tags corresponding to each lab (lab-1, lab-3, etc.). Using these tags, you can easily update your local copy to the state it would be after completing the lab. This way, you can continue following along on the next lab, even if you didn't manage to fully complete all previous ones.

For example, to get yourself ready for lab 5, you can run the command: git checkout --force lab-4
WARNING: Running this command will cause any changes you made within your working directory to be lost.

At the end of each lab, there is an "Experiments" section that contains ideas for things you can experiment with in order to deepen your understanding of the topic. These will not be covered during the workshop, but you can give them a try on your own if you are looking for an extra challenge (either during the workshop or later).

There is also a list of relevant resources for each lab. They are good starting points, if you want to explore each topic in more depth.

Back to top

Labs

💡 Tip
If you have cloned the repository earlier and want to make sure you have the latest changes (and Git tags), run the following commands:

git fetch --force --tags origin master
git reset --hard origin/master

Lab 1

Objective: Scaffold and run a basic extension.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-0...lab-1
Git tag: lab-1

Notes:
The easiest way to create a basic extension is by using Yeoman and the VS Code Extension Generator:

npm install --global yo generator-code
yo code

By checking out the repository at tag lab-1 (git checkout --force lab-1), you will get the code generated by running the above commands (so you do not need to run them now, but that is how you can scaffold a new extension). The only difference is that this repository includes a .workshop-infra/ directory, which contains workshop infrastructure related stuff and you should completely ignore. (The scaffolded extension has already been tweaked to ignore that directory.)

Before examining what our extension is made of, let's see it in action.

If you haven't already, open the extension's project directory in VS Code, either via File > Open Folder... (from VS Code's menu bar) or from the command line:

code path/to/my-first-vscode-extension

Inside the editor, press F5 to launch a new, special VS Code window - called the Extension Development Host window - that runs the extension under development. As we will see in the next lab, our extension contributes a new "Hello World" command. To run it, bring up the Command Palette in the Extension Development Host window (View > Command Palette or Ctrl+Shift+P/Cmd+Shift+P) and type "Hello World". Press ENTER to see a "Hello World" notification show up.

Congratulations! You just extended VS Code with custom functionality.

Experiments: -

Resources:

Back to top

Lab 2

Objective: Understand the extension layout.
Code diff: -
Git tag: -

Notes:
Let's now examine the different parts of the extension:

  1. Generic project files:

    • .gitignore: To configure Git to ignore certain files/directories (such as node_modules/).
    • CHANGELOG.md: To keep track of notable changes in your project.
    • package.json/package-lock.json: Metadata about your project/extension and its dependencies. More on this later.
    • README.md: To keep general information about your project.
  2. TypeScript-related project files:

    • tsconfig.json: To configure the TypeScript compilation (for transpiling our extension code and tests to JavaScript).
    • tslint.json: To specifiy linting rules for TSLint.
  3. VS Code-related files/directories:

    • .vscode/: VS Code configuration files (for any type of project; not specific to extensions).
      • extensions.json: List of extension recommendations for people working on the project.
      • launch.json: List of launch configurations (available in the Debug view) that can be used for Debugging.
      • settings.json: Project-specific settings (that override global ones).
      • tasks.json: List and configuration of external tasks (e.g. npm scripts) for integration with VS Code (e.g. "Run task" command or use in launch configurations). See here for more details.
    • .vscodeignore: Similar to .gitignore but for excluding files/directories from your packaged extension (once you decide to package/publish your extension). See here for more details.
  4. Project source code:

    • vsc-extension-quickstart.md: Basic overview of the extension, quick instructions on how to try it out and ideas for next steps.
    • src/: The source code of your extension (and its tests).

You generally don't have to worry about the contents of the above files with the exception of package.json and src/.

Let's take a closer look at what is in package.json. In addition to its regular role in a Node.js-based project, it also serves as the Extension Manifest - a special file containing extension-specific metadata, such as the extension name and description, the publisher name, extension icons, the extension's contributions (commands, keybindings, menus, views, etc.), the extension's activation events and more.

Looking at specific fields in our basic extension's package.json, we have:

  • scripts: { ... }: A minimal set of npm scripts for compiling the extension (once or in watch mode) and running tests.

  • engines: {vscode: '^1.37.0'}: This specifies the minimum VS Code version supported by the extension. VS Code will prevent users of older versions from installing the extension, since it might be using VS Code APIs that are not available in their version. See here for more details.

    💡 Tip
    The version of @types/vscode in devDependencies should always match the minimum supported version of VS Code (as specified in engines.vscode) to ensure that your extension only uses APIs that will be available at runtime.

  • activationEvents: ['onCommand:extension.helloWorld']: This specifies that the extension should be activated, when the user invokes the extension.helloWorld command (e.g. from the Command Palette).

  • contributes: {commands: [ ... ]}: This defines the commands contributed by the extension (here just the extension.helloWorld command).

    ✔️ Best practice
    Prefix command names with your extension's ID/name to easily distinguish them from built-in commands or commands contributed by other extensions and avoid name conflicts. E.g.: my-first-vscode-extension.someCommand

Let's now look at the src/ directory layout:

  • extension.ts: This is the extension's entry point. We will examine it more closely later.
  • test/: This directory contains code related to testing the extension.
    • runTest.ts: The script that kicks off the end-to-end (e2e) tests (used in the test npm script in package.json).
    • suite/
      • extension.test.ts: The tests entry point. This file contains the e2e tests (or imports other files that contain tests).
      • index.ts: This file configures and starts the test runner (e.g. Mocha or Jasmine).

Above, we peeked at the src/test/ directory and mentioned e2e testing. We will ignore testing (and related files) for the time being. We will dive deeper into testing in subsequent labs.

Finally, let's break down the main extension file: extension.ts
It exports two functions, activate and deactivate. activate is executed when one of your registered activation events is triggered and it basically spins up the extension. deactivate is called just before the extension becomes deactivated and is a good place to run any necessary clean up.

Experiments:

  • Examine the contents of the extension.ts file.

    • Read the comments to gain a better understanding of what each line of code does.
    • Hover over the various VS Code APIs used (such as vscode.commands.registerCommand and context.subscriptions) to find out more about their purpose.
  • Change the command's name to use the extension's ID as prefix (instead of the generic extension. prefix).

    • Find the extension's ID/name in package.json > name.
    • Update the command name in all necessary locations (in both extension.ts and package.json).
    • Run the extension (F5) and invoke the "Hello World" command to make sure everything still works correctly.

Resources:

  • VS Code: Extension anatomy:
    An overview of the various files included in a basic extension and further resources with more in-depth information.

Back to top

Lab 3

Objective: Develop and debug the extension.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-1...lab-3
Git tag: lab-3

Notes:
When you want to work on the extension, you can first launch an Extension Development Host window, by pressing F5, and then start hacking on the src/extension.ts file.

3.a: Change the message to Hello Angular. | Git tag: lab-3.a

For example, let's change the notification message from Hello World to Hello Angular. Open src/extension.ts, locate the vscode.window.showInformationMessage() call and change the message. As soon as you change the file, the TypeScript compiler will re-build the extension in the background. To see the change in action, switch to the Extension Development Host window, reload the window (via Command Palette > Reload window) and invoke the extension.helloWorld command again (via Command Palette > Hello World).

You should see a notification with the updated "Hello Angular" message show up.

3.b: Add a comma and introduce a bug. | Git tag: lab-3.b

According to some resources, we should put a comma after "Hello" in a salutation. Let's fix our message to include a comma. One way to achieve that is putting the two words in an array and then use the .join() method to join them. (Yes, it is not necessarily the best way, but it is a way :D)

You can see the code for an implementation of this on GitHub or locally by running git show lab-3.b. Once you have made the change, try it out by switching to the Extension Development Host window, reloading it and running the "Hello World" command.

NOTE: Use the code exactly as shown above or you will ruin the workshop flow 😉

If you have a keen eye for punctuation errors, you might have noticed that there is an undesired leading comma (before "Hello"). Yay, we have introduced our first VS Code extension bug!

3: Debug and fix the bug. | Git tag: lab-3

Now that we have a bug in our extension, how do we go about identifying the root cause and fixing it?

Since our message is not what we expect it to be, it sounds like a good idea to put a breakpoint right before the message is shown and inspect the state at that point. In src/extension.ts, locate the line containing the vscode.window.showInformationMessage() call and put a breakpoint by clicking on the editor margin (or putting the cursor on that line and pressing F9).

Now, switch back to the Extension Development Host window and invoke the "Hello World" command again. Once execution reaches the breakpoint, VS Code will stop and focus on the target line in the editor. Here, we can inspect the state and step through the code in the built-in debugger. For example, hover over the words variable and see its current value. We can also type expressions in the Debug console REPL and have them evaluated in the current execution context. For example, type words and then expand the evaluated Array value to see the elements in the array.

By observing the value of words, we realize that it contains three elements (instead of two) - the first element being undefined. This explains the extra comma when joining its elements. Taking a closer look at the code that populates the words array, we can see that the indices are wrong. Changing the indices from 1 and 2 to 0 and 1 (since JavaScript arrays are 0-indexed) should fix the bug.

✔️ Best practice
To avoid the overhead of loading unrelated extensions when launching/reloading the Extension Development Host window, you can disable them in .vscode/launch.json. Find the "Run Extension" configuration and add "--disable-extensions" in the args array.

After fixing the code in src/extension.ts, save the file, reload the Extension Development Host window and invoke the "Hello World" command once more. Unless you have removed the breakpoint, VS Code will again stop execution when the target line is reached. You can hover over the words variable and confirm that it now contains the right elements. Hit the "Continue" button in the Debug toolbar (or press F5) to resume execution. Now back to the Extension Development Host window to see the notification show up with the correct message (no extra commas).

(If you haven't done so already, it is now a good time to remove the breakpoint. It has served its purpose for now.)

Good job! That bug is no more 💪

💡 Tip
You need to reload the Extension Development Host window whenever your extension's code changes (e.g. when you modify a .ts file.

Experiments:

  • Add a breakpoint and step through code (including VS Code internals).

    • Put a breakpoint on the first line of the "Hello World" command callback in src/extension.ts.
    • Invoke the command and wait for execution to reach the target line.
    • Step through the code (by clicking the "Step Into" button in the Debug toolbar or pressing F11).
    • Step into the vscode.window.showInformationMessage() call and take a look around. You are now stepping through actual code of the VS Code application 👩‍💻
    • Remember to remove the breakpoint when done.
  • Learn how to disable breakpoints.

    • Put a breakpoint somewhere in src/extension.ts.
    • Find out how to disable (but not remove it). Read about breakpoints here.
    • Run the extension and confirm that execution does not stop on disabled breakpoints.
  • Learn how to use logpoints.

    • Find out what is a logpoint
    • Put a logpoint in src/extension.ts on the line that shows the notification. Make it log something like Words: followed by the actual words in the words variable (without hard-coding them in the logpoint).

Resources:

Back to top

Lab 4

Objective: Implement feature: Show Angular template on hover.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-3...lab-4
Git tag: lab-4

Notes:
Hopefully, you now have a solid grasp on how to modify, preview and debug the extension. It is time to start working on the actual functionality. Since there are likely many Angular developers among us, let's build something that will make our lives easier when working on Angular projects.

In particular, we will focus on working with components, which are the heart and soul of an Angular application. As you may know, all components have a template, which is specified in the @Component metadata. The template can be defined either inline (using the template property) or as a relative path to an external HTML file (using the templateUrl property). Both options have pros and cons:

  • Using an inline template makes it easy to see and edit the template alongside the component logic, but one loses HTML intellisense features (such as auto-complete, syntax highlighting, etc.).
  • Using an HTML file offers a better HTML editing experience, but one needs to do extra work to get from the component file to the template file and vice versa.

One possible way to improve the situation is to show the contents of the HTML template file in a popup when hovering over the template URL. This way we can have a good HTML editing experience (by keeping the templates in separate HTML files), but still be able to look at the template content without leaving the component file. Still not ideal, but an improvement for sure!

NOTE:
There are already extensions available on the VS Code Marketplace that provide similar features (and more). They are also a good resource if you are looking for ideas to enrich your extension's functionality:

4.a: Create a fixture. | Git tag: lab-4.a

Before starting to implement the new functionality, we need a component to try it on. If you have an Angular project lying around, you can use it. However, it makes things a little simpler (and more predictable) if we create a dummy component (a fixture) inside the extension project and use that instead.

✔️ Best practice
By using fixtures, you get several benefits:

  • It creates a more consistent development environment for other people that might want to contibute to your extension.
  • You can easily reproduce any condition you need to try the extension on.
  • You can use the same fixtures in tests (as we will see later on), which may run in a different environment (e.g. on a Continuous Integration platform) and not have access to your local projects.

First, create a fixtures/simple-component/ directory. Then, create a basic Angular component. At the very least, you need to have a *.component.ts and a *.component.html file. You can see the sample code for this on GitHub or locally by running git show lab-4.a.

The fixtures are not part of the extension's code, so we do not want them to be compiled or published along with it. To prevent that from happening, we have to update tsconfig.json and .vscodeignore accordingly.

Now, when we launch the Extension Development Host window, we can click File > Open Folder... (in VS Code's menu bar) and select the fixtures/simple-component/ directory. Once we implement more features, we can use our simple component to try out the extension.

💡 Tip
If you want VS Code to automatically load a specific directory when launching the Extension Development Host window, you can configure it in .vscode/launch.json. Find the "Run Extension" configuration and add the directory path (e.g. "${workspaceFolder}/fixtures/simple-component/") in the args array.

4.b: Implement a no-op HoverProvider. | Git tag: lab-4.b

In order for an extension to provide popups on hover, it needs to implement a HoverProvider and register it.

We will begin by creating a new template-url-intellisense-provider.ts file under src/ and add a simple class that implements the HoverProvider interface. All we need is a provideHover() method that receives a document and a position and returns either a Hover instance (if there is info to show for that position) or null/undefined otherwise. We will always return a Hover containing "Not implemented" for now.

💡 Tip
You can see the code for this step on GitHub or locally by running git show lab-4.b.

Now that we have a basic HoverProvider, we need to register it with VS Code. Inside the activate() function in src/extension.ts, we create an instance of the TemplateUrlIntellisenseProvider class and pass it to vscode.languages.registerHoverProvider(). We also have to specify a DocumentSelector to limit the type of files our provider applies to. This allows VS Code to avoid unnecessary work on other files. For our needs, it suffices to limit the provider to TypeScript files with a .component.ts extension. Look at the code (either on GitHub or locally, as described above) for more details.

Last but not least, we need to let VS Code know that our extension needs to be activated when a TypeScript file is opened. Up until now, our extension would only be activated when invoking the "Hello World" command. Open package.json and add "onLanguage:typescript" in the activationEvents array.

With all bits in place, you can now switch to the Extension Development Host window (reload if necessary) and preview the hover functionality by opening a .component.ts file and trying to hover over anything. (You should see a little Not implemented popup.)

4: Implement the actual HoverProvider logic. | Git tag: lab-4

Finally, we shall implement the actual HoverProvider logic necessary to show template content on hover. You can look at the code for details (on GitHub or locally with git show lab-4), but in a nutshell here is how it goes:

  1. Get the text of the line being hovered.
  2. Check whether the line is a templateUrl line and extract the template URL (relative path).
  3. Resolve the path to the template HTML file and read the HTML content.
  4. Return a Hover with the content of the component template (formatted as HTML).

This is not a bullet-proof algorithm, but it works for our purposes. Most of it is also not specific to VS Code extensions or Angular (extracting information from some text, resolving file names, reading file contents, etc.).

Again, reload the Extension Development Host window and see the new functionality in action. (Note that the template content is nicely formatted as HTML.)

Amazing! We can now look at the HTML template without leaving the component file 🎉

Experiments:

  • Implement the same functionality for styleUrls.

    • Modify TemplateUrlIntellisenseProvider#provideHover() to also look for styleUrls lines. To keep things simple, only recognize lines that look like this: styleUrls: ['./path/to/styles'],
    • If a styleUrls line is found, extract the relative path to the styles file, resolve it to an absolute file path, read its content and return a Hover.
    • Ensure the code in the Hover is correctly styled as CSS/Sass.
  • Provide the second, optional argument to Hover: a Range.

    • Notice that when you move the cursor from .component. to .html while a popup is shown, the popup will momentarily disappear and reappear. This is because (by default) VS Code scopes each provided Hover to the current word.
    • Use intellisense (e.g. Right click > Go to Type Definition on the Hover class) to discover the second, optional Range argument.
    • Provide the second argument to let VS Code know that the provided Hover applies to the whole template URL string.
  • Modify TemplateUrlIntellisenseProvider#provideHover() to use vscode.workspace.fs, a FileSystem that allows access to files from contributed file systems, such as the SSH or WSL remote file systems. See here for more details.

    • Modify the TemplateUrlIntellisenseProvider#provideHover() method to be asynchronous (e.g. replace the synchronous Node.js fs methods with their asynchronous counterparts).
    • Find out what is the third argument passed to HoverProvider#provideHover() method and use it. (Tip: You can use intellisense to discover it via the provided typings.)
    • Switch to vscode.workspace.fs. (Note, this is not a drop-in replacement.)

Resources:

Back to top

Lab 5

Objective: Implement feature: Peek/Go to Angular template definition.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-4...lab-5
Git tag: lab-5

Notes:
Being able to see the template from the component file is great, but it still leaves a lot to be desired. For example, there is still no easy way to make a quick edit without leaving the component file. Let's address that by providing a definition for the template URL, which points to the actual HTML file. This way, we will be able to take advantage of VS Code's capabilities and either peek the template content inline or go straight to the template file. These two functionalities can be invoked by right-clicking on the template URL and choosing Peek Definition or Go to Definition respectively.

5.a: Implement a no-op DefinitionProvider. | Git tag: lab-5.a

In order for an extension to provide definitions, it needs to implement a DefinitionProvider and register it.

Similar to what we did with the HoverProvider in the previous lab, we are first going to provide a no-op DefinitionProvider. Since much of the provideDefinition() logic will be the same as the provideHover() logic (e.g. extracting the relative path, resolving the file path, etc.), we will expand the TemplateUrlIntellisenseProvider (which quite conveniently has a generic name 😉) to also implement the DefinitionProvider interface. All we need is a provideDefinition() method that receives a document and a position and returns either a Definition instance or null/undefined if it can't provide a definition for the target symbol. We will always return null for now.

💡 Tip
You can see the code for this first step on GitHub or locally by running git show lab-5.a.

Now that we have a basic DefinitionProvider, we need to register it with VS Code. Inside the activate() function in src/extension.ts, we need to pass an instance of the TemplateUrlIntellisenseProvider class to vscode.languages.registerDefinitionProvider(). Since we already have an instance of this class (used as a HoverProvider), we can reuse it. We will also reuse the DocumentSelector from registerHoverProvider(), since both providers target the same types of files.

Normally, we would also need to add "onLanguage:typescript" to the activationEvents array in package.json. Notice, however, that we don't have to do anything here, because we already did that when adding the HoverProvider in the previous lab.

5: Implement the actual DefinitionProvider logic. | Git tag: lab-5

We shall now implement the actual DefinitionProvider logic necessary for the Peek/Go to Definition features to work. You can look at the code for details (on GitHub or locally with git show lab-5), but in a nutshell here is how it goes:

  1. Refactor the common logic for extracting the template file path to a getTemplateFilePath() method.
  2. Use that method in provideDefinitionProvider() to get the template file path.
  3. Provide a Definition, which is basically a Location object pointing to the URI of the template file and a Range or Position. (Here we use position (0, 0) because the whole file represents the definition of the template URL, so we don't need to specify a range).

Finally, you can reload the Extension Development Host window and see the new functionality in action. Right-click on the template URL and try both Peek Definition and Go to Definition

Experiments:

  • Implement the same functionality for styleUrls.
    • Modify TemplateUrlIntellisenseProvider#provideDefinition() to also look for styleUrls lines. To keep things simple, only recognize lines that look like this: styleUrls: ['./path/to/styles'],
    • If a styleUrls line is found, extract the relative path to the styles file, resolve it to an absolute file path, and return a Definition.
    • Try it out in the Extension Development Host window.

Resources:

Back to top

Lab 6

Objective: End-to-end (E2E) testing.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-5...lab-6
Git tag: lab-6

Notes:
As we keep improving the extension and add more features, we need to ensure that nothing breaks. Extensions can grow complex before you know it, so manually testing everything is tedious, error prone and boooooriiiiing. We need automated tests 💡

✔️ Best practice
Always test your code. It is good karma (pun intended) 👌

Fortunately, VS Code has quite good support for running and debugging tests for extensions. End-to-end (e2e) tests - also referred to as integration tests - require an environment that is (almost) identical to the one where the actual extension will be run. VS Code offers the Extension Development Host window (that we are already familiar with from previous labs), which provides full access to the VS Code API during testing.

As is always true for e2e tests, they are slower than unit tests but can exercise more parts of the extension and how they work together. When scaffolding an extension with yo code (as we have done), the e2e boilerplate files are automatically created for us. Let's run the e2e tests and then we will take a closer look at the related files in src/test/.

To run the e2e tests, switch to the Debug view and choose "Extension Tests" in the debug target dropdown. Then click the "Start Debugging" button (or press F5) to run the tests. The new VS Code window will open shortly, the tests will be run and then the window will be closed again. You can see the results of the tests in the Debug console. All tests should pass.

NOTE:
You may remember that we have previously used F5 to run the extension (in a new Extension Development Host window), not the tests. In fact, F5 starts the debug target that is currently selected in Debug view's dropdown. The dropdown is populated from the configurations found in .vscode/launch.json.

Now that we have seen how to run the tests, let's better understand the files involved:

  • runTest.ts calls the runTests() helper from vscode-test (a package from the VS Code team that provides helpers for testing VS Code extensions). This will launch the Extension Development Host window and pass necessary options to it.
  • The scripts under suite/ are executed in the Extension Development Host window. They contain the extension's e2e tests and test runner setup code.
  • suite/index.ts must export a run() function that sets up the test runner and kicks it off. VS Code will call this function to run the tests.

💡 Tip
Currently, running e2e tests from the command line (e.g. via npm test) does not work if an instance of the same version of VS Code is already running. You can read more about this limitation here.

In addition to the work-arounds mentioned in the docs, one could also specify a different version of VS Code to run the tests than the one used for development (e.g. developing on v1.37.1 and running the tests on v1.37.0). To specify the version used in tests, pass version to runTests() in runTest.ts.

✔️ Best practice
In your Continuous Integration (CI) environment, it is a good idea to run e2e tests on both the latest VS Code version and the minimum supported one, to ensure that the extension is compatible with all supported versions.

The scaffolded extension comes set up with Mocha as the test runner, so we will go with that.

💡 Tip
Theoretically, one could use any test runner, such as Jasmine, which is a popular choice among Angular developers. However, there are some caveats related to internal VS Code implementation details. In particular, the reporter used to report test results to the main window must use console.log() (and not for example process.stdout.write()).

For Jasmine (whose default reporter uses process.stdout.write()), this means that extra work is needed to have test results be reported correctly. Discussing this in more detail is outside the scope of this introductory workshop, but you could see an example of Jasmine being used as the test runner for an extension here.

6.a: Minor tweaks and tips. | Git tag: lab-6.a

Before we start writing our own tests, we will tweak the testing configuration a bit to make our lives easier later on. You can see the changes on GitHub or locally by running git show lab-6.a. Here is an overview:

  • Automatically open fixtures/simple-component/ in test VS Code instances.
  • Disable other extensions during tests (via --disable-extensions).
  • Rename src/test/suite/ to src/test/e2e/ to more easily distinguish from unit tests in a subsequent lab.

6.b: Add tests for extension.helloWorld and HoverProvider. | Git tag: lab-6.b

Let's now write some tests for the extension.helloWorld command and the provided hovers. We will also install Sinon.JS to be able to use spies and stubs in our tests. Run the following command to install Sinon.JS and its typings (for intellisense):

npm install --save-dev sinon @types/sinon

Open e2e/extension.test.ts and add a test for the extension.helloWorld command. We will stub vscode.window.showInformationMessage() and use vscode.commands.executeCommand() to programmatically execute the extension.helloWorld command.

Similarly, we will create a test for hovers and again use vscode.commands.executeCommand() with the built-in vscode.executeHoverProvider command to execute our HoverProvider. We pass appropriate arguments to simulate the HoverProvider being invoked in the simple.component.ts file (from the simple-component fixture) on the 5th line, which contains the templateUrl property (and should thus result in a hover).

The exact code used can be seen on GitHub or locally by running git show lab-6.b.

This is a good time to run the tests again and see if everything passes. Press F5 and observe the test results in the Debug console.

6: Debug and fix the failing test. | Git tag: lab-6

Oh, no! One of the tests is failing. Time for debugging.

We expect the vscode.executeHoverProvider command to result in a Hover (the one provided by our extension), but an empty list is returned instead. We need to figure out why TemplateUrlIntellisenseProvider#provideHover() does not return a Hover.

As we have seen in Lab 3, we can put a breakpoint inside the provideHover() method and run the e2e tests again. By stepping through the code in the debugger, we find out that the target line does not contain the templateUrl property, but styleUrls. No surprise that the extension did not provide any hover.

We now realize that the position passed to the vscode.executeHoverProvider command in e2e/extension.test.ts is incorrect. This is because lines in a TextDocument are 0-indexed, so in order to target the 5th line we should pass 4 (not 5) as the line index.

You can now remove the breakpoint, fix the test code and rerun the tests. Everything should pass this time.

Phew! At least the bug was in the test and not in the extension logic 😅

Experiments:

  • Add a test to verify that no hover is provided on non-templateUrl lines.

  • Add a test to verify that the DefinitionProvider works.

    • Add a test similar to the one for HoverProvider, but use the vscode.executeDefinitionProvider command.
    • Invoking definition providers is more expensive: If necessary, increase the timeout for that specific test.
    • Add assertions to verify that the returned definition targets the correct template HTML file.

Resources:

  • VS Code: Testing extension:
    An overview of how to write and debug e2e tests for VS Code extensions.

  • Mocha:
    Info about the Mocha JavaScript test framework.

  • vscoce-test:
    Usage instructions for the vscode-test helper package.

  • Built-in command reference:
    A list of the build-in VS Code commands. (Useful for programmatically triggering operations in tests.)

Back to top

Lab 7

Objective: Unit testing.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-6...lab-7
Git tag: lab-7

Notes:
In the previous lab, we saw how to write e2e tests. Another equally important type of testing is unit testing. Unit tests only cover a specific unit of functionality (such as a class or function). Unlike e2e tests, they are not run in a dedicated VS Code window, so they do not have access to actual vscode APIs. On the other hand, they are super fast and thus suitable for running continuously during development and providing immediate feedback on the correctness of our code.

7.a: Add unit testing infrastructure. | Git tag: lab-7.a

Before we can start writing unit tests, we need to set up a few things:

  1. Create a src/test/unit/ directory that will hold our unit tests.
  2. Create a src/test/unit/index.ts file that configures Mocha, our test runner, to run all tests inside the src/test/unit/.
  3. Create a src/test/unit/extension.test.ts file with a minimal failing test.
  4. Add a test-unit npm script for running unit tests in package.json.

As usual, you can look at the code on GitHub or locally by running git show lab-7.a.

Once you have made the above changes, you can run the unit tests using the command npm run test-unit. The tests should fail, but even failing tests are preferable to no tests at all 😉

7.b: Introduce mocking vscode APIs. | Git tag: lab-7.b

We would like to add some tests for extension.ts > activate(), but it relies on vscode APIs. Remember that vscode APIs are not available in unit tests. In order for our tests to work, we need to mock vscode. To achieve that, we are going to use mock-require:

npm install --save-dev mock-require @types/mock-require

In a nutshell, mock-require allows you to specify an arbitrary value to be returned when a package is imported. We will modify unit/index.ts to provide a mock value (an empty object for now) when importing vscode.

Find the exact code on GitHub or locally by running git show lab-7.b.

7.c: Add a test for extension.ts. | Git tag: lab-7.c

We would like to add a test for the activate() method in extension.ts. Note that this function uses some vscode APIs, such as:

  • commands.registerCommand()
  • languages.registerDefinitionProvider()
  • languages.registerHoverProvider()

Currently, they will be undefined in unit tests, so we need to augment our vscode mock in (unit/vscode.mock.ts and provide dummy implementations for those.

This step's code can be found on GitHub or locally by running git show lab-7.c.

Finally, run the unit tests (via npm run test-unit) and see the reported results in the terminal. Everything should pass.

💡 Tip
Unit tests are run against the transpiled JavaScript files. Before running the tests, you need to ensure the out/ directory is up-to-date and contains the latest transpiled code. One way to do that is to start the default build task, which runs the TypeScript compiler in the background and recompiles files on save. In VS Code's menu bar, click Terminal > Run Build Task.

✔️ Best practice
Ideally, you want unit tests to be run on every change, so that you get immediate feedback. Manually running the command every time is tedious. Therefore, it is a good idea to use a package such as watch and create an npm script that watches the out/ directory and reruns the unit tests on every change; e.g.: "test-unit-watch": "watch \"npm run test-unit\" out/ --wait 1"

7: Add more tests for extension.ts. | Git tag: lab-7

Let's add some more tests for extension.ts. The main pattern is that we stub the vscode API we need to run assertions against, then we call activate() or any other function we need and finally we verify that the right API was called.

See the code on GitHub or locally by running git show lab-7.

Once again, run the unit tests to confirm that everything still passes.

Experiments:

  • Add some unit tests for registerDefinitionProvider()/registerHoverProvider().
  • Add unit tests for TemplateUrlIntellisenseProvider.

Resources:

  • mock-require:
    A Node.js package that allows mocking imported modules.

Back to top

Lab 8

Objective: Package and share the extension.
Code diff: https://github.com/gkalpak/my-first-vscode-extension/compare/lab-7...lab-8
Git tag: lab-8

Notes:
OK, now that we have this great, feature-packed, well-tested extension how do we use it for real?

The main way of using a VS Code extension is to install it from the VS Code Extension Marketplace. You can search the marketplace in the browser or from inside VS Code (from the Extensions view). From the Extensions view you can manage extensions (e.g. install/uninstall, enable/disable, etc.). See here for more details.

If you want to share your extension with everyone, the most straight-forward way is to publish it to the marketplace. Publishing requires an Azure DevOps account. Going over the process of registering for such an account and publishing the extension is outside the scope of this workshop, but you can find detailed instructions here.

A different way to use your extensions on your local install of VS Code or distribute it to your friends or team mates is to package the extension and share the produced .vsix file. You can find detailed instructions on that here. We will go through the process of packaging and installing the extension from the packaged VSIX file in this lab.

To both package and publish an extension, we use a tool (provided by the VS Code team), called vsce. vsce is a CLI tool that can be used for managing (e.g. packaging, publishing, unpublishing, retrieving metadata for) VS Code extensions. Let's install the tool now with the following command:

npm install --save-dev vsce

There are several commands you can run with vsce. Here we are going to focus on the package commands which can be used to...you guessed it...package an extension. So, we will create an npm script to run that. Open package.json and add the following script in the scripts object:

"vsce-package": "vsce package"

Since the packaged extension includes the transpiled JavaScript files, we want to ensure that the compile task is always run before packaging. Fortunately, the special vscode:prepublish task (which is already is our scripts) is automatically run as part of vsce package, so we are covered.

In order to avoid unintentionally publishing incomplete extensions to the marketplace, vsce requires having a few things in place, before it can run. If you try to run npm run vsce-package without taking care of the prerequisites, it will complain and stop. So, to make vsce happy, we have to make the following changes:

  • Add a publisher property in package.json. Since we are not publishing the extension for now (just packaging), any string will do (e.g. you GitHub username).
  • Add a repository property in package.json. For real extensions, you should use the actual URL of the repository that holds the extension's source code.
  • Modify the README.md. (This is vsce trying to make sure that you have replaced the auto-generated boilerplate content with actual, useful info about the extension.)

As usual, the code can be found on GitHub or locally by running git show lab-8.

With all necessary changes in place, we can now package the extension with npm run vsce-package. This will result in a VSIX file in the root directory. You can use this file to install the extension on your local install of VS Code. To install an extension from a VSIX file, simply run:

code --install-extension path/to/your-extension-X.Y.Z.vsix

You might need to close VS Code and reopen for the extension to be activated. Go ahead and try it out: Open an Angular project (or open our simple-component from fixtures) and hover over a component's template URL or right-click and choose Peek Definition or Go to Definition.

All you have to do to privately (i.e. not through the marketplace) share your extension with someone, is send them this packaged VSIX file. Piece of cake! 🍰

Experiments:

  • Package your extension and share it with a friend or team mate.

  • Improve your extension's bundle size and start-up time.

    • Read about using a bundler (such as Webpack) to bundle your extension here.
    • Use Webpack (or another bundler) to bundle the extension and package it.
  • Make a high-quality extension and publish it to the VS Code Extension Marketplace.

Resources:

Back to top