DEV Community

Cover image for VS Code Extensions: Basic Concepts & Architecture
Jessvin Thomas
Jessvin Thomas

Posted on

VS Code Extensions: Basic Concepts & Architecture

Introduction

I wrote this article after writing my first VS Code Extension: VS Code Assist AI, a utility extension to make it easier to work with chat interface AIs like ChatGPT or Claude. I primarily did this to learn how extensions worked. In retrospect, while there were a lot of guides and step by step tutorials, I wished I had found a general architectural overview first. This article intends to provide that overview. I cover:

  1. What you can extend: The customization possibilities that exist to add to VS Code
  2. Core Building blocks: the main methods for application logic and UI
  3. Wiring together: Connecting the building blocks together
  4. WebView UI: Some key points to implementing UI
  5. Hello World Example: Simple stripped down extension example

What you can extend

First it is key to understand what can be added. These are called contribution points. In particular I was early on confused by the names VS Code used for parts of its UI. What's the sidebar vs a panel? Below is a list of some example contributions and extension points. This list is not exhaustive but provides a good starting point for understanding how you can customize VS Code:

Contribution Points:

UI Related:

  • Activity Bar: Custom icons on the left side that execute commands
  • Primary Sidebar: UI elements that place next to activity bar (usually opened by activity bar)
  • Secondary Sidebar: UI elements that are places on the right side.
  • Panel: The area under the editor windows where terminal and similar items go
  • Status Bar: Custom status messages and icons at the bottom of the app
  • Custom Editor Views: Add specialized editors for custom file types (e.g., JSON, YAML, or proprietary formats)

VS Code UI Elements

Commands and Menus:

  • Menus: Add new menu items in various contexts, such as Right-click menus for files or folders in the explorer or context menus in the editor for code blocks or selections. Example: Adding “Refactor Function” or “Run Tests” to a file’s right-click menu.
  • Commands: Extend the Command Palette to allow users to execute your custom commands. Example: Adding “Generate Documentation” or “Open Debug Dashboard” commands.

!https://miro.medium.com/v2/resize:fit:1260/1*QZpo3j9v7a2In9yf74aIvA.png

Example Command Pallete and Menu

Editor & Language Related

  • Language Features: Add functionality like syntax highlighting, IntelliSense (code completions), or linting for a specific language. Examples: Providing autocomplete for a custom domain-specific language (DSL)., Implementing a language server for real-time syntax analysis and hover tooltips.
  • Code Actions: Offer fixes or refactoring options directly in the editor (e.g., quick fixes for errors or warnings). Examples: “Convert to arrow function” for JavaScript. “Add missing imports” for Python.
  • Inline Decorations: Enhance the editor with visual elements like highlights, icons, or annotations. Example: Highlighting TODO comments or displaying real-time test results inline.
  • Debugging Support: Add custom debug configurations, launch scripts, or integrate with external debugging tools. Example: A debug adapter for your programming language or framework.

Example Editor Integration

Workspace and File Management:

  • File Watchers: Monitor file or folder changes in the workspace and trigger logic in response. Example: Automatically lint files or rebuild assets when they are saved.
  • Workspace Configuration: Provide workspace-specific settings and manage their persistence. Example: Enabling or disabling a feature based on user-defined settings in .vscode/settings.json.

Core Building Blocks

Extending these points consists of using a few key items:

Manifest Registration — tell VS Code what you want to extend

Activation Events — tell VS Code when your extensions should kick in

VS Code API — core hooks to work with VS Code

Manifest registration

Many of these contributions need to be listed in package.json for VSCode to allow them. Examples include commands, menus and UI elements:

