Creating a VS Code Extension Part 2: Navigating your Extension

What are all these files and folders and what do I do with them?

Haris Khan

15 minute read

Introduction

In this post, we’re going to cover the file structure of the extension, diving into what individual files are for and discuss the fundamental configuration of them. We’re also going to give an overview of the Extension Manifest (package.json), which is what VS Code will use to determine how and when users will interact with your plugin. Finally, we’ll introduce Contribution Points and Activation Events, two important pieces of the Command Pipeline (my terminology for the flow of functionality in VS Code extensions). A more in-depth look at the pipeline will be coming in the next blog post.

The Anatomy of a VS Code extension

.
├── .gitignore
├── .vscode                     // VS Code integration
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── .vscodeignore
├── README.md
├── src                         // sources
│   └── extension.ts            // extension.js, if you picked JavaScript (WHY?!)
├── test                        // tests folder
│   ├── extension.test.ts       // extension.test.js, if you picked JavaScript (WHY?!)
│   └── index.ts                // index.js, if you picked JavaScript (WHY?!)
├── node_modules
│   ├── vscode                  // language services
│   └── typescript              // compiler for typescript (TypeScript only)
├── out                         // compilation output (TypeScript only)
│   ├── src
│   |   ├── extension.js
│   |   └── extension.js.map
│   └── test
│       ├── extension.test.js
│       ├── extension.test.js.map
│       ├── index.js
│       └── index.js.map
├── package.json                // extension's manifest
├── tsconfig.json               // jsconfig.json, if you picked JavaScript (WHY?!)
└── vsc-extension-quickstart.md // extension development quick start

If you’re like me, the first time you saw this, it was a bit intimidating. Don’t worry, though! We’ll break this down piece-by-piece. Let take it from the top!

.gitignore

The .gitignore file is a git configuration file that tells git not to add certain files/folders to the repository. By default, the out and node_modules folders are gitignored. The first is where our transpiled JavaScript lives, and the second is where our Node dependencies live. Neither need to be pushed to the repo, since anyone who forks it can just recreate them by compiling the extension and running npm install respectively.

.vscode folder

This folder contains all the integration information for the extension, including debug and testing settings. This isn’t unique to just building extensions; the configuration files in this folder can be set up to help you debug and test any JavaScript/TypeScript projects. You can also configure them for projects in other languages, providing that those languages have debugger extensions published for them.

launch.json

This file contains the semantic version information for the file format (which you can update as you please, it has no effect on anything), as well as launch configuration settings. Here’s the default file:

{
    "version": "0.1.0",
    "configurations": [
        {
            "name": "Launch Extension",
            "type": "extensionHost",
            "request": "launch",
            "runtimeExecutable": "${execPath}",
            "args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
            "stopOnEntry": false,
            "sourceMaps": true,
            "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ],
            "preLaunchTask": "npm"
        },
        {
            "name": "Launch Tests",
            "type": "extensionHost",
            "request": "launch",
            "runtimeExecutable": "${execPath}",
            "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
            "stopOnEntry": false,
            "sourceMaps": true,
            "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ],
            "preLaunchTask": "npm"
        }
    ]
}

The star of the show here is the configurations array. This contains a series of objects that correspond to launch configurations that you can select from VS Code’s debug menu.

LaunchJson

The first configuration is the default one, which ran when you hit F5 in the previous blog post. The second is for running tests, and closes the extension host window automatically when it’s finished.

  • name: The name of the launch configuration. Will be displayed in the debug dropdown.
  • type: Setting this to extensionHost tells VS Code that we’re running an extension and to open up a new window to host it while we debug.
  • request: The two options here are launch or attach. We’re using the first one because we want to launch our program, but the attach request can be used to attach to a Node.js server by specifying a port.
  • runtimeExecutable: Absolute path to the runtime executable to be used. Default is usually node but for extension development is always ${execPath}, which tells VS Code to open itself.
  • args: An array of command line arguments to be passed to the VS Code upon launch. The extensionDevelopmentPath option is always ${workspaceRoot} by default, and should remain so (I don’t recommend developing multiple extensions in the same workspace/git repo!). For the testing config, the extensionTestsPath should point to the JavaScript test files, by default in ${workspaceRoot}/out/test. Remember, you can’t use the TypeScript ones, as they haven’t been transpiled yet!
  • stopOnEntry: Setting this to true will automatically stop the extension host after launch. If you want to create a launch configuration that tests if your extension crashes VS Code or if your testing how it affects start-up time (perhaps you’re using the "*" activation event, which I hope you’re not; you’ll see why when we introduce it below and cover it more deeply in the next post.)
  • sourceMaps: Setting this to true will allow you to set breakpoints in your TypeScript files instead of needing to set them in the transpiled JavaScript files (in the /out directory). VS Code will use the the .js.map files that are output along with the transpiled .js files to stop execution at the correct point. Very useful for debugging, and I recommend you leave this set to true.
  • outFiles: Whenever you set a breakpoint in the original source (.ts) files, this is where VS Code will look for corresponding .js files to replicate the breakpoint in IF you set sourceMaps to true.
  • preLaunchTask: This is a useful tool for running tasks defined in the tasks.json file before a launch. In our case, leave it set to npm.

