I consider writing a custom Visual Studio Code extension to be an exciting project and learning experience which allows me to enhance my experience of writing this website and tailor the editor to my specific needs. While VSCode offers a rich ecosystem of extensions covering a wide range of functionalities, there are times when you might find yourself wanting a unique feature or just wanting to learn something new. On this page, I describe my experience of creating my own VSCode extension.

Most of the functionality will reflect my working structure, but there might be interesting bits and pieces for other developers as well. Feedback, suggestions, or questions are always welcome via the Discord.

The source code for the extension can be found here.

Features

  • Working with to-do items:
    • Creating a new item
    • Finding all items in articles
  • Validate repository
  • Validate links in Markdown files

Working with to-do items

I only use a centralized to-do list for high-level requirements and larger-scale projects. As a result, most of the working items are not put on a centralized list. This means that each to-do item is linked to the specific page or even section that it belongs to. This simplifies focusing on related requirements without having to maintain and filter a long list.

Although there are many upsides to this approach, it does make it easy to forget about to-do items because there is no overview. This feature attempts to solve some of those problems by generating a list of all to-do items from different pages into a single list.

To-do items are added to Markdown files in the form of HTML comments, as they are not rendered in the compiled version of the page. They can have the following two forms, with and without dates:

<!--TODO:
A to-do item
-->
 
<!--TODO (2023-01-31):
Another to-do item
-->

For now, the version without dates still exists, but they are gradually being phased out.

Creating a new item

The best way of introducing text in a file (besides just typing it), is by using the concept of Snippets. Using this functionality, itโ€™s possible to automatically mark locations with $<number> where the user will probably want to fill in the information.

Using this, the snippet can be defined as <!--TODO: $1 --> and be inserted using TextEditor.insertSnippet:

const todoSnippet = new vscode.SnippetString("<!--TODO: $1 -->")
const window = vscode.window.activeTextEditor!!
window.insertSnippet(todoSnippet, window.selection.active)

Adding dates provides some visibility on how old to-do items are and that can help to avoid extremely long outstanding items.

This can be done by updating the snippet to <!--TODO (${date}): $1-->. This uses template literals for cleaner construction of strings. Now all that is left is to generate the date string, which turns out to be much more difficult in JavaScript than you would expect.

Getting the date string looks like this:

export function dateToString(date: Date): String {
    var mm = (date.getMonth() + 1).toString(); // getMonth() is zero-based
    var dd = date.getDate().toString();
  
    return [
        date.getFullYear(),
        '-',
        mm.length===2 ? '' : '0',
        mm,
        '-',
        dd.length===2 ? '' : '0',
        dd
    ].join(''); // padding
}

Which leads to the final result:

export function createTodo() {
    const date = lib.dateToString(new Date())
    const todoSnippet = new vscode.SnippetString(`<!--TODO (${date}): $1-->`)
    const window = vscode.window.activeTextEditor!!
    window.insertSnippet(todoSnippet, window.selection.active)
}

Finding all items in articles

The items are gathered from the workspace using the following process:

  • Find all Markdown files in the workspace (vscode.workspace.findFiles)
  • For each file:
  • Concatenate the resulting items from each file (Array.flat)

The information is sorted by date and is displayed in reverse chronological order (you see the oldest items first). In case an item does not have a date, it is added to the end of the list (it is considered older than the oldest item with a date). This information is then displayed using an Output Channel.

Validate repository

Throughout time many changes have been made to the repository structure. To help me finalize those migrations, Iโ€™ve made a command that checks which files I still need to migrate or update.

An example is migrating all articles from the active folder to the topics folder, the latter one being deployed on Google App Engine.

const baseUri = vscode.workspace.workspaceFolders!![0].uri
const activeFolderUri = vscode.Uri.joinPath(baseUri, 'active')
 
vscode.workspace.fs.readDirectory(activeFolderUri).then(
    () => {
        console.warn("There are still articles in the 'active' folder. Please migrate them to GAE.")
    },
    () => {
        console.warn("There are no more articles in the 'active' folder. Please remove this rule.")
    }
)

A generalized implementation of this is:

const baseUri = vscode.workspace.workspaceFolders!![0].uri;
const folders = ['active', 'archive', 'pages'];
folders.forEach((folder) => {
    const folderUri = vscode.Uri.joinPath(baseUri, folder);
    vscode.workspace.fs.readDirectory(folderUri).then(
        () => {
            console.warn(`There are still articles in the '${folder}' folder. Please migrate them to GAE.`);
        },
        () => {
            console.warn(`There are no more articles in the '${folder}' folder. Please remove this rule.`);
        }
    );
});

I want links between pages to be bi-directional. This means that when linking to a page, I expect to have an explicit link back within the text or a link within a โ€˜related pagesโ€™ section. This ensures better connectivity on the website.

Testing the extension

To open a different folder when testing the extension (using F5), pass it as a first argument in the args of the run configuration:

{
    "name": "Run Extension",
    "type": "extensionHost",
    "request": "launch",
    "args": [
        "${workspaceFolder}/../../..",
        "--extensionDevelopmentPath=${workspaceFolder}"
    ],
    "outFiles": [
        "${workspaceFolder}/out/**/*.js"
    ],
    "preLaunchTask": "${defaultBuildTask}",
},

Here the startup folder is defined as ${workspaceFolder}/../../...

Installing the extension locally

To package the extension, run

vsce package

Packaging is also where the .vscodeignore file comes in. It defines the files to be excluded from the package.

Make sure vsce is installed which can be done through

npm install -g vsce

Afterwards, the extension can be installed through Extensions > โ€ฆ > Install from VSIX.

Discontinued features