"contributes": {
    "configuration": {
      "title": "Extension Title",
      "properties": {
        "StringtoSave": {
          "type": "string",
          "default": "important",
          "description": "Something to save",
          "scope": "resource"
      }
    },
    "views": {
      "sidebar": [
        {
          "id": "Extension Sidebar",
          "type": "webview",
          "name": "Main"
        }
      ]
    },
    "commands": [
      {
        "command": "Set a Value",
        "title": "My Extension: Set Value"
      },
    ],
    "menus": {
      "editor/context": [
        {
          "command": "Special Command in Menu",
          "when": "editorTextFocus && editorHasSelection",
          "group": "9_cutcopypaste"
        }
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

Activation Events

When to kick of these commands. Examples include:

  • onCommand: Activates the extension when a specific command is executed (e.g., extension.helloWorld).
  • onLanguage: Activates the extension when a file of a specific language is opened (e.g., javascript).
  • onFileSystem: Activates the extension when a specific file or folder is opened (e.g., myCustomFileSystem).

VS Code API

If registered these items provide useful application logic generally by using the VS Code API. This API has hundreds of methods that can be used by extensions. Here are some examples:

  • vscode.workspace.workspaceFolders(): Get the currently open workspace folders.
  • vscode.workspace.onDidSaveTextDocument(): React to file save events.
  • vscode.TextEditor.revealRange(): Scroll to and reveal a specific range in the editor.
  • vscode.commands.registerCommand(): Create custom commands.
  • vscode.window.setStatusBarMessage(): Add a status bar message.
  • vscode.workspace.onDidChangeTextDocument: Detect changes in a document.
  • vscode.languages.registerCompletionItemProvider(): Add autocomplete suggestions.
  • vscode.languages.registerDocumentFormattingEditProvider(): Add formatting rules.
  • vscode.Terminal.sendText(): Send commands or text to the terminal.
  • vscode.secrets.store(): Save sensitive data securely.

Wiring together

The general concept is that a registration (package.json) identifies → application location (entry point) which describes → activation events and what do with them which calls→ either backend functions (API) or frontend functions (UI).

It may be best to think of it as a webservice with UI as frontend code and App Logic as backend code. Instead of http as the communication medium, VSCode uses an internal message bus. Here is a way to think about it:

Backend/Frontend Architecture

Webview UI

When writing UI, there are some key concepts to consider: Webview, Sandboxing, Messages.

WebView Type:

The WebView type gives you complete flexibility to create any custom interface using HTML & JS. This is comparison to TreeView which is only design for hierarchy. The Webview page is stand alone and has no visibility to VS Code or backend imports with one exception — API bridge. It has a special function that can act as a bridge to the backend via messages. This function is acquireVsCodeApi which returns a set of functions to send messages and store state.

As for the actual UI, the Webview runs within an IFrame so it can be as simple or complex as you want. You can view this by opening Developer Tools via command palette and checking elements. You will see where ever you loaded a WebView there is an IFrame. However, this means you cannot get any information unless requested from backend. You must get it via messages.

Messages

Because the frontend is essentially totally isolated, any data needs to be requested from the backend and returned to the frontend as a message. You cannot just query the backend though. You listen to messages provided by VSCode API. An example is to even load data on start you may send a message with vscode.post to ask for initial starting data. The backend also registers a listener and when it hears the request it sends the response as a message. Frontend is then listening for messages and when it receives a message for example with all the initial data it can process it. The detail on messages is beyond this article but it’s a critical piece to UI Extensions.

Sandboxing

For security reasons, each page is complete sandboxed and runs in a specific VS Code URL that is generate at run time. For this reason you can’t just point to imports. When a webview is loaded the actual root URL it is coming from is randomly generated so any not to allow seeing the file system of the extension host. In your HTML you can can import files in the same direcotry or subdirectory with relative links like ./ but you cannot go up a directory. You cannot access things like node_modules which is not a subdirectory of source. There are workarounds to this but it out of scope for this article. Just be away that the URL is random.

Hello World example

See the example below where we:1

  1. register a command to open a panel in package.json
  2. activate the extension and register a command in extension.js
  3. Send a message to frontend in extension.js
  4. Setup message receiving and logic in frontend.js
  5. Display the content and send back a message when complete in frontend.js

Package.json (manifest):

{
  "name": "hello-world-webview",
  "displayName": "Hello World Webview",
  "description": "A simple VS Code extension to show Hello World in a Webview",
  "version": "1.0.0",
  "engines": {
    "vscode": "^1.80.0"
  },
  "activationEvents": [
    "onCommand:helloWorld.showWebview"
  ],
  "main": "extension.js",
  "contributes": {
    "commands": [
      {
        "command": "helloWorld.showWebview",
        "title": "Show Hello World Webview"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

extension.js (backend)

const vscode = require('vscode');
const { getWebviewContent } = require('./frontend')
/**
 * Activates the extension.
 *
 * This function registers a command, creates a Webview panel,
 * and sends/receives messages between the backend and frontend.
 *
 *@param {vscode.ExtensionContext} context - The extension context.
 */
function activate(context) {
  context.subscriptions.push(
    vscode.commands.registerCommand('helloWorld.showWebview', () => {
      // Create the Webview panel
      const panel = vscode.window.createWebviewPanel(
        'helloWorldWebview',
        'Hello World',
        vscode.ViewColumn.One,
        { enableScripts: true } // Allow scripts in Webview
      );// Set the Webview's HTML content using the imported function
      panel.webview.html = getWebviewContent(panel.webview, context.extensionUri);
      // Send a message to the Webview
      panel.webview.postMessage({ text: 'Hello World' });
      // Handle messages from the Webview
      panel.webview.onDidReceiveMessage(
        (message) => {
          console.log('Message from Webview:', message.text);
        },
        undefined,
        context.subscriptions
      );
    })
  );
}
function deactivate() {}
module.exports = {
  activate,
  deactivate
};
Enter fullscreen mode Exit fullscreen mode

frontend.js (frontend):

/**
 * Generates the HTML content for the Webview.
 *
 * @param {vscode.Webview} webview - The Webview instance.
 * @param {vscode.Uri} extensionUri - The URI of the extension.
 * @returns {string} The HTML content for the Webview.
 */
function getWebviewContent(webview, extensionUri) {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Hello World Webview</title>
    </head>
    <body>
      <h1 id="message">Waiting for message...</h1>
      <script>
        // Access the VS Code API
        const vscode = acquireVsCodeApi();
        // Handle messages from the backend
        window.addEventListener('message', (event) => {
          const message = event.data.text;
          document.getElementById('message').innerText = message;
          // Send a response back to the backend
          vscode.postMessage({ text: \\`Message received: \\${message}\\` });
        });
      </script>
    </body>
    </html>
  `;
}
module.exports = { getWebviewContent };
Enter fullscreen mode Exit fullscreen mode

Closing

I found the process of creating an extension rewarding in the end because I could publish it and see work out there. There were definitely some headaches especially around bundling and importing frontend libraries that I’ll save for another time.

I hope you have found this article helpful on understanding the general architecture of VS Code Extensions.

Top comments (0)