settings.json

Small file which contains user setting overrides in the development environment. By default this excludes the out folder from workspace-wide search results and includes the option to hide it on your workspace explorer entirely. Add any additional settings you wish to change for your extension workspace here.

tasks.json

This is the setup location for the preLaunchTask we saw earlier. It runs npm compile --loglevel silent in your local flavor of shell to compile the extension from TypeScript to JavaScript before we run it. The file comments explain each line pretty well, so take a look if you’re interested.

.vscodeignore

This is kind of like a .gitignore file; it tells VS Code what not to include when the extension is installed. By default, everything except for the transpiled JavaScript code, Node dependencies, and the Extension Manifest (what VS Code calls package.json) are excluded from the install.

I’m not sure why Microsoft felt the need to use this convention, as it replicates functionality already present (and in my opinion, better implemented) in package.json with the files property. files basically acts as a whitelist, telling npm to only install the files that are explicitly listed. I find this is much less error prone than blacklisting, but to each his own. VS Code supports both files property in package.json and the .vscodeignore file, so use whichever you like.

README.md

README for your git repository. Put info about your extension here. Yo Code provides a handy little template you can fill in.

The src folder

This is where the functional TypeScript code for your extension will live. You can make your code as modular as you like, but the star of the show here is extension.ts.

This file will be the covered in depth throughout the rest of this blog series. For now, just think of this file as the beating heart of your extension. This is where user commands that you register in the Extension Manifest are connected to functionality.

The test folder

This folder contains two files, one which you should touch, and the other which you should NOT.

index.ts

This file sets up a Mocha test runner. You can use another test client if you want to, but you’ll have to modify the dependencies in the Extension Manifest and this file. I recommend NOT touching this file and just using Mocha, though. It works well and learning it isn’t that hard, especially if you’re already used to something else.

extension.test.ts

This is the basic test file for your extension. It comes with two completely useless tests built right in! Hooray!

Seriously though, Microsoft, you could have given us a basic test to see if, say, all extension commands in the Extension Manifest are correctly registered to functions in extension.ts.

Anyway, I suggest you familiarize yourself with Mocha (and Test-Driven Development in general if you haven’t used any testing suite before). I’ll cover testing extensively in a future blog post, but it’ll be a lot easier to follow if you understand the basic concepts already (and maybe even have taken a crack at it yourself!)

The node_modules folder

This is where all your npm modules get installed. The VS Code API lives here (in the vscode folder, as you might imagine), and you’ll find yourself digging around there pretty regularly.

The out folder

As we covered earlier in the .vscode section, the out folder is where all your transpiled JavaScript lives: tests, source maps, and all. The out/src/ folder contains the code that will actually run when someone installs your extension. The rest of it is blacklisted by default in the .vscodeignore file or should be whitelisted in the optional files property of your Extension Manifest.

The tsconfig.json file

We’ll skip package.json and come back to it in a bit, as there’s a lot to cover there. tsconfig.json contains all the compiler options for your TypeScript compiler. There are a lot of options here, but since Yo Code sets this up for us with all we need to compile a working extension, we can just leave this alone. If you’d like more information on the extensive compiler configuration possible with TypeScript (which is beyond the scope of this series) you can find it here.

The Quickstart Guide

An included Yo Code generated guide. It basically says “Hi! Hit F5. See, it works! Godspeed.” You can just delete it.

An aside: the media/resources folder

You may be wondering, “Wait, what media folder? I don’t see that on the list. That’s because Yo Code doesn’t build it out for you. Make a media folder with subfolders for light and dark themes if you intend to use custom images or icons for your extension. Make the dark icons brightly-colored and the light icons darkly-colored so that they are visible by contrast. You modify the icons when you extend any API interface with an optional IconPath property (we’ll talk more about that when we start digging into the API itself).

The Extension Manifest formerly known as package.json

