Skip to content
Back home

Tutorial — Intermediate

Build & publish a VS Code extension

Build snipref, an extension that copies selected code as a Markdown snippet or a GitHub permalink, then test it and publish it to the Marketplace and Open VSX.
Reading time
40 min
Stack
VS Code API · JavaScript · vsce · Open VSX
Published
3 November 2026

I copy code out of my editor dozens of times a day: into pull request comments, Slack threads, AI chats and documentation. Every time, I end up hand-typing the same wrapper: a code fence, the language, the file path and the line numbers. Or I open GitHub to find the permalink for the exact lines I'm looking at.

So we'll build an extension that does it in one keystroke. snipref adds two commands:

  • Copy as Markdown Snippet: the selection, dedented, in a fenced code block with the right language, plus the file path and line numbers
  • Copy GitHub Permalink: a link to the exact lines on GitHub, pinned to the current commit so it never goes stale

The result looks like this when you paste it:

`src/cart/total.js` lines 12–14

```javascript
export function total(items) {
  return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
```

Small, genuinely useful, and it touches most of the VS Code API you'll need for bigger extensions: commands, menus, keybindings, settings, the clipboard and the built-in Git extension.

How an extension is put together

A VS Code extension has two halves, and understanding the split makes everything else easier.

1. The manifest: package.json

The manifest declares what your extension adds to VS Code, under a contributes key: commands, menu items, keybindings, settings, views and more. VS Code reads it at start-up without running any of your code. That's how your commands appear in the Command Palette before your extension has even loaded.

2. The code: your main file

Your code exports an activate function. VS Code calls it the first time your extension is needed, for example when someone runs one of your commands. In activate, you connect the declared commands to real functions.

This lazy loading is why VS Code starts quickly with dozens of extensions installed: most of them never run until you use them.

Activation events used to be manual. Older tutorials tell you to list every command under activationEvents in package.json. Since VS Code 1.74, commands you declare in contributes.commands activate your extension automatically, so you can leave that list empty.

Scaffold the project

Microsoft's generator creates a working extension in a few seconds:

npx --package yo --package generator-code -- yo code

Choose:

  • New Extension (JavaScript). TypeScript works too, but JavaScript means no build step, which keeps this tutorial focused on the VS Code API.
  • Name: snipref
  • Say no to checking JavaScript types and yes to initialising a Git repository.

Open the folder in VS Code.

Run it: the Extension Development Host

Press F5. A second VS Code window opens, titled [Extension Development Host]. It's a normal VS Code window with your extension loaded from source.

Open the Command Palette in that window (Cmd+Shift+P on macOS, Ctrl+Shift+P elsewhere) and run Hello World. You'll see a notification. That's your extension running.

The development loop from here:

  1. Edit your code in the main window.
  2. In the development window, run Developer: Reload Window (or press Cmd+R / Ctrl+R).
  3. Try your change.

Breakpoints set in the main window work in your extension code, which makes debugging much easier than console.log.

The first real command

Replace the generated hello-world command with the simplest version of snipref: copy the selection as a fenced code block.

In package.json, change the command under contributes:

"contributes": {
  "commands": [
    {
      "command": "snipref.copyMarkdown",
      "title": "Copy as Markdown Snippet",
      "category": "snipref"
    }
  ]
}

And in extension.js:

const vscode = require('vscode');

function activate(context) {
  context.subscriptions.push(
    vscode.commands.registerTextEditorCommand('snipref.copyMarkdown', async (editor) => {
      const code = editor.document.getText(editor.selection);
      const markdown = '```' + editor.document.languageId + '\n' + code + '\n```\n';
      await vscode.env.clipboard.writeText(markdown);
      vscode.window.setStatusBarMessage('Copied as Markdown', 2000);
    })
  );
}

module.exports = { activate };

Reload the development window, select some code, and run snipref: Copy as Markdown Snippet. Paste it somewhere: you've built a working extension.

Two API choices worth noticing already:

  • registerTextEditorCommand instead of registerCommand. It only runs when a text editor is active, and passes that editor straight in, so there's no "is anything open?" check to write.
  • context.subscriptions.push(...). Everything you register returns a disposable. Adding it to context.subscriptions means VS Code cleans it up when your extension is deactivated or reloaded.

What's next

This first version has problems you'll notice quickly: indented code keeps its indentation, there's no file path, empty selections copy nothing, and code containing backticks breaks the fence. In the full tutorial we'll fix all of that and finish the extension:

  • dedenting, file paths, line numbers and safe fences
  • a GitHub permalink command using VS Code's built-in Git extension
  • a right-click menu, a keyboard shortcut and user settings
  • unit tests that run without launching VS Code
  • packaging and publishing to both the VS Code Marketplace and Open VSX, so it also works in Cursor, Windsurf and VSCodium

Checking access…

More tutorials