This file, which we’ll refer to as the Extension Manifest instead of package.json because of it’s non-standard functionality, is a key part of what I call the VS Code Command Pipeline. A full overview of package.json is beyond the scope of this course; we’re just going to cover the VS Code related properties that transform this unassuming file into the super celebrity we will soon come to hate love.

{
    "name": "vscode-dynamodb",
    "displayName": "DynamoDB",
    "description": "Create, browse, and update local and cloud-based DynamoDB instances.",
    "version": "0.0.1",
    "publisher": "hariscodes",
    "engines": {
        "vscode": "^1.13.0"
    },
    "categories": [
        "Other"
    ],
    "homepage": "https://github.com/hariscodes/vscode-dynamodb/blob/master/README.md",
    "bugs": {
        "url": "https://github.com/hariscodes/vscode-dynamodb/issues"
    },
    "license": "SEE LICENSE IN LICENSE.md",
    "repository": {
        "type": "git",
        "url": "https://github.com/hariscodes/vscode-dynamodb"
    },
    "activationEvents": [
        "onView:dynamoExplorer",
        "onCommand:dynamo.changeServer",
        "onCommand:dynamo.createTable",
        "onCommand:dynamo.deleteTable"
    ],
    "main": "./out/src/extension",
    "contributes": {
        "views": {
            "explorer": [
                {
                    "id": "dynamoExplorer",
                    "name": "dynamo",
                    "when": "config.dynamo.showExplorer == true"
                }
            ]
        },
        "commands": [
            {
                "category": "Dynamo",
                "command": "dynamo.changeServer",
                "title": "Connect"
            },
            {
                "category": "Dynamo",
                "command": "dynamo.refreshExplorer",
                "title": "Refresh",
                "icon": {
                    "light": "resources/icons/light/refresh.svg",
                    "dark": "resources/icons/dark/refresh.svg"
                }
            },
            {
				"category": "Dynamo",
				"command": "dynamo.createTable",
				"title": "Create Table",
				"icon": {
					"light": "resources/icons/light/add.svg",
					"dark": "resources/icons/dark/add.svg"
				}
            },
            {
				"category": "Dynamo",
				"command": "dynamo.deleteTable",
				"title": "Delete Table"
            },
            {
				"category": "Dynamo",
				"command": "dynamo.updateTable",
				"title": "Update Table"
            }
        ],
        "configuration": {
            "type": "object",
            "title": "dynamo configuration",
            "properties": {
                "dynamo.showExplorer": {
                    "type": "boolean",
                    "default": false,
                    "description": "Show or Hide the dynamo explorer"
                },
                "dynamo.sslEnabled": {
                    "type": "boolean",
                    "default": false,
                    "description": "Enable or Disable SSL on requests."
                },
                "dynamo.region": {
                    "type": "string",
                    "default": "us-east-2",
                    "description": "If running on EC2, specify the region."
                }
            }
        },
        "menus": {
            "view/title": [
                {
                    "command": "dynamo.refreshExplorer",
                    "when": "view == dynamoExplorer",
                    "group": "navigation"
                },
                {
                    "command": "dynamo.changeServer",
                    "when": "view == dynamoExplorer",
                    "group": "navigation"
                },
                {
                    "command": "dynamo.createTable",
                    "when": "view == dynamoExplorer",
                    "group": "navigation"
                }
            ],
            "view/item/context": [
                {
                    "command": "dynamo.deleteTable",
                    "when": "view == dynamoExplorer && viewItem == dynamoTable"
                },
                {
                    "command": "dynamo.updateTable",
                    "when": "view == dynamoExplorer && viewItem == dynamoTable"
                }
            ]
        }
    },
    "scripts": {
        "vscode:prepublish": "tsc -p ./",
        "compile": "tsc -watch -p ./",
        "postinstall": "node ./node_modules/vscode/bin/install",
        "test": "node ./node_modules/vscode/bin/test"
    },
    "devDependencies": {
        "typescript": "^2.4.2",
        "vscode": "^1.1.4",
        "mocha": "^3.5.0",
        "@types/node": "^8.0.20",
        "@types/mocha": "^2.2.32"
    },
    "dependencies": {
        "aws-sdk": "^2.97.0"
    }
}

The Extension Manifest that ships with the Hello World boilerplate extension is missing some stuff I wanna talk about, so I grabbed the one from my extension instead. Some of this is standard package.json but much of it is not. You can find the full documentation here, but we’ll cover the two key fields here, along with some important secondary ones.

Contribution Points

A Contribution Point is something that your extension contributes to the functional ecosystem of VS Code. Under contributes, you can list all the UI and UX changes that the user will be experience when using your extension while specifying the circumstances under which those changes will be visible/accessible.

This is the first stage of the Command Pipeline; you’re defining where and how user input is accepted.

There are many categories of contributions where you can potentially interact with your user:

  • configuration: contribute to the user/workspace configuration with user-adjustable values.
  • commands: contribute Commands to the Command Palette
  • menus: contribute Commands to one of the many different types of general and context-specific menus
  • keybindings: contribute custom keybindings for use with your extension
  • languages (skipping)
  • debuggers (skipping)
  • breakpoints (skipping)
  • grammars (skipping)
  • themes (skipping)
  • snippets: contribute code snippets for auto-completion
  • jsonValidation: contribute a schema to validate JSON by file extension
  • views: contribute an explorer or debug view
  • problemMatchers (skipping)
  • problemPatterns (skipping)

Each of these has it’s own sub-objects with different properties we’ll cover in detail in the next post.

Remember, we’re skipping Language Servers, Debugging Extensions, and Themes…and all their related API concepts. The first two require a more in-depth look than I can provide in this series, and I don’t have the design chops to discuss the last one.

Activation Events

The big advantage that VS Code has over Atom in terms of performance comes primarily from the way it handles extensions: it lazy loads them. This means that the markdown spell checker that I’m using to write this blog with didn’t activate when I opened VS Code, but instead when I opened a markdown file. It also means the Go Language Server Extension that I have installed will only run when I open a .go file.

VS Code manages it’s lazy loading with Activation Events. These are triggers that tell VS Code to load your extension from its extension.ts file and run the activate() function within it. These are the Activation Events you can use:

  • onLanguage: activate when a file using a certain language is opened in the editor
  • onCommand: activate when a certain Command is run
  • onDebug: activate when a debug session of a particular type (e.g. node) is started
  • workspaceContains: activate when a folder is opened that has a file of the type specified
  • onView: activate when a view of the specified id (declared as part of the Contribution Point) is expanded.
  • *: activate when VS Code opens, bypassing lazy loading. Avoid using this at all costs to ensure a sound user experience!

Important Secondary Fields

These are other fields that aren’t as critical as the two we just discussed, but are still important enough to warrant at least a mention.

categories, icon, and galleryBanner

categories is An array of strings where you pick the categories that you want to list the extension under in the marketplace. The following categories are available:

  • Languages
  • Snippets
  • Linters
  • Themes
  • Debuggers
  • Formatters
  • Keymaps
  • Extension Packs
  • Other

If your extension doesn’t fall into any of these categories, just pick “other”.

Use icon to specify the path to a 128x128 pixel image that users will see next to your extension in the extension marketplace. galleryBanner sets the color of the background banner on the Marketplace website to complement the color of your icon. You can set these later, but I’d spend some time with these before release so you can get maximum impact with your extension.

engines

This is where you declare the VS Code version number (and thus, API version) that you’re targeting. The default is configured to set a minimum version number using the npm convention "^[SemVer]". In the example above, the extension is targeting any VS Code version greater than or equal to 1.0.0, which should be your default, unless there’s specific API functionality that you’re going to need that isn’t available in lower versions. For example, the vscode-dynamodb extension utilizes the Custom Explorer API, which wasn’t ready for primetime until 1.13.0.

main

Set this property to the path of the compiled extension file. In most cases this is either "./out/extension" or "./out/src/extension" but make sure it’s set properly before trying to run your extension. Note: you don’t need to include the .js file extension.

bugs, repository and homepage.

These set links in the Marketplace. By convention these are the issues page on your repository (GitHub, GitLab, etc), your repository main page, and a direct link to your README.md respectively.

scripts

This functions just like the vanilla package.json, but with the added ability to specify a command to run before your extension is published via vsce using the vscode:prepublish property. By default this is just "tsc -p ./" (i.e. compile all the .ts files in ./ and all subfolders using the compiler settings found in the .tsconfig file in the root directory. The -p is shorthand for --project.) There are also postinstall and test scripts are located in the bin folder in the vscode package directory under node_modules. We’ll look at these when we start talking about the package and the API it contains in more depth.

Wrapping Up

That about covers the bones of the extension. We learned about the important files common to every extension, and particularly about how the .vscode folder will help us to test and debug our function. We also covered the key fields in the Extension Manifest that distinguish it from a run-of-the-mill package.json, including Contribution Points (places where we expose our extension’s functionality to our users) and Activation Events (triggers that cause our Extension to be loaded and activated).

As always, feel free to ask questions in the comments or hit me up on Twitter.

comments powered by Disqus