Wโ
All docs
๐
Sign Up/Sign In
code.visualstudio.com/api/extension-guides
Public Link
Apr 8, 2025, 5:30:49 AM - complete - 400.7 kB
Starting URLs:
https://code.visualstudio.com/api/extension-guides/overview
Crawl Prefixes:
https://code.visualstudio.com/api/extension-guides
Max Pages:
2000
## Page: https://code.visualstudio.com/api/extension-guides/overview * Overview * Get Started * Extension Capabilities * Extension Guides * UX Guidelines * Language Extensions * Testing and Publishing * Advanced Topics * References Topics In this article Once you have learned the basics of Visual Studio Code Extension API in the Hello World sample, it's time to build some real-world extensions. While the Extension Capabilities section offers high-level overviews of what an extension **can** do, this section contains a list of detailed code guides and samples that explains **how** to use a specific VS Code API. In each guide or sample, you can expect to find: * Thoroughly commented source code. * A gif or image showing the usage of the sample extension. * Instructions for running the sample extension. * Listing of VS Code API being used. * Listing of Contribution Points being used. * Real-world extensions resembling the sample. * Explanation of API concepts. ## Guides & Samples Here are the guides on the VS Code website, including their usage of the VS Code API and Contribution Points. Don't forget to refer to the UX Guidelines to learn the user interface best practices for creating extensions. | Guide on VS Code Website | API & Contribution | | --- | --- | | Command | commands contributes.commands | | Color Theme | contributes.themes | | File Icon Theme | contributes.iconThemes | | Product Icon Theme | contributes.productIconThemes | | Tree View | window.createTreeView window.registerTreeDataProvider TreeView TreeDataProvider contributes.views contributes.viewsContainers | | Webview | window.createWebviewPanel window.registerWebviewPanelSerializer | | Custom Editors | window.registerCustomEditorProvider CustomTextEditorProvider contributes.customEditors | | Virtual Documents | workspace.registerTextDocumentContentProvider commands.registerCommand window.showInputBox | | Virtual Workspaces | workspace.fs capabilities.virtualWorkspaces | | Workspace Trust | workspace.isTrusted workspace.onDidGrantWorkspaceTrust capabilities.untrustedWorkspaces | | Task Provider | tasks.registerTaskProvider Task ShellExecution contributes.taskDefinitions | | Source Control | workspace.workspaceFolders SourceControl SourceControlResourceGroup scm.createSourceControl TextDocumentContentProvider contributes.menus | | Debugger Extension | contributes.breakpoints contributes.debuggers debug | | Markdown Extension | markdown.previewStyles markdown.markdownItPlugins markdown.previewScripts | | Test Extension | TestController TestItem | | Custom Data Extension | contributes.html.customData contributes.css.customData | | | | Here is a list of additional samples from the VS Code Extensions samples repo. | Sample on GitHub Repo | API & Contribution | | --- | --- | | Webview Sample | window.createWebviewPanel window.registerWebviewPanelSerializer | | Status Bar Sample | window.createStatusBarItem StatusBarItem | | Tree View Sample | window.createTreeView window.registerTreeDataProvider TreeView TreeDataProvider contributes.views contributes.viewsContainers | | Task Provider Sample | tasks.registerTaskProvider Task ShellExecution contributes.taskDefinitions | | Multi Root Sample | workspace.getWorkspaceFolder workspace.onDidChangeWorkspaceFolders | | Completion Provider Sample | languages.registerCompletionItemProvider CompletionItem SnippetString | | File System Provider Sample | workspace.registerFileSystemProvider | | Editor Decorator Sample | TextEditor.setDecorations DecorationOptions DecorationInstanceRenderOptions ThemableDecorationInstanceRenderOptions window.createTextEditorDecorationType TextEditorDecorationType contributes.colors | | L10N Sample | | | Terminal Sample | window.createTerminal window.onDidChangeActiveTerminal window.onDidCloseTerminal window.onDidOpenTerminal window.Terminal window.terminals | | Vim Sample | commands StatusBarItem window.createStatusBarItem TextEditorCursorStyle window.activeTextEditor Position Range Selection TextEditor TextEditorRevealType TextDocument | | Source Control Sample | workspace.workspaceFolders SourceControl SourceControlResourceGroup scm.createSourceControl TextDocumentContentProvider contributes.menus | | Commenting API Sample | | | Document Editing Sample | commands contributes.commands | | Getting Started Sample | contributes.walkthroughs | | Test extension | TestController TestItem | ## Language Extension Samples These samples are Language Extensions samples: | Sample | Guide on VS Code Website | | --- | --- | | Snippet Sample | /api/language-extensions/snippet-guide | | Language Configuration Sample | /api/language-extensions/language-configuration-guide | | LSP Sample | /api/language-extensions/language-server-extension-guide | | LSP Log Streaming Sample | N/A | | LSP Multi Root Server Sample | https://github.com/microsoft/vscode/wiki/Adopting-Multi-Root-Workspace-APIs#language-client--language-server (GitHub repo wiki) | | LSP Web Extension Sample | /api/language-extensions/language-server-extension-guide | 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/command Commands trigger actions in Visual Studio Code. If you have ever configured a keybinding, then you've worked with commands. Commands are also used by extensions to expose functionality to users, bind to actions in VS Code's UI, and implement internal logic. ## Using Commands VS Code includes a large set of built-in commands that you can use to interact with the editor, control the user interface, or perform background operations. Many extensions also expose their core functionality as commands that users and other extensions can leverage. ### Programmatically executing a command The `vscode.commands.executeCommand` API programmatically executes a command. This lets you use VS Code's built-in functionality, and build on extensions such as VS Code's built-in Git and Markdown extensions. The `editor.action.addCommentLine` command, for example, comments the currently selected lines in the active text editor: import * as vscode from 'vscode'; function commentLine() { vscode.commands.executeCommand('editor.action.addCommentLine'); } Some commands take arguments that control their behavior. Commands may also return a result. The API-like `vscode.executeDefinitionProvider` command, for example, queries a document for definitions at a given position. It takes a document URI and a position as arguments, and returns a promise with a list of definitions: import * as vscode from 'vscode'; async function printDefinitionsForActiveEditor() { const activeEditor = vscode.window.activeTextEditor; if (!activeEditor) { return; } const definitions = await vscode.commands.executeCommand<vscode.Location[]>( 'vscode.executeDefinitionProvider', activeEditor.document.uri, activeEditor.selection.active ); for (const definition of definitions) { console.log(definition); } } To find available commands: * Browse the keyboard shortcuts * Look through VS Code's built-in advanced commands api ### Command URIs Commands URIs are links that execute a given command. They can be used as clickable links in hover text, completion item details, or inside of webviews. A command URI uses the `command` scheme followed by the command name. The command URI for the `editor.action.addCommentLine` command, for example, is `command:editor.action.addCommentLine`. Here's a hover provider that shows a link in the comments of the current line in the active text editor: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { vscode.languages.registerHoverProvider( 'javascript', new (class implements vscode.HoverProvider { provideHover( _document: vscode.TextDocument, _position: vscode.Position, _token: vscode.CancellationToken ): vscode.ProviderResult<vscode.Hover> { const commentCommandUri = vscode.Uri.parse(`command:editor.action.addCommentLine`); const contents = new vscode.MarkdownString(`[Add comment](${commentCommandUri})`); // To enable command URIs in Markdown content, you must set the `isTrusted` flag. // When creating trusted Markdown string, make sure to properly sanitize all the // input content so that only expected command URIs can be executed contents.isTrusted = true; return new vscode.Hover(contents); } })() ); } The list of arguments to the command is passed as a JSON array that has been properly URI encoded: The example below uses the `git.stage` command to create a hover link that stages the current file: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { vscode.languages.registerHoverProvider( 'javascript', new (class implements vscode.HoverProvider { provideHover( document: vscode.TextDocument, _position: vscode.Position, _token: vscode.CancellationToken ): vscode.ProviderResult<vscode.Hover> { const args = [{ resourceUri: document.uri }]; const stageCommandUri = vscode.Uri.parse( `command:git.stage?${encodeURIComponent(JSON.stringify(args))}` ); const contents = new vscode.MarkdownString(`[Stage file](${stageCommandUri})`); contents.isTrusted = true; return new vscode.Hover(contents); } })() ); } You can enable command URIs in webviews by setting `enableCommandUris` in the `WebviewOptions` when the webview is created. ## Creating new commands ### Registering a command `vscode.commands.registerCommand` binds a command ID to a handler function in your extension: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { const command = 'myExtension.sayHello'; const commandHandler = (name: string = 'world') => { console.log(`Hello ${name}!!!`); }; context.subscriptions.push(vscode.commands.registerCommand(command, commandHandler)); } The handler function will be invoked whenever the `myExtension.sayHello` command is executed, be it programmatically with `executeCommand`, from the VS Code UI, or through a keybinding. ### Creating a user facing command `vscode.commands.registerCommand` only binds a command ID to a handler function. To expose this command in the Command Palette so it is discoverable by users, you also need a corresponding command `contribution` in your extension's `package.json`: { "contributes": { "commands": [ { "command": "myExtension.sayHello", "title": "Say Hello" } ] } } The `commands` contribution tells VS Code that your extension provides a given command and should be activated when that command is invoked, and also lets you control how the command is displayed in the UI. Make sure to follow the command naming conventions when creating commands.  Now when a user first invokes the `myExtension.sayHello` command from the Command Palette or through a keybinding, the extension will be activated and `registerCommand` will bind `myExtension.sayHello` to the proper handler. > **Note**: Extensions targeting VS Code versions prior to 1.74.0 must explicitly register an `onCommand` `activationEvent` for all user facing commands so that the extension activates and `registerCommand` executes: > > { > "activationEvents": ["onCommand:myExtension.sayHello"] > } > You do not need an `onCommand` activation event for internal commands but you must define them for any commands that: * Can be invoked using the Command Palette. * Can be invoked using a keybinding. * Can be invoked through the VS Code UI, such as through the editor title bar. * Is intended as an API for other extensions to consume. ### Controlling when a command shows up in the Command Palette By default, all user facing commands contributed through the `commands` section of the `package.json` show up in the Command Palette. However, many commands are only relevant in certain circumstances, such as when there is an active text editor of a given language or when the user has a certain configuration option set. The `menus.commandPalette` contribution point lets you restrict when a command should show in the Command Palette. It takes the ID of the target command and a when clause that controls when the command is shown: { "contributes": { "menus": { "commandPalette": [ { "command": "myExtension.sayHello", "when": "editorLangId == markdown" } ] } } } Now the `myExtension.sayHello` command will only show up in the Command Palette when the user is in a Markdown file. ### Enablement of commands Commands support enablement via an `enablement` property - its value is a when-clause. Enablement applies to all menus and to registered keybindings. > **Note**: There is semantic overlap between `enablement` and the `when` condition of menu items. The latter is used to prevent menus full of disabled items. For example, a command that analyzes a JavaScript regular expression should show **when** the file is JavaScript and be **enabled** only when the cursor is over a regular expression. The `when` clause prevents clutter, by not showing the command for all other language files. Preventing cluttered menus is highly recommended. Last, menus showing commands, like the Command Palette or context menus, implement different ways of dealing with enablement. Editor and explorer context menus render enablement/disablement items while the Command Palette filters them. ### Using a custom when clause context If you are authoring your own VS Code extension and need to enable/disable commands, menus, or views by using a `when` clause context and none of the existing keys suit your needs, then you can add your own context. The first example below sets the key `myExtension.showMyCommand` to true, which you can use in enablement of commands or with the `when` property. The second example stores a value that you could use with a `when` clause to check if the number of cool open things is greater than 2. vscode.commands.executeCommand('setContext', 'myExtension.showMyCommand', true); vscode.commands.executeCommand('setContext', 'myExtension.numberOfCoolOpenThings', 2); ## Naming conventions When creating commands, you should follow these naming conventions: * Command title * Use title-style capitalization. Don't capitalize prepositions of four or fewer letters (such as on, to, in, of, with, and for) unless the preposition is the first or last word. * Start with a verb to describe the action that will be performed. * Use a noun to describe the target of the action. * Avoid using "command" in the title. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/color-theme In this article Colors visible in the Visual Studio Code user interface fall in two categories: * Workbench colors used in views and editors, from the Activity Bar to the Status Bar. A complete list of all these colors can be found in the theme color reference. * Syntax colors and styles used for source code in the editor. The theming of these colors is different as syntax colorization is based on TextMate grammars and TextMate themes as well as semantic tokens. This guide will cover the different ways in which you can create themes. ## Workbench colors The easiest way to create a new workbench color theme is to start with an existing color theme and customize it. First switch to the color theme that you want to modify, then open your settings and make changes to the `workbench.colorCustomizations` setting. Changes are applied live to your VS Code instance. The following, for example, would change the background color of the title bar: { "workbench.colorCustomizations": { "titleBar.activeBackground": "#ff0000" } } A complete list of all themable colors can be found in the color reference. ## Syntax colors For syntax highlighting colors, there are two approaches. You can reference an existing TextMate theme (`.tmTheme` file) from the community, or you can create your own theming rules. The easiest way is to start with an existing theme and customize it, much like in the workbench colors section above. First switch to the color theme to customize and use the `editor.tokenColorCustomizations` settings. Changes are applied live to your VS Code instance and no refreshing or reloading is necessary. For example, the following would change the color of comments within the editor: { "editor.tokenColorCustomizations": { "comments": "#FF0000" } } The setting supports a simple model with a set of common token types such as 'comments', 'strings' and 'numbers' available. If you want to color more than that, you need to use TextMate theme rules directly, which are explained in detail in the Syntax Highlighting guide. ## Semantic colors Semantic highlighting is available for TypeScript and JavaScript in VS Code release 1.43. We expect it to be adopted by other languages soon. Semantic highlighting enriches syntax coloring based on symbol information from the language service, which has more complete understanding of the project. The coloring changes appear once the language server is running and has computed the semantic tokens. Each theme controls whether to enable semantic highlighting with a specific setting that is part of the theme definition. The style of each semantic token is defined by the theme's styling rules. Users can override the semantic highlighting feature and colorization rules using the `editor.tokenColorCustomizations` setting: Enable semantic highlighting for a specific theme: "editor.tokenColorCustomizations": { "[Material Theme]": { "semanticHighlighting": true } }, Themes can define theming rules for semantic tokens as described in the Syntax Highlighting guide. ## Create a new Color Theme Once you have tweaked your theme colors using `workbench.colorCustomizations` and `editor.tokenColorCustomizations`, it's time to create the actual theme. 1. Generate a theme file using the **Developer: Generate Color Theme from Current Settings** command from the **Command Palette** 2. Use VS Code's Yeoman extension generator to generate a new theme extension: npm install -g yo generator-code yo code 3. If you customized a theme as described above, select 'Start fresh'.  4. Copy the theme file generated from your settings to the new extension. You can also use an existing TextMate theme by telling the extension generator to import a TextMate theme file (.tmTheme) and package it for use in VS Code. Alternatively, if you have already downloaded the theme, replace the `tokenColors` section with a link to the `.tmTheme` file to use. { "type": "dark", "colors": { "editor.background": "#1e1e1e", "editor.foreground": "#d4d4d4", "editorIndentGuide.background": "#404040", "editorRuler.foreground": "#333333", "activityBarBadge.background": "#007acc", "sideBarTitle.foreground": "#bbbbbb" }, "tokenColors": "./Diner.tmTheme" } > **Tip:** Give your color definition file the `-color-theme.json` suffix and you will get hovers, code completion, color decorators, and color pickers when editing. > **Tip:** ColorSublime has hundreds of existing TextMate themes to choose from. Pick a theme you like and copy the Download link to use in the Yeoman generator or into your extension. It will be in a format like `"https://raw.githubusercontent.com/Colorsublime/Colorsublime-Themes/master/themes/(name).tmTheme"` ## Test a new Color Theme To try out the new theme, press F5 to launch an Extension Development Host window. There, open the Color Theme picker with **File** > **Preferences** > **Theme** > **Color Theme** and you can see your theme in the dropdown list. Arrow up and down to see a live preview of your theme.  Changes to the theme file are applied live in the `Extension Development Host` window. ## Publishing a Theme to the Extension Marketplace If you'd like to share your new theme with the community, you can publish it to the Extension Marketplace. Use the vsce publishing tool to package your theme and publish it to the VS Code Marketplace. > **Tip:** To make it easy for users to find your theme, include the word "theme" in the extension description and set the `Category` to `Themes` in your `package.json`. We also have recommendations on how to make your extension look great on the VS Code Marketplace, see Marketplace Presentation Tips. ## Adding a new Color ID Color IDs can also be contributed by extensions through the color contribution point. These colors also appear when using code complete in the `workbench.colorCustomizations` settings and the color theme definition file. Users can see what colors an extension defines in the extension contributions tab. ## Further reading * CSS Tricks - Creating a VS Code theme 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/file-icon-theme Visual Studio Code displays icons next to filenames throughout its UI, and extensions can contribute new sets of file icons that users can choose from. ## Adding a new File Icon Theme You can create your own file icon theme from icons (preferably SVG) and from icon fonts. As example, check out the two built-in themes: Minimal and Seti. To begin, create a VS Code extension and add the `iconTheme` contribution point. { "contributes": { "iconThemes": [ { "id": "turtles", "label": "Turtles", "path": "./fileicons/turtles-icon-theme.json" } ] } } The `id` is the identifier for the icon theme. It is used as an identifier in the settings, so make it unique but also readable. `label` is shown in the file icon theme picker dropdown. The `path` points to a file in the extension that defines the icon set. If your icon set name follows the `*icon-theme.json` name scheme, you will get completion support and hovers in VS Code. ### File Icon Set File The file icon set file is a JSON file consisting of file icon associations and icon definitions. An icon association maps a file type ('file', 'folder', 'json-file'...) to an icon definition. Icon definitions define where the icon is located: That can be an image file or also glyph in a font. ### Icon definitions The `iconDefinitions` section contains all definitions. Each definition has an id, which will be used to reference the definition. A definition can be referenced also by more than one file association. { "iconDefinitions": { "_folder_dark": { "iconPath": "./images/Folder_16x_inverse.svg" } } } This icon definition above contains a definition with the identifier `_folder_dark`. The following properties are supported: * `iconPath`: When using a svg/png: the path to the image. * `fontCharacter`: When using a glyph font: The character in the font to use. * `fontColor`: When using a glyph font: The color to use for the glyph. * `fontSize`: When using a font: The font size. By default, the size specified in the font specification is used. Should be a relative size (e.g. 150%) to the parent font size. * `fontId`: When using a font: The id of the font. If not specified, the first font specified in font specification section will be picked. ### File association Icons can be associated to folders, folder names, files, file extensions, file names and language IDs. Additionally each of these associations can be refined for 'light' and 'highContrast' color themes. Each file association points to an icon definition. { "file": "_file_dark", "folder": "_folder_dark", "folderExpanded": "_folder_open_dark", "folderNames": { ".vscode": "_vscode_folder" }, "fileExtensions": { "ini": "_ini_file" }, "fileNames": { "win.ini": "_win_ini_file" }, "languageIds": { "ini": "_ini_file" }, "light": { "folderExpanded": "_folder_open_light", "folder": "_folder_light", "file": "_file_light", "fileExtensions": { "ini": "_ini_file_light" } }, "highContrast": {} } * `file` is the default file icon, shown for all files that don't match any extension, filename or language ID. Currently all properties defined by the definition of the file icon will be inherited (only relevant for font glyphs, useful for the fontSize). * `folder` is the folder icon for collapsed folders, and if `folderExpanded` is not set, also for expanded folders. Icons for specific folder names can be associated using the `folderNames` property. The folder icon is optional. If not set, no icon will be shown for folder. * `folderExpanded` is the folder icon for expanded folders. The expanded folder icon is optional. If not set, the icon defined for `folder` will be shown. * `folderNames` associates folder names to icons. The key of the set is the folder name, optionally prefixed by a single parent path segment (\*). Patterns or wildcards are not supported. Folder name matching is case insensitive. * `folderNamesExpanded` associates folder names to icons for expanded folder. The key of the set is the folder name, optionally prefixed by a single parent path segment (\*). Patterns or wildcards are not supported. Folder name matching is case insensitive. * `rootFolder` is the folder icon for collapsed workspace root folders , and if `rootFolderExpanded` is not set, also for expanded workspace root folders. If not set, the icon defined for `folder` will be shown for workspace root folders. * `rootFolderExpanded` is the folder icon for expanded workspace root folders. If not set, the icon defined for `rootFolder` will be shown for expanded workspace root folders. * `rootFolderNames` associates root folder names to icons. The key of the set is the folder name. Patterns or wildcards are not supported. Root folder name matching is case insensitive. * `rootFolderNamesExpanded` associates root folder names to icons for expanded folder. The key of the set is the folder name. Patterns or wildcards are not supported. Root folder name matching is case insensitive. * `languageIds` associates languages to icons. The key in the set is the language ID as defined in the language contribution point. The language of a file is evaluated based on the file extensions and file names as defined in the language contribution. Note that the 'first line match' of the language contribution is not considered. * `fileExtensions` associates file extensions to icons. The key in the set is the file extension name. The extension name is a file name segment after a dot (not including the dot). File names with multiple dots such as `lib.d.ts` can match multiple extensions; 'd.ts' and 'ts'. Optionally, the file extension name can be prefixed by a single parent path segment (\*). Extensions are compared case insensitive. * `fileNames` associates file names to icons. The key in the set is the full file name, not including any path segments. Optionally, the file extension name can be prefixed by a single parent path segment (\*). Patterns or wildcards are not supported. File name matching is case insensitive. A 'fileName' match is the strongest match, and the icon associated to the file name will be preferred over an icon of a matching fileExtension and also of a matching language ID. (\*) Some property keys (`folderNames`, `folderNamesExpanded`, `fileExtensions`, `fileNames`) can be prefixed by a single parent path segment. The icon will only be used if the resource's direct parent folder matches the parent path folder. This can be used to give resources in a particular folder (for example, `system`) a different appearance: "fileNames": { "system/win.ini": "_win_ini_file" }, `system/win.ini` means that the association matches files called `win.ini` directly in a folder `system` "fileExtensions": { "system/ini": "_ini_file" }, `system/ini` means that the association matches files called `*.ini` directly in a folder `system` A file extension match is preferred over a language match, but is weaker than a file name match. A match with a parent path segment is preferred over a match without such a segment of the same kind. `file name match with parent > file name match > file extension match with parent > file extension match > language match ...` The `light` and the `highContrast` section have the same file association properties as just listed. They allow to override icons for the corresponding themes. ### Font definitions The `fonts` section lets you declare any number of glyph fonts that you want to use. You can later reference these fonts in the icon definitions. The font declared first will be used as the default if an icon definition does not specify a font id. Copy the font file into your extension and set the path accordingly. It is recommended to use WOFF fonts. * Set 'woff' as the format. * the weight property values are defined here. * the style property values are defined here. * the size should be relative to the font size where the icon is used. Therefore, always use percentage. { "fonts": [ { "id": "turtles-font", "src": [ { "path": "./turtles.woff", "format": "woff" } ], "weight": "normal", "style": "normal", "size": "150%" } ], "iconDefinitions": { "_file": { "fontCharacter": "\\E002", "fontColor": "#5f8b3b", "fontId": "turtles-font" } } } ### Folder icons in File Icon Themes File Icon themes can instruct the File Explorer not to show the default folder icon (the rotating triangles or "twisties") when the folder icons are good enough to indicate the expansion state of a folder. This mode is enabled by setting `"hidesExplorerArrows":true` in the File Icon theme definition file. ### Language default icons Language contributors can define an icon for the language. { "contributes": { "languages": [ { "id": "latex", // ... "icon": { "light": "./icons/latex-light.png", "dark": "./icons/latex-dark.png" } } ] } } The icon is used if a file icon theme only has a generic file icon for the language. Language default icons are only shown if: * the file icon theme has specific file icons. E.g. `Minimal` does not have specific file icons and therefore does not use the language default icons * the file icon theme does not contain an icon for the given language, file extension or file name. * the file icon theme does not define `"showLanguageModeIcons":false` Language default icons are always shown if * the file icon theme does define `"showLanguageModeIcons":true` 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/product-icon-theme In this article Visual Studio Code contains a set of built-in icons that are used in views and the editor, but can also be referenced in hovers, the status bar, and even by extensions. Examples are the icons in filter action buttons and view icons, in the status bar, breakpoints, and the folding icons in trees and the editor. A product icon theme allows an extension to redefine these icons to give VS Code a custom appearance. Not covered by product icon themes are the file icons (covered by file icon themes) and icons contributed by extensions. VS Code requires the icons to be defined as glyph in an icon font and (currently) limits product icons to consist of a single color. The color used for an icon is specific to the place where it is shown and is defined by the active color theme. ## Adding a new product icon theme To define your own product icon theme, start by creating a VS Code extension and add the `productIconThemes` contribution point to the extension's `package.json`. { "contributes": { "productIconThemes": [ { "id": "aliensAreBack", "label": "Aliens Are Back", "path": "./producticons/aliens-product-icon-theme.json" } ] } } The `id` is the identifier for the product icon theme. It is used in the settings, so make it unique but also readable. `label` is shown in the product icon theme picker dropdown. The `path` points to a file in the extension that defines the icon set. If your file name follows the `*product-icon-theme.json` name scheme, you will get completion support and hovers when editing the product icon theme file in VS Code. ## Product icon definition file The product icon definition file is a JSON file defining one or more icon fonts and a set of icon definitions. ### Font definitions The `fonts` section lets you declare any number of glyph fonts that you want to use, but must define at least one font definition. These fonts can later be referenced in the icon definitions. The font declared first will be used as the default if an icon definition does not specify a font ID. Copy the font file into your extension and set the path accordingly. It is recommended that you use WOFF fonts. * Set 'woff' as the format. * The weight property values are defined here. * The style property values are defined here. { "fonts": [ { "id": "alien-font", "src": [ { "path": "./alien.woff", "format": "woff" } ], "weight": "normal", "style": "normal" } ] } ### Icon definitions VS Code defines a list of icon IDs through which the icons are referenced by the views. The product icon's `iconDefinitions` section assigns new icons to these IDs. Each definition uses `fontId` to reference one of the fonts defined in the `fonts` section. If `fontId` is omitted, the first font listed in the font definitions is taken. { "iconDefinitions": { "dialog-close": { "fontCharacter": "\\43", "fontId": "alien-font" } } } A list of all icon identifiers can be found in the icon reference. ## Develop and test VS Code has built-in editing support for the `package.json` file as well as for product icon theme files. To get that, your theme file name needs to end with `product-icon-theme.json`. This enables code completion on all properties including the known icon IDs as well as hovers and validation. To try out a product icon theme, open the extension folder in VS Code and press F5. This will run the extension in an extension development host window. The window has your extension enabled and the extension will automatically switch to the first product icon theme. Also, the theme file is watched for changes and updates to the icons will be applied automatically whenever the theme file is modified. As you work on the product icon definition file, you will see the changes live on save. To switch between product icon themes, use the command **Preferences: Product Icon Theme**. To find out which icon is used at a certain location in the VS Code UI, open Developer Tools by running **Help > Toggle Developer Tools** and then: * Click on the Developer Tools inspect tool in the upper left. * Move the mouse over the icon to inspect. * If the icon's class name is `codicon.codicon-remote`, then the icon ID is `remote`.  ## Sample The Product Color Theme sample can be used as a playground. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/chat In this article Visual Studio Code's Copilot Chat architecture enables extension authors to integrate with the GitHub Copilot Chat experience. A chat extension is a VS Code extension that uses the Chat extension API by contributing a _Chat participant_. Chat participants are domain experts that can answer user queries within a specific domain. Participants can use different approaches to process a user query: * Use AI to interpret the request and generate a response, for example by using the Language Model API * Forward the user request to a backend service * Use procedural logic and local resources Participants can use the language model in a wide range of ways. Some participants only make use of the language model to get answers to custom prompts, for example the sample chat participant. Other participants are more advanced and act like autonomous agents that invoke multiple tools with the help of the language model. An example of such an advanced participant is the built-in `@workspace` that knows about your workspace and can answer questions about it. Internally, `@workspace` is powered by multiple tools: GitHub's knowledge graph, combined with semantic search, local code indexes, and VS Code's language services. When a user explicitly mentions a `@participant` in their chat prompt, that prompt is forwarded to the extension that contributed that specific chat participant. The participant then uses a `ResponseStream` to respond to the request. To provide a smooth user experience, the Chat API is streaming-based. A chat response can contain rich content, such as Markdown, file trees, command buttons, and more. Get more info about the supported response output types. To help the user take the conversation further, participants can provide _follow-ups_ for each response. Follow-up questions are suggestions that are presented in the chat user interface and might give the user inspiration about the chat extension's capabilities. Participants can also contribute _commands_, which are a shorthand notation for common user intents, and are indicated by the `/` symbol. The extension can then use the command to prompt the language model accordingly. For example, `/explain` is a command for the `@workspace` participant that corresponds with the intent that the language model should explain some code. ## Extending GitHub Copilot via GitHub Apps Alternatively, it is possible to extend GitHub Copilot by creating a GitHub App that contributes a chat participant in the Chat view. A GitHub App is backed by a service and works across all GitHub Copilot surfaces, such as github.com, Visual Studio, or VS Code. On the other hand, GitHub Apps do not have full access to the VS Code API. To learn more about extending GitHub Copilot through a GitHub App see the GitHub documentation. ## Links * Chat extension sample * ChatParticipant API ## Parts of the chat user experience The following screenshot shows the different chat concepts in the Visual Studio Code chat experience for the sample extension.  1. Use the `@` syntax to invoke the `@cat` chat participant 2. Use the `/` syntax to call the `/teach` command 3. User-provided query, also known as the user prompt 4. Icon and participant `fullName` that indicate that Copilot is using the `@cat` chat participant 5. Markdown response, provided by `@cat` 6. Code fragment included in the markdown response 7. Button included in the `@cat` response, the button invokes a VS Code command 8. Suggested follow-up questions provided by the chat participant 9. Chat input field with the placeholder text provided by the chat participant's `description` property ## Develop a chat extension A chat extension is an extension that contributes a chat participant to the Chat view. The minimum functionality that is needed for implementing a chat extension is: * Register the chat participant, to let users invoke it by using the `@` symbol in the VS Code Chat view. * Define a request handler that interprets the user's question, and returns a response in the Chat view. You can further expand the functionality of the chat extension with the following optional features: * Register chat commands to provide users with a shorthand notation for common questions * Define suggested follow-up questions to help the user continue a conversation As a starting point for developing a chat extension, you can refer to our chat extension sample. This sample implements a simple cat tutor that can explain computer science topics using cat metaphors.  ### Register the chat extension The first step to create a chat extension is to register it in your `package.json` by providing a unique `id`, the `name`, and `description`. "contributes": { "chatParticipants": [ { "id": "chat-sample.cat", "name": "cat", "fullName": "Cat", "description": "Meow! What can I teach you?", "isSticky": true } ] } Users can then reference the chat participant in the Chat view by using the `@` symbol and the `name` you provided. The `fullName` is shown in the title area of a response from your participant. The `description` is used as placeholder text in the chat input field. The `isSticky` property controls whether the chat participant is persistent, which means that the participant name is automatically prepended in the chat input field after the user has started interacting with the participant. We suggest using a lowercase `name` and using title case for the `fullName` to align with existing chat participants. Get more info about the naming conventions for chat participants. Note Some participant names are reserved, and in case you use a reserved name VS Code will display the fully qualified name of your participant (including the extension ID). Up-front registration of participants and commands in `package.json` is required, so that VS Code can activate your extension at the right time, and not before it is needed. After registration, all your extension has to do is create the participant by using `vscode.chat.createChatParticipant`. When creating the participant, you have to provide the ID, which you defined in `package.json`, and a request handler. The following code snippet shows how to create the `@cat` chat participant (after you register it in your `package.json`): export function activate(context: vscode.ExtensionContext) { // Register the chat participant and its request handler const cat = vscode.chat.createChatParticipant('chat-sample.cat', handler); // Optionally, set some properties for @cat cat.iconPath = vscode.Uri.joinPath(context.extensionUri, 'cat.jpeg'); // Add the chat request handler here } After registering and creating the chat participant, you now need to implement the request handler to process a user's request. ### Implement a request handler The request handler is responsible for processing the user's chat requests in the VS Code Chat view. Each time a user enters a prompt in the chat input field, the chat request handler is invoked. These are the typical steps for implementing a chat request handler: 1. Define the request handler 2. Determine the intent of the user's request 3. Perform logic to answer the user's question 4. Return a response to the user #### Define the request handler You define the request handler (`vscode.ChatRequestHandler`) inside the extension's `activate` function. The following code snippet shows how to define a request handler: const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ): Promise<ICatChatResult> => { // Chat request handler implementation goes here }; #### Determine the request intent To determine the intent of the user's request, you can reference the `vscode.ChatRequest` parameter to access the user's prompt, commands, and chat location. Optionally, you can take advantage of the language model to determine the user's intent, rather than using traditional logic. As part of the `request` object you get a language model instance that the user picked in the chat model dropdown. Learn how you can use the Language Model API in your extension. The following code snippet shows the basic structure of first using the command, and then the user prompt to determine the user intent: const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ): Promise<ICatChatResult> => { // Test for the `teach` command if (request.command == 'teach') { // Add logic here to handle the teaching scenario doTeaching(request.prompt, request.variables); } else { // Determine the user's intent const intent = determineUserIntent(request.prompt, request.variables, request.model); // Add logic here to handle other scenarios } }; #### Process the request Next, you need to implement the actual logic for processing the user request. Often, chat extensions use the `request.model` language model instance to process the request. In this case, you might adjust the language model prompt to match the user's intent. Alternately, you can implement the extension logic by invoking a backend service, by using traditional programming logic, or by using a combination of all these options. For example, you could invoke a web search to gather additional information, which you then provide as context to the language model. While processing the current request, you might want to refer to previous chat messages. For example, if a previous response returned a C# code snippet, the user's current request might be "give the code in Python". Learn how you can use the chat message history. If you want to process a request differently based on the location of the chat input, you can use the `location` property of the `vscode.ChatRequest`. For example, if the user sends a request from the terminal inline chat, you might look up a shell command. Whereas, if the user uses the Chat view, you could return a more elaborate response. #### Return the chat response Once you've processed the request, you have to return a response to the user in the Chat view. Chat extensions can use streaming to respond to user queries. Responses can contain different content types: markdown, images, references, progress, buttons, and file trees. For example to generate this response:  An extension can use the response stream in the following way: stream.progress('Picking the right topic to teach...'); stream.markdown(`\`\`\`typescript const myStack = new Stack(); myStack.push(1); // pushing a number on the stack (or let's say, adding a fish to the stack) myStack.push(2); // adding another fish (number 2) console.log(myStack.pop()); // eating the top fish, will output: 2 \`\`\` So remember, Code Kitten, in a stack, the last fish in is the first fish out - which we tech cats call LIFO (Last In, First Out).`); stream.button({ command: 'cat.meow', title: vscode.l10n.t('Meow!'), arguments: [] }); Get more info about the supported chat response output types. In practice, extensions typically send a request to the language model. Once they get a response from the language model, they might further process it, and decide if they should stream anything back to the user. The VS Code Chat API is streaming-based, and is compatible with the streaming Language Model API. This allows extensions to report progress and results continuously with the goal of having a smooth user experience. Learn how you can use the Language Model API. #### Use the chat message history Participants have access to the message history of the current chat session. A participant can only access messages where it was mentioned. A `history` item is either a `ChatRequestTurn` or a `ChatResponseTurn`. For example, use the following code snippet to retrieve all the previous requests that the user sent to your participant in the current chat session: const previousMessages = context.history.filter(h => h instanceof vscode.ChatRequestTurn); History will not be automatically included in the prompt, it is up to the participant to decide if it wants to add history as additional context when passing messages to the language model. ### Register commands A chat participant can contribute commands, which are shortcuts to specific functionality provided by the extension. Users can reference commands in chat by using the `/` syntax, for example `/explain`. One of the tasks when answering questions is to determine the user intent. For example, VS Code could infer that `Create a new workspace with Node.js Express Pug TypeScript` means that you want a new project, but `@workspace /new Node.js Express Pug TypeScript` is more explicit, concise, and saves typing time. If you type `/` in the chat input field, VS Code offers a list of registered commands with their description.  Chat participants can contribute commands with their description by adding them in `package.json`: "contributes": { "chatParticipants": [ { "id": "chat-sample.cat", "name": "cat", "fullName": "Cat", "description": "Meow! What can I teach you?", "isSticky": true, "commands": [ { "name": "teach", "description": "Pick at random a computer science concept then explain it in purfect way of a cat" }, { "name": "play", "description": "Do whatever you want, you are a cat after all" } ] } ] } Get more info about the naming conventions for slash commands. ### Register follow-up requests After each chat request, VS Code invokes follow-up providers to get suggested follow-up questions to show to the user. The user can then select the follow-up question, and immediately send it to the chat extension. Follow-up questions can provide inspiration to the user to take the conversation further, or to discover more capabilities of the chat extension. The following code snippet shows how to register follow-up requests in a chat extension: cat.followupProvider = { provideFollowups(result: ICatChatResult, context: vscode.ChatContext, token: vscode.CancellationToken) { if (result.metadata.command === 'teach') { return [{ prompt: 'let us play', title: vscode.l10n.t('Play with the cat') } satisfies vscode.ChatFollowup]; } } }; Tip Follow-ups should be written as questions or directions, not just concise commands. ### Implement participant detection To make it easier to use chat participants with natural language, you can implement participant detection. Participant detection is a way to automatically route the user's question to a suitable participant, without having to explicitly mention the participant in the prompt. For example, if the user asks "How do I add a login page to my project?", the question would be automatically routed to the `@workspace` participant because it can answer questions about the user's project. VS Code uses the chat participant description and examples to determine which participant to route a chat prompt to. You can specify this information in the `disambiguation` property in the extension `package.json` file. The `disambiguation` property contains a list of detection categories, each with a description and examples. | Property | Description | Examples | | --- | --- | --- | | `category` | The detection category. If the participant serves different purposes, you can have a category for each. | * `cat` * `workspace_questions` * `web_questions` | | `description` | A detailed description of the kinds of questions that are suitable for this participant. | * `The user wants to learn a specific computer science topic in an informal way.` * `The user just wants to relax and see the cat play.` | | `examples` | A list of representative example questions. | * `Teach me C++ pointers using metaphors` * `Explain to me what is a linked list in a simple way` * `Can you show me a cat playing with a laser pointer?` | You can define participant detection for the overall chat participant, for specific commands, or a combination of both. The following code snippet shows how to implement participant detection at the participant level. "contributes": { "chatParticipants": [ { "id": "chat-sample.cat", "fullName": "Cat", "name": "cat", "description": "Meow! What can I teach you?", "disambiguation": [ { "category": "cat", "description": "The user wants to learn a specific computer science topic in an informal way.", "examples": [ "Teach me C++ pointers using metaphors", "Explain to me what is a linked list in a simple way", "Can you explain to me what is a function in programming?" ] } ] } ] } Similarly, you can also configure participant detection at the command level by adding a `disambiguation` property for one or more items in the `commands` property. Apply the following guidelines to improve the accuracy of participant detection for your extension: * **Be specific**: The description and examples should be as specific as possible to avoid conflicts with other participants. Avoid using generic terminology in the participant and command information. * **Use examples**: The examples should be representative of the kinds of questions that are suitable for the participant. Use synonyms and variations to cover a wide range of user queries. * **Use natural language**: The description and examples should be written in natural language, as if you were explaining the participant to a user. * **Test the detection**: Test the participant detection with a variation of example questions and verify there's no conflict with built-in chat participants. Note Built-in chat participants take precedence for participant detection. For example, a chat participant that operates on workspace files might conflict with the built-in `@workspace` participant. ## Supported chat response output types To return a response to a chat request, you use the `ChatResponseStream` parameter on the `ChatRequestHandler`. The following list provides the output types for a chat response in the Chat view. A chat response can combine multiple different output types. * **Markdown** Render a fragment of Markdown text simple text or images. You can use any Markdown syntax that is part of the CommonMark specification. Use the `ChatResponseStream.markdown` method and provide the Markdown text. Example code snippet: // Render Markdown text stream.markdown('# This is a title \n'); stream.markdown('This is stylized text that uses _italics_ and **bold**. '); stream.markdown('This is a [link](https://code.visualstudio.com).\n\n'); stream.markdown(''); * **Code block** Render a code block that supports IntelliSense, code formatting, and interactive controls to apply the code to the active editor. To show a code block, use the `ChatResponseStream.markdown` method and apply the Markdown syntax for code blocks (using backticks). Example code snippet: // Render a code block that enables users to interact with stream.markdown('```bash\n'); stream.markdown('```ls -l\n'); stream.markdown('```'); * **Command link** Render a link inline in the chat response that users can select to invoke a VS Code command. To show a command link, use the `ChatResponseStream.markdown` method and use the Markdown syntax for links `[link text](command:commandId)`, where you provide the command ID in the URL. For example, the following link opens the Command Palette: `[Command Palette](command:workbench.action.showCommands)`. To protect against command injection when you load the Markdown text from a service, you have to use a `vscode.MarkdownString` object with the `isTrusted` property set to the list of trusted VS Code command IDs. This property is required to enable the command link to work. If the `isTrusted` property is not set or a command is not listed, the command link will not work. Example code snippet: // Use command URIs to link to commands from Markdown let markdownCommandString: vscode.MarkdownString = new vscode.MarkdownString( `[Use cat names](command:${CAT_NAMES_COMMAND_ID})` ); markdownCommandString.isTrusted = { enabledCommands: [CAT_NAMES_COMMAND_ID] }; stream.markdown(markdownCommandString); If the command takes arguments, you need to first JSON encode the arguments and then encode the JSON string as a URI component. You then append the encoded arguments as a query string to the command link. // Encode the command arguments const encodedArgs = encodeURIComponent(JSON.stringify(args)); // Use command URIs with arguments to link to commands from Markdown let markdownCommandString: vscode.MarkdownString = new vscode.MarkdownString( `[Use cat names](command:${CAT_NAMES_COMMAND_ID}?${encodedArgs})` ); markdownCommandString.isTrusted = { enabledCommands: [CAT_NAMES_COMMAND_ID] }; stream.markdown(markdownCommandString); * **Command button** Render a button that invokes a VS Code command. The command can be a built-in command or one that you define in your extension. Use the `ChatResponseStream.button` method and provide the button text and command ID. Example code snippet: // Render a button to trigger a VS Code command stream.button({ command: 'my.command', title: vscode.l10n.t('Run my command') }); * **File tree** Render a file tree control that lets users preview individual files. For example, to show a workspace preview when proposing to create a new workspace. Use the `ChatResponseStream.filetree` method and provide an array of file tree elements and the base location (folder) of the files. Example code snippet: // Create a file tree instance var tree: vscode.ChatResponseFileTree[] = [ { name: 'myworkspace', children: [{ name: 'README.md' }, { name: 'app.js' }, { name: 'package.json' }] } ]; // Render the file tree control at a base location stream.filetree(tree, baseLocation); * **Progress message** Render a progress message during a long-running operation to provide the user with intermediate feedback. For example, to report the completion of each step in a multi-step operation. Use the `ChatResponseStream.progress` method and provide the message. Example code snippet: // Render a progress message stream.progress('Connecting to the database.'); * **Reference** Add a reference for an external URL or editor location in the list references to indicate which information you use as context. Use the `ChatResponseStream.reference` method and provide the reference location. Example code snippet: const fileUri: vscode.Uri = vscode.Uri.file('/path/to/workspace/app.js'); // On Windows, the path should be in the format of 'c:\\path\\to\\workspace\\app.js' const fileRange: vscode.Range = new vscode.Range(0, 0, 3, 0); const externalUri: vscode.Uri = vscode.Uri.parse('https://code.visualstudio.com'); // Add a reference to an entire file stream.reference(fileUri); // Add a reference to a specific selection within a file stream.reference(new vscode.Location(fileUri, fileRange)); // Add a reference to an external URL stream.reference(externalUri); * **Inline reference** Add an inline reference to a URI or editor location. Use the `ChatResponseStream.anchor` method and provide the anchor location and optional title. To reference a symbol (for example, a class or variable), you would use a location in an editor. Example code snippet: const symbolLocation: vscode.Uri = vscode.Uri.parse('location-to-a-symbol'); // Render an inline anchor to a symbol in the workspace stream.anchor(symbolLocation, 'MySymbol'); > **Important**: Images and links are only available when they originate from a domain that is in the trusted domain list. Get more info about link protection in VS Code. ## Implement tool calling To respond to a user request, a chat extension can invoke language model tools. Learn more about language model tools and the tool-calling flow. You can implement tool calling in two ways: * By using the `@vscode/chat-extension-utils` library to simplify the process of calling tools in a chat extension. * By implementing tool calling yourself, which gives you more control over the tool-calling process. For example, to perform additional validation or to handle tool responses in a specific way before sending them to the LLM. ### Implement tool calling with the chat extension library You can use the `@vscode/chat-extension-utils` library to simplify the process of calling tools in a chat extension. Implement tool calling in the `vscode.ChatRequestHandler` function of your chat participant. 1. Determine the relevant tools for the current chat context. You can access all available tools by using `vscode.lm.tools`. The following code snippet shows how to filter the tools to only those that have a specific tag. const tools = request.command === 'all' ? vscode.lm.tools : vscode.lm.tools.filter(tool => tool.tags.includes('chat-tools-sample')); 2. Send the request and tool definitions to the LLM by using `sendChatParticipantRequest`. const libResult = chatUtils.sendChatParticipantRequest( request, chatContext, { prompt: 'You are a cat! Answer as a cat.', responseStreamOptions: { stream, references: true, responseText: true }, tools }, token ); The `ChatHandlerOptions` object has the following properties: * `prompt`: (optional) Instructions for the chat participant prompt. * `model`: (optional) The model to use for the request. If not specified, the model from the chat context is used. * `tools`: (optional) The list of tools to consider for the request. * `requestJustification`: (optional) A string that describes why the request is being made. * `responseStreamOptions`: (optional) Enable `sendChatParticipantRequest` to stream the response back to VS Code. Optionally, you can also enable references and/or response text. 3. Return the result from the LLM. This might contain error details or tool-calling metadata. return await libResult.result; The full source code of this tool-calling sample is available in the VS Code Extension Samples repository. ### Implement tool calling yourself For more advanced scenarios, you can also implement tool calling yourself. Optionally, you can use the `@vscode/prompt-tsx` library for crafting the LLM prompts. By implementing tool calling yourself, you have more control over the tool-calling process. For example, to perform additional validation or to handle tool responses in a specific way before sending them to the LLM. View the full source code for implementing tool calling by using prompt-tsx in the VS Code Extension Samples repository. ## Measuring success We recommend that you measure the success of your participant by adding telemetry logging for `Unhelpful` user feedback events, and for the total number of requests that your participant handled. An initial participant success metric can then be defined as: `unhelpful_feedback_count / total_requests`. const logger = vscode.env.createTelemetryLogger({ // telemetry logging implementation goes here }); cat.onDidReceiveFeedback((feedback: vscode.ChatResultFeedback) => { // Log chat result feedback to be able to compute the success metric of the participant logger.logUsage('chatResultFeedback', { kind: feedback.kind }); }); Any other user interaction with your chat response should be measured as a positive metric (for example, the user selecting a button that was generated in a chat response). Measuring success with telemetry is crucial when working with AI, since it is a nondeterministic technology. Run experiments, measure and iteratively improve your participant to ensure a good user experience. ## Naming restrictions and conventions ### Chat participant naming conventions | Property | Description | Naming guidelines | | --- | --- | --- | | `id` | Globally unique identifier for the chat participant | * String value * Use the extension name as a prefix, followed by a unique ID for your extension * Example: `chat-sample.cat`, `code-visualizer.code-visualizer-participant` | | `name` | Name of the chat participant, referenced by users through the `@` symbol | * String value consisting of alphanumeric characters, underscores, and hyphens * It's recommended to only use lowercase to ensure consistency with existing chat participants * Ensure the purpose of the participant is obvious from its name by referencing your company name or its functionality * Some participant names are reserved. If you use a reserved name, the fully qualified name is shown, including the extension ID * Examples: `vscode`, `terminal`, `code-visualizer` | | `fullName` | (Optional) The full name of the participant, which is shown as the label for responses coming from the participant | * String value * It's recommended to use title case * Use your company name, brand name, or user-friendly name for your participant * Examples: `GitHub Copilot`, `VS Code`, `Math Tutor` | | `description` | (Optional) Short description of what the chat participant does, shown as placeholder text in the chat input field or in the list of participants | * String value * It's recommended to use sentence case, without punctuation at the end * Keep the description short to avoid horizontal scrolling * Examples: `Ask questions about VS Code`, `Generate UML diagrams for your code` | When referring to your chat participant in any of the user-facing elements, such as properties, chat responses, or chat user interface, it's recommended to not use the term _participant_, as it's the name of the API. For example, the `@cat` extension could be called "Cat extension for GitHub Copilot". ### Slash command naming conventions | Property | Description | Naming guidelines | | --- | --- | --- | | `name` | Name of the slash command, referenced by users through the `/` symbol | * String value * It's recommended to use lower camel case to align with existing slash commands * Ensure the purpose of the command is obvious from its name * Examples: `fix`, `explain`, `runCommand` | | `description` | (Optional) Short description of what the slash command does, shown as placeholder text in the chat input field or in the list of participants and commands | * String value * It's recommended to use sentence case, without punctuation at the end * Keep the description short to avoid horizontal scrolling * Examples: `Search for and execute a command in VS Code`, `Generate unit tests for the selected code` | ## Guidelines Chat participants should not be purely question-answering bots. When building a chat participant, be creative and use the existing VS Code API to create rich integrations in VS Code. Users also love rich and convenient interactions, such as buttons in your responses, menu items that bring users to your participant in chat. Think about real life scenarios where AI can help your users. It doesn't make sense for every extension to contribute a chat participant. Having too many participants in chat might lead to a bad user experience. Chat participants are best when you want to control the full prompt, including instructions to the language model. You can reuse the carefully crafted Copilot system message and you can contribute context to other participants. For example, language extensions (such as the C++ extension) can contribute in various other ways: * Contribute tools that bring language service smarts to the user query. For example, the C++ extension could resolve the `#cpp` tool to the C++ state of the workspace. This gives the Copilot language model the right C++ context to improve the quality of Copilot answers for C++. * Contribute smart actions that use the language model, optionally in combination with traditional language service knowledge, to deliver a great user experience. For example, C++ might already offer an "extract to method" smart action that uses the language model to generate a fitting default name for the new method. Chat extensions should explicitly ask for user consent if they are about to do a costly operation or are about to edit or delete something that can't be undone. To have a great user experience, we discourage extensions from contributing multiple chat participants. Up to one chat participant per extension is a simple model that scales well in the UI. ## Publishing your extension Once you have created your AI extension, you can publish your extension to the Visual Studio Marketplace: * Before publishing to the VS Marketplace we recommend that you read the Microsoft AI tools and practices guidelines. These guidelines provide best practices for the responsible development and use of AI technologies. * By publishing to the VS Marketplace, your extension is adhering to the GitHub Copilot extensibility acceptable development and use policy. * Upload to the Marketplace as described in Publishing Extension. * If your extension already contributes functionality other than chat, we recommend that you do not introduce an extension dependency on GitHub Copilot in the extension manifest. This ensures that extension users that do not use GitHub Copilot can use the non-chat functionality without having to install GitHub Copilot. ## Related content * Video: Enhancing VS Code extensions with GitHub Copilot * Use the Language Model API in your extension * GitHub Copilot Trust Center 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/chat-tutorial In this article In this tutorial, you'll learn how to create a Visual Studio Code extension that integrates with the GitHub Copilot Chat experience. You'll use the Chat extension API to contribute a chat participant. Your participant will be a code tutor that can provide explanations and sample exercises for programming concepts. ## Prerequisites You'll need the following tools and accounts to complete this tutorial: * Visual Studio Code * GitHub Copilot * Node.js ## Step 1: Set up your project First, generate the extension project by using Yeoman and the VS Code Extension Generator. npx --package yo --package generator-code -- yo code Select the following options to complete the setup: # ? What type of extension do you want to create? New Extension (TypeScript) # ? What's the name of your extension? Code Tutor ### Press <Enter> to choose default for all options below ### # ? What's the identifier of your extension? code-tutor # ? What's the description of your extension? LEAVE BLANK # ? Initialize a git repository? Yes # ? Bundle the source code with webpack? No # ? Which package manager to use? npm # ? Do you want to open the new folder with Visual Studio Code? Open with `code` Once your extension project is generated, there are two files you will be working in: `extension.ts` and `package.json`, which you can learn more about in the Extension Anatomy docs. As a quick overview: * `extension.ts` is the main entry point for your extension and contains the logic for your chat participant. * `package.json` contains the metadata for your extension, such as the name and description of your participant. Delete the auto-generated code in the `extension.ts` `activate()` method. This is where you will place our logic for our chat participant. ## Step 2: Register a Chat participant In the `package.json` file, replace the auto-generated `contributes` section with the following: "contributes":{ "chatParticipants": [ { "id": "chat-tutorial.code-tutor", "fullName": "Code Tutor", "name": "tutor", "description": "What can I teach you?", "isSticky": true } ] } This code registers a chat participant with the following attributes: * Unique ID `chat-tutorial.code-tutor`, which will be referenced in the code * Full name `Code Tutor`, which will be shown in the title area of a response from your participant * Name `tutor`, which will be used to reference the chat participant as `@tutor` in the Chat view * Description "What can I teach you?", which will be shown in the chat input field as a placeholder text Finally, setting `isSticky: true` will automatically prepend the participant name in the chat input field after the user has started interacting with the participant. ## Step 3: Craft the prompt Now that the participant is registered, you can start implementing the logic for the code tutor. In the `extension.ts` file, you will define a prompt for the requests. Crafting a good prompt is the key to getting the best response from your participant. Check out this article for tips on prompt engineering. Your code tutor should emulate a real-world tutor by guiding the student to understand the concept instead of providing direct answers. Additionally, the tutor should remain focused on the topic and refrain from answering non-programming questions. Consider the following two prompts. Which is more likely to give the specified behavior? 1. > You are a helpful code tutor. Your job is to teach the user with simple descriptions and sample code of the concept. 2. > You are a helpful code tutor. Your job is to teach the user with simple descriptions and sample code of the concept. Respond with a guided overview of the concept in a series of messages. Do not give the user the answer directly, but guide them to find the answer themselves. If the user asks a non-programming question, politely decline to respond. The second prompt is more specific and gives the participant a clear direction on how to respond. Add this prompt in the `extension.ts` file. const BASE_PROMPT = 'You are a helpful code tutor. Your job is to teach the user with simple descriptions and sample code of the concept. Respond with a guided overview of the concept in a series of messages. Do not give the user the answer directly, but guide them to find the answer themselves. If the user asks a non-programming question, politely decline to respond.'; ## Step 4: Implement the request handler Now that the prompt is selected, you need to implement the request handler. This is what will process the user's chat request. You will define the request handler, perform logic for processing the request, and return a response to the user. First, define the handler: // define a chat handler const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ) => { return; }; Within the body of this handler, initialize the prompt and a `messages` array with the prompt. Then, send in what the user typed in the chat box. You can access this through `request.prompt`. Send the request using `request.model.sendRequest`, which will send the request using the currently selected model. Finally, stream the response to the user. // define a chat handler const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ) => { // initialize the prompt let prompt = BASE_PROMPT; // initialize the messages array with the prompt const messages = [vscode.LanguageModelChatMessage.User(prompt)]; // add in the user's message messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); // send the request const chatResponse = await request.model.sendRequest(messages, {}, token); // stream the response for await (const fragment of chatResponse.text) { stream.markdown(fragment); } return; }; ## Step 5: Create the chat participant Once the handler is implemented, the last step is to create the chat participant by using the `createChatParticipant` method in the Chat extension API. Make sure to use the same ID that you used in the `package.json`. You should further customize your participant by adding an icon for it. This will show in the Chat view when interacting with the participant. // define a chat handler const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ) => { // initialize the prompt let prompt = BASE_PROMPT; // initialize the messages array with the prompt const messages = [vscode.LanguageModelChatMessage.User(prompt)]; // add in the user's message messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); // send the request const chatResponse = await request.model.sendRequest(messages, {}, token); // stream the response for await (const fragment of chatResponse.text) { stream.markdown(fragment); } return; }; // create participant const tutor = vscode.chat.createChatParticipant('chat-tutorial.code-tutor', handler); // add icon to participant tutor.iconPath = vscode.Uri.joinPath(context.extensionUri, 'tutor.jpeg'); ## Step 6: Run the code You are now ready to try out your chat participant! Press F5 to run the code. A new window of VS Code will open with your chat participant. In the Copilot Chat pane, you can now invoke your participant by typing `@tutor`!  Test it out by typing what you want to learn about. You should see a response giving you an overview of the concept! If you type a related message to continue the conversation, you'll notice that the participant doesn't give a follow-up response based on your conversation. That's because our current participant is only sending in the user's current message, and not the participant message history. In the screenshot below, the tutor correctly responds with a starting explanation of stacks. However, in the follow-up, it does not understand that the user is continuing the conversation to see an implementation of stacks in Python, so it instead gives a generic response about Python.  ## Step 7: Add message history for more context One of the biggest values of Copilot Chat is the ability to iterate over several messages to get the best response. To do this, you want to send in the participant's message history to the chat request. You can access this through `context.history`. You'll need to retrieve that history and add it to the `messages` array. You will need to do this before the `request.prompt` is added. // define a chat handler const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ) => { // initialize the prompt let prompt = BASE_PROMPT; // initialize the messages array with the prompt const messages = [vscode.LanguageModelChatMessage.User(prompt)]; // get all the previous participant messages const previousMessages = context.history.filter( h => h instanceof vscode.ChatResponseTurn ); // add the previous messages to the messages array previousMessages.forEach(m => { let fullMessage = ''; m.response.forEach(r => { const mdPart = r as vscode.ChatResponseMarkdownPart; fullMessage += mdPart.value.value; }); messages.push(vscode.LanguageModelChatMessage.Assistant(fullMessage)); }); // add in the user's message messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); // send the request const chatResponse = await request.model.sendRequest(messages, {}, token); // stream the response for await (const fragment of chatResponse.text) { stream.markdown(fragment); } return; }; Now when you run the code, you can have a conversation with your participant with all the context of the previous messages! In the screenshot below, the participant correctly understands that the user is requesting to see an implementation of stacks in Python.  ## Step 8: Add a command Now that the basic participant is implemented, you can extend it by adding a command. Commands are a shorthand notation for common user intents, and are indicated by the `/` symbol. The extension can then use the command to prompt the language model accordingly. It would be great to add a command to prompt your tutor to give a practice exercise for a concept. You'll need to register the command in the `package.json` file and implement the logic in `extension.ts`. You can name the command `exercise` so that it can be invoked by typing `/exercise`. In `package.json` add the `commands` property to the `chatParticipants` property. Here, you'll specify the name of the command and a quick description: "contributes": { "chatParticipants": [ { "id": "chat-tutorial.code-tutor", "fullName": "Code Tutor", "name": "tutor", "description": "What can I teach you?", "isSticky": true, "commands": [ { "name": "exercise", "description": "Provide exercises to practice a concept." } ] } ] }, To implement the logic for getting sample exercises from the tutor, the simplest way is to change the prompt that you send in to the request. Create a new prompt, `EXERCISES_PROMPT`, that asks the participant to return sample exercises. Here's an example of what that could look like: const EXERCISES_PROMPT = 'You are a helpful tutor. Your job is to teach the user with fun, simple exercises that they can complete in the editor. Your exercises should start simple and get more complex as the user progresses. Move one concept at a time, and do not move on to the next concept until the user provides the correct answer. Give hints in your exercises to help the user learn. If the user is stuck, you can provide the answer and explain why it is the answer. If the user asks a non-programming question, politely decline to respond.'; In the request handler, you then need to add logic to detect that the user referenced the command. You can do this through the `request.command` property. If the command is referenced, update the prompt to the newly created `EXERCISES_PROMPT` // define a chat handler const handler: vscode.ChatRequestHandler = async ( request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken ) => { // initialize the prompt let prompt = BASE_PROMPT; if (request.command === 'exercise') { prompt = EXERCISES_PROMPT; } // initialize the messages array with the prompt const messages = [vscode.LanguageModelChatMessage.User(prompt)]; // get all the previous participant messages const previousMessages = context.history.filter( h => h instanceof vscode.ChatResponseTurn ); // add the previous messages to the messages array previousMessages.forEach(m => { let fullMessage = ''; m.response.forEach(r => { const mdPart = r as vscode.ChatResponseMarkdownPart; fullMessage += mdPart.value.value; }); messages.push(vscode.LanguageModelChatMessage.Assistant(fullMessage)); }); // add in the user's message messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); // send the request const chatResponse = await request.model.sendRequest(messages, {}, token); // stream the response for await (const fragment of chatResponse.text) { stream.markdown(fragment); } return; }; And that's all that needs to be added! The rest of the logic to get the message history, send the request, and stream the request all stays the same. Now you can type `/exercise`, which will bring up your chat participant, and you can get interactive exercises to practice coding!  ## Next steps Congratulations! You have successfully created a chat participant that can provide explanations and sample exercises for programming concepts. You can further extend your participant by fine-tuning the prompts, adding more slash commands, or leveraging other APIs like the Language Model API. Once ready, you can also publish your extension to the Visual Studio Code Marketplace. You can find the complete source code for this tutorial in the vscode-extensions-sample repository. ## Related content * Chat API extension guide * Tutorial: Generate AI-powered code annotations by using the Language Model API * Language Model API extension guide 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/language-model In this article The Language Model API enables you to use the Language Model and integrate AI-powered features and natural language processing in your Visual Studio Code extension. You can use the Language Model API in different types of extensions. A typical use for this API is in chat extensions, where you use a language model to interpret the user's request and help provide an answer. However, the use of the Language Model API is not limited to this scenario. You might use a language model in a language or debugger extension, or as part of a command or task in a custom extension. For example, the Rust extension might use the Language Model to offer default names to improve its rename experience. The process for using the Language Model API consists of the following steps: 1. Build the language model prompt 2. Send the language model request 3. Interpret the response The following sections provide more details on how to implement these steps in your extension. ## Links * Chat extension sample * LanguageModels API * @vscode/prompt-tsx npm package ## Build the language model prompt To interact with a language model, extensions should first craft their prompt, and then send a request to the language model. You can use prompts to provide instructions to the language model on the broad task that you're using the model for. Prompts can also define the context in which user messages are interpreted. The Language Model API supports two types of messages when building the language model prompt: * **User** - used for providing instructions and the user's request * **Assistant** - used for adding the history of previous language model responses as context to the prompt > **Note**: Currently, the Language Model API doesn't support the use of system messages. You can use two approaches for building the language model prompt: * `LanguageModelChatMessage` - create the prompt by providing one or more messages as strings. You might use this approach if you're just getting started with the Language Model API. * `@vscode/prompt-tsx` - declare the prompt by using the TSX syntax. You can use the `prompt-tsx` library if you want more control over how the language model prompt is composed. For example, the library can help with dynamically adapting the length of the prompt to each language model's context window size. Learn more about `@vscode/prompt-tsx` or explore the chat extension sample to get started. To learn more about the concepts of prompt engineering, we suggest reading OpenAI's excellent Prompt engineering guidelines. > **Tip:** take advantage of the rich VS Code extension API to get the most relevant context and include it in your prompt. For example, to include the contents of the active file in the editor. ### Use the `LanguageModelChatMessage` class The Language Model API provides the `LanguageModelChatMessage` class to represent and create chat messages. You can use the `LanguageModelChatMessage.User` or `LanguageModelChatMessage.Assistant` methods to create user or assistant messages respectively. In the following example, the first message provides context for the prompt: * The persona used by the model in its replies (in this case, a cat) * The rules the model should follow when generating responses (in this case, explaining computer science concepts in a funny manner by using cat metaphors) The second message then provides the specific request or instruction coming from the user. It determines the specific task to be accomplished, given the context provided by the first message. const craftedPrompt = [ vscode.LanguageModelChatMessage.User( 'You are a cat! Think carefully and step by step like a cat would. Your job is to explain computer science concepts in the funny manner of a cat, using cat metaphors. Always start your response by stating what concept you are explaining. Always include code samples.' ), vscode.LanguageModelChatMessage.User('I want to understand recursion') ]; ## Send the language model request Once you've built the prompt for the language model, you first select the language model you want to use with the `selectChatModels` method. This method returns an array of language models that match the specified criteria. If you are implementing a chat participant, we recommend that you instead use the model that is passed as part of the `request` object in your chat request handler. This ensures that your extension respects the model that the user chose in the chat model dropdown. Then, you send the request to the language model by using the `sendRequest` method. To select the language model, you can specify the following properties: `vendor`, `id`, `family`, or `version`. Use these properties to either broadly match all models of a given vendor or family, or select one specific model by its ID. Learn more about these properties in the API reference. > **Note**: Currently, `gpt-4o`, `gpt-4o-mini`, `o1`, `o1-mini`, `claude-3.5-sonnet` are supported for the language model family. If you are unsure what model to use, we recommend `gpt-4o` for it's performance and quality. For interactions directly in the editor, we recommend `gpt-4o-mini` for it's performance. If there are no models that match the specified criteria, the `selectChatModels` method returns an empty array. Your extension must appropriately handle this case. The following example shows how to select all `Copilot` models, regardless of the family or version: const models = await vscode.lm.selectChatModels({ vendor: 'copilot' }); // No models available if (models.length === 0) { // TODO: handle the case when no models are available } > **Important**: Copilot's language models require consent from the user before an extension can use them. Consent is implemented as an authentication dialog. Because of that, `selectChatModels` should be called as part of a user-initiated action, such as a command. After you select a model, you can send a request to the language model by invoking the `sendRequest` method on the model instance. You pass the prompt you crafted earlier, along with any additional options, and a cancellation token. When you make a request to the Language Model API, the request might fail. For example, because the model doesn't exist, or the user didn't give consent to use the Language Model API, or because quota limits are exceeded. Use `LanguageModelError` to distinguish between different types of errors. The following code snippet shows how to make a language model request: try { const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' }); const request = model.sendRequest(craftedPrompt, {}, token); } catch (err) { // Making the chat request might fail because // - model does not exist // - user consent not given // - quota limits were exceeded if (err instanceof vscode.LanguageModelError) { console.log(err.message, err.code, err.cause); if (err.cause instanceof Error && err.cause.message.includes('off_topic')) { stream.markdown( vscode.l10n.t("I'm sorry, I can only explain computer science concepts.") ); } } else { // add other error handling logic throw err; } } ## Interpret the response After you've sent the request, you have to process the response from the language model API. Depending on your usage scenario, you can pass the response directly on to the user, or you can interpret the response and perform extra logic. The response (`LanguageModelChatResponse`) from the Language Model API is streaming-based, which enables you to provide a smooth user experience. For example, by reporting results and progress continuously when you use the API in combination with the Chat API. Errors might occur while processing the streaming response, such as network connection issues. Make sure to add appropriate error handling in your code to handle these errors. The following code snippet shows how an extension can register a command, which uses the language model to change all variable names in the active editor with funny cat names. Notice that the extension streams the code back to the editor for a smooth user experience. vscode.commands.registerTextEditorCommand( 'cat.namesInEditor', async (textEditor: vscode.TextEditor) => { // Replace all variables in active editor with cat names and words const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' }); let chatResponse: vscode.LanguageModelChatResponse | undefined; const text = textEditor.document.getText(); const messages = [ vscode.LanguageModelChatMessage .User(`You are a cat! Think carefully and step by step like a cat would. Your job is to replace all variable names in the following code with funny cat variable names. Be creative. IMPORTANT respond just with code. Do not use markdown!`), vscode.LanguageModelChatMessage.User(text) ]; try { chatResponse = await model.sendRequest( messages, {}, new vscode.CancellationTokenSource().token ); } catch (err) { if (err instanceof vscode.LanguageModelError) { console.log(err.message, err.code, err.cause); } else { throw err; } return; } // Clear the editor content before inserting new content await textEditor.edit(edit => { const start = new vscode.Position(0, 0); const end = new vscode.Position( textEditor.document.lineCount - 1, textEditor.document.lineAt(textEditor.document.lineCount - 1).text.length ); edit.delete(new vscode.Range(start, end)); }); try { // Stream the code into the editor as it is coming in from the Language Model for await (const fragment of chatResponse.text) { await textEditor.edit(edit => { const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1); const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length); edit.insert(position, fragment); }); } } catch (err) { // async response stream may fail, e.g network interruption or server side error await textEditor.edit(edit => { const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1); const position = new vscode.Position(lastLine.lineNumber, lastLine.text.length); edit.insert(position, (<Error>err).message); }); } } ); ## Considerations ### Model availability We don't expect specific models to stay supported forever. When you reference a language model in your extension, make sure to take a "defensive" approach when sending requests to that language model. This means that you should gracefully handle cases where you don't have access to a particular model. ### Choosing the appropriate model Extension authors can choose which model is the most appropriate for their extension. We recommend using `gpt-4o` for its performance and quality. To get a full list of available models, you can use this code snippet: const allModels = await vscode.lm.selectChatModels(MODEL_SELECTOR); > **Note**: The recommended GPT-4o model has a limit of `64K` tokens. The returned model object from the `selectChatModels` call has a `maxInputTokens` attribute that shows the token limit. These limits will be expanded as we learn more about how extensions are using the language models. ### Rate limiting Extensions should responsibly use the language model and be aware of rate limiting. VS Code is transparent to the user regarding how extensions are using language models and how many requests each extension is sending and how that influences their respective quotas. Extensions should not use the Language Model API for integration tests due to rate-limitations. Internally, VS Code uses a dedicated non-production language model for simulation testing, and we are currently thinking how to provide a scalable language model testing solution for extensions. ## Testing your extension The responses that the Language Model API provides are nondeterministic, which means that you might get a different response for an identical request. This behavior can be challenging for testing your extension. The part of the extension for building prompts and interpreting language model responses is deterministic, and can thus be unit tested without using an actual language model. However, interacting and getting responses from the language model itself, is nondeterministic and canโt be easily tested. Consider designing your extension code in a modular way to enable you to unit test the specific parts that can be tested. ## Publishing your extension Once you have created your AI extension, you can publish your extension to the Visual Studio Marketplace: * Before publishing to the VS Marketplace we recommend that you read the Microsoft AI tools and practices guidelines. These guidelines provide best practices for the responsible development and use of AI technologies. * By publishing to the VS Marketplace, your extension is adhering to the GitHub Copilot extensibility acceptable development and use policy. * If your extension already contributes functionality other than using the Language Model API, we recommend that you do not introduce an extension dependency on GitHub Copilot in the extension manifest. This ensures that extension users that do not use GitHub Copilot can use the non language model functionality without having to install GitHub Copilot. Make sure to have appropriate error handling when accessing language models for this case. * Upload to the Marketplace as described in Publishing Extension. ## Related content * Build a VS Code chat extension * Learn more about @vscode/prompt-tsx * Chat extension sample * GitHub Copilot Trust Center 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/language-model-tutorial In this article In this tutorial, You'll learn how to create a VS Code extension to build an AI-powered Code Tutor. You use the Language Model (LM) API to generate suggestions to improve your code and take advantage of the VS Code extension APIs to integrate it seamlessly in the editor as inline annotations that the user can hover over for more information. After you complete this tutorial, you will know how to implement custom AI features in VS Code.  ## Prerequisites You'll need the following tools and accounts to complete this tutorial: * Visual Studio Code * GitHub Copilot * Node.js ## Scaffold out the extension First, use Yeoman and VS Code Extension Generator to scaffold a TypeScript or JavaScript project ready for development. npx --package yo --package generator-code -- yo code Select the following options to complete the new extension wizard... # ? What type of extension do you want to create? New Extension (TypeScript) # ? What's the name of your extension? Code Tutor ### Press <Enter> to choose default for all options below ### # ? What's the identifier of your extension? code-tutor # ? What's the description of your extension? LEAVE BLANK # ? Initialize a git repository? Yes # ? Bundle the source code with webpack? No # ? Which package manager to use? npm # ? Do you want to open the new folder with Visual Studio Code? Open with `code` ## Modify the package.json file to include the correct commands The scaffolded project includes a single "helloWorld" command in the `package.json` file. This command is what shows up in the Command Palette when your extension is installed. "contributes": { "commands": [ { "command": "code-tutor.helloWorld", "title": "Hello World" } ] } Since we're building a Code Tutor extension that will be adding annotations to lines, we'll need a command to allow the user to toggle these annotations on and off. Update the `command` and `title` properties: "contributes": { "commands": [ { "command": "code-tutor.annotate", "title": "Toggle Tutor Annotations" } ] } While the `package.json` defines the commands and UI elements for an extension, the `src/extension.ts` file is where you put the code that should be executed for those commands. Open the `src/extension.ts` file and change the `registerCommand` method so that it matches the `command` property in the `package.json` file. const disposable = vscode.commands.registerCommand('code-tutor.annotate', () => { Run the extension by pressing F5. This will open a new VS Code instance with the extension installed. Open the Command Palette by pressing โงโP (Windows, Linux Ctrl+Shift+P), and search for "tutor". You should see the "Tutor Annotations" command.  If you select the "Tutor Annotations" command, you'll see a "Hello World" notification message.  ## Implement the "annotate" command To get our Code Tutor annotations working, we need to send it some code and ask it to provide annotations. We'll do this in three steps: 1. Get the code with line numbers from the current tab the user has open. 2. Send that code to the Language Model API along with a custom prompt that instructs the model on how to provide annotations. 3. Parse the annotations and display them in the editor. ### Step 1: Get the code with line numbers To get the code from the current tab, we need a reference to the tab that the user has open. We can get that by modifying the `registerCommand` method to be a `registerTextEditorCommand`. The difference between these two commands is that the latter gives us a reference to the tab that the user has open, called the `TextEditor`. const disposable = vscode.commands.registerTextEditorCommand('code-tutor.annotate', async (textEditor: vscode.TextEditor) => { Now we can use the `textEditor` reference to get all of the code in the "viewable editor space". This is the code that can be seen on the screen - it does not include code that is either above or below what is in the viewable editor space. Add the following method directly above the `export function deactivate() { }` line at the bottom of the `extension.ts` file. function getVisibleCodeWithLineNumbers(textEditor: vscode.TextEditor) { // get the position of the first and last visible lines let currentLine = textEditor.visibleRanges[0].start.line; const endLine = textEditor.visibleRanges[0].end.line; let code = ''; // get the text from the line at the current position. // The line number is 0-based, so we add 1 to it to make it 1-based. while (currentLine < endLine) { code += `${currentLine + 1}: ${textEditor.document.lineAt(currentLine).text} \n`; // move to the next line position currentLine++; } return code; } This code uses the `visibleRanges` property of the TextEditor to get the position of the lines that are currently visible in the editor. It then starts with the first line position and moves to the last line position, adding each line of code to a string along with the line number. Finally, it returns the string that contains all the viewable code with line numbers. Now we can call this method from the `code-tutor.annotate` command. Modify the implementation of the command so that it looks like this: const disposable = vscode.commands.registerTextEditorCommand( 'code-tutor.annotate', async (textEditor: vscode.TextEditor) => { // Get the code with line numbers from the current editor const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor); } ); ### Step 2: Send code and prompt to language model API The next step is to call the GitHub Copilot language model and send it the user's code along with instructions to create the annotations. To do this, we first need to specify which chat model we want to use. We select 4o here because it is a fast and capable model for the kind of interaction we are building. const disposable = vscode.commands.registerTextEditorCommand( 'code-tutor.annotate', async (textEditor: vscode.TextEditor) => { // Get the code with line numbers from the current editor const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor); // select the 4o chat model let [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' }); } ); We need instructions - or a "prompt" - that will tell the model to create the annotations and what format we want the response to be. Add the following code to the top of the file directly under the imports. const ANNOTATION_PROMPT = `You are a code tutor who helps students learn how to write better code. Your job is to evaluate a block of code that the user gives you and then annotate any lines that could be improved with a brief suggestion and the reason why you are making that suggestion. Only make suggestions when you feel the severity is enough that it will impact the readability and maintainability of the code. Be friendly with your suggestions and remember that these are students so they need gentle guidance. Format each suggestion as a single JSON object. It is not necessary to wrap your response in triple backticks. Here is an example of what your response should look like: { "line": 1, "suggestion": "I think you should use a for loop instead of a while loop. A for loop is more concise and easier to read." }{ "line": 12, "suggestion": "I think you should use a for loop instead of a while loop. A for loop is more concise and easier to read." } `; This is a special prompt that instructs the language model on how to generate annotations. It also includes examples for how the model should format its response. These examples (also called, "multi-shot") are what enable us to define what the format the response will be so that we can parse it and display it as annotations. We pass messages to the model in an array. This array can contain as many messages as you like. In our case, it contains the prompt followed by the users code with line numbers. const disposable = vscode.commands.registerTextEditorCommand( 'code-tutor.annotate', async (textEditor: vscode.TextEditor) => { // Get the code with line numbers from the current editor const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor); // select the 4o chat model let [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' }); // init the chat message const messages = [ vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT), vscode.LanguageModelChatMessage.User(codeWithLineNumbers) ]; } ); To send the messages to the model, we need to first make sure the selected model is available. This handles cases where the extension is not ready or the user is not signed in to GitHub Copilot. Then we send the messages to the model. const disposable = vscode.commands.registerTextEditorCommand( 'code-tutor.annotate', async (textEditor: vscode.TextEditor) => { // Get the code with line numbers from the current editor const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor); // select the 4o chat model let [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' }); // init the chat message const messages = [ vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT), vscode.LanguageModelChatMessage.User(codeWithLineNumbers) ]; // make sure the model is available if (model) { // send the messages array to the model and get the response let chatResponse = await model.sendRequest( messages, {}, new vscode.CancellationTokenSource().token ); // handle chat response await parseChatResponse(chatResponse, textEditor); } } ); Chat responses come in as fragments. These fragments usually contain single words, but sometimes they contain just punctuation. In order to display annotations as the response streams in, we want to wait until we have a complete annotation before we display it. Because of the way we have instructed our model to return its response, we know that when we see a closing `}` we have a complete annotation. We can then parse the annotation and display it in the editor. Add the missing `parseChatResponse` function above the `getVisibleCodeWithLineNumbers` method in the `extension.ts` file. async function parseChatResponse( chatResponse: vscode.LanguageModelChatResponse, textEditor: vscode.TextEditor ) { let accumulatedResponse = ''; for await (const fragment of chatResponse.text) { accumulatedResponse += fragment; // if the fragment is a }, we can try to parse the whole line if (fragment.includes('}')) { try { const annotation = JSON.parse(accumulatedResponse); applyDecoration(textEditor, annotation.line, annotation.suggestion); // reset the accumulator for the next line accumulatedResponse = ''; } catch (e) { // do nothing } } } } We need one last method to actually display the annotations. VS Code calls these "decorations". Add the following method above the `parseChatResponse` method in the `extension.ts` file. function applyDecoration(editor: vscode.TextEditor, line: number, suggestion: string) { const decorationType = vscode.window.createTextEditorDecorationType({ after: { contentText: ` ${suggestion.substring(0, 25) + '...'}`, color: 'grey' } }); // get the end of the line with the specified line number const lineLength = editor.document.lineAt(line - 1).text.length; const range = new vscode.Range( new vscode.Position(line - 1, lineLength), new vscode.Position(line - 1, lineLength) ); const decoration = { range: range, hoverMessage: suggestion }; vscode.window.activeTextEditor?.setDecorations(decorationType, [decoration]); } This method takes in our parsed annotation from the model and uses it to create a decoration. This is done by first creating a `TextEditorDecorationType` that specifies the appearance of the decoration. In this case, we are just adding a grey annotation and truncating it to 25 characters. We'll show the full message when the user hovers over the message. We are then setting where the decoration should appear. We need it to be on the line number that was specified in the annotation, and at the end of the line. Finally, we set the decoration on the active text editor which is what causes the annotation to appear in the editor. If your extension is still running, restart it by selecting the green arrow from the debug bar. If you closed the debug session, press F5 to run the extension. Open a code file in the new VS Code window instance that opens. When you select "Toggle Tutor Annotations" from the Command Palette, you should see the code annotations appear in the editor.  ## Add a button to the editor title bar You can enable your command to be invoked from places other than the Command Palette. In our case, we can add a button to the top of the current tab that allows the user to easily toggle the annotations. To do this, modify the "contributes" portion of the `package.json` as follows: "contributes": { "commands": [ { "command": "code-tutor.annotate", "title": "Toggle Tutor Annotations", "icon": "$(comment)" } ], "menus": { "editor/title": [ { "command": "code-tutor.annotate", "group": "navigation" } ] } } This causes a button to appear in the navigation area (right-side) of the editor title bar. The "icon" comes from the Product Icon Reference. Restart your extension with the green arrow or press F5 if the extension is not already running. You should now see a comment icon that will trigger the "Toggle Tutor Annotations" command.  ## Next Steps In this tutorial, you learned how to create a VS Code extension that integrates AI into the editor with the language model API. You used the VS Code extension API to get the code from the current tab, sent it to the model with a custom prompt, and then parsed and displayed the model result right in the editor using decorators. Next, you can extend your Code Tutor extension to include a chat participant as well which will allow users to interact directly with your extension via the GitHub Copilot chat interface. You can also explore the full range of API's in VS Code to explore new ways of building custom AI experiences your editor. You can find the complete source code for this tutorial in the vscode-extensions-sample repository. ## Related content * Language Model API extension guide * Tutorial: Create a code tutor chat participant with the Chat API * VS Code Chat API reference 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/tools In this article Language model tools enable you to extend the functionality of a large language model (LLM). VS Code surfaces tools contributed by extensions in Copilot agent mode. By contributing a tool in a VS Code extension, you can combine the power of agentic coding with deep VS Code integration via its extension APIs. In this extension guide, you'll learn how to create a language model tool and how to implement tool calling in a chat extension. ## What is tool calling in an LLM? A language model tool is a function that can be invoked as part of a language model request. For example, you might have a function that retrieves information from a database, performs some calculation, or calls some online API. When you contribute a tool in a VS Code extension, agent mode can then invoke the tool based on the context of the conversation. The LLM never actually executes the tool itself, instead the LLM generates the parameters that are used to call your tool. It's important to clearly describe the tool's purpose, functionality, and input parameters so that the tool can be invoked in the right context. Read more about function calling in the OpenAI documentation. ## Why implement a language model tool in your extension? By implementing a language model tool in your extension, you can: * **Extend agent mode** with specialized tools that are automatically invoked as part of responding to a user prompt. For example, enable database scaffolding and querying as part of a chat conversation. * **Deeply integrate with VS Code** by using the broad set of extension APIs. For example, use the debug APIs to augment a user's debugging experience. ## Create a language model tool You can create a tool that performs a specific task, such as retrieving information from a database, finding files, or performing calculations. Implementing a language model tool consists of two main parts: 1. Define the tool's configuration in the `package.json` file of your extension. 2. Implement the tool in your extension code. ### Static configuration in `package.json` The static configuration of the tool is defined in the `package.json` file of your extension. This includes the tool name, description, input schema, and other metadata. 1. Add an entry for your tool in the `contributes.languageModelTools` section of your extension's `package.json` file. 2. Give the tool a unique name: | Property | Description | | --- | --- | | `name` | The unique name of the tool, used to reference the tool in the extension implementation code. Format the name in the format `{verb}_{noun}`. See naming guidelines. | | `displayName` | The user-friendly name of the tool, used for displaying in the UI. | 3. If the tool can be used in agent mode or referenced in a chat prompt, add the following properties: Users can enable or disable the tool in the Chat view, similar to how this is done for Model Context Protocol (MCP) tools. | Property | Description | | --- | --- | | `canBeReferencedInPrompt` | Set to `true` if the tool can be used in agent mode or referenced in chat. | | `toolReferenceName` | The name for users to reference the tool in a chat prompt via `#`. | | `icon` | The icon to display for the tool in the UI. | | `userDescription` | User-friendly description of the tool, used for displaying in the UI. | 4. Add a detailed description in `modelDescription`: * What exactly does the tool do? * What kind of information does it return? * When should and shouldn't it be used? * Describe important limitations or constraints of the tool. 5. If the tool takes input parameters, add an `inputSchema` property that describes the tool's input parameters. This JSON schema describes an object with the properties that the tool takes as input, and whether they are required. File paths should be absolute paths. Describe what each parameter does and how it relates to the tool's functionality. 6. Control when the tool is available by using a `when` clause. The `languageModelTools` contribution point lets you restrict when a tool is available for agent mode or can be referenced in a prompt by using a when clause. For example, a tool that gets the debug call stack information, should only be available when the user is debugging. "contributes": { "languageModelTools": [ { "name": "chat-tools-sample_tabCount", ... "when": "debugState == 'running'" } ] } **Example tool definition**: The following example shows how to define a tool that counts the number of active tabs in a tab group. "contributes": { "languageModelTools": [ { "name": "chat-tools-sample_tabCount", "tags": [ "editors", "chat-tools-sample" ], "toolReferenceName": "tabCount", "displayName": "Tab Count", "modelDescription": "The number of active tabs in a tab group in VS Code.", "userDescription": "Count the number of active tabs in a tab group.", "canBeReferencedInPrompt": true, "icon": "$(files)", "inputSchema": { "type": "object", "properties": { "tabGroup": { "type": "number", "description": "The index of the tab group to check. This is optional- if not specified, the active tab group will be checked.", "default": 0 } } } } ] } ### Tool implementation 1. On activation of the extension, register the tool with `vscode.lm.registerTool`. Provide the name of the tool as you specified it in the `name` property in `package.json`. If you want the tool to be private to your extension, skip the tool registration step. export function registerChatTools(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.lm.registerTool('chat-tools-sample_tabCount', new TabCountTool()) ); } 2. Create a class that implements the `vscode.LanguageModelTool<>` interface. 3. Add tool confirmation messages in the `prepareInvocation` method. A generic confirmation dialog will always be shown for tools from extensions, but the tool can customize the confirmation message. Give enough context to the user to understand what the tool is doing. The message can be a `MarkdownString` containing a code block. The following example shows how to provide a confirmation message for the tab count tool. async prepareInvocation( options: vscode.LanguageModelToolInvocationPrepareOptions<ITabCountParameters>, _token: vscode.CancellationToken ) { const confirmationMessages = { title: 'Count the number of open tabs', message: new vscode.MarkdownString( `Count the number of open tabs?` + (options.input.tabGroup !== undefined ? ` in tab group ${options.input.tabGroup}` : '') ), }; return { invocationMessage: 'Counting the number of tabs', confirmationMessages, }; } If `prepareInvocation` returned `undefined`, the generic confirmation message will be shown. Note that the user can also select to "Always Allow" a certain tool. 4. Define an interface that describes the tool input parameters. The interface is used in the `invoke` method. The input parameters are validated against the JSON schema defined in `inutSchema` in `package.json`. The following example shows the interface for the tab count tool. export interface ITabCountParameters { tabGroup?: number; } 5. Implement the `invoke` method, which is called when the tool is invoked. The `invoke` method receives the tool input parameters in the `options` parameter. The parameters are validated against the JSON schema defined in `inputSchema` in `package.json`. When an error occurs, throw an error with a message that makes sense to the LLM. Optionally, provide instructions on what the LLM should do next, such as retrying with different parameters, or performing a different action. The following example shows the implementation of the tab count tool. The result of the tool is an instance of type `vscode.LanguageModelToolResult`. async invoke( options: vscode.LanguageModelToolInvocationOptions<ITabCountParameters>, _token: vscode.CancellationToken ) { const params = options.input; if (typeof params.tabGroup === 'number') { const group = vscode.window.tabGroups.all[Math.max(params.tabGroup - 1, 0)]; const nth = params.tabGroup === 1 ? '1st' : params.tabGroup === 2 ? '2nd' : params.tabGroup === 3 ? '3rd' : `${params.tabGroup}th`; return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open in the ${nth} tab group.`)]); } else { const group = vscode.window.tabGroups.activeTabGroup; return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open.`)]); } } View the full source code for implementing a language model tool in the VS Code Extension Samples repository. ## Tool-calling flow When a user sends a chat prompt, the following steps occur: 1. Copilot determines the list of available tools based on the user's configuration. The list of tools consists of built-in tools, tools registered by extensions, and tools from MCP servers. You can contribute to agent mode via extensions or MCP servers (shown in green in the diagram). 2. Copilot sends the request to the LLM, providing it with the prompt, chat context, and the list of tool definitions to consider. The LLM generates a response, which might include one or more requests to invoke a tool. 3. If needed, Copilot invokes the suggested tool(s) with the parameter values provided by the LLM. A tool response might result in additional requests for tool invocations. 4. In case of errors or follow-up tool requests, Copilot iterates over the tool-calling flow until all tool requests are resolved. 5. Copilot returns the final response to the user, which might include responses from multiple tools. The following diagram shows the Copilot tool-calling flow.  ## Getting started * Chat extension sample ## Guidelines * **Naming**: write clear and descriptive names for tools and parameters. * **Tool name**: should be unique, and clearly describe their intent. Structure the tool name in the format `{verb}_{noun}`. For example, `get_weather`, `get_azure_deployment`, or `get_terminal_output`. * **Parameter name**: should describe the parameter's purpose. Structure the parameter name in the format `{noun}`. For example, `destination_location`, `ticker`, or `file_name`. * **Descriptions**: write detailed descriptions for tools and parameters. * Describe what the tool does and when it should and shouldn't be used. For example, "This tool retrieves the weather for a given location." * Describe what each parameter does and how it relates to the tool's functionality. For example, "The `destination_location` parameter specifies the location for which to retrieve the weather. It should be a valid location name or coordinates." * Describe important limitations or constraints of the tool. For example, "This tool only retrieves weather data for locations in the United States. It might not work for other regions." * **User confirmation**: provide a confirmation message for the tool invocation. A generic confirmation dialog will always be shown for tools from extensions, but the tool can customize the confirmation message. Give enough context to the user to understand what the tool is doing. * **Error handling**: when an error occurs, throw an error with a message that makes sense to the LLM. Optionally, provide instructions on what the LLM should do next, such as retrying with different parameters, or performing a different action. Get more best practices for creating tools in the OpenAI documentation and Anthropic documentation. ## Related content * Get started with the Language Model API * Use Prompt-tsx * Add MCP servers to chat 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/prompt-tsx In this article You can build language model prompts by using string concatenation, but it's hard to compose features and make sure your prompts stay within the context window of language models. To overcome these limitations, you can use the `@vscode/prompt-tsx` library. The `@vscode/prompt-tsx` library provides the following features: * **TSX-based prompt rendering**: Compose prompts using TSX components, making them more readable and maintainable * **Priority-based pruning**: Automatically prune less important parts of prompts to fit within the model's context window * **Flexible token management**: Use properties like `flexGrow`, `flexReserve`, and `flexBasis` to cooperatively use token budgets * **Tool integration**: Integrate with VS Code's language model tools API For a complete overview of all features and detailed usage instructions, refer to the full README. This article describes practical examples of prompt design with the library. The complete code for these examples can be found in the prompt-tsx repository. ## Manage priorities in the conversation history Including conversation history in your prompt is important as it enables the user to ask follow-up questions to previous messages. However, you want to make sure its priority is treated appropriately because history can grow large over time. We've found that the pattern which makes the most sense is usually to prioritize, in order: 1. The base prompt instructions 2. The current user query 3. The last couple of turns of chat history 4. Any supporting data 5. As much of the remaining history as you can fit For this reason, split the history into two parts in the prompt, where recent prompt turns are prioritized over general contextual information. In this library, each TSX node in the tree has a priority that is conceptually similar to a zIndex where a higher number means a higher priority. ### Step 1: Define the HistoryMessages component To list history messages, define a `HistoryMessages` component. This example provides a good starting point, but you might have to expand it if you deal with more complex data types. This example uses the `PrioritizedList` helper component, which automatically assigns ascending or descending priorities to each of its children. import { UserMessage, AssistantMessage, PromptElement, BasePromptElementProps, PrioritizedList, } from '@vscode/prompt-tsx'; import { ChatContext, ChatRequestTurn, ChatResponseTurn, ChatResponseMarkdownPart } from 'vscode'; interface IHistoryMessagesProps extends BasePromptElementProps { history: ChatContext['history']; } export class HistoryMessages extends PromptElement<IHistoryMessagesProps> { render(): PromptPiece { const history: (UserMessage | AssistantMessage)[] = []; for (const turn of this.props.history) { if (turn instanceof ChatRequestTurn) { history.push(<UserMessage>{turn.prompt}</UserMessage>); } else if (turn instanceof ChatResponseTurn) { history.push( <AssistantMessage name={turn.participant}> {chatResponseToMarkdown(turn)} </AssistantMessage> ); } } return ( <PrioritizedList priority={0} descending={false}> {history} </PrioritizedList> ); } } ### Step 2: Define the Prompt component Next, define a `MyPrompt` component that includes the base instructions, user query, and history messages with their appropriate priorities. Priority values are local among siblings. Remember that you might want to trim older messages in the history before touching anything else in the prompt, so you need to split up two `<HistoryMessages>` elements: import { UserMessage, PromptElement, BasePromptElementProps, } from '@vscode/prompt-tsx'; interface IMyPromptProps extends BasePromptElementProps { history: ChatContext['history']; userQuery: string; } export class MyPrompt extends PromptElement<IMyPromptProps> { render() { return ( <> <UserMessage priority={100}> Here are your base instructions. They have the highest priority because you want to make sure they're always included! </UserMessage> {/* Older messages in the history have the lowest priority since they're less relevant */} <HistoryMessages history={this.props.history.slice(0, -2)} priority={0} /> {/* The last 2 history messages are preferred over any workspace context you have below */} <HistoryMessages history={this.props.history.slice(-2)} priority={80} /> {/* The user query is right behind the based instructions in priority */} <UserMessage priority={90}>{this.props.userQuery}</UserMessage> <UserMessage priority={70}> With a slightly lower priority, you can include some contextual data about the workspace or files here... </UserMessage> </> ); } } Now, all older history messages are pruned before the library tries to prune other elements of the prompt. ### Step 3: Define the History component To make consumption a little easier, define a `History` component that wraps the history messages and uses the `passPriority` attribute to act as a pass-through container. With `passPriority`, its children are treated as if they are direct children of the containing element for prioritization purposes. import { PromptElement, BasePromptElementProps } from '@vscode/prompt-tsx'; interface IHistoryProps extends BasePromptElementProps { history: ChatContext['history']; newer: number; // last 2 message priority values older: number; // previous message priority values passPriority: true; // require this prop be set! } export class History extends PromptElement<IHistoryProps> { render(): PromptPiece { return ( <> <HistoryMessages history={this.props.history.slice(0, -2)} priority={this.props.older} /> <HistoryMessages history={this.props.history.slice(-2)} priority={this.props.newer} /> </> ); } } Now, you can use and reuse this single element to include chat history: <History history={this.props.history} passPriority older={0} newer={80}/> ## Grow file contents to fit In this example, you want to include the contents of all files the user is currently looking at in their prompt. These files could be large, to the point where including all of them would lead to their text being pruned! This example shows how to use the `flexGrow` property to cooperatively size the file contents to fit within the token budget. ### Step 1: Define base instructions and user query First, you define a `UserMessage` component that includes the base instructions. <UserMessage priority={100}>Here are your base instructions.</UserMessage> You then include the user query by using the `UserMessage` component. This component has a high priority to ensure it is included right after the base instructions. <UserMessage priority={90}>{this.props.userQuery}</UserMessage> ### Step 2: Include the File Contents You can now include the file contents by using the `FileContext` component. You assign it a `flexGrow` value of `1` to ensure it is rendered after the base instructions, user query, and history. <FileContext priority={70} flexGrow={1} files={this.props.files} /> With a `flexGrow` value, the element gets any _unused_ token budget in its `PromptSizing` object that's passed into its `render()` and `prepare()` calls. You can read more about the behavior of flex elements in the prompt-tsx documentation. ### Step 3: Include the history Next, include the history messages using the `History` component that you created previously. This is a little trickier, since you do want some history to be shown, but also want the file contents to take up most the prompt. Therefore, assign the `History` component a `flexGrow` value of `2` to ensure it is rendered after all other elements, including `<FileContext />`. But, also set a `flexReserve` value of `"/5"` to reserve 1/5th of the total budget for history. <History history={this.props.history} passPriority older={0} newer={80} flexGrow={2} flexReserve="/5" /> ### Step 3: Combine all elements of the prompt Now, combine all the elements into the `MyPrompt` component. import { UserMessage, PromptElement, BasePromptElementProps, } from '@vscode/prompt-tsx'; import { History } from './history'; interface IFilesToInclude { document: TextDocument; line: number; } interface IMyPromptProps extends BasePromptElementProps { history: ChatContext['history']; userQuery: string; files: IFilesToInclude[]; } export class MyPrompt extends PromptElement<IMyPromptProps> { render() { return ( <> <UserMessage priority={100}>Here are your base instructions.</UserMessage> <History history={this.props.history} passPriority older={0} newer={80} flexGrow={2} flexReserve="/5" /> <UserMessage priority={90}>{this.props.userQuery}</UserMessage> <FileContext priority={70} flexGrow={1} files={this.props.files} /> </> ); } } ### Step 4: Define the FileContext component Finally, define a `FileContext` component that includes the contents of the files the user is currently looking at. Because you used `flexGrow`, you can implement logic that gets as many of the lines around the 'interesting' line for each file by using the information in `PromptSizing`. For brevity, the implementation logic for `getExpandedFiles` is omitted. You can check it out in the prompt-tsx repo. import { PromptElement, BasePromptElementProps, PromptSizing, PromptPiece } from '@vscode/prompt-tsx'; class FileContext extends PromptElement<{ files: IFilesToInclude[] } & BasePromptElementProps> { async render(_state: void, sizing: PromptSizing): Promise<PromptPiece> { const files = await this.getExpandedFiles(sizing); return <>{files.map(f => f.toString())}</>; } private async getExpandedFiles(sizing: PromptSizing) { // Implementation details are summarized here. // Refer to the repo for the complete implementation. } } ## Summary In these examples, you created a `MyPrompt` component that includes base instructions, user query, history messages, and file contents with different priorities. You used `flexGrow` to cooperatively size the file contents to fit within the token budget. By following this pattern, you can ensure that the most important parts of your prompt are always included, while less important parts are pruned as needed to fit within the model's context window. For the complete implementation details of the `getExpandedFiles` method and the `FileContextTracker` class, refer to the prompt-tsx repo. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/tree-view In this article The Tree View API allows extensions to show content in the sidebar in Visual Studio Code. This content is structured as a tree and conforms to the style of the built-in views of VS Code. For example, the built-in References Search View extension shows reference search results as a separate view.  The **Find All References** results are displayed in a **References: Results** Tree View, which is in the **References** View Container. This guide teaches you how to write an extension that contributes Tree Views and View Containers to Visual Studio Code. ## Tree View API Basics To explain the Tree View API, we are going to build a sample extension called **Node Dependencies**. This extension will use a treeview to display all Node.js dependencies in the current folder. The steps for adding a treeview are to contribute the treeview in your `package.json`, create a `TreeDataProvider`, and register the `TreeDataProvider`. You can find the complete source code of this sample extension in the `tree-view-sample` in the vscode-extension-samples GitHub repository. ### package.json Contribution First you have to let VS Code know that you are contributing a view, using the contributes.views Contribution Point in `package.json`. Here's the `package.json` for the first version of our extension: { "name": "custom-view-samples", "displayName": "Custom view Samples", "description": "Samples for VS Code's view API", "version": "0.0.1", "publisher": "alexr00", "engines": { "vscode": "^1.74.0" }, "activationEvents": [], "main": "./out/extension.js", "contributes": { "views": { "explorer": [ { "id": "nodeDependencies", "name": "Node Dependencies" } ] } }, "scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./" }, "devDependencies": { "@types/node": "^10.12.21", "@types/vscode": "^1.42.0", "typescript": "^3.5.1", "tslint": "^5.12.1" } } > **Note**: If your extension targets a VS Code version prior to 1.74, you must explicitly list `onView:nodeDependencies` in `activationEvents`. You must specify an identifier and name for the view, and you can contribute to following locations: * `explorer`: Explorer view in the Side Bar * `debug`: Run and Debug view in the Side Bar * `scm`: Source Control view in the Side Bar * `test`: Test explorer view in the Side Bar * Custom View Containers ### Tree Data Provider The second step is to provide data to the view you registered so that VS Code can display the data in the view. To do so, you should first implement the TreeDataProvider. Our `TreeDataProvider` will provide node dependencies data, but you can have a data provider that provides other types of data. There are two necessary methods in this API that you need to implement: * `getChildren(element?: T): ProviderResult<T[]>` - Implement this to return the children for the given `element` or root (if no element is passed). * `getTreeItem(element: T): TreeItem | Thenable<TreeItem>` - Implement this to return the UI representation (TreeItem) of the element that gets displayed in the view. When the user opens the Tree View, the `getChildren` method will be called without an `element`. From there, your `TreeDataProvider` should return your top-level tree items. In our example, the `collapsibleState` of the top-level tree items is `TreeItemCollapsibleState.Collapsed`, meaning that the top-level tree items will show as collapsed. Setting the `collapsibleState` to `TreeItemCollapsibleState.Expanded` will cause tree items to show as expanded. Leaving the `collapsibleState` as its default of `TreeItemCollapsibleState.None` indicates that the tree item has no children. `getChildren` will not be called for tree items with a `collapsibleState` of `TreeItemCollapsibleState.None`. Here is an example of a `TreeDataProvider` implementation that provides node dependencies data: import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; export class NodeDependenciesProvider implements vscode.TreeDataProvider<Dependency> { constructor(private workspaceRoot: string) {} getTreeItem(element: Dependency): vscode.TreeItem { return element; } getChildren(element?: Dependency): Thenable<Dependency[]> { if (!this.workspaceRoot) { vscode.window.showInformationMessage('No dependency in empty workspace'); return Promise.resolve([]); } if (element) { return Promise.resolve( this.getDepsInPackageJson( path.join(this.workspaceRoot, 'node_modules', element.label, 'package.json') ) ); } else { const packageJsonPath = path.join(this.workspaceRoot, 'package.json'); if (this.pathExists(packageJsonPath)) { return Promise.resolve(this.getDepsInPackageJson(packageJsonPath)); } else { vscode.window.showInformationMessage('Workspace has no package.json'); return Promise.resolve([]); } } } /** * Given the path to package.json, read all its dependencies and devDependencies. */ private getDepsInPackageJson(packageJsonPath: string): Dependency[] { if (this.pathExists(packageJsonPath)) { const toDep = (moduleName: string, version: string): Dependency => { if (this.pathExists(path.join(this.workspaceRoot, 'node_modules', moduleName))) { return new Dependency( moduleName, version, vscode.TreeItemCollapsibleState.Collapsed ); } else { return new Dependency(moduleName, version, vscode.TreeItemCollapsibleState.None); } }; const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const deps = packageJson.dependencies ? Object.keys(packageJson.dependencies).map(dep => toDep(dep, packageJson.dependencies[dep]) ) : []; const devDeps = packageJson.devDependencies ? Object.keys(packageJson.devDependencies).map(dep => toDep(dep, packageJson.devDependencies[dep]) ) : []; return deps.concat(devDeps); } else { return []; } } private pathExists(p: string): boolean { try { fs.accessSync(p); } catch (err) { return false; } return true; } } class Dependency extends vscode.TreeItem { constructor( public readonly label: string, private version: string, public readonly collapsibleState: vscode.TreeItemCollapsibleState ) { super(label, collapsibleState); this.tooltip = `${this.label}-${this.version}`; this.description = this.version; } iconPath = { light: path.join(__filename, '..', '..', 'resources', 'light', 'dependency.svg'), dark: path.join(__filename, '..', '..', 'resources', 'dark', 'dependency.svg') }; } ### Registering the TreeDataProvider The third step is to register the above data provider to your view. This can be done in the following two ways: * `vscode.window.registerTreeDataProvider` - Register the tree data provider by providing the registered view ID and above data provider. const rootPath = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined; vscode.window.registerTreeDataProvider( 'nodeDependencies', new NodeDependenciesProvider(rootPath) ); * `vscode.window.createTreeView` - Create the Tree View by providing the registered view ID and above data provider. This will give access to the TreeView, which you can use for performing other view operations. Use `createTreeView`, if you need the `TreeView` API. vscode.window.createTreeView('nodeDependencies', { treeDataProvider: new NodeDependenciesProvider(rootPath) }); Here's the extension in action:  ### Updating Tree View content Our node dependencies view is simple, and once the data is shown, it isn't updated. However, it would be useful to have a refresh button in the view and update the node dependencies view with the current contents of the `package.json`. To do this, we can use the `onDidChangeTreeData` event. * `onDidChangeTreeData?: Event<T | undefined | null | void>` - Implement this if your tree data can change and you want to update the treeview. Add the following to your `NodeDependenciesProvider`. private _onDidChangeTreeData: vscode.EventEmitter<Dependency | undefined | null | void> = new vscode.EventEmitter<Dependency | undefined | null | void>(); readonly onDidChangeTreeData: vscode.Event<Dependency | undefined | null | void> = this._onDidChangeTreeData.event; refresh(): void { this._onDidChangeTreeData.fire(); } Now we have a refresh method, but no one is calling it. We can add a command to call refresh. In the `contributes` section of your `package.json`, add: "commands": [ { "command": "nodeDependencies.refreshEntry", "title": "Refresh", "icon": { "light": "resources/light/refresh.svg", "dark": "resources/dark/refresh.svg" } }, ] And register the command in your extension activation: import * as vscode from 'vscode'; import { NodeDependenciesProvider } from './nodeDependencies'; export function activate(context: vscode.ExtensionContext) { const rootPath = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined; const nodeDependenciesProvider = new NodeDependenciesProvider(rootPath); vscode.window.registerTreeDataProvider('nodeDependencies', nodeDependenciesProvider); vscode.commands.registerCommand('nodeDependencies.refreshEntry', () => nodeDependenciesProvider.refresh() ); } Now we have a command that will refresh the node dependencies view, but a button on the view would be even better. We already added an `icon` to the command, so it will show up with that icon when we add it to the view. In the `contributes` section of your `package.json`, add: "menus": { "view/title": [ { "command": "nodeDependencies.refreshEntry", "when": "view == nodeDependencies", "group": "navigation" }, ] } ## Activation It is important that your extension is activated only when user needs the functionality that your extension provides. In this case, you should consider activating your extension only when the user starts using the view. VS Code automatically does this for you when your extension declares a view contribution. VS Code emits an activationEvent onView:${viewId} (`onView:nodeDependencies` for the example above) when the user opens the view. > **Note**: For VS Code versions prior to 1.74.0, you must explicitly register this activation event in `package.json` for VS Code to activate your extension on this view: > > "activationEvents": [ > "onView:nodeDependencies", > ], > ## View Container A View Container contains a list of views that are displayed in the Activity Bar or Panel along with the built-in View Containers. Examples of built-in View Containers are Source Control and Explorer.  To contribute a View Container, you should first register it using contributes.viewsContainers Contribution Point in `package.json`. You have to specify the following required fields: * `id` - The ID of the new view container you're creating. * `title` - The name that will show up at the top of the view container. * `icon` - An image that will be displayed for the view container when in the Activity Bar. "contributes": { "viewsContainers": { "activitybar": [ { "id": "package-explorer", "title": "Package Explorer", "icon": "media/dep.svg" } ] } } Alternatively, you could contribute this view to the panel by placing it under the `panel` node. "contributes": { "viewsContainers": { "panel": [ { "id": "package-explorer", "title": "Package Explorer", "icon": "media/dep.svg" } ] } } ## Contributing views to View Containers Once you've created a View Container, you can use the contributes.views Contribution Point in `package.json`. "contributes": { "views": { "package-explorer": [ { "id": "nodeDependencies", "name": "Node Dependencies", "icon": "media/dep.svg", "contextualTitle": "Package Explorer" } ] } } A view can also have an optional `visibility` property which can be set to `visible`, `collapsed`, or `hidden`. This property is only respected by VS Code the first time a workspace is opened with this view. After that, the visibility is set to whatever the user has chosen. If you have a view container with many views, or if your view will not be useful to every user of your extension, consider setting the view the `collapsed` or `hidden`. A `hidden` view will appear in the view containers "Views" menu:  ## View Actions Actions are available as inline icons on your individual tree items, in tree item context menus, and at the top of your view in the view title. Actions are commands that you set to show up in these locations by adding contributions to your `package.json`. To contribute to these three places, you can use the following menu contribution points in your package.json: * `view/title` - Location to show actions in the view title. Primary or inline actions use `"group": "navigation"` and rest are secondary actions, which are in `...` menu. * `view/item/context` - Location to show actions for the tree item. Inline actions use `"group": "inline"` and rest are secondary actions, which are in `...` menu. You can control the visibility of these actions using a when clause.  Examples: "contributes": { "commands": [ { "command": "nodeDependencies.refreshEntry", "title": "Refresh", "icon": { "light": "resources/light/refresh.svg", "dark": "resources/dark/refresh.svg" } }, { "command": "nodeDependencies.addEntry", "title": "Add" }, { "command": "nodeDependencies.editEntry", "title": "Edit", "icon": { "light": "resources/light/edit.svg", "dark": "resources/dark/edit.svg" } }, { "command": "nodeDependencies.deleteEntry", "title": "Delete" } ], "menus": { "view/title": [ { "command": "nodeDependencies.refreshEntry", "when": "view == nodeDependencies", "group": "navigation" }, { "command": "nodeDependencies.addEntry", "when": "view == nodeDependencies" } ], "view/item/context": [ { "command": "nodeDependencies.editEntry", "when": "view == nodeDependencies && viewItem == dependency", "group": "inline" }, { "command": "nodeDependencies.deleteEntry", "when": "view == nodeDependencies && viewItem == dependency" } ] } } By default, actions are ordered alphabetically. To specify a different ordering, add `@` followed by the order you want to the group. For example, `navigation@3` will cause the action to show up 3rd in the `navigation` group. You can further separate items in the `...` menu by creating different groups. These group names are arbitrary and are ordered alphabetically by group name. **Note:** If you want to show an action for specific tree items, you can do so by defining the context of a tree item using `TreeItem.contextValue` and you can specify the context value for key `viewItem` in `when` expression. Examples: "contributes": { "menus": { "view/item/context": [ { "command": "nodeDependencies.deleteEntry", "when": "view == nodeDependencies && viewItem == dependency" } ] } } ## Welcome content If your view can be empty, or if you want to add Welcome content to another extension's empty view, you can contribute `viewsWelcome` content. An empty view is a view that has no `TreeView.message` and an empty tree. "contributes": { "viewsWelcome": [ { "view": "nodeDependencies", "contents": "No node dependencies found [learn more](https://www.npmjs.com/).\n[Add Dependency](command:nodeDependencies.addEntry)" } ] }  Links are supported in Welcome content. By convention, a link on a line by itself is a button. Each Welcome content can also contain a `when` clause. For more examples, see the built-in Git extension. ## TreeDataProvider Extension writers should register a TreeDataProvider programmatically to populate data in the view. vscode.window.registerTreeDataProvider('nodeDependencies', new DepNodeProvider()); See nodeDependencies.ts in the `tree-view-sample` for the implementation. ## TreeView If you would like to perform some UI operations on the view programmatically, you can use `window.createTreeView` instead of `window.registerTreeDataProvider`. This will give access to the view, which you can use for performing view operations. vscode.window.createTreeView('ftpExplorer', { treeDataProvider: new FtpTreeDataProvider() }); See ftpExplorer.ts in the `tree-view-sample` for the implementation. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/webview In this article The webview API allows extensions to create fully customizable views within Visual Studio Code. For example, the built-in Markdown extension uses webviews to render Markdown previews. Webviews can also be used to build complex user interfaces beyond what VS Code's native APIs support. Think of a webview as an `iframe` within VS Code that your extension controls. A webview can render almost any HTML content in this frame, and it communicates with extensions using message passing. This freedom makes webviews incredibly powerful, and opens up a whole new range of extension possibilities. Webviews are used in several VS Code APIs: * With Webview Panels created using `createWebviewPanel`. In this case, Webview panels are shown in VS Code as distinct editors. This makes them useful for displaying custom UI and custom visualizations. * As the view for a custom editor. Custom editors allow extensions to provide a custom UI for editing any file in the workspace. The custom editor API also lets your extension hook into editor events such as undo and redo, as well as file events such as save. * In Webview views that are rendered in the sidebar or panel areas. See the webview view sample extension for more details. This page focuses on the basic webview panel API, although almost everything covered here applies to the webviews used in custom editors and webview views as well. Even if you are more interested in those APIs, we recommend reading through this page first to familiarize yourself with the webview basics. ## Links * Webview sample * Custom Editors documentation * Webview View sample ### VS Code API Usage * `window.createWebviewPanel` * `window.registerWebviewPanelSerializer` ## Should I use a webview? Webviews are pretty amazing, but they should also be used sparingly and only when VS Code's native API is inadequate. Webviews are resource heavy and run in a separate context from normal extensions. A poorly designed webview can also easily feel out of place within VS Code. Before using a webview, please consider the following: * Does this functionality really need to live within VS Code? Would it be better as a separate application or website? * Is a webview the only way to implement your feature? Can you use the regular VS Code APIs instead? * Will your webview add enough user value to justify its high resource cost? Remember: Just because you can do something with webviews, doesn't mean you should. However, if you are confident that you need to use webviews, then this document is here to help. Let's get started. ## Webviews API basics To explain the webview API, we are going to build a simple extension called **Cat Coding**. This extension will use a webview to show a gif of a cat writing some code (presumably in VS Code). As we work through the API, we'll continue adding functionality to the extension, including a counter that keeps track of how many lines of source code our cat has written and notifications that inform the user when the cat introduces a bug. Here's the `package.json` for the first version of the **Cat Coding** extension. You can find the complete code for the example app here. The first version of our extension contributes a command called `catCoding.start`. When a user invokes this command, we will show a simple webview with our cat in it. Users will be able to invoke this command from the **Command Palette** as **Cat Coding: Start new cat coding session** or even create a keybinding for it if they are so inclined. { "name": "cat-coding", "description": "Cat Coding", "version": "0.0.1", "publisher": "bierner", "engines": { "vscode": "^1.74.0" }, "activationEvents": [], "main": "./out/extension.js", "contributes": { "commands": [ { "command": "catCoding.start", "title": "Start new cat coding session", "category": "Cat Coding" } ] }, "scripts": { "vscode:prepublish": "tsc -p ./", "compile": "tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install" }, "dependencies": { "vscode": "*" }, "devDependencies": { "@types/node": "^9.4.6", "typescript": "^2.8.3" } } > **Note**: If your extension targets a VS Code version prior to 1.74, you must explicitly list `onCommand:catCoding.start` in `activationEvents`. Now let's implement the `catCoding.start` command. In our extension's main file, we register the `catCoding.start` command and use it to show a basic webview: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { // Create and show a new webview const panel = vscode.window.createWebviewPanel( 'catCoding', // Identifies the type of the webview. Used internally 'Cat Coding', // Title of the panel displayed to the user vscode.ViewColumn.One, // Editor column to show the new webview panel in. {} // Webview options. More on these later. ); }) ); } The `vscode.window.createWebviewPanel` function creates and shows a webview in the editor. Here is what you see if you try running the `catCoding.start` command in its current state:  Our command opens a new webview panel with the correct title, but with no content! To add our cat to new panel, we also need to set the HTML content of the webview using `webview.html`: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { // Create and show panel const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, {} ); // And set its HTML content panel.webview.html = getWebviewContent(); }) ); } function getWebviewContent() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" /> </body> </html>`; } If you run the command again, now the webview looks like this:  Progress! `webview.html` should always be a complete HTML document. HTML fragments or malformed HTML may cause unexpected behavior. ### Updating webview content `webview.html` can also update a webview's content after it has been created. Let's use this to make **Cat Coding** more dynamic by introducing a rotation of cats: import * as vscode from 'vscode'; const cats = { 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif', 'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif' }; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, {} ); let iteration = 0; const updateWebview = () => { const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat'; panel.title = cat; panel.webview.html = getWebviewContent(cat); }; // Set initial content updateWebview(); // And schedule updates to the content every second setInterval(updateWebview, 1000); }) ); } function getWebviewContent(cat: keyof typeof cats) { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="${cats[cat]}" width="300" /> </body> </html>`; }  Setting `webview.html` replaces the entire webview content, similar to reloading an iframe. This is important to remember once you start using scripts in a webview, since it means that setting `webview.html` also resets the script's state. The example above also uses `webview.title` to change the title of the document displayed in the editor. Setting the title does not cause the webview to be reloaded. ### Lifecycle Webview panels are owned by the extension that creates them. The extension must hold onto the webview returned from `createWebviewPanel`. If your extension loses this reference, it cannot regain access to that webview again, even though the webview will continue to show in VS Code. As with text editors, a user can also close a webview panel at any time. When a webview panel is closed by the user, the webview itself is destroyed. Attempting to use a destroyed webview throws an exception. This means that the example above using `setInterval` actually has an important bug: if the user closes the panel, `setInterval` will continue to fire, which will try to update `panel.webview.html`, which of course will throw an exception. Cats hate exceptions. Let's fix this! The `onDidDispose` event is fired when a webview is destroyed. We can use this event to cancel further updates and clean up the webview's resources: import * as vscode from 'vscode'; const cats = { 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif', 'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif' }; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, {} ); let iteration = 0; const updateWebview = () => { const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat'; panel.title = cat; panel.webview.html = getWebviewContent(cat); }; updateWebview(); const interval = setInterval(updateWebview, 1000); panel.onDidDispose( () => { // When the panel is closed, cancel any future updates to the webview content clearInterval(interval); }, null, context.subscriptions ); }) ); } Extensions can also programmatically close webviews by calling `dispose()` on them. If, for example, we wanted to restrict our cat's workday to five seconds: export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, {} ); panel.webview.html = getWebviewContent('Coding Cat'); // After 5sec, programmatically close the webview panel const timeout = setTimeout(() => panel.dispose(), 5000); panel.onDidDispose( () => { // Handle user closing panel before the 5sec have passed clearTimeout(timeout); }, null, context.subscriptions ); }) ); } ### Visibility and Moving When a webview panel is moved into a background tab, it becomes hidden. It is not destroyed however. VS Code will automatically restore the webview's content from `webview.html` when the panel is brought to the foreground again:  The `.visible` property tells you if the webview panel is currently visible or not. Extensions can programmatically bring a webview panel to the foreground by calling `reveal()`. This method takes an optional target view column to show the panel in. A webview panel may only show in a single editor column at a time. Calling `reveal()` or dragging a webview panel to a new editor column moves the webview into that new column.  Let's update our extension to only allow a single webview to exist at a time. If the panel is in the background, then the `catCoding.start` command will bring it to the foreground: export function activate(context: vscode.ExtensionContext) { // Track the current panel with a webview let currentPanel: vscode.WebviewPanel | undefined = undefined; context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const columnToShowIn = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; if (currentPanel) { // If we already have a panel, show it in the target column currentPanel.reveal(columnToShowIn); } else { // Otherwise, create a new panel currentPanel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', columnToShowIn || vscode.ViewColumn.One, {} ); currentPanel.webview.html = getWebviewContent('Coding Cat'); // Reset when the current panel is closed currentPanel.onDidDispose( () => { currentPanel = undefined; }, null, context.subscriptions ); } }) ); } Here's the new extension in action:  Whenever a webview's visibility changes, or when a webview is moved into a new column, the `onDidChangeViewState` event is fired. Our extension can use this event to change cats based on which column the webview is showing in: const cats = { 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif', 'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif', 'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif' }; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, {} ); panel.webview.html = getWebviewContent('Coding Cat'); // Update contents based on view state changes panel.onDidChangeViewState( e => { const panel = e.webviewPanel; switch (panel.viewColumn) { case vscode.ViewColumn.One: updateWebviewForCat(panel, 'Coding Cat'); return; case vscode.ViewColumn.Two: updateWebviewForCat(panel, 'Compiling Cat'); return; case vscode.ViewColumn.Three: updateWebviewForCat(panel, 'Testing Cat'); return; } }, null, context.subscriptions ); }) ); } function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) { panel.title = catName; panel.webview.html = getWebviewContent(catName); }  ### Inspecting and debugging webviews The **Developer: Toggle Developer Tools** command opens a Developer Tools window that you can use debug and inspect your webviews.  Note that if you are using a version of VS Code older than 1.56, or if you are trying to debug a webview that sets `enableFindWidget`, you must instead use the **Developer: Open Webview Developer Tools** command. This command opens a dedicated Developer Tools page for each webview instead of using a Developer Tools page that is shared by all webviews and the editor itself. From the Developer Tools, you can start inspecting the contents of your webview using the inspect tool located in the top left corner of the Developer Tools window:  You can also view all of the errors and logs from your webview in the developer tools console:  To evaluate an expression in the context of your webview, make sure to select the **active frame** environment from the dropdown in the top left corner of the Developer tools console panel:  The **active frame** environment is where the webview scripts themselves are executed. In addition, the **Developer: Reload Webview** command reloads all active webviews. This can be helpful if you need to reset a webview's state, or if some webview content on disk has changed and you want the new content to be loaded. ## Loading local content Webviews run in isolated contexts that cannot directly access local resources. This is done for security reasons. This means that in order to load images, stylesheets, and other resources from your extension, or to load any content from the user's current workspace, you must use the `Webview.asWebviewUri` function to convert a local `file:` URI into a special URI that VS Code can use to load a subset of local resources. Imagine that we want to start bundling the cat gifs into our extension rather than pulling them from Giphy. To do this, we first create a URI to the file on disk and then pass these URIs through the `asWebviewUri` function: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, {} ); // Get path to resource on disk const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif'); // And get the special URI to use with the webview const catGifSrc = panel.webview.asWebviewUri(onDiskPath); panel.webview.html = getWebviewContent(catGifSrc); }) ); } function getWebviewContent(catGifSrc: vscode.Uri) { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="${catGifSrc}" width="300" /> </body> </html>`; } If we debug this code, we'd see that the actual value for `catGifSrc` is something like: vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif VS Code understands this special URI and will use it to load our gif from the disk! By default, webviews can only access resources in the following locations: * Within your extension's install directory. * Within the user's currently active workspace. Use the `WebviewOptions.localResourceRoots` to allow access to additional local resources. You can also always use data URIs to embed resources directly within the webview. ### Controlling access to local resources Webviews can control which resources can be loaded from the user's machine with `localResourceRoots` option. `localResourceRoots` defines a set of root URIs from which local content may be loaded. We can use `localResourceRoots` to restrict **Cat Coding** webviews to only load resources from a `media` directory in our extension: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, { // Only allow the webview to access resources in our extension's media directory localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')] } ); const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif'); const catGifSrc = panel.webview.asWebviewUri(onDiskPath); panel.webview.html = getWebviewContent(catGifSrc); }) ); } To disallow all local resources, just set `localResourceRoots` to `[]`. In general, webviews should be as restrictive as possible in loading local resources. However, keep in mind that `localResourceRoots` does not offer complete security protection on its own. Make sure your webview also follows security best practices, and add a content security policy to further restrict the content that can be loaded. ### Theming webview content Webview can use CSS to change their appearance based on VS Code's current theme. VS Code groups themes into three categories, and adds a special class to the `body` element to indicate the current theme: * `vscode-light` - Light themes. * `vscode-dark` - Dark themes. * `vscode-high-contrast` - High contrast themes. The following CSS changes the text color of the webview based on the user's current theme: body.vscode-light { color: black; } body.vscode-dark { color: white; } body.vscode-high-contrast { color: red; } When developing a webview application, make sure that it works for the three types of themes. And always test your webview in high-contrast mode to make sure it will be usable by people with visual disabilities. Webviews can also access VS Code theme colors using CSS variables. These variable names are prefixed with `vscode` and replace the `.` with `-`. For example `editor.foreground` becomes `var(--vscode-editor-foreground)`: code { color: var(--vscode-editor-foreground); } Review the Theme Color Reference for the available theme variables. An extension is available which provides IntelliSense suggestions for the variables. The following font related variables are also defined: * `--vscode-editor-font-family` - Editor font family (from the `editor.fontFamily` setting). * `--vscode-editor-font-weight` - Editor font weight (from the `editor.fontWeight` setting). * `--vscode-editor-font-size` - Editor font size (from the `editor.fontSize` setting). Finally, for special cases where you need to write CSS that targets a single theme, the body element of webviews has a data attribute called `vscode-theme-id` that stores the ID of the currently active theme. This lets you write theme-specific CSS for webviews: body[data-vscode-theme-id="One Dark Pro"] { background: hotpink; } ### Supported media formats Webviews support audio and video, however not every media codec or media file container type is supported. The following audio formats can be used in Webviews: * Wav * Mp3 * Ogg * Flac The following video formats can be used in webviews: * H.264 * VP8 For video files, make sure that both the video and audio track's media formats are supported. Many `.mp4` files for example use `H.264` for video and `AAC` audio. VS Code will be able to play the video part of the `mp4`, but since `AAC` audio is not supported there won't be any sound. Instead you need to use `mp3` for the audio track. Advanced webviews can customize the context menu that shows when a user right-clicks inside of a webview. This is done using a contribution point similarly to VS Code's normal context menus, so custom menus fit right in with the rest of the editor. Webviews can also show custom context menus for different sections of the webview. To add a new context menu item to your webview, first add a new entry in `menus` under the new `webview/context` section. Each contribution takes a `command` (which is also where the item's title comes from) and a `when` clause. The when clause should include `webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'` to make sure the context menus only apply to your extension's webviews: "contributes": { "menus": { "webview/context": [ { "command": "catCoding.yarn", "when": "webviewId == 'catCoding'" }, { "command": "catCoding.insertLion", "when": "webviewId == 'catCoding' && webviewSection == 'editor'" } ] }, "commands": [ { "command": "catCoding.yarn", "title": "Yarn ๐งถ", "category": "Cat Coding" }, { "command": "catCoding.insertLion", "title": "Insert ๐ฆ", "category": "Cat Coding" }, ... ] } Inside of the webview, you can also set the contexts for specific areas of the HTML using the `data-vscode-context` data attribute (or in JavaScript with `dataset.vscodeContext`). The `data-vscode-context` value is a JSON object that specifies the contexts to set when the user right-clicks on the element. The final context is determined by going from the document root to the element that was clicked. Consider this HTML for example: <div class="main" data-vscode-context='{"webviewSection": "main", "mouseCount": 4}'> <h1>Cat Coding</h1> <textarea data-vscode-context='{"webviewSection": "editor", "preventDefaultContextMenuItems": true}'></textarea> </div> If the user right-clicks on the `textarea`, the following contexts will be set: * `webviewSection == 'editor'` - This overrides `webviewSection` from the parent element. * `mouseCount == 4` - This is inherited from the parent element. * `preventDefaultContextMenuItems == true` - This is a special context that hides the copy and paste entries that VS Code normally adds to webview context menus. If the user right-clicks inside of the `<textarea>`, they will see:  Sometimes it can be useful to show a menu on left/primary click. For example, to show a menu on a split button. You can do this by dispatching the `contextmenu` event in an `onClick` event: <button data-vscode-context='{"preventDefaultContextMenuItems": true }' onClick='((e) => { e.preventDefault(); e.target.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, clientX: e.clientX, clientY: e.clientY })); e.stopPropagation(); })(event)'>Create</button>  ## Scripts and message passing Webviews are just like iframes, which means that they can also run scripts. JavaScript is disabled in webviews by default, but it can easily re-enable by passing in the `enableScripts: true` option. Let's use a script to add a counter tracking the lines of source code our cat has written. Running a basic script is pretty simple, but note that this example is only for demonstration purposes. In practice, your webview should always disable inline scripts using a content security policy: import * as path from 'path'; import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, { // Enable scripts in the webview enableScripts: true } ); panel.webview.html = getWebviewContent(); }) ); } function getWebviewContent() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" /> <h1 id="lines-of-code-counter">0</h1> <script> const counter = document.getElementById('lines-of-code-counter'); let count = 0; setInterval(() => { counter.textContent = count++; }, 100); </script> </body> </html>`; }  Wow! That's one productive cat. Webview scripts can do just about anything that a script on a normal webpage can. Keep in mind though that webviews exist in their own context, so scripts in a webview do not have access to the VS Code API. That's where message passing comes in! ### Passing messages from an extension to a webview An extension can send data to its webviews using `webview.postMessage()`. This method sends any JSON serializable data to the webview. The message is received inside the webview through the standard `message` event. To demonstrate this, let's add a new command to **Cat Coding** that instructs the currently coding cat to refactor their code (thereby reducing the total number of lines). The new `catCoding.doRefactor` command use `postMessage` to send the instruction to the current webview, and `window.addEventListener('message', event => { ... })` inside the webview itself to handle the message: export function activate(context: vscode.ExtensionContext) { // Only allow a single Cat Coder let currentPanel: vscode.WebviewPanel | undefined = undefined; context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { if (currentPanel) { currentPanel.reveal(vscode.ViewColumn.One); } else { currentPanel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, { enableScripts: true } ); currentPanel.webview.html = getWebviewContent(); currentPanel.onDidDispose( () => { currentPanel = undefined; }, undefined, context.subscriptions ); } }) ); // Our new command context.subscriptions.push( vscode.commands.registerCommand('catCoding.doRefactor', () => { if (!currentPanel) { return; } // Send a message to our webview. // You can send any JSON serializable data. currentPanel.webview.postMessage({ command: 'refactor' }); }) ); } function getWebviewContent() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" /> <h1 id="lines-of-code-counter">0</h1> <script> const counter = document.getElementById('lines-of-code-counter'); let count = 0; setInterval(() => { counter.textContent = count++; }, 100); // Handle the message inside the webview window.addEventListener('message', event => { const message = event.data; // The JSON data our extension sent switch (message.command) { case 'refactor': count = Math.ceil(count * 0.5); counter.textContent = count; break; } }); </script> </body> </html>`; }  ### Passing messages from a webview to an extension Webviews can also pass messages back to their extension. This is accomplished using a `postMessage` function on a special VS Code API object inside the webview. To access the VS Code API object, call `acquireVsCodeApi` inside the webview. This function can only be invoked once per session. You must hang onto the instance of the VS Code API returned by this method, and hand it out to any other functions that need to use it. We can use the VS Code API and `postMessage` in our **Cat Coding** webview to alert the extension when our cat introduces a bug in their code: export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, { enableScripts: true } ); panel.webview.html = getWebviewContent(); // Handle messages from the webview panel.webview.onDidReceiveMessage( message => { switch (message.command) { case 'alert': vscode.window.showErrorMessage(message.text); return; } }, undefined, context.subscriptions ); }) ); } function getWebviewContent() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" /> <h1 id="lines-of-code-counter">0</h1> <script> (function() { const vscode = acquireVsCodeApi(); const counter = document.getElementById('lines-of-code-counter'); let count = 0; setInterval(() => { counter.textContent = count++; // Alert the extension when our cat introduces a bug if (Math.random() < 0.001 * count) { vscode.postMessage({ command: 'alert', text: '๐ on line ' + count }) } }, 100); }()) </script> </body> </html>`; }  For security reasons, you must keep the VS Code API object private and make sure it is never leaked into the global scope. ### Using Web Workers Web Workers are supported inside of webviews but there are a few important restrictions to be aware of. First off, workers can only be loaded using either a `data:` or `blob:` URI. You cannot directly load a worker from your extension's folder. If you do need to load worker code from a JavaScript file in your extension, try using `fetch`: const workerSource = 'absolute/path/to/worker.js'; fetch(workerSource) .then(result => result.blob()) .then(blob => { const blobUrl = URL.createObjectURL(blob); new Worker(blobUrl); }); Worker scripts also do not support importing source code using `importScripts` or `import(...)`. If your worker loads code dynamically, try using a bundler such as webpack to package the worker script into a single file. With `webpack`, you can use `LimitChunkCountPlugin` to force the compiled worker JavaScript to be a single file: const path = require('path'); const webpack = require('webpack'); module.exports = { target: 'webworker', entry: './worker/src/index.js', output: { filename: 'worker.js', path: path.resolve(__dirname, 'media') }, plugins: [ new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }) ] }; ## Security As with any webpage, when creating a webview, you must follow some basic security best practices. ### Limit capabilities A webview should have the minimum set of capabilities that it needs. For example, if your webview does not need to run scripts, do not set the `enableScripts: true`. If your webview does not need to load resources from the user's workspace, set `localResourceRoots` to `[vscode.Uri.file(extensionContext.extensionPath)]` or even `[]` to disallow access to all local resources. ### Content security policy Content security policies further restrict the content that can be loaded and executed in webviews. For example, a content security policy can make sure that only a list of allowed scripts can be run in the webview, or even tell the webview to only load images over `https`. To add a content security policy, put a `<meta http-equiv="Content-Security-Policy">` directive at the top of the webview's `<head>` function getWebviewContent() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'none';"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> ... </body> </html>`; } The policy `default-src 'none';` disallows all content. We can then turn back on the minimal amount of content that our extension needs to function. Here's a content security policy that allows loading local scripts and stylesheets, and loading images over `https`: <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};" /> The `${webview.cspSource}` value is a placeholder for a value that comes from the webview object itself. See the webview sample for a complete example of how to use this value. This content security policy also implicitly disables inline scripts and styles. It is a best practice to extract all inline styles and scripts to external files so that they can be properly loaded without relaxing the content security policy. ### Only load content over https If your webview allows loading external resources, it is strongly recommended that you only allow these resources to be loaded over `https` and not over http. The example content security policy above already does this by only allowing images to be loaded over `https:`. ### Sanitize all user input Just as you would for a normal webpage, when constructing the HTML for a webview, you must sanitize all user input. Failing to properly sanitize input can allow content injections, which may open your users up to a security risk. Example values that must be sanitized: * File contents. * File and folder paths. * User and workspace settings. Consider using a helper library to construct your HTML strings, or at least ensure that all content from the user's workspace is properly sanitized. Never rely on sanitization alone for security. Make sure to follow the other security best practices, such as having a content security policy to minimize the impact of any potential content injections. ## Persistence In the standard webview lifecycle, webviews are created by `createWebviewPanel` and destroyed when the user closes them or when `.dispose()` is called. The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab. The best way to solve this is to make your webview stateless. Use message passing to save off the webview's state and then restore the state when the webview becomes visible again. ### getState and setState Scripts running inside a webview can use the `getState` and `setState` methods to save off and restore a JSON serializable state object. This state is persisted even after the webview content itself is destroyed when a webview panel becomes hidden. The state is destroyed when the webview panel is destroyed. // Inside a webview script const vscode = acquireVsCodeApi(); const counter = document.getElementById('lines-of-code-counter'); // Check if we have an old state to restore from const previousState = vscode.getState(); let count = previousState ? previousState.count : 0; counter.textContent = count; setInterval(() => { counter.textContent = count++; // Update the saved state vscode.setState({ count }); }, 100); `getState` and `setState` are the preferred way to persist state, as they have much lower performance overhead than `retainContextWhenHidden`. ### Serialization By implementing a `WebviewPanelSerializer`, your webviews can be automatically restored when VS Code restarts. Serialization builds on `getState` and `setState`, and is only enabled if your extension registers a `WebviewPanelSerializer` for your webviews. To make our coding cats persist across VS Code restarts, first add a `onWebviewPanel` activation event to the extension's `package.json`: "activationEvents": [ ..., "onWebviewPanel:catCoding" ] This activation event ensures that our extension will be activated whenever VS Code needs to restore a webview with the viewType: `catCoding`. Then, in our extension's `activate` method, call `registerWebviewPanelSerializer` to register a new `WebviewPanelSerializer`. The `WebviewPanelSerializer` is responsible for restoring the contents of the webview from its persisted state. This state is the JSON blob that the webview contents set using `setState`. export function activate(context: vscode.ExtensionContext) { // Normal setup... // And make sure we register a serializer for our webview type vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer()); } class CatCodingSerializer implements vscode.WebviewPanelSerializer { async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) { // `state` is the state persisted using `setState` inside the webview console.log(`Got state: ${state}`); // Restore the content of our webview. // // Make sure we hold on to the `webviewPanel` passed in here and // also restore any event listeners we need on it. webviewPanel.webview.html = getWebviewContent(); } } Now if you restart VS Code with a cat coding panel open, the panel will be automatically restored in the same editor position. ### retainContextWhenHidden For webviews with very complex UI or state that cannot be quickly saved and restored, you can instead use the `retainContextWhenHidden` option. This option makes a webview keep its content around but in a hidden state, even when the webview itself is no longer in the foreground. Although **Cat Coding** can hardly be said to have complex state, let's try enabling `retainContextWhenHidden` to see how the option changes a webview's behavior: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('catCoding.start', () => { const panel = vscode.window.createWebviewPanel( 'catCoding', 'Cat Coding', vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true } ); panel.webview.html = getWebviewContent(); }) ); } function getWebviewContent() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cat Coding</title> </head> <body> <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" /> <h1 id="lines-of-code-counter">0</h1> <script> const counter = document.getElementById('lines-of-code-counter'); let count = 0; setInterval(() => { counter.textContent = count++; }, 100); </script> </body> </html>`; }  Notice how the counter does not reset now when the webview is hidden and then restored. No extra code required! With `retainContextWhenHidden`, the webview acts similarly to a background tab in a web browser. Scripts and other dynamic content keep running even when the tab is not active or visible. You can also send messages to a hidden webview when `retainContextWhenHidden` is enabled. Although `retainContextWhenHidden` may be appealing, keep in mind that this has high memory overhead and should only be used when other persistence techniques will not work. ## Accessibility The class `vscode-using-screen-reader` will be added to your webview's main body in contexts where the user is operating VS Code with a screen reader. Additionally, the class `vscode-reduce-motion` will be added to the document's main body element in cases where the user has expressed a preference to reduce the amount of motion in the window. By observing these classes and adjusting your rendering accordingly, your webview content can better reflect the user's preferences. ## Next steps If you'd like to learn more about VS Code extensibility, try these topics: * Extension API - Learn about the full VS Code Extension API. * Extension Capabilities - Take a look at other ways to extend VS Code. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/notebook In this article The Notebook API allows Visual Studio Code extensions to open files as notebooks, execute notebook code cells, and render notebook outputs in a variety of rich and interactive formats. You may know of popular notebook interfaces like Jupyter Notebook or Google Colab โ the Notebook API allows for similar experiences inside Visual Studio Code. ## Parts of a Notebook A notebook consists of a sequence of cells and their outputs. The cells of a notebook can be either **Markdown cells** or **code cells**, and are rendered within the core of VS Code. The outputs can be of various formats. Some output formats, such as plain text, JSON, images, and HTML are rendered by VS Code core. Others, such as application-specific data or interactive applets, are rendered by extensions. Cells in a notebook are read and written to the file system by a `NotebookSerializer`, which handles reading data from the file system and converting it into a description of cells, as well as persisting modifications to the notebook back to the file system. The **code cells** of a notebook can be executed by a `NotebookController`, which takes the contents of a cell and from it produces zero or more outputs in a variety of formats ranging from plain text to formatted documents or interactive applets. Application-specific output formats and interactive applet outputs are rendered by a `NotebookRenderer`. Visually:  ## Serializer NotebookSerializer API Reference A `NotebookSerializer` is responsible for taking the serialized bytes of a notebook and deserializing those bytes into `NotebookData`, which contains list of Markdown and code cells. It is responsible for the opposite conversion as well: taking `NotebookData` and converting the data into serialized bytes to be saved. Samples: * JSON Notebook Serializer: Simple example notebook that takes JSON input and outputs prettified JSON in a custom `NotebookRenderer`. * Markdown Serializer: Open and edit Markdown files as a notebook. ### Example In this example, we build a simplified notebook provider extension for viewing files in the Jupyter Notebook format with a `.notebook` extension (instead of its traditional file extension `.ipynb`). A notebook serializer is declared in `package.json` under the `contributes.notebooks` section as follows: { ... "contributes": { ... "notebooks": [ { "type": "my-notebook", "displayName": "My Notebook", "selector": [ { "filenamePattern": "*.notebook" } ] } ] } } The notebook serializer is then registered in the extension's activation event: import { TextDecoder, TextEncoder } from 'util'; import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.workspace.registerNotebookSerializer('my-notebook', new SampleSerializer()) ); } interface RawNotebook { cells: RawNotebookCell[]; } interface RawNotebookCell { source: string[]; cell_type: 'code' | 'markdown'; } class SampleSerializer implements vscode.NotebookSerializer { async deserializeNotebook( content: Uint8Array, _token: vscode.CancellationToken ): Promise<vscode.NotebookData> { var contents = new TextDecoder().decode(content); let raw: RawNotebookCell[]; try { raw = (<RawNotebook>JSON.parse(contents)).cells; } catch { raw = []; } const cells = raw.map( item => new vscode.NotebookCellData( item.cell_type === 'code' ? vscode.NotebookCellKind.Code : vscode.NotebookCellKind.Markup, item.source.join('\n'), item.cell_type === 'code' ? 'python' : 'markdown' ) ); return new vscode.NotebookData(cells); } async serializeNotebook( data: vscode.NotebookData, _token: vscode.CancellationToken ): Promise<Uint8Array> { let contents: RawNotebookCell[] = []; for (const cell of data.cells) { contents.push({ cell_type: cell.kind === vscode.NotebookCellKind.Code ? 'code' : 'markdown', source: cell.value.split(/\r?\n/g) }); } return new TextEncoder().encode(JSON.stringify(contents)); } } Now try running your extension and opening a Jupyter Notebook formatted file saved with the `.notebook` extension:  You should be able to open Jupyter-formatted notebooks and view their cells as both plain text and rendered Markdown, as well as edit the cells. However, outputs will not be persisted to disk; to save outputs you would need to also serialize and deserialize the outputs of cells from `NotebookData`. To run a cell, you will need to implement a `NotebookController`. ## Controller NotebookController API Reference A `NotebookController` is responsible for taking a **code cell** and executing the code to produce some or no outputs. A controller is directly associated with a notebook serializer and a type of notebook by setting the `NotebookController#notebookType` property on creation of the controller. Then the controller is registered globally by pushing the controller onto the extension subscriptions on activate of the extension. export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(new Controller()); } class Controller { readonly controllerId = 'my-notebook-controller-id'; readonly notebookType = 'my-notebook'; readonly label = 'My Notebook'; readonly supportedLanguages = ['python']; private readonly _controller: vscode.NotebookController; private _executionOrder = 0; constructor() { this._controller = vscode.notebooks.createNotebookController( this.controllerId, this.notebookType, this.label ); this._controller.supportedLanguages = this.supportedLanguages; this._controller.supportsExecutionOrder = true; this._controller.executeHandler = this._execute.bind(this); } private _execute( cells: vscode.NotebookCell[], _notebook: vscode.NotebookDocument, _controller: vscode.NotebookController ): void { for (let cell of cells) { this._doExecution(cell); } } private async _doExecution(cell: vscode.NotebookCell): Promise<void> { const execution = this._controller.createNotebookCellExecution(cell); execution.executionOrder = ++this._executionOrder; execution.start(Date.now()); // Keep track of elapsed time to execute cell. /* Do some execution here; not implemented */ execution.replaceOutput([ new vscode.NotebookCellOutput([ vscode.NotebookCellOutputItem.text('Dummy output text!') ]) ]); execution.end(true, Date.now()); } } If you're publishing a `NotebookController`\-providing extension separately from its serializer, then add an entry like `notebookKernel<ViewTypeUpperCamelCased>` to the `keywords` in its `package.json`. For example, if you published an alternative kernel for the `github-issues` notebook type, you should add a keyword `notebookKernelGithubIssues` keyword to your extension. This improves the discoverability of the extension when opening notebooks of the type `<ViewTypeUpperCamelCased>` from within Visual Studio Code. Samples: * GitHub Issues Notebook: Controller to execute queries for GitHub Issues * REST Book: Controller to run REST queries. * Regexper notebooks: Controller to visualize regular expressions. ## Output types Outputs must be in one of three formats: Text Output, Error Output, or Rich Output. A kernel may provide multiple outputs for a single execution of a cell, in which case they will be displayed as a list. Simple formats like Text Output, Error Output, or "simple" variants of Rich Output (HTML, Markdown, JSON, etc.) are rendered by VS Code core, whereas application specific Rich Output types are rendered by a NotebookRenderer. An extension may optionally choose to render "simple" Rich Outputs itself, for instance to add LaTeX support to Markdown outputs.  ### Text Output Text outputs are the most simple output format, and work much like many REPLs you may be familiar with. They consist only of a `text` field, which is rendered as plain text in the cell's output element: vscode.NotebookCellOutputItem.text('This is the output...');  ### Error Output Error outputs are helpful for displaying runtime errors in a consistent and understandable manner. They support standard `Error` objects. try { /* Some code */ } catch (error) { vscode.NotebookCellOutputItem.error(error); }  ### Rich Output Rich outputs are the most advanced form of displaying cell outputs. They allow for providing many different representations of the output data, keyed by mimetype. For example, if a cell output was to represent a GitHub Issue, the kernel might produce a rich output with several properties on its `data` field: * A `text/html` field containing a formatted view of the issue. * A `text/x-json` field containing a machine readable view. * An `application/github-issue` field that a `NotebookRenderer` could use to create a fully interactive view of the issue. In this case, the `text/html` and `text/x-json` views will be rendered by VS Code natively, but the `application/github-issue` view will display an error if no `NotebookRenderer` was registered to that mimetype. execution.replaceOutput([new vscode.NotebookCellOutput([ vscode.NotebookCellOutputItem.text('<b>Hello</b> World', 'text/html'), vscode.NotebookCellOutputItem.json({ hello: 'world' }), vscode.NotebookCellOutputItem.json({ custom-data-for-custom-renderer: 'data' }, 'application/custom'), ])]);  By default, VS Code can render the following mimetypes: * application/javascript * text/html * image/svg+xml * text/markdown * image/png * image/jpeg * text/plain VS Code will render these mimetypes as code in a built-in editor: * text/x-json * text/x-javascript * text/x-html * text/x-rust * ... text/x-LANGUAGE\_ID for any other built-in or installed languages. This notebook is using the built-in editor to display some Rust code:  To render an alternative mimetype, a `NotebookRenderer` must be registered for that mimetype. ## Notebook Renderer A notebook renderer is responsible for taking output data of a specific mimetype and providing a rendered view of that data. A renderer shared by output cells can maintain global state between these cells. The complexity of the rendered view can range from simple static HTML to dynamic fully interactive applets. In this section, we'll explore various techniques for rendering an output representing a GitHub Issue. You can get started quickly using boilerplate from our Yeoman generators. To do so, first install Yeoman and the VS Code Generators using: npm install -g yo generator-code Then, run `yo code` and choose `New Notebook Renderer (TypeScript)`. If you don't use this template, you'll just want to make sure that you add `notebookRenderer` to the `keywords` in your extension's `package.json`, and mention its mimetype somewhere in the extension name or description, so that users can find your renderer. ### A Simple, Non-Interactive Renderer Renderers are declared for a set of mimetypes by contributing to the `contributes.notebookRenderer` property of an extension's `package.json`. This renderer will work with input in the `ms-vscode.github-issue-notebook/github-issue` format, which we will assume some installed controller is able to provide: { "activationEvents": ["...."], "contributes": { ... "notebookRenderer": [ { "id": "github-issue-renderer", "displayName": "GitHub Issue Renderer", "entrypoint": "./out/renderer.js", "mimeTypes": [ "ms-vscode.github-issue-notebook/github-issue" ] } ] } } Output renderers are always rendered in a single `iframe`, separate from the rest of VS Code's UI, to ensure they don't accidentally interfere or cause slowdowns in VS Code. The contribution refers to an "entrypoint" script, which is loaded into the notebook's `iframe` right before any output needs to be rendered. Your entrypoint needs to be a single file, which you can write yourself, or use a bundler like Webpack, Rollup, or Parcel to create. When it's loaded, your entrypoint script should export `ActivationFunction` from `vscode-notebook-renderer` to render your UI once VS Code is ready to render your renderer. For example, this will put all your GitHub issue data as JSON into the cell output: import type { ActivationFunction } from 'vscode-notebook-renderer'; export const activate: ActivationFunction = context => ({ renderOutputItem(data, element) { element.innerText = JSON.stringify(data.json()); } }); You can refer to the complete API definition here. If you're using TypeScript, you can install `@types/vscode-notebook-renderer` and then add `vscode-notebook-renderer` to the `types` array in your `tsconfig.json` to make these types available in your code. To create richer content, you could manually create DOM elements, or use a framework like Preact and render it into the output element, for example: import type { ActivationFunction } from 'vscode-notebook-renderer'; import { h, render } from 'preact'; const Issue: FunctionComponent<{ issue: GithubIssue }> = ({ issue }) => ( <div key={issue.number}> <h2> {issue.title} (<a href={`https://github.com/${issue.repo}/issues/${issue.number}`}>#{issue.number}</a>) </h2> <img src={issue.user.avatar_url} style={{ float: 'left', width: 32, borderRadius: '50%', marginRight: 20 }} /> <i>@{issue.user.login}</i> Opened: <div style="margin-top: 10px">{issue.body}</div> </div> ); const GithubIssues: FunctionComponent<{ issues: GithubIssue[]; }> = ({ issues }) => ( <div>{issues.map(issue => <Issue key={issue.number} issue={issue} />)}</div> ); export const activate: ActivationFunction = (context) => ({ renderOutputItem(data, element) { render(<GithubIssues issues={data.json()} />, element); } }); Running this renderer on an output cell with a `ms-vscode.github-issue-notebook/github-issue` data field gives us the following static HTML view:  If you have elements outside of the container or other asynchronous processes, you can use `disposeOutputItem` to tear them down. This event will fire when output is cleared, a cell is deleted, and before new output is rendered for an existing cell. For example: const intervals = new Map(); export const activate: ActivationFunction = (context) => ({ renderOutputItem(data, element) { render(<GithubIssues issues={data.json()} />, element); intervals.set(data.mime, setInterval(() => { if(element.querySelector('h2')) { element.querySelector('h2')!.style.color = `hsl(${Math.random() * 360}, 100%, 50%)`; } }, 1000)); }, disposeOutputItem(id) { clearInterval(intervals.get(id)); intervals.delete(id); } }); It's important to bear in mind that all outputs for a notebook are rendered in different elements in the same iframe. If you use functions like `document.querySelector`, make sure to scope it to the specific output you're interested in to avoid conflicting with other outputs. In this example, we use `element.querySelector` to avoid that issue. ### Interactive Notebooks (communicating with the controller) Imagine we want to add the ability to view an issue's comments after clicking a button in the rendered output. Assuming a controller can provide issue data with comments under the `ms-vscode.github-issue-notebook/github-issue-with-comments` mimetype, we might try to retrieve all the comments up front and implement it as follows: const Issue: FunctionComponent<{ issue: GithubIssueWithComments }> = ({ issue }) => { const [showComments, setShowComments] = useState(false); return ( <div key={issue.number}> <h2> {issue.title} (<a href={`https://github.com/${issue.repo}/issues/${issue.number}`}>#{issue.number}</a>) </h2> <img src={issue.user.avatar_url} style={{ float: 'left', width: 32, borderRadius: '50%', marginRight: 20 }} /> <i>@{issue.user.login}</i> Opened: <div style="margin-top: 10px">{issue.body}</div> <button onClick={() => setShowComments(true)}>Show Comments</button> {showComments && issue.comments.map(comment => <div>{comment.text}</div>)} </div> ); }; This immediately raises some flags. For one, we're loading full comment data for all issues, even before we've clicked the button. Additionally, we require controller support for a whole different mimetype even though we just want to show a bit more data. Instead, the controller can provide additional functionality to the renderer by including a preload script which VS Code will load in the iframe as well. This script has access global functions `postKernelMessage` and `onDidReceiveKernelMessage` that can be used to communicate with the controller.  For example, you might modify your controller `rendererScripts` to reference a new file where you create a connection back to the Extension Host, and expose a global communication script for the renderer to use. In your controller: class Controller { // ... readonly rendererScriptId = 'my-renderer-script'; constructor() { // ... this._controller.rendererScripts.push( new vscode.NotebookRendererScript( vscode.Uri.file(/* path to script */), rendererScriptId ) ); } } In your `package.json` specify your script as a dependency of your renderer: { "activationEvents": ["...."], "contributes": { ... "notebookRenderer": [ { "id": "github-issue-renderer", "displayName": "GitHub Issue Renderer", "entrypoint": "./out/renderer.js", "mimeTypes": [...], "dependencies": [ "my-renderer-script" ] } ] } } In your script file you can declare communication functions to communicate with the controller: import 'vscode-notebook-renderer/preload'; globalThis.githubIssueCommentProvider = { loadComments(issueId: string, callback: (comments: GithubComment[]) => void) { postKernelMessage({ command: 'comments', issueId }); onDidReceiveKernelMessage(event => { if (event.data.type === 'comments' && event.data.issueId === issueId) { callback(event.data.comments); } }); } }; And then you can consume that in the renderer. You want to make sure that you check whether the global exposed by the controller's render scripts is available, since other developers might create github issue output in other notebooks and controllers that don't implement the `githubIssueCommentProvider`. In this case, we'll only show the **Load Comments** button if the global is available: const canLoadComments = globalThis.githubIssueCommentProvider !== undefined; const Issue: FunctionComponent<{ issue: GithubIssue }> = ({ issue }) => { const [comments, setComments] = useState([]); const loadComments = () => globalThis.githubIssueCommentProvider.loadComments(issue.id, setComments); return ( <div key={issue.number}> <h2> {issue.title} (<a href={`https://github.com/${issue.repo}/issues/${issue.number}`}>#{issue.number}</a>) </h2> <img src={issue.user.avatar_url} style={{ float: 'left', width: 32, borderRadius: '50%', marginRight: 20 }} /> <i>@{issue.user.login}</i> Opened: <div style="margin-top: 10px">{issue.body}</div> {canLoadComments && <button onClick={loadComments}>Load Comments</button>} {comments.map(comment => <div>{comment.text}</div>)} </div> ); }; Finally, we want to set up communication to the controller. `NotebookController.onDidReceiveMessage` method is called when a renderer posts a message using the global `postKernelMessage` function. To implement this method, attach to `onDidReceiveMessage` to listen for messages: class Controller { // ... constructor() { // ... this._controller.onDidReceiveMessage(event => { if (event.message.command === 'comments') { _getCommentsForIssue(event.message.issueId).then( comments => this._controller.postMessage({ type: 'comments', issueId: event.message.issueId, comments }), event.editor ); } }); } } ### Interactive Notebooks (communicating with the extension host) Imagine we want to add the ability to open the output item within a separate editor. To make this possible, the renderer needs to be able to send a message to the extension host, which will then launch the editor. This would be useful in scenarios where the renderer and controller are two separate extensions. In the `package.json` of the renderer extension specify the value for `requiresMessaging` as `optional` which allows your renderer to work in both situations when it has and doesn't have access to the extension host. { "activationEvents": ["...."], "contributes": { ... "notebookRenderer": [ { "id": "output-editor-renderer", "displayName": "Output Editor Renderer", "entrypoint": "./out/renderer.js", "mimeTypes": [...], "requiresMessaging": "optional" } ] } } The possible values for `requiresMessaging` include: * `always` : Messaging is required. The renderer will only be used when it's part of an extension that can be run in an extension host. * `optional`: The renderer is better with messaging when the extension host is available, but it's not required to install and run the renderer. * `never` : The renderer does not require messaging. The last two options are preferred, as this ensures the portability of renderer extensions to other contexts where the extension host might not necessarily be available. The renderer script file can setup communications as follows: import { ActivationFunction } from 'vscode-notebook-renderer'; export const activate: ActivationFunction = (context) => ({ renderOutputItem(data, element) { // Render the output using the output `data` .... // The availability of messaging depends on the value in `requiresMessaging` if (!context.postMessage){ return; } // Upon some user action in the output (such as clicking a button), // send a message to the extension host requesting the launch of the editor. document.querySelector('#openEditor').addEventListener('click', () => { context.postMessage({ request: 'showEditor', data: '<custom data>' }) }); } }); And then you can consume that message in the extension host as follows: const messageChannel = notebooks.createRendererMessaging('output-editor-renderer'); messageChannel.onDidReceiveMessage(e => { if (e.message.request === 'showEditor') { // Launch the editor for the output identified by `e.message.data` } }); Note: * To ensure your extension is running in the extension host before messages are delivered, add `onRenderer:<your renderer id>` to your `activationEvents` and set up communication in your extension's `activate` function. * Not all messages sent by the renderer extension to the extension host are guaranteed to be delivered. A user could close the notebook before messages from the renderer are delivered. ## Supporting debugging For some controllers, such as those that implement a programming language, it can be desirable to allow debugging a cell's execution. To add debugging support, a notebook kernel can implement a debug adapter, either by directly implementing the debug adapter protocol (DAP), or by delegating and transforming the protocol to an existing notebook debugger (as done in the 'vscode-simple-jupyter-notebook' sample). A much simpler approach is to use an existing unmodified debug extension and transform the DAP for notebook needs on the fly (as done in 'vscode-nodebook'). Samples: * vscode-nodebook: Node.js notebook with debugging support provided by VS Code's built-in JavaScript debugger and some simple protocol transformations * vscode-simple-jupyter-notebook: Jupyter notebook with debugging support provided by the existing Xeus debugger 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/custom-editors In this article Custom editors allow extensions to create fully customizable read/write editors that are used in place of VS Code's standard text editor for specific types of resources. They have a wide variety of use cases, such as: * Previewing assets, such as shaders or 3D models, directly in VS Code. * Creating WYSIWYG editors for languages such as Markdown or XAML. * Offering alternative visual renderings for data files such as CSV or JSON or XML. * Building fully customizable editing experiences for binary or text files. This document provides an overview of the custom editor API and the basics of implementing a custom editor. We'll take a look at the two types of custom editors and how they differ, as well as which one is right for your use case. Then for each of these custom editor types, we'll cover the basics of building a well behaved custom editor. Although custom editors are a powerful new extension point, implementing a basic custom editor is not actually that difficult! Still, if you are working on your first VS Code extension, you may want to consider holding off on diving into custom editors until you are more familiar with the basics of the VS Code API. Custom editors build on a lot of VS Code conceptsโsuch as webviews and text documentsโso it may be a bit overwhelming if you are learning all of these new ideas at the same time. But if you're feeling ready and are thinking about all the cool custom editors you are going to build, then let's get started! Be sure to download the custom editor extension sample so you can follow along with the documentation and see how the custom editor API comes together. ## Links * Custom Editor sample ### VS Code API Usage * `window.registerCustomEditorProvider` * `CustomTextEditorProvider` ## Custom Editor API basics A custom editor is an alternative view that is shown in place of VS Code's standard text editor for specific resources. There are two parts to a custom editor: the view that users interact with and the document model that your extension uses to interact with the underlying resource. The view side of a custom editor is implemented using a webview. This lets you build the user interface of your custom editor using standard HTML, CSS, and JavaScript. Webviews cannot access the VS Code API directly but they can talk with extensions by passing messages back and forth. Check out our webview documentation for more information on webviews and best practices for working with them. The other part of a custom editor is the document model. This model is how your extension understands the resource (file) it is working with. A `CustomTextEditorProvider` uses VS Code's standard TextDocument as its document model and all changes to the file are expressed using VS Code's standard text editing APIs. `CustomReadonlyEditorProvider` and `CustomEditorProvider` on the other hand let you provide your own document model, which lets them be used for non-text file formats. Custom editors have a single document model per resource but there may be multiple editor instances (views) of this document. For example, imagine that you open a file that has a `CustomTextEditorProvider` and then run the **View: Split editor** command. In this case, there is still just a single `TextDocument` since there is still just a single copy of the resource in the workspace, but there are now two webviews for that resource. ### `CustomEditor` vs `CustomTextEditor` There are two classes of custom editors: custom text editors and custom editors. The main difference between these is how they define their document model. A `CustomTextEditorProvider` uses VS Code's standard `TextDocument` as its data model. You can use a `CustomTextEditor` for any text based file types. `CustomTextEditor`s are considerably easier to implement because VS Code already knows how to work with text files and can therefore implement operations such as save and backing up files for hot exit. With a `CustomEditorProvider` on the other hand, your extension brings its own document model. This means that you can use a `CustomEditor` for binary formats such as images, but it also means that your extension is responsible for a lot more, including implementing save and backing. You can skip over much of this complexity if your custom editor is readonly, such as custom editors for previews. When trying to decide which type of custom editor to use, the decision is usually simple: if you are working with a text based file format use `CustomTextEditorProvider`, for binary file formats use `CustomEditorProvider`. ### Contribution point The `customEditors` contribution point is how your extension tells VS Code about the custom editors that it provides. For example, VS Code needs to know what types of files your custom editor works with as well as how to identify your custom editor in any UI. Here's a basic `customEditor` contribution for the custom editor extension sample: "contributes": { "customEditors": [ { "viewType": "catEdit.catScratch", "displayName": "Cat Scratch", "selector": [ { "filenamePattern": "*.cscratch" } ], "priority": "default" } ] } `customEditors` is an array, so your extension can contribute multiple custom editors. Let's break down the custom editor entry itself: * `viewType` - Unique identifier for your custom editor. This is how VS Code ties a custom editor contribution in the `package.json` to your custom editor implementation in code. This must be unique across all extensions, so instead of a generic `viewType` such as `"preview"` make sure to use one that is unique to your extension, for example `"viewType": "myAmazingExtension.svgPreview"` * `displayName` - Name that identifies the custom editor in VS Code's UI. The display name is shown to the user in VS Code UI such as the **View: Reopen with** dropdown. * `selector` - Specifies which files a custom editor is active for. The `selector` is an array of one or more glob patterns. These glob patterns are matched against file names to determine if the custom editor can be used for them. A `filenamePattern` such as `*.png` will enable the custom editor for all PNG files. You can also create more specific patterns that match on file or directory names, for example `**/translations/*.json`. * `priority` -ย (optional) Specifies when the custom editor is used. `priority` controls when a custom editor is used when a resource is open. Possible values are: * `"default"` -ย Try to use the custom editor for every file that matches the custom editor's `selector`. If there are multiple custom editors for a given file, the user will have to select which custom editor they want to use. * `"option"` -ย Do not use the custom editor by default but allow users to switch to it or configure it as their default. ### Custom editor activation When a user opens one of your custom editors, VS Code fires an `onCustomEditor:VIEW_TYPE` activation event. During activation, your extension must call `registerCustomEditorProvider` to register a custom editor with the expected `viewType`. It's important to note that `onCustomEditor` is only called when VS Code needs to create an instance of your custom editor. If VS Code is merely showing the user some information about an available custom editorโsuch as with the **View: Reopen with** commandโyour extension will not be activated. ## Custom Text Editor Custom text editors let you create custom editors for text files. This can be anything from plain unstructured text to CSV to JSON or XML. Custom text editors use VS Code's standard TextDocument as their document model. The custom editor extension sample includes a simple example custom text editor for cat scratch files (which are just JSON files that end with a `.cscratch` file extension). Let's take a look at some of the important bits of implementing a custom text editor. ### Custom Text Editor lifecycle VS Code handles the lifecycle of both the view component of custom text editors (the webviews) and the model component (`TextDocument`). VS Code calls out to your extension when it needs to create a new custom editor instance and cleans up the editor instances and document model when the user closes their tabs. To understand how this all works in practice, let's work through what happens from an extension's point of view when a user opens a custom text editor and then when a user closes a custom text editor. **Opening a custom text editor** Using the custom editor extension sample, here's what happens when the user first opens a `.cscratch` file: 1. VS Code fires an `onCustomEditor:catCustoms.catScratch` activation event. This activates our extension if it has not already been activated. During activation, our extension must ensure the extension registers a `CustomTextEditorProvider` for `catCustoms.catScratch` by calling `registerCustomEditorProvider`. 2. VS Code then invokes `resolveCustomTextEditor` on the registered `CustomTextEditorProvider` for `catCustoms.catScratch`. This method takes the `TextDocument` for the resource that is being opened and a `WebviewPanel`. The extension must fill in the initial HTML contents for this webview panel. Once `resolveCustomTextEditor` returns, our custom editor is displayed to the user. What is drawn inside the webview is entirely up to our extension. This same flow happens every time a custom editor is opened, even when you split a custom editor. Every instance of a custom editor has its own `WebviewPanel`, although multiple custom text editors will share the same `TextDocument` if they are for the same resource. Remember: think of the `TextDocument` as being the model for the resource while the webview panels are views of that model. **Closing custom text editors** When a user closes a custom text editor, VS Code fires the `WebviewPanel.onDidDispose` event on the `WebviewPanel`. At this point, your extension should clean up any resources associated with that editor (event subscriptions, file watchers, etc.) When the last custom editor for a given resource is closed, the `TextDocument` for that resource will also be disposed provided there are no other editors using it and no other extensions are holding onto it. You can check the `TextDocument.isClosed` property to see if the `TextDocument` has been closed. Once a `TextDocument` is closed, opening the same resource using a custom editor will cause a new `TextDocument` to be opened. ### Synchronizing changes with the TextDocument Since custom text editors use a `TextDocument` as their document model, they are responsible for updating the `TextDocument` whenever an edit occurs in a custom editor as well as updating themselves whenever the `TextDocument` changes. **From webview to `TextDocument`** Edits in custom text editors can take many different formsโclicking a button, changing some text, dragging some items around. Whenever a user edits the file itself inside the custom text editor, the extension must update the `TextDocument`. Here's how the cat scratch extension implements this: 1. User clicks the **Add scratch** button in the webview. This posts a message from the webview back to the extension. 2. The extension receives the message. It then updates its internal model of the document (which in the cat scratch example just consists of adding a new entry to the JSON). 3. The extension creates a `WorkspaceEdit` that writes the updated JSON to the document. This edit is applied using `vscode.workspace.applyEdit`. Try to keep your workspace edit to the minimal change required to update the document. Also keep in mind that if you are working with a language such as JSON, your extension should try to observe the user's existing formatting conventions (spaces vs tabs, indent size, etc.). **From `TextDocument` to webviews** When a `TextDocument` changes, your extension also needs to make sure its webviews reflect the documents new state. TextDocuments can be changed by user actions such as undo, redo, or revert file; by other extensions using a `WorkspaceEdit`; or by a user who opens the file in VS Code's default text editor. Here's how the cat scratch extension implements this: 1. In the extension, we subscribe to the `vscode.workspace.onDidChangeTextDocument` event. This event is fired for every change to the `TextDocument` (including changes that our custom editor makes!) 2. When a change comes in for a document that we have an editor for, we post a message to the webview with its new document state. This webview then updates itself to render the updated document. It's important to remember that any file edits that a custom editor triggers will cause `onDidChangeTextDocument` to fire. Make sure your extension does not get into an update loop where the user makes an edit in the webview, which fires `onDidChangeTextDocument`, which causes the webview to update, which causes the webview to trigger another update on your extension, which fires `onDidChangeTextDocument`, and so on. Also remember that if you are working with a structured language such as JSON or XML, the document may not always be in a valid state. Your extension must either be able to gracefully handle errors or display an error message to the user so that they understand what is wrong and how to fix it. Finally, if updating your webviews is expensive, consider debouncing the updates to your webview. ## Custom Editor `CustomEditorProvider` and `CustomReadonlyEditorProvider` let you create custom editors for binary file formats. This API gives you full control over how the file is displayed to users, how edits are made to it, and lets your extension hook into `save` and other file operations. Again, if you are building an editor for a text based file format, strongly consider using a `CustomTextEditor` instead as they are far simpler to implement. The custom editor extension sample includes a simple example custom binary editor for paw draw files (which are just jpeg files that end with a `.pawdraw` file extension). Let's take a look at what goes into building a custom editor for binary files. ### CustomDocument With custom editors, your extension is responsible for implementing its own document model with the `CustomDocument` interface. This leaves your extension free to store whatever data it needs on a `CustomDocument` in order to interact with your custom editor, but it also means that your extension must implement basic document operations such as saving and backing up file data for hot exit. There is one `CustomDocument` per opened file. Users can open multiple editors for a single resourceโsuch as by splitting the current custom editorโbut all those editors will be backed by the same `CustomDocument`. ### Custom Editor lifecycle **supportsMultipleEditorsPerDocument** By default, VS Code only allows there to be one editor for each custom document. This limitation makes it easier to correctly implement a custom editor as you do not have to worry about synchronizing multiple custom editor instances with each other. If your extension can support it however, we recommend setting `supportsMultipleEditorsPerDocument: true` when registering your custom editor so that multiple editor instances can be opened for the same document. This will make your custom editors behave more like VS Code's normal text editors. **Opening Custom Editors** When the user opens a file that matches the `customEditor` contribution point, VS Code fires an `onCustomEditor` activation event and then invokes the provider registered for the provided view type. A `CustomEditorProvider` has two roles: providing the document for the custom editor and then providing the editor itself. Here's an ordered list of what happens for the `catCustoms.pawDraw` editor from the custom editor extension sample: 1. VS Code fires an `onCustomEditor:catCustoms.pawDraw` activation event. This activates our extension if it has not already been activated. We must also make sure our extension registers a `CustomReadonlyEditorProvider` or `CustomEditorProvider` for `catCustoms.pawDraw` during activation. 2. VS Code calls `openCustomDocument` on our `CustomReadonlyEditorProvider` or `CustomEditorProvider` registered for `catCustoms.pawDraw` editors. Here our extension is given a resource uri and must return a new `CustomDocument` for that resource. This is the point at which our extension should create its document internal model for that resource. This may involve reading and parsing the initial resource state from disk or initializing our new `CustomDocument`. Our extension can define this model by creating a new class that implements `CustomDocument`. Remember that this initialization stage is entirely up to extensions; VS Code does not care about any additional information extensions store on a `CustomDocument`. 3. VS Code calls `resolveCustomEditor` with the `CustomDocument` from step 2 and a new `WebviewPanel`. Here our extension must fill in the initial html for the custom editor. If we need, we can also hold onto a reference to the `WebviewPanel` so that we can reference it later, for example inside commands. Once `resolveCustomEditor` returns, our custom editor is displayed to the user. If the user opens the same resource in another editor group using our custom editorโfor example by splitting the first editorโthe extension's job is simplified. In this case, VS Code just calls `resolveCustomEditor` with the same `CustomDocument` we created when the first editor was opened. **Closing Custom Editors** Say we have two instance of our custom editors open for the same resource. When the user closes these editors, VS Code signals our extension so that it can clean up any resources associated with the editor. When the first editor instance is closed, VS Code fires the `WebviewPanel.onDidDispose` event on the `WebviewPanel` from the closed editor. At this point, our extension must clean up any resources associated with that specific editor instance. When the second editor is closed, VS Code again fires `WebviewPanel.onDidDispose`. However now we've also closed all the editors associated with the `CustomDocument`. When there are no more editors for a `CustomDocument`, VS Code calls the `CustomDocument.dispose` on it. Our extension's implementation of `dispose` must clean up any resources associated with the document. If the user then reopens the same resource using our custom editor, we will go back through the whole `openCustomDocument`, `resolveCustomEditor` flow with a new `CustomDocument`. ### Readonly Custom editors Many of the following sections only apply to custom editors that support editing and, while it may sound paradoxical, many custom editors don't require editing capabilities at all. Consider a image preview for example. Or a visual rendering of a memory dump. Both can be implemented using custom editors but neither need to be editable. That's where `CustomReadonlyEditorProvider` comes in. A `CustomReadonlyEditorProvider` lets you create custom editors that do not support editing. They can still be interactive but don't support operations such as undo and save. It is also much simpler to implement a readonly custom editor compared to a fully editable one. ### Editable Custom Editor Basics Editable custom editors let you hook in to standard VS Code operations such as undo and redo, save, and hot exit. This makes editable custom editors very powerful, but also means that properly implementing one is much more complex than implementing an editable custom text editor or a readonly custom editor. Editable custom editors are implemented by `CustomEditorProvider`. This interface extends `CustomReadonlyEditorProvider`, so you'll have to implement basic operations such as `openCustomDocument` and `resolveCustomEditor`, along with a set of editing specific operations. Let's take a look at the editing specific parts of `CustomEditorProvider`. **Edits** Changes to a editable custom document are expressed through edits. An edit can be anything from a text change, to an image rotation, to reordering a list. VS Code leaves the specifics of what an edit does entirely up to your extension, but VS Code does need to know when an edit takes places. Editing is how VS Code marks documents as dirty, which in turn enables auto save and back ups. Whenever a user makes an edit in any of the webviews for your custom editor, your extension must fire a `onDidChangeCustomDocument` event from its `CustomEditorProvider`. The `onDidChangeCustomDocument` event can fire two event types depending on your custom editor implementation: `CustomDocumentContentChangeEvent` and `CustomDocumentEditEvent`. **CustomDocumentContentChangeEvent** A `CustomDocumentContentChangeEvent` is a bare-bones edit. Its only function is to tell VS Code that a document has been edited. When an extension fires a `CustomDocumentContentChangeEvent` from `onDidChangeCustomDocument`, VS Code will mark the associated document as being dirty. At this point, the only way for the document to become non-dirty is for the user to either save or revert it. Custom editors that use `CustomDocumentContentChangeEvent` do not support undo/redo. **CustomDocumentEditEvent** A `CustomDocumentEditEvent` is a more complex edit that allows for undo/redo. You should always try to implement your custom editor using `CustomDocumentEditEvent` and only fallback to using `CustomDocumentContentChangeEvent` if implementing undo/redo is not possible. A `CustomDocumentEditEvent` has the following fields: * `document` โย The `CustomDocument` the edit was for. * `label` โ Optional text that that describes what type of edit was made (for example: "Crop", "Insert", ...) * `undo` โย Function invoked by VS Code when the edit needs to be undone. * `redo` โย Function invoked by VS Code when the edits needs to be redone. When an extension fires a `CustomDocumentEditEvent` from `onDidChangeCustomDocument`, VS Code marks the associated document as being dirty. To make the document no longer dirty, a user can then either save or revert the document, or undo/redo back to the document's last saved state. The `undo` and `redo` methods on an editor are called by VS Code when that specific edit needs to be undone or reapplied. VS Code maintains an internal stack of edits, so if your extension fires `onDidChangeCustomDocument` with three edits, let's call them `a`, `b`, `c`: onDidChangeCustomDocument(a); onDidChangeCustomDocument(b); onDidChangeCustomDocument(c); The following sequence of user actions results in these calls: undo โ c.undo() undo โย b.undo() redo โย b.redo() redo โย c.redo() redo โย no op, no more edits To implement undo/redo, your extension must update its associated custom document's internal state, as well as updating all associated webviews for the document so that they reflect the document's new state. Keep in mind that there may be multiple webviews for a single resource. These must always show the same document data. Multiple instances of an image editor for example must always show the same pixel data but may allow each editor instance to have its own zoom level and UI state. ### Saving When a user saves a custom editor, your extension is responsible for writing the saved resource in its current state to disk. How your custom editor does this depends largely on your extension's `CustomDocument` type and how your extension tracks edits internally. The first step to saving is getting the data stream to write to disk. Common approaches to this include: * Track the resource's state so that it can be quickly serialized. A basic image editor for example may maintain a buffer of pixel data. * Replay edit since the last save to generate the new file. A more efficient image editor for example might track the edits since the last save, such as `crop`, `rotate`, `scale`. On save, it would then apply these edits to file's last saved state to generate the new file. * Ask a `WebviewPanel` for the custom editor for file data to save. Keep in mind though that custom editors can be saved even when they are not visible. For this reason, it is recommended that that your extension's implementation of `save` does not depend on a `WebviewPanel`. If this is not possible, you can use the `WebviewPanelOptions.retainContextWhenHidden` setting so that the webview stays alive even when it is hidden. `retainContextWhenHidden` does have significant memory overhead so be conservative about using it. After getting the data for the resource, you generally should use the workspace FS API to write it to disk. The FS APIs take a `UInt8Array` of data and can write out both binary and text based files. For binary file data, simply put the binary data into the `UInt8Array`. For text file data, use `Buffer` to convert a string into a `UInt8Array`: const writeData = Buffer.from('my text data', 'utf8'); vscode.workspace.fs.writeFile(fileUri, writeData); ## Next steps If you'd like to learn more about VS Code extensibility, try these topics: * Extension API - Learn about the full VS Code Extension API. * Extension Capabilities - Take a look at other ways to extend VS Code. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/virtual-documents ## Virtual Documents The text document content provider API allows you to create readonly documents in Visual Studio Code from arbitrary sources. You can find a sample extension with source code at: https://github.com/microsoft/vscode-extension-samples/blob/main/virtual-document-sample/README.md. ## TextDocumentContentProvider The API works by claiming an uri-scheme for which your provider then returns text contents. The scheme must be provided when registering a provider and cannot change afterwards. The same provider can be used for multiple schemes and multiple providers can be registered for a single scheme. vscode.workspace.registerTextDocumentContentProvider(myScheme, myProvider); Calling `registerTextDocumentContentProvider` returns a disposable with which the registration can be undone. A provider must only implement the `provideTextDocumentContent`\-function which is called with an uri and cancellation token. const myProvider = new (class implements vscode.TextDocumentContentProvider { provideTextDocumentContent(uri: vscode.Uri): string { // invoke cowsay, use uri-path as text return cowsay.say({ text: uri.path }); } })(); Note how the provider doesn't create uris for virtual documents - its role is to **provide** contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered. This sample uses a 'cowsay'-command that crafts an uri which the editor should then show: vscode.commands.registerCommand('cowsay.say', async () => { let what = await vscode.window.showInputBox({ placeHolder: 'cow say?' }); if (what) { let uri = vscode.Uri.parse('cowsay:' + what); let doc = await vscode.workspace.openTextDocument(uri); // calls back into the provider await vscode.window.showTextDocument(doc, { preview: false }); } }); The command prompts for input, creates an uri of the `cowsay`\-scheme, opens a document for the uri, and finally opens an editor for that document. In step 3, opening the document, the provider is being asked to provide contents for that uri. With this we have a fully functional text document content provider. The next sections describe how virtual documents can be updated and how UI commands can be registered for virtual documents. ### Update Virtual Documents Depending on the scenario virtual documents might change. To support that, providers can implement a `onDidChange`\-event. The `vscode.Event`\-type defines the contract for eventing in VS Code. The easiest way to implement an event is `vscode.EventEmitter`, like so: const myProvider = new (class implements vscode.TextDocumentContentProvider { // emitter and its event onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>(); onDidChange = this.onDidChangeEmitter.event; //... })(); The event emitter has a `fire` method which can be used to notify VS Code when a change has happened in a document. The document which has changed is identified by its uri given as argument to the `fire` method. The provider will then be called again to provide the updated content, assuming the document is still open. That's all what's needed to make VS Code listen for changes of virtual document. To see a more complex example making use of this feature, look at: https://github.com/microsoft/vscode-extension-samples/blob/main/contentprovider-sample/README.md. ### Add Editor Commands Editor actions can be added which only interact with documents provided by an associated content provider. This is a sample command that reverses what the cow just said: // register a command that updates the current cowsay subscriptions.push( vscode.commands.registerCommand('cowsay.backwards', async () => { if (!vscode.window.activeTextEditor) { return; // no editor } let { document } = vscode.window.activeTextEditor; if (document.uri.scheme !== myScheme) { return; // not my scheme } // get path-components, reverse it, and create a new uri let say = document.uri.path; let newSay = say .split('') .reverse() .join(''); let newUri = document.uri.with({ path: newSay }); await vscode.window.showTextDocument(newUri, { preview: false }); }) ); The snippet above checks that we have an active editor and that its document is one of our scheme. These checks are needed because commands are available (and executable) to everyone. Then the path-component of the uri is reversed and a new uri is created from it, last an editor is opened. To top things with an editor command a declarative part in `package.json` is needed. In the `contributes`\-section add this config: "menus": { "editor/title": [ { "command": "cowsay.backwards", "group": "navigation", "when": "resourceScheme == cowsay" } ] } This references the `cowsay.backwards`\-command that defined in the `contributes/commands`\-section and says it should appear in the editor title menu (the toolbar in the upper right corner). Now, just that would mean the command always shows, for every editor. That's what the `when`\-clause is used for - it describes what condition must be true to show the action. In this sample it states that the scheme of the document in the editor must be the `cowsay`\-scheme. The configuration is then repeated for the `commandPalette`\-menu - it shows all commands by default.  ### Events and Visibility Document providers are first class citizens in VS Code, their contents appears in regular text documents, they use the same infrastructure as files etc. However, that also means that "your" documents cannot hide, they will appear in `onDidOpenTextDocument` and `onDidCloseTextDocument`\-events, they are part of `vscode.workspace.textDocuments` and more. The rule for everyone is check the `scheme` of documents and then decide if you want to do something with/for the document. If you need more flexibility and power take a look at the `FileSystemProvider` API. It allows to implement a full file system, having files, folders, binary data, file-deletion, creation and more. You can find a sample extension with source code at: https://github.com/microsoft/vscode-extension-samples/tree/main/fsprovider-sample/README.md. When VS Code is opened on a folder or workspace of a such a file system we call it a virtual workspace. When a virtual workspace is open in a VS Code window, this is shown by a label in the remote indicator in the lower left corner, similar to remote windows. See the Virtual Workspace Guide how extensions can support that setup. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/virtual-workspaces In this article Extensions like the GitHub Repositories extension open VS Code on one or more folders backed by a file system provider. When an extension implements a file system provider, workspace resources may not be located on the local disk, but be **virtual**, located on a server or the cloud, and editing operations happen there. This configuration is called a **virtual workspace**. When a virtual workspace is open in a VS Code window, this is indicated by a label in the remote indicator in the lower left corner, similar to other remote development windows.  Not all extensions are able to work with virtual resources and may require resources to be on disk. Some extensions use tools that rely on disk access, need synchronous file access, or don't have the necessary file system abstractions. In these cases, when in a virtual workspace, VS Code indicates to the user that they are running in a restricted mode and that some extensions are deactivated or work with limited functionality. In general, users want as many extensions as possible to work in virtual workspaces and to have a good user experience when browsing and editing remote resources. This guide shows how extensions can test against virtual workspaces, describes modifications to allow them to work in virtual workspaces, and introduces the `virtualWorkspaces` capability property. Modifying an extension to work with virtual workspaces is also an important step for working well in VS Code for the Web. VS Code for the Web runs entirely inside a browser and workspaces are virtual due to the browser sandbox. See the Web Extensions guide for more details. ## Is my extension affected? When an extension has no executable code but is purely declarative like themes, keybindings, snippets, or grammar extensions, it can run in a virtual workspace and no modification is necessary. Extensions with code, meaning extensions that define a `main` entry point, require inspection and, possibly, modification. ## Run your extension against a virtual workspace Install the GitHub Repositories extension and run the **Open GitHub Repository...** command from the Command Palette. The command shows a Quick Pick dropdown and you can paste in any GitHub URL, or choose to search for a specific repository or pull request. This opens a VS Code window for a virtual workspace where all resources are virtual. ## Review that the extension code is ready for virtual resources The VS Code API support for virtual file systems has been around for quite a while. You can check out the file system provider API. A file system provider is registered for a new URI scheme (for example, `vscode-vfs`) and resources on that file system will be represented by URIs using that schema (`vscode-vfs://github/microsoft/vscode/package.json`) Check how your extension deals with URIs returned from the VS Code APIs: * Never assume that the URI scheme is `file`. `URI.fsPath` can only be used when the URI scheme is `file`. * Look out for usages of the `fs` node module for file system operations. If possible, use the `vscode.workspace.fs` API, which delegates to the appropriate file system provider. * Check for third-party components that depend on a `fs` access (for example, a language server or a node module). * If you run executables and tasks from commands, check whether these commands make sense in a virtual workspace window or whether they should be disabled. ## Signal whether your extension can handle virtual workspaces The `virtualWorkspaces` property under `capabilities` in `package.json` is used to signal whether an extension works with virtual workspaces. ### No support for virtual workspaces The example below declares that an extension does not support virtual workspaces and should not be enabled by VS Code in this setup. { "capabilities": { "virtualWorkspaces": { "supported": false, "description": "Debugging is not possible in virtual workspaces." } } } ### Partial and full support for virtual workspaces When an extension works or partially works with virtual workspaces, it should define `"virtualWorkspaces": true`. { "capabilities": { "virtualWorkspaces": true } } If an extension works, but has limited functionality, it should explain the limitation to the user: { "capabilities": { "virtualWorkspaces": { "supported": "limited", "description": "In virtual workspaces, resolving and finding references across files is not supported." } } } The description is shown in the Extensions view:  The extension should then disable the features that are not supported in a virtual workspace as described below. ### Default `"virtualWorkspaces": true` is the default for all extensions that have not yet filled in the `virtualWorkspaces` capability. However, while testing virtual workspaces, we came up list of extensions that we think should be disabled in virtual workspaces. The list can be found in issue #122836. These extensions have `"virtualWorkspaces": false` as default. Of course, extension authors are in a better position to make this decision. The `virtualWorkspaces` capability in an extension's `package.json` will override our default and we will eventually retire our list. ## Disable functionality when a virtual workspace is opened ### Disable commands and view contributions The availability of commands and views and many other contributions can be controlled through context keys in when clauses. The `virtualWorkspace` context key is set when all workspace folders are located on virtual file systems. The example below only shows the command `npm.publish` in the Command Palette when not in a virtual workspace: { "menus": { "commandPalette": [ { "command": "npm.publish", "when": "!virtualWorkspace" } ] } } The `resourceScheme` context key is set to the URI scheme of the currently selected element in the File Explorer or the element open in the editor. In the example below, the `npm.runSelectedScript` command is only displayed in the editor context menu if the underlying resource is on the local disk. { "menus": { "editor/context": [ { "command": "npm.runSelectedScript", "when": "resourceFilename == 'package.json' && resourceScheme == file" } ] } } ### Detect virtual workspaces programmatically To check whether the current workspace consists of non-`file` schemes and is virtual, you can use the following source code: const isVirtualWorkspace = workspace.workspaceFolders && workspace.workspaceFolders.every(f => f.uri.scheme !== 'file'); ## Language extensions and virtual workspaces ### What are the expectations for language support with virtual workspaces? It's not realistic that all extensions be able to fully work with virtual resources. Many extensions use external tools that require synchronous file access and files on disk. It's therefore fine to only provide limited functionality, such as the **Basic** and the **Single-file** support as listed below. A. **Basic** language support: * TextMate tokenization and colorization * Language-specific editing support: bracket pairs, comments, on enter rules, folding markers * Code snippets B. **Single-file** language support: * Document symbols (outline), folding, selection ranges * Document highlights, semantic highlighting, document colors * Completions, hovers, signature help, find references/declarations based on symbols on the current file and on static language libraries * Formatting, linked editing * Syntax validation and same-file semantic validation and Code Actions C. **Cross-file, workspace-aware** language support: * References across files * Workspace symbols * Validation of all files in the workspace/project The rich language extensions that ship with VS Code (TypeScript, JSON, CSS, HTML, Markdown) are limited to single-file language support when working on virtual resources. ### Disabling a language extension If working on a single file is not option, language extensions can also decide to disable the extension when in a virtual workspace. If your extension provides both grammars and rich language support that needs to be disabled, the grammars will also be disabled. To avoid this, you can create a basic language extension (grammars, language configuration, snippets) separate from the rich language support and have two extensions. * The basic language extension has `"virtualWorkspaces": true` and provides the language ID, configuration, grammar, and snippets. * The rich language extension has `"virtualWorkspaces": false` and contains the `main` file. It contributes language support, commands, and has an extension dependency (`extensionDependencies`) on the basic language extension. The rich language extension should keep the extension ID of the established extension, so the user can continue to have the full functionality by installing a single extension. You can see this approach with the built-in language extensions, such as JSON, which consists of a JSON extension and a JSON language feature extension. This separation also helps with Untrusted Workspaces running in Restricted Mode. Rich language extensions often require trust while basic language features can run in any setup. ### Language selectors When registering a provider for a language feature (for example, completions, hovers, Code Actions, etc.) make sure to specify the schemes the provider supports: return vscode.languages.registerCompletionItemProvider( { language: 'typescript', scheme: 'file' }, { provideCompletionItems(document, position, token) { // ... } } ); ### What about support in the Language Server Protocol (LSP) for accessing virtual resources? Work is under way that will add file system provider support to LSP. Tracked in Language Server Protocol issue #1264. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/web-extensions In this article Visual Studio Code can run as an editor in the browser. One example is the `github.dev` user interface reached by pressing `.` (the period key) when browsing a repository or Pull Request in GitHub. When VS Code is used in the Web, installed extensions are run in an extension host in the browser, called the 'web extension host'. An extension that can run in a web extension host is called a 'web extension'. Web extensions share the same structure as regular extensions, but given the different runtime, don't run with the same code as extensions written for a Node.js runtime. Web extensions still have access to the full VS Code API, but no longer to the Node.js APIs and module loading. Instead, web extensions are restricted by the browser sandbox and therefore have limitations compared to normal extensions. The web extension runtime is supported on VSย Code desktop too. If you decide to create your extension as a web extension, it will be supported on VSย Code for the Web (including `vscode.dev` and `github.dev`) as well as on the desktop and in services like GitHub Codespaces. ## Web extension anatomy A web extension is structured like a regular extension. The extension manifest (`package.json`) defines the entry file for the extension's source code and declares extension contributions. For web extensions, the main entry file is defined by the `browser` property, and not by the `main` property as with regular extensions. The `contributes` property works the same way for both web and regular extensions. The example below shows the `package.json` for a simple hello world extension, that runs in the web extension host only (it only has a `browser` entry point): { "name": "helloworld-web-sample", "displayName": "helloworld-web-sample", "description": "HelloWorld example for VS Code in the browser", "version": "0.0.1", "publisher": "vscode-samples", "repository": "https://github.com/microsoft/vscode-extension-samples/helloworld-web-sample", "engines": { "vscode": "^1.74.0" }, "categories": ["Other"], "activationEvents": [], "browser": "./dist/web/extension.js", "contributes": { "commands": [ { "command": "helloworld-web-sample.helloWorld", "title": "Hello World" } ] }, "scripts": { "vscode:prepublish": "npm run package-web", "compile-web": "webpack", "watch-web": "webpack --watch", "package-web": "webpack --mode production --devtool hidden-source-map" }, "devDependencies": { "@types/vscode": "^1.59.0", "ts-loader": "^9.2.2", "webpack": "^5.38.1", "webpack-cli": "^4.7.0", "@types/webpack-env": "^1.16.0", "process": "^0.11.10" } } > **Note**: If your extension targets a VS Code version prior to 1.74, you must explicitly list `onCommand:helloworld-web-sample.helloWorld` in `activationEvents`. Extensions that have only a `main` entry point, but no `browser` are not web extensions. They are ignored by the web extension host and not available for download in the Extensions view.  Extensions with only declarative contributions (only `contributes`, no `main` or `browser`) can be web extensions. They can be installed and run in VS Code for the Web without any modifications by the extension author. Examples of extensions with declarative contributions include themes, grammars, and snippets. Extensions can have both `browser` and `main` entry points in order to run in browser and in Node.js runtimes. The Update existing extensions to Web extensions section shows how to migrate an extension to work in both runtimes. The web extension enablement section lists the rules used to decide whether an extension can be loaded in a web extension host. ### Web extension main file The web extension's main file is defined by the `browser` property. The script runs in the web extension host in a Browser WebWorker environment. It is restricted by the browser worker sandbox and has limitations compared to normal extensions running in a Node.js runtime. * Importing or requiring other modules is not supported. `importScripts` is not available as well. As a consequence, the code must be packaged to a single file. * The VS Code API can be loaded via the pattern `require('vscode')`. This will work because there is a shim for `require`, but this shim cannot be used to load additional extension files or additional node modules. It only works with `require('vscode')`. * Node.js globals and libraries such as `process`, `os`, `setImmediate`, `path`, `util`, `url` are not available at runtime. They can, however, be added with tools like webpack. The webpack configuration section explains how this is done. * The opened workspace or folder is on a virtual file system. Access to workspace files needs to go through the VS Code file system API accessible at `vscode.workspace.fs`. * Extension context locations (`ExtensionContext.extensionUri`) and storage locations (`ExtensionContext.storageUri`, `globalStorageUri`) are also on a virtual file system and need to go through `vscode.workspace.fs`. * For accessing web resources, the Fetch API must be used. Accessed resources need to support Cross-Origin Resource Sharing (CORS) * Creating child processes or running executables is not possible. However, web workers can be created through the Worker API. This is used for running language servers as described in the Language Server Protocol in web extensions section. * As with regular extensions, the extension's `activate/deactivate` functions need to be exported via the pattern `exports.activate = ...`. ## Develop a web extension Thankfully, tools like TypeScript and webpack can hide many of the browser runtime constraints and allow you to write web extensions the same way as regular extensions. Both a web extension and a regular extension can often be generated from the same source code. For example, the `Hello Web Extension` created by the `yo code` generator only differs in the build scripts. You can run and debug the generated extension just like traditional Node.js extensions by using the provided launch configurations accessible using the **Debug: Select and Start Debugging** command. ## Create a web extension To scaffold a new web extension, use `yo code` and pick **New Web Extension**. Make sure to have the latest version of generator-code (>= generator-code@1.6) installed. To update the generator and yo, run `npm i -g yo generator-code`. The extension that is created consists of the extension's source code (a command showing a hello world notification), the `package.json` manifest file, and a webpack or esbuild configuration file. To keep things simpler, we assume you use `webpack` as the bundler. At the end of the article we also explain what is different when choosing `esbuild`. * `src/web/extension.ts` is the extension's entry source code file. It's identical to the regular hello extension. * `package.json` is the extension manifest. * It points to the entry file using the `browser` property. * It provides scripts: `compile-web`, `watch-web` and `package-web` to compile, watch, and package. * `webpack.config.js` is the webpack config file that compiles and bundles the extension sources into a single file. * `.vscode/launch.json` contains the launch configurations that run the web extension and the tests in the VS Code desktop with a web extension host (setting `extensions.webWorker` is no longer needed). * `.vscode/task.json` contains the build task used by the launch configuration. It uses `npm run watch-web` and depends on the webpack specific `ts-webpack-watch` problem matcher. * `.vscode/extensions.json` contains the extensions that provide the problem matchers. These extensions need to be installed for the launch configurations to work. * `tsconfig.json` defines the compile options matching the `webworker` runtime. The source code in the helloworld-web-sample is similar to what's created by the generator. ### Webpack configuration The webpack configuration file is automatically generated by `yo code`. It bundles the source code from your extension into a single JavaScript file to be loaded in the web extension host. Later we explain how to use esbuild as bundler, but for now we start with webpack. webpack.config.js const path = require('path'); const webpack = require('webpack'); /** @typedef {import('webpack').Configuration} WebpackConfig **/ /** @type WebpackConfig */ const webExtensionConfig = { mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') target: 'webworker', // extensions run in a webworker context entry: { extension: './src/web/extension.ts', // source of the web extension main file 'test/suite/index': './src/web/test/suite/index.ts' // source of the web extension test runner }, output: { filename: '[name].js', path: path.join(__dirname, './dist/web'), libraryTarget: 'commonjs', devtoolModuleFilenameTemplate: '../../[resource-path]' }, resolve: { mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules extensions: ['.ts', '.js'], // support ts-files and js-files alias: { // provides alternate implementation for node module and source files }, fallback: { // Webpack 5 no longer polyfills Node.js core modules automatically. // see https://webpack.js.org/configuration/resolve/#resolvefallback // for the list of Node.js core module polyfills. assert: require.resolve('assert') } }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: 'ts-loader' } ] } ] }, plugins: [ new webpack.ProvidePlugin({ process: 'process/browser' // provide a shim for the global `process` variable }) ], externals: { vscode: 'commonjs vscode' // ignored because it doesn't exist }, performance: { hints: false }, devtool: 'nosources-source-map' // create a source map that points to the original source file }; module.exports = [webExtensionConfig]; Some important fields of `webpack.config.js` are: * The `entry` field contains the main entry point into your extension and test suite. * You may need to adjust this path to appropriately point to the entry point of your extension. * For an existing extension, you can start by pointing this path to the file you're using currently for `main` of your `package.json`. * If you do not want to package your tests, you can omit the test suite field. * The `output` field indicates where the compiled file will be located. * `[name]` will be replaced by the key used in `entry`. So in the generated config file, it will produce `dist/web/extension.js` and `dist/web/test/suite/index.js`. * The `target` field indicates which type of environment the compiled JavaScript file will run. For web extensions, you want this to be `webworker`. * The `resolve` field contains the ability to add aliases and fallbacks for node libraries that don't work in the browser. * If you're using a library like `path`, you can specify how to resolve `path` in a web compiled context. For instance, you can point to a file in the project that defines `path` with `path: path.resolve(__dirname, 'src/my-path-implementation-for-web.js')`. Or you can use the Browserify node packaged version of the library called `path-browserify` and specify `path: require.resolve('path-browserify')`. * See webpack resolve.fallback for the list of Node.js core module polyfills. * The `plugins` section uses the DefinePlugin plugin to polyfill globals such as the `process` Node.js global. ## Test your web extension There are currently three ways to test a web extension before publishing it to the Marketplace. * Use VS Code running on the desktop with the `--extensionDevelopmentKind=web` option to run your web extension in a web extension host running in VS Code. * Use the @vscode/test-web node module to open a browser containing VS Code for the Web including your extension, served from a local server. * Sideload your extension onto vscode.dev to see your extension in the actual environment. ### Test your web extension in VS Code running on desktop To use the existing VS Code extension development experience, VS Code running on the desktop supports running a web extension host along with the regular Node.js extension host. Use the `pwa-extensionhost` launch configuration provided by the **New Web Extension** generator: { "version": "0.2.0", "configurations": [ { "name": "Run Web Extension in VS Code", "type": "pwa-extensionHost", "debugWebWorkerHost": true, "request": "launch", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web" ], "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], "preLaunchTask": "npm: watch-web" } ] } It uses the task `npm: watch-web` to compile the extension by calling `npm run watch-web`. That task is expected in `tasks.json`: { "version": "2.0.0", "tasks": [ { "type": "npm", "script": "watch-web", "group": "build", "isBackground": true, "problemMatcher": ["$ts-webpack-watch"] } ] } `$ts-webpack-watch` is a problem matcher that can parse the output from the webpack tool. It is provided by the TypeScript + Webpack Problem Matchers extension. In the **Extension Development Host** instance that launches, the web extension will be available and running in a web extension host. Run the `Hello World` command to activate the extension. Open the **Running Extensions** view (command: **Developer: Show Running Extensions**) to see which extensions are running in the web extension host. ### Test your web extension in a browser using @vscode/test-web The @vscode/test-web node module offers a CLI and API to test a web extension in a browser. The node module contributes an npm binary `vscode-test-web` that can open VS Code for the Web from the command line: * It downloads the web bits of VS Code into `.vscode-test-web`. * Starts a local server on `localhost:3000`. * Opens a browser (Chromium, Firefox, or Webkit). You can run it from command line: npx @vscode/test-web --extensionDevelopmentPath=$extensionFolderPath $testDataPath Or better, add `@vscode/test-web` as a development dependency to your extension and invoke it in a script: "devDependencies": { "@vscode/test-web": "*" }, "scripts": { "open-in-browser": "vscode-test-web --extensionDevelopmentPath=. ." } Check the @vscode/test-web README for more CLI options: | Option | Argument Description | | --- | --- | | \--browserType | The browser to launch: `chromium` (default), `firefox` or `webkit` | | \--extensionDevelopmentPath | A path pointing to an extension under development to include. | | \--extensionTestsPath | A path to a test module to run. | | \--permission | Permission granted to the opened browser: e.g. `clipboard-read`, `clipboard-write`. See full list of options. Argument can be provided multiple times. | | \--folder-uri | URI of the workspace to open VS Code on. Ignored when `folderPath` is provided | | \--extensionPath | A path pointing to a folder containing additional extensions to include. Argument can be provided multiple times. | | folderPath | A local folder to open VS Code on. The folder content will be available as a virtual file system and opened as workspace. | The web bits of VS Code are downloaded to a folder `.vscode-test-web`. You want to add this to your `.gitignore` file. ### Test your web extension in vscode.dev Before you publish your extension for everyone to use on VS Code for the Web, you can verify how your extension behaves in the actual vscode.dev environment. To see your extension on vscode.dev, you first need to host it from your machine for vscode.dev to download and run. First, you'll need to install `mkcert`. Then, generate the `localhost.pem` and `localhost-key.pem` files into a location you won't lose them (for example `$HOME/certs`): $ mkdir -p $HOME/certs $ cd $HOME/certs $ mkcert -install $ mkcert localhost Then, from your extension's path, start an HTTP server by running `npx serve`: $ npx serve --cors -l 5000 --ssl-cert $HOME/certs/localhost.pem --ssl-key $HOME/certs/localhost-key.pem npx: installed 78 in 2.196s โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ Serving! โ โ โ โ - Local: https://localhost:5000 โ โ - On Your Network: https://172.19.255.26:5000 โ โ โ โ Copied local address to clipboard! โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Finally, open vscode.dev, run **Developer: Install Extension From Location...** from the Command Palette (โงโP (Windows, Linux Ctrl+Shift+P)), paste the URL from above, `https://localhost:5000` in the example, and select **Install**. **Check the logs** You can check the logs in the console of the Developer Tools of your browser to see any errors, status, and logs from your extension. You may see other logs from vscode.dev itself. In addition, you can't easily set breakpoints nor see the source code of your extension. These limitations make debugging in vscode.dev not the most pleasant experience so we recommend using the first two options for testing before sideloading onto vscode.dev. Sideloading is a good final sanity check before publishing your extension. ## Web extension tests Web extension tests are supported and can be implemented similar to regular extension tests. See the Testing Extensions article to learn the basic structure of extension tests. The @vscode/test-web node module is the equivalent to @vscode/test-electron (previously named `vscode-test`). It allows you to run extension tests from the command line on Chromium, Firefox, and Safari. The utility does the following steps: 1. Starts a VS Code for the Web editor from a local web server. 2. Opens the specified browser. 3. Runs the provided test runner script. You can run the tests in continuous builds to ensure that the extension works on all browsers. The test runner script is running on the web extension host with the same restrictions as the web extension main file: * All files are bundled into a single file. It should contain the test runner (for example, Mocha) and all tests (typically `*.test.ts`). * Only `require('vscode')` is supported. The webpack config that is created by the `yo code` web extension generator has a section for tests. It expects the test runner script at `./src/web/test/suite/index.ts`. The provided test runner script uses the web version of Mocha and contains webpack-specific syntax to import all test files. require('mocha/mocha'); // import the mocha web build export function run(): Promise<void> { return new Promise((c, e) => { mocha.setup({ ui: 'tdd', reporter: undefined }); // bundles all files in the current directory matching `*.test` const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r); importAll(require.context('.', true, /\.test$/)); try { // Run the mocha test mocha.run(failures => { if (failures > 0) { e(new Error(`${failures} tests failed.`)); } else { c(); } }); } catch (err) { console.error(err); e(err); } }); } To run the web test from the command line, add the following to your `package.json` and run it with `npm test`. "devDependencies": { "@vscode/test-web": "*" }, "scripts": { "test": "vscode-test-web --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/index.js" } To open VS Code on a folder with test data, pass a local folder path (`folderPath`) as the last parameter. To run (and debug) extension tests in VS Code (Insiders) desktop, use the `Extension Tests in VS Code` launch configuration: { "version": "0.2.0", "configurations": [ { "name": "Extension Tests in VS Code", "type": "extensionHost", "debugWebWorkerHost": true, "request": "launch", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web", "--extensionTestsPath=${workspaceFolder}/dist/web/test/suite/index" ], "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], "preLaunchTask": "npm: watch-web" } ] } ## Publish a web extension Web extensions are hosted on the Marketplace along with other extensions. Make sure to use the latest version of `vsce` to publish your extension. `vsce` tags all extensions that are web extension. For that `vsce` is using the rules listed in the web extension enablement section. ## Update existing extensions to Web extensions ### Extension without code Extensions that have no code, but only contribution points (for example, themes, snippets, and basic language extensions) don't need any modification. They can run in a web extension host and can be installed from the Extensions view. Republishing is not necessary, but when publishing a new version of the extension, make sure to use the most current version of `vsce`. ### Migrate extension with code Extensions with source code (defined by the `main` property) need to provide a web extension main file and set the `browser` property in `package.json`. Use these steps to recompile your extension code for the browser environment: * Add a webpack config file as shown in the webpack configuration section. If you already have a webpack file for your Node.js extension code, you can add a new section for web. Check out the vscode-css-formatter as an example. * Add the `launch.json` and `tasks.json` files as shown in the Test your web extension section. * In the webpack config file, set the input file to the existing Node.js main file or create a new main file for the web extension. * In `package.json`, add a `browser` and the `scripts` properties as shown in the Web extension anatomy section. * Run `npm run compile-web` to invoke webpack and see where work is needed to make your extension run in the web. To make sure as much source code as possible can be reused, here are a few techniques: * To polyfill a Node.js core module such as `path`, add an entry to resolve.fallback. * To provide a Node.js global such as `process` use the DefinePlugin plugin. * Use node modules that work in both browser and node runtime. Node modules can do that by defining both `browser` and `main` entry points. Webpack will automatically use the one matching its target. Examples of node modules that do this are request-light and @vscode/l10n. * To provide an alternate implementation for a node module or source file, use resolve.alias. * Separate your code in a browser part, Node.js part, and common part. In common, only use code that works in both the browser and Node.js runtime. Create abstractions for functionality that has different implementations in Node.js and the browser. * Look out for usages of `path`, `URI.file`, `context.extensionPath`, `rootPath`. `uri.fsPath`. These will not work with virtual workspaces (non-file system) as they are used in VS Code for the Web. Instead use URIs with `URI.parse`, `context.extensionUri`. The vscode-uri node module provides `joinPath`, `dirName`, `baseName`, `extName`, `resolvePath`. * Look out for usages of `fs`. Replace by using vscode `workspace.fs`. It is fine to provide less functionality when your extension is running in the web. Use when clause contexts to control which commands, views, and tasks are available or hidden with running in a virtual workspace on the web. * Use the `virtualWorkspace` context variable to find out if the current workspace is a non-file system workspace. * Use `resourceScheme` to check if the current resource is a `file` resource. * Use `shellExecutionSupported` if there is a platform shell present. * Implement alternative command handlers that show a dialog to explain why the command is not applicable. WebWorkers can be used as an alternative to forking processes. We have updated several language servers to run as web extensions, including the built-in JSON, CSS, and HTML language servers. The Language Server Protocol section below gives more details. The browser runtime environment only supports the execution of JavaScript and WebAssembly. Libraries written in other programming languages need to be cross-compiled, for instance there is tooling to compile C/C++ and Rust to WebAssembly. The vscode-anycode extension, for example, uses tree-sitter, which is C/C++ code compiled to WebAssembly. ### Language Server Protocol in web extensions vscode-languageserver-node is an implementation of the Language Server Protocol (LSP) that is used as a foundation to language server implementations such as JSON, CSS, and HTML. Since 3.16.0, the client and server now also provide a browser implementation. The server can run in a web worker and the connection is based on the webworkers `postMessage` protocol. The client for the browser can be found at 'vscode-languageclient/browser': import { LanguageClient } from `vscode-languageclient/browser`; The server at `vscode-languageserver/browser`. The lsp-web-extension-sample shows how this works. ## Web extension enablement VS Code automatically treats an extension as a web extension if: * The extension manifest (`package.json`) has `browser` entry point. * The extension manifest has no `main` entry point and none of the following contribution points: `localizations`, `debuggers`, `terminal`, `typescriptServerPlugins`. If an extension wants to provide a debugger or terminal that also work in the web extension host, a `browser` entry point needs to be defined. ## Using ESBuild If you want to use esbuild instead of webpack, do the following: Add a `esbuild.js` build script: const esbuild = require('esbuild'); const glob = require('glob'); const path = require('path'); const polyfill = require('@esbuild-plugins/node-globals-polyfill'); const production = process.argv.includes('--production'); const watch = process.argv.includes('--watch'); async function main() { const ctx = await esbuild.context({ entryPoints: ['src/web/extension.ts', 'src/web/test/suite/extensionTests.ts'], bundle: true, format: 'cjs', minify: production, sourcemap: !production, sourcesContent: false, platform: 'browser', outdir: 'dist/web', external: ['vscode'], logLevel: 'warning', // Node.js global to browser globalThis define: { global: 'globalThis' }, plugins: [ polyfill.NodeGlobalsPolyfillPlugin({ process: true, buffer: true }), testBundlePlugin, esbuildProblemMatcherPlugin /* add to the end of plugins array */ ] }); if (watch) { await ctx.watch(); } else { await ctx.rebuild(); await ctx.dispose(); } } /** * For web extension, all tests, including the test runner, need to be bundled into * a single module that has a exported `run` function . * This plugin bundles implements a virtual file extensionTests.ts that bundles all these together. * @type {import('esbuild').Plugin} */ const testBundlePlugin = { name: 'testBundlePlugin', setup(build) { build.onResolve({ filter: /[\/\\]extensionTests\.ts$/ }, args => { if (args.kind === 'entry-point') { return { path: path.resolve(args.path) }; } }); build.onLoad({ filter: /[\/\\]extensionTests\.ts$/ }, async args => { const testsRoot = path.join(__dirname, 'src/web/test/suite'); const files = await glob.glob('*.test.{ts,tsx}', { cwd: testsRoot, posix: true }); return { contents: `export { run } from './mochaTestRunner.ts';` + files.map(f => `import('./${f}');`).join(''), watchDirs: files.map(f => path.dirname(path.resolve(testsRoot, f))), watchFiles: files.map(f => path.resolve(testsRoot, f)) }; }); } }; /** * This plugin hooks into the build process to print errors in a format that the problem matcher in * Visual Studio Code can understand. * @type {import('esbuild').Plugin} */ const esbuildProblemMatcherPlugin = { name: 'esbuild-problem-matcher', setup(build) { build.onStart(() => { console.log('[watch] build started'); }); build.onEnd(result => { result.errors.forEach(({ text, location }) => { console.error(`โ [ERROR] ${text}`); if (location == null) return; console.error(` ${location.file}:${location.line}:${location.column}:`); }); console.log('[watch] build finished'); }); } }; main().catch(e => { console.error(e); process.exit(1); }); The build script does the following: * It creates a build context with esbuild. The context is configured to: * Bundle the code in `src/web/extension.ts` into a single file `dist/web/extension.js`. * Bundle all tests, including the test runner (mocha) into a single file `dist/web/test/suite/extensionTests.js`. * Minify the code if the `--production` flag was passed. * Generate source maps unless the `--production` flag was passed. * Exclude the 'vscode' module from the bundle (since it's provided by the VS Code runtime). * creates polyfills for `process` and `buffer` * Use the esbuildProblemMatcherPlugin plugin to report errors that prevented the bundler to complete. This plugin emits the errors in a format that is detected by the `esbuild` problem matcher with also needs to be installed as an extension. * Use the testBundlePlugin to implement a test main file (`extensionTests.js`) that references all tests files and the mocha test runner `mochaTestRunner.js` * If the `--watch` flag was passed, it starts watching the source files for changes and rebuilds the bundle whenever a change is detected. esbuild can work directly with TypeScript files. However, esbuild simply strips off all type declarations without doing any type checks. Only syntax error are reported and can cause esbuild to fail. For that reason, we separately run the TypeScript compiler (`tsc`) to check the types, but without emitting any code (flag `--noEmit`). The `scripts` section in `package.json` now looks like that "scripts": { "vscode:prepublish": "npm run package-web", "compile-web": "npm run check-types && node esbuild.js", "watch-web": "npm-run-all -p watch-web:*", "watch-web:esbuild": "node esbuild.js --watch", "watch-web:tsc": "tsc --noEmit --watch --project tsconfig.json", "package-web": "npm run check-types && node esbuild.js --production", "check-types": "tsc --noEmit", "pretest": "npm run compile-web", "test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/extensionTests.js", "run-in-browser": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. ." } `npm-run-all` is a node module that runs scripts in parallel whose name match a given prefix. For us, it runs the `watch-web:esbuild` and `watch-web:tsc` scripts. You need to add `npm-run-all` to the `devDependencies` section in `package.json`. The following `tasks.json` files gives you separate terminals for each watch task: { "version": "2.0.0", "tasks": [ { "label": "watch-web", "dependsOn": ["npm: watch-web:tsc", "npm: watch-web:esbuild"], "presentation": { "reveal": "never" }, "group": { "kind": "build", "isDefault": true }, "runOptions": { "runOn": "folderOpen" } }, { "type": "npm", "script": "watch-web:esbuild", "group": "build", "problemMatcher": "$esbuild-watch", "isBackground": true, "label": "npm: watch-web:esbuild", "presentation": { "group": "watch", "reveal": "never" } }, { "type": "npm", "script": "watch-web:tsc", "group": "build", "problemMatcher": "$tsc-watch", "isBackground": true, "label": "npm: watch-web:tsc", "presentation": { "group": "watch", "reveal": "never" } }, { "label": "compile", "type": "npm", "script": "compile-web", "problemMatcher": ["$tsc", "$esbuild"] } ] } This is the `mochaTestRunner.js` referenced in the esbuild build script: // Imports mocha for the browser, defining the `mocha` global. import 'mocha/mocha'; mocha.setup({ ui: 'tdd', reporter: undefined }); export function run(): Promise<void> { return new Promise((c, e) => { try { // Run the mocha test mocha.run(failures => { if (failures > 0) { e(new Error(`${failures} tests failed.`)); } else { c(); } }); } catch (err) { console.error(err); e(err); } }); } ## Samples * helloworld-web-sample * lsp-web-extension-sample 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/workspace-trust In this article ## What is Workspace Trust? Workspace Trust is a feature driven by the security risks associated with unintended code execution when a user opens a workspace in VS Code. For example, consider that a language extension, in order to provide functionality, may execute code from the currently loaded workspace. In this scenario, the user should trust that the contents of the workspace are not malicious. Workspace Trust centralizes this decision within VS Code and supports a Restricted Mode to protect against automatic code execution so that extension authors do not have to handle this infrastructure themselves. VS Code offers static declaration and API support to onboard extensions quickly without the need to duplicate code across extensions. ## Onboarding ### Static declarations In your extension's `package.json`, VS Code supports the following new `capabilities` property `untrustedWorkspaces`: capabilities: untrustedWorkspaces: { supported: true } | { supported: false, description: string } | { supported: 'limited', description: string, restrictedConfigurations?: string[] } For the `supported` property, the following values are accepted: * `true` - The extension is fully supported in Restricted Mode as it does not need Workspace Trust to perform any functionality. It will be enabled exactly as before. * `false` - The extension is not supported in Restricted Mode as it cannot function without Workspace Trust. It will remain disabled until Workspace Trust is granted. * `'limited'` - Some features of the extension are supported in Restricted Mode. Trust-sensitive features should be disabled until Workspace Trust is granted. The extension can use the VS Code API to hide or disable these features. Workspace settings can be gated by trust automatically using the `restrictedConfigurations` property. For the `description` property, a description of why trust is needed must be provided to help the user understand what features will be disabled or what they should review before granting or denying Workspace Trust. If `supported` is set to `true`, this property is ignored. The value for the `description` property should be added to `package.nls.json` and then referenced in the `package.json` file for localization support. The `restrictedConfigurations` property takes an array of configuration setting IDs. For the settings listed, the extension will not be given workspace-defined values when in Restricted Mode for an untrusted workspace. ## How to support Restricted Mode? To help extension authors understand what is in scope for Workspace Trust and what types of features are safe in Restricted Mode, here are a list of questions to consider. ### Does my extension have a main entry point? If an extension does not have a `main` entry point (for example themes and language grammars), the extension does not require Workspace Trust. Extension authors do not need to take any action for such extensions as they will continue to function independent whether the workspace is trusted or not. ### Does my extension rely on files in the opened workspace to provide features? This can mean things like settings that can be set by the workspace or actual code in the workspace. If the extension never uses any of the contents of the workspace, it probably doesn't require trust. Otherwise, take a look at the other questions. ### Does my extension treat any contents of the workspace as code? The most common example of this is using a project's workspace dependencies, such as the Node.js modules stored in the local workspace. A malicious workspace might check in a compromised version of the module. Thus, this is a security risk for the user and extension. In addition, an extension may rely on JavaScript or other configuration files that control the extension or other modules' behavior. There are many other examples, such as executing an opened code file to determine its output for error reporting. ### Does my extension use settings that determine code execution that can be defined in the workspace? Your extension might use settings values as flags to a CLI that your extension executes. If these settings are overridden by a malicious workspace, they could be used as an attack vector against your extension. On the other hand, if the settings' values are only used to detect certain conditions, then it may not be a security risk and does not require Workspace Trust. For example, an extension might check whether the value of a preferred shell setting is `bash` or `pwsh` to determine what documentation to show. The Configurations (settings) section below has guidance on settings to help you find the optimal configuration for your extension. This is not an exhaustive list of cases that might require Workspace Trust. As we review more extensions, we will update this list. Use this list to think of similar behavior your extension might be doing when considering Workspace Trust. ### What if I don't make changes to my extension? As mentioned above, an extension that does not contribute anything to their `package.json` will be treated as not supporting Workspace Trust. It will be disabled when a workspace is in Restricted Mode and the user will be notified that some extensions are not working due to Workspace Trust. This measure is the most security-conscious approach for the user. Even though this is the default, it is a best practice to set the appropriate value indicating that as an extension author, you have made the effort to protect the user and your extension from malicious workspace content. ## Workspace Trust API As described above, the first step to using the API is adding the static declarations to your `package.json`. The easiest method of onboarding would be to use a `false` value for the `supported` property. Once again, this is the default behavior even if you do nothing, but it's a good signal to the user that you have made a deliberate choice. In this case, your extension does not need to do anything else. It will not be activated until trust is given and then your extension will know that it is executing with the consent of the user. However, if your extension only requires trust for part of its functionality, this is likely not the best option. For extensions that wish to gate their features on Workspace Trust, they should use the `'limited'` value for the `supported` property, and VS Code provides the following API: export namespace workspace { /** * When true, the user has explicitly trusted the contents of the workspace. */ export const isTrusted: boolean; /** * Event that fires when the current workspace has been trusted. */ export const onDidGrantWorkspaceTrust: Event<void>; } Use the `isTrusted` property to determine if the current workspace is trusted and the `onDidGrantWorkspaceTrust` event to listen for when trust has been granted to the workspace. You can use this API to block specific code paths and perform any necessary registrations once the workspace has been trusted. VS Code also exposes a context key `isWorkspaceTrusted` for use in `when` clauses as described below. ## Contribution points ### Commands, views, or other UI When the user has not trusted the workspace, they will be operating in Restricted Mode with limited functionality geared towards browsing code. Any features that you disable in Restricted Mode should be hidden from the user. This can be done via when clause contexts and the context key `isWorkspaceTrusted`. A command can still be called even if it is not presented in the UI, so you should block execution or not register a command based on the API above in your extension code. ### Configurations (settings) First, you should review your settings to determine if they need to take trust into account. As described above, a workspace may define a value for a setting that your extension consumes that is malicious to the use. If you identify settings that are vulnerable, you should use `'limited'` for the `supported` property and list the setting ID in the `restrictedConfigurations` array. When you add a setting ID to the `restrictedConfigurations` array, VS Code will only return the user-defined value of the setting in Restricted Mode. Your extension then doesn't need to make any additional code changes to handle the setting. When trust is granted, a configuration change event will fire in addition to the Workspace Trust event. ### Debug extensions VS Code will prevent debugging in Restricted Mode. For this reason, debugging extensions generally do not need to require trust and should select `true` for the `supported` property. However, if your extension provides additional functionality, commands, or settings that are not part of the built-in debugging flow, you should use `'limited'` and follow the above guidance. ### Task providers Similar to debugging, VS Code prevents running tasks in Restricted Mode. If your extension provides additional functionality, commands, or settings that are not part of the built-in tasks flow, you should use `'limited'` and follow the above guidance. Otherwise, you can specify `supported: true`. ## Testing Workspace Trust See the Workspace Trust user guide for details on enabling and configuring Workspace Trust. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/task-provider Users normally define tasks in Visual Studio Code in a `tasks.json` file. However, there are some tasks during software development that can be automatically detected by a VS Code extension with a Task Provider. When the **Tasks: Run Task** command is run from VS Code, all active Task Providers contribute tasks that the user can run. While the `tasks.json` file lets the user manually define a task for a specific folder or workspace, a Task Provider can detect details about a workspace and then automatically create a corresponding VS Code Task. For example, a Task Provider could check if there is a specific build file, such as `make` or `Rakefile`, and create a build task. This topic describes how extensions can auto-detect and provide tasks to end-users. This guide teaches you how to build a Task Provider that auto-detects tasks defined in Rakefiles. The complete source code is at: https://github.com/microsoft/vscode-extension-samples/tree/main/task-provider-sample. ## Task Definition To uniquely identify a task in the system, an extension contributing a task needs to define the properties that identify a task. In the Rake example, the task definition looks like this: "taskDefinitions": [ { "type": "rake", "required": [ "task" ], "properties": { "task": { "type": "string", "description": "The Rake task to customize" }, "file": { "type": "string", "description": "The Rake file that provides the task. Can be omitted." } } } ] This contributes a task definition for `rake` tasks. The task definition has two attributes `task` and `file`. `task` is the name of the Rake task and `file` points to the `Rakefile` that contains the task. The `task` property is required, the `file` property is optional. If the `file` attribute is omitted, the `Rakefile` in the root of the workspace folder is used. ### When clause A task definition may optionally have a `when` property. The `when` property specifies the condition under which a task of this type will be available. The `when` property functions in the same way as other places in VS Code, where there is a `when` property. The following contexts should always be considered when creating a task definition: * `shellExecutionSupported`: True when VS Code can run `ShellExecution` tasks, such as when VS Code is run as a desktop application or when using one of the remote extensions, such as Dev Containers. * `processExecutionSupported`: True when VS Code can run `ProcessExecution` tasks, such as when VS Code is run as a desktop application or when using one of the remote extensions, such as Dev Containers. Currently, it will always have the same value as `shellExecutionSupported`. * `customExecutionSupported`: True when VS Code can run `CustomExecution`. This is always true. ## Task provider Analogous to language providers that let extensions support code completion, an extension can register a task provider to compute all available tasks. This is done using the `vscode.tasks` namespace as shown in the following code snippet: import * as vscode from 'vscode'; let rakePromise: Thenable<vscode.Task[]> | undefined = undefined; const taskProvider = vscode.tasks.registerTaskProvider('rake', { provideTasks: () => { if (!rakePromise) { rakePromise = getRakeTasks(); } return rakePromise; }, resolveTask(_task: vscode.Task): vscode.Task | undefined { const task = _task.definition.task; // A Rake task consists of a task and an optional file as specified in RakeTaskDefinition // Make sure that this looks like a Rake task by checking that there is a task. if (task) { // resolveTask requires that the same definition object be used. const definition: RakeTaskDefinition = <any>_task.definition; return new vscode.Task( definition, _task.scope ?? vscode.TaskScope.Workspace, definition.task, 'rake', new vscode.ShellExecution(`rake ${definition.task}`) ); } return undefined; } }); Like `provideTasks`, the `resolveTask` method is called by VS Code to get tasks from the extension. `resolveTask` can be called instead of `provideTasks`, and is intended to provide an optional performance increase for providers that implement it. For example, if a user has a keybinding that runs an extension provided task, it would be better for VS Code to call `resolveTask` for that task provider and just get the one task quickly instead of having to call `provideTasks` and wait for the extension to provide all of its tasks. It is good practice to have a setting that allows users to turn off individual task providers, so this is common. A user might notice that tasks from a specific provider are slower to get and turn off the provider. In this case, the user might still reference some of the tasks from this provider in their `tasks.json`. If `resolveTask` is not implemented, then there will be a warning that the task in their `tasks.json` was not created. With `resolveTask` an extension can still provide a task for the task defined in `tasks.json`. The `getRakeTasks` implementation does the following: * Lists all rake tasks defined in a `Rakefile` using the `rake -AT -f Rakefile` command for each workspace folder. * Parses the stdio output. * For every listed task, creates a `vscode.Task` implementation. Since a Rake task instantiation needs a task definition as defined in the `package.json` file, VS Code also defines the structure using a TypeScript interface like this: interface RakeTaskDefinition extends vscode.TaskDefinition { /** * The task name */ task: string; /** * The rake file containing the task */ file?: string; } Assuming that the output comes from a task called `compile` in the first workspace folder, the corresponding task creation then looks like this: let task = new vscode.Task( { type: 'rake', task: 'compile' }, vscode.workspace.workspaceFolders[0], 'compile', 'rake', new vscode.ShellExecution('rake compile') ); For every task listed in the output, a corresponding VS Code task is created using the above pattern and then returns the array of all tasks from the `getRakeTasks` call. The `ShellExecution` executes the `rake compile` command in the shell that is specific for the OS (for example under Windows the command would be executed in PowerShell, under Ubuntu it'd be executed in bash). If the task should directly execute a process (without spawning a shell), `vscode.ProcessExecution` can be used. `ProcessExecution` has the advantage that the extension has full control over the arguments passed to the process. Using `ShellExecution` makes use of the shell command interpretation (like wildcard expansion under bash). If the `ShellExecution` is created with a single command line, then the extension needs to ensure proper quoting and escaping (for example to handle whitespace) inside the command. ## CustomExecution In general, it is best to use a `ShellExecution` or `ProcessExecution` because they are simple. However, if your task requires a lot of saved state between runs, doesn't work well as a separate script or process, or requires extensive handling of output a `CustomExecution` might be a good fit. Existing uses of `CustomExecution` are usually for complex build systems. A `CustomExecution` has only a callback which is executed at the time that the task is run. This allows for greater flexibility in what the task can do, but it also means that the task provider is responsible for any process management and output parsing that needs to happen. The task provider is also responsible for implementing `Pseudoterminal` and returning it from the `CustomExecution` callback. return new vscode.Task( definition, vscode.TaskScope.Workspace, `${flavor} ${flags.join(' ')}`, CustomBuildTaskProvider.CustomBuildScriptType, new vscode.CustomExecution( async (): Promise<vscode.Pseudoterminal> => { // When the task is executed, this callback will run. Here, we setup for running the task. return new CustomBuildTaskTerminal( this.workspaceRoot, flavor, flags, () => this.sharedState, (state: string) => (this.sharedState = state) ); } ) ); The full example, including the implementation of `Pseudoterminal` is at https://github.com/microsoft/vscode-extension-samples/tree/main/task-provider-sample/src/customTaskProvider.ts. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/scm-provider The Source Control API allows extension authors to define Source Control Management (SCM) features. There is a slim, yet powerful API surface which allows many different SCM systems to be integrated in Visual Studio Code, while having a common user interface with all of them.  VS Code itself ships with one Source Control provider, the Git extension, which is the best reference for this API and is a great starting point if you'd like to contribute your very own SCM provider. There are other great examples in the Marketplace such as the SVN extension. This documentation will help you build an extension which can make any SCM system work with VS Code. > **Note:** that you can always refer to the `vscode` namespace API reference in our documentation. ## Source Control Model A `SourceControl` is the entity responsible for populating the Source Control model with **resource states**, instances of `SourceControlResourceState`. Resource states are themselves organized in **groups**, instances of `SourceControlResourceGroup`. You can create a new SourceControl with `vscode.scm.createSourceControl`. In order to better understand how these three entities correlate with each other, let's take Git as an example. Consider the following output of `git status`: vsce main* โ git status On branch main Your branch is up-to-date with 'origin/main'. Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: README.md renamed: src/api.ts -> src/test/api.ts Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) deleted: .travis.yml modified: README.md There are many things going on in this workspace. First, the `README.md` file has been modified, staged and then modified once again. Second, the `src/api.ts` file has been moved to `src/test/api.ts` and that move was staged. Finally, the `.travis.yml` file has been deleted. For this workspace, Git defines two resource groups: the **working tree** and the **index**. Each **file change** within that group is **resource state**: * **Index** - resource group * `README.md`, modified - resource state * `src/test/api.ts`, renamed from `src/api.ts` - resource state * **Working Tree** - resource group * `.travis.yml`, deleted - resource state * `README.md`, modified - resource state Note how the same file, `README.md`, is part of two distinct resource states. Here's how Git creates this model: function createResourceUri(relativePath: string): vscode.Uri { const absolutePath = path.join(vscode.workspace.rootPath, relativePath); return vscode.Uri.file(absolutePath); } const gitSCM = vscode.scm.createSourceControl('git', 'Git'); const index = gitSCM.createResourceGroup('index', 'Index'); index.resourceStates = [ { resourceUri: createResourceUri('README.md') }, { resourceUri: createResourceUri('src/test/api.ts') } ]; const workingTree = gitSCM.createResourceGroup('workingTree', 'Changes'); workingTree.resourceStates = [ { resourceUri: createResourceUri('.travis.yml') }, { resourceUri: createResourceUri('README.md') } ]; Changes made to the source control and resource groups will be propagated to the Source Control view. ## Source Control View VS Code is able to populate the Source Control view, as the Source Control model changes. Resource states are customizable using `SourceControlResourceDecorations`: export interface SourceControlResourceState { readonly decorations?: SourceControlResourceDecorations; } The previous example would be sufficient to populate a simple list in the Source Control view, but there are many user interactions that the user might want to perform with each resource. For instance, what happens when the user clicks a resource state? The resource state can optionally provide a command to handle this action: export interface SourceControlResourceState { readonly command?: Command; } There are six Source Control menu ids where you can place menu items, in order to provide the user with a much richer user interface. The `scm/title` menu is located to the right of the SCM view title. The menu items in the `navigation` group will be inline, while all the others will be within the `โฆ` dropdown menu. These three are similar: * `scm/resourceGroup/context` adds commands to `SourceControlResourceGroup` items. * `scm/resourceState/context` adds commands to `SourceControlResourceState` items. * `scm/resourceFolder/context` add commands to the intermediate folders that appear when a `SourceControlResourceState`'s resourceUri path includes folders and the user has opted for tree-view rather than list-view mode. Place menu items in the `inline` group to have them inline. All other menu item groups will be represented in a context menu usually accessible using the mouse right-click. Note that the SCM view supports multiple selection, so a command receives as its argument an array of one or more resources. For example, Git supports staging multiple files by adding the `git.stage` command to the `scm/resourceState/context` menu and using such a method declaration: stage(...resourceStates: SourceControlResourceState[]): Promise<void>; When creating them, `SourceControl` and `SourceControlResourceGroup` instances require you to provide an `id` string. These values will be populated in the `scmProvider` and `scmResourceGroup` context keys, respectively. You can rely on these context keys in the `when` clauses of your menu items. Here's how Git is able to show an inline menu item for its `git.stage` command: { "command": "git.stage", "when": "scmProvider == git && scmResourceGroup == merge", "group": "inline" } The `scm/sourceControl` menu is the context menu on each `SourceControl` instance in the **Source Control Repositories** view:  The `scm/change/title` allows you to contribute commands to the title bar of the Quick Diff inline diff editor, described further ahead. The command will be passed as arguments the URI of the document, the array of changes within it, and the index of the change which the inline change diff editor is currently focused on. For example, here's the declaration of the `stageChange` Git command which is contributed to this menu with a `when` clause testing that the `originalResourceScheme` context key equals `git`: async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void>; ### SCM Input Box The Source Control Input Box, located atop of each Source Control view, allows the user to input a message. You can get (and set) this message in order to perform operations. In Git, for example, this is used as the commit box, in which users type in commit messages and `git commit` commands pick them up. export interface SourceControlInputBox { value: string; } export interface SourceControl { readonly inputBox: SourceControlInputBox; } The user can type Ctrl+Enter (or Cmd+Enter on macOS) to accept any message. You can handle this event by providing a `acceptInputCommand` to your `SourceControl` instance. export interface SourceControl { readonly acceptInputCommand?: Command; } ## Quick Diff VS Code also supports displaying **quick diff** editor gutter decorations. Clicking those decorations will reveal an inline diff experience, to which you can contribute contextual commands:  These decorations are computed by VS Code itself. All you need to do is provide VS Code with the original contents of any given file. export interface SourceControl { quickDiffProvider?: QuickDiffProvider; } Using a `QuickDiffProvider`'s `provideOriginalResource` method, your implementation is able to tell VS Code the `Uri` of the original resource that matches the resource whose `Uri` is provided as an argument to the method. Combine this API with the `registerTextDocumentContentProvider` method in the `workspace` namespace, which lets you provide contents for arbitrary resources, given a `Uri` matching the custom `scheme` that it registered for. ## Next steps To learn more about VS Code extensibility model, try these topics: * SCM API Reference - Read the full SCM API documentation * Git Extension - Learn by reading the Git extension implementation * Extension API Overview - Learn about the full VS Code extensibility model. * Extension Manifest File - VS Code package.json extension manifest file reference * Contribution Points - VS Code contribution points reference 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/debugger-extension In this article Visual Studio Code's debugging architecture allows extension authors to easily integrate existing debuggers into VS Code, while having a common user interface with all of them. VS Code ships with one built-in debugger extension, the Node.js debugger extension, which is an excellent showcase for the many debugger features supported by VS Code:  This screenshot shows the following debugging features: 1. Debug configuration management. 2. Debug actions for starting/stopping and stepping. 3. Source-, function-, conditional-, inline breakpoints, and log points. 4. Stack traces, including multi-thread and multi-process support. 5. Navigating through complex data structures in views and hovers. 6. Variable values shown in hovers or inlined in the source. 7. Managing watch expressions. 8. Debug console for interactive evaluation with autocomplete. This documentation will help you create a debugger extension which can make any debugger work with VS Code. ## Debugging Architecture of VS Code VS Code implements a generic (language-agnostic) debugger UI based on an abstract protocol that we've introduced to communicate with debugger backends. Because debuggers typically do not implement this protocol, some intermediary is needed to "adapt" the debugger to the protocol. This intermediary is typically a standalone process that communicates with the debugger.  We call this intermediary the **Debug Adapter** (or **DA** for short) and the abstract protocol that is used between the DA and VS Code is the **Debug Adapter Protocol** (**DAP** for short). Since the Debug Adapter Protocol is independent from VS Code, it has its own web site where you can find an introduction and overview, the detailed specification, and some lists with known implementations and supporting tools. The history of and motivation behind DAP is explained in this blog post. Since debug adapters are independent from VS Code and can be used in other developments tools, they do not match VS Code's extensibility architecture which is based on extensions and contribution points. For this reason VS Code provides a contribution point, `debuggers`, where a debug adapter can be contributed under a specific debug type (e.g. `node` for the Node.js debugger). VS Code launches the registered DA whenever the user starts a debug session of that type. So in its most minimal form, a debugger extension is just a declarative contribution of a debug adapter implementation and the extension is basically a packaging container for the debug adapter without any additional code.  A more realistic debugger extension contributes many or all of the following declarative items to VS Code: * List of languages supported by the debugger. VS Code enables the UI to set breakpoints for those languages. * JSON schema for the debug configuration attributes introduced by the debugger. VS Code uses this schema to verify the configuration in the launch.json editor and provides IntelliSense. Please note that the JSON schema constructs `$ref` and `definition` are not supported. * Default debug configurations for the initial launch.json created by VS Code. * Debug configuration snippets that a user can add to a launch.json file. * Declaration of variables that can be used in debug configurations. You can find more information in `contributes.breakpoints` and `contributes.debuggers` references. In addition to the purely declarative contributions from above, the Debug Extension API enables this code-based functionality: * Dynamically generated default debug configurations for the initial launch.json created by VS Code. * Determine the debug adapter to use dynamically. * Verify or modify debug configurations before they are passed to the debug adapter. * Communicate with the debug adapter. * Send messages to the debug console. In the rest of this document we show how to develop a debugger extension. ## The Mock Debug Extension Since creating a debug adapter from scratch is a bit heavy for this tutorial, we will start with a simple DA which we have created as an educational "debug adapter starter kit". It is called _Mock Debug_ because it does not talk to a real debugger, but mocks one. Mock Debug simulates a debugger and supports step, continue, breakpoints, exceptions, and variable access, but it is not connected to any real debugger. Before delving into the development setup for mock-debug, let's first install a pre-built version from the VS Code Marketplace and play with it: * Switch to the Extensions viewlet and type "mock" to search for the Mock Debug extension, * "Install" and "Reload" the extension. To try Mock Debug: * Create a new empty folder `mock test` and open it in VS Code. * Create a file `readme.md` and enter several lines of arbitrary text. * Switch to the Run and Debug view (โงโD (Windows, Linux Ctrl+Shift+D)) and select the **create a launch.json file** link. * VS Code will let you select an "debugger" in order to create a default launch configuration. Pick "Mock Debug". * Press the green **Start** button and then Enter to confirm the suggested file `readme.md`. A debug session starts and you can "step" through the readme file, set and hit breakpoints, and run into exceptions (if the word `exception` appears in a line).  Before using Mock Debug as a starting point for your own development, we recommend to uninstall the pre-built version first: * Switch to the Extensions viewlet and click on the gear icon of the Mock Debug extension. * Run the "Uninstall" action and then "Reload" the window. ## Development Setup for Mock Debug Now let's get the source for Mock Debug and start development on it within VS Code: git clone https://github.com/microsoft/vscode-mock-debug.git cd vscode-mock-debug yarn Open the project folder `vscode-mock-debug` in VS Code. What's in the package? * `package.json` is the manifest for the mock-debug extension: * It lists the contributions of the mock-debug extension. * The `compile` and `watch` scripts are used to transpile the TypeScript source into the `out` folder and watch for subsequent source modifications. * The dependencies `vscode-debugprotocol`, `vscode-debugadapter`, and `vscode-debugadapter-testsupport` are NPM modules that simplify the development of node-based debug adapters. * `src/mockRuntime.ts` is a _mock_ runtime with a simple debug API. * The code that _adapts_ the runtime to the Debug Adapter Protocol lives in `src/mockDebug.ts`. Here you find the handlers for the various requests of the DAP. * Since the implementation of debugger extension lives in the debug adapter, there is no need to have extension code at all (i.e. code that runs in the extension host process). However, Mock Debug has a small `src/extension.ts` because it illustrates what can be done in the extension code of a debugger extension. Now build and launch the Mock Debug extension by selecting the **Extension** launch configuration and hitting `F5`. Initially, this will do a full transpile of the TypeScript sources into the `out` folder. After the full build, a _watcher task_ is started that transpiles any changes you make. After transpiling the source, a new VS Code window labelled "\[Extension Development Host\]" appears with the Mock Debug extension now running in debug mode. From that window open your `mock test` project with the `readme.md` file, start a debug session with 'F5', and then step through it:  Since you are running the extension in debug mode, you could now set and hit breakpoints in `src/extension.ts` but as I've mentioned above, there is not much interesting code executing in the extension. The interesting code runs in the debug adapter which is a separate process. In order to debug the debug adapter itself, we have to run it in debug mode. This is most easily achieved by running the debug adapter in _server mode_ and configure VS Code to connect to it. In your VS Code vscode-mock-debug project select the launch configuration **Server** from the dropdown menu and press the green start button. Since we already had an active debug session for the extension the VS Code debugger UI now enters _multi session_ mode which is indicated by seeing the names of the two debug sessions **Extension** and **Server** showing up in the CALL STACK view:  Now we are able to debug both the extension and the DA simultaneously. A faster way to arrive here is by using the **Extension + Server** launch configuration which launches both sessions automatically. An alternative, even simpler approach for debugging the extension and the DA can be found below. Set a breakpoint at the beginning of method `launchRequest(...)` in file `src/mockDebug.ts` and as a last step configure the mock debugger to connect to the DA server by adding a `debugServer` attribute for port `4711` to your mock test launch config: { "version": "0.2.0", "configurations": [ { "type": "mock", "request": "launch", "name": "mock test", "program": "${workspaceFolder}/readme.md", "stopOnEntry": true, "debugServer": 4711 } ] } If you now launch this debug configuration, VS Code does not start the mock debug adapter as a separate process, but directly connects to local port 4711 of the already running server, and you should hit the breakpoint in `launchRequest`. With this setup, you can now easily edit, transpile, and debug Mock Debug. But now the real work begins: you will have to replace the mock implementation of the debug adapter in `src/mockDebug.ts` and `src/mockRuntime.ts` by some code that talks to a "real" debugger or runtime. This involves understanding and implementing the Debug Adapter Protocol. More details about this can be found here. ## Anatomy of the package.json of a Debugger Extension Besides providing a debugger-specific implementation of the debug adapter a debugger extension needs a `package.json` that contributes to the various debug-related contribution points. So let's have a closer look at the `package.json` of Mock Debug. Like every VS Code extension, the `package.json` declares the fundamental properties **name**, **publisher**, and **version** of the extension. Use the **categories** field to make the extension easier to find in the VS Code Extension Marketplace. { "name": "mock-debug", "displayName": "Mock Debug", "version": "0.24.0", "publisher": "...", "description": "Starter extension for developing debug adapters for VS Code.", "author": { "name": "...", "email": "..." }, "engines": { "vscode": "^1.17.0", "node": "^7.9.0" }, "icon": "images/mock-debug-icon.png", "categories": ["Debuggers"], "contributes": { "breakpoints": [{ "language": "markdown" }], "debuggers": [ { "type": "mock", "label": "Mock Debug", "program": "./out/mockDebug.js", "runtime": "node", "configurationAttributes": { "launch": { "required": ["program"], "properties": { "program": { "type": "string", "description": "Absolute path to a text file.", "default": "${workspaceFolder}/${command:AskForProgramName}" }, "stopOnEntry": { "type": "boolean", "description": "Automatically stop after launch.", "default": true } } } }, "initialConfigurations": [ { "type": "mock", "request": "launch", "name": "Ask for file name", "program": "${workspaceFolder}/${command:AskForProgramName}", "stopOnEntry": true } ], "configurationSnippets": [ { "label": "Mock Debug: Launch", "description": "A new configuration for launching a mock debug program", "body": { "type": "mock", "request": "launch", "name": "${2:Launch Program}", "program": "^\"\\${workspaceFolder}/${1:Program}\"" } } ], "variables": { "AskForProgramName": "extension.mock-debug.getProgramName" } } ] }, "activationEvents": ["onDebug", "onCommand:extension.mock-debug.getProgramName"] } Now take a look at the **contributes** section which contains the contributions specific to debug extensions. First, we use the **breakpoints** contribution point to list the languages for which setting breakpoints will be enabled. Without this, it would not be possible to set breakpoints in Markdown files. Next is the **debuggers** section. Here, one debugger is introduced under a debug **type** `mock`. The user can reference this type in launch configurations. The optional attribute **label** can be used to give the debug type a nice name when showing it in the UI. Since the debug extension uses a debug adapter, a relative path to its code is given as the **program** attribute. In order to make the extension self-contained the application must live inside the extension folder. By convention, we keep this application inside a folder named `out` or `bin`, but you are free to use a different name. Since VS Code runs on different platforms, we have to make sure that the DA program supports the different platforms as well. For this we have the following options: 1. If the program is implemented in a platform independent way, e.g. as program that runs on a runtime that is available on all supported platforms, you can specify this runtime via the **runtime** attribute. As of today, VS Code supports `node` and `mono` runtimes. Our Mock debug adapter from above uses this approach. 2. If your DA implementation needs different executables on different platforms, the **program** attribute can be qualified for specific platforms like this: "debuggers": [{ "type": "gdb", "windows": { "program": "./bin/gdbDebug.exe", }, "osx": { "program": "./bin/gdbDebug.sh", }, "linux": { "program": "./bin/gdbDebug.sh", } }] 3. A combination of both approaches is possible too. The following example is from the Mono DA which is implemented as a mono application that needs a runtime on macOS and Linux but not on Windows: "debuggers": [{ "type": "mono", "program": "./bin/monoDebug.exe", "osx": { "runtime": "mono" }, "linux": { "runtime": "mono" } }] **configurationAttributes** declares the schema for the `launch.json` attributes that are available for this debugger. This schema is used for validating the `launch.json` and supporting IntelliSense and hover help when editing the launch configuration. The **initialConfigurations** define the initial content of the default `launch.json` for this debugger. This information is used when a project does not have a `launch.json` and a user starts a debug session or selects the **create a launch.json file** link in the Run and Debug view. In this case VS Code lets the user pick a debug environment and then creates the corresponding `launch.json`:  Instead of defining the initial content of the `launch.json` statically in the `package.json`, it is possible to compute the initial configurations dynamically by implementing a `DebugConfigurationProvider` (for details see the section Using a DebugConfigurationProvider below). **configurationSnippets** define launch configuration snippets that get surfaced in IntelliSense when editing the `launch.json`. As a convention, prefix the `label` attribute of a snippet by the debug environment name so that it can be clearly identified when presented in a list of many snippet proposals. The **variables** contribution binds "variables" to "commands". These variables can be used in the launch configuration using the **${command:xyz}** syntax and the variables are substituted by the value returned from the bound command when a debug session is started. The implementation of a command lives in the extension and it can range from a simple expression with no UI, to sophisticated functionality based on the UI features available in the extension API. Mock Debug binds a variable `AskForProgramName` to the command `extension.mock-debug.getProgramName`. The implementation of this command in `src/extension.ts` uses the `showInputBox` to let the user enter a program name: vscode.commands.registerCommand('extension.mock-debug.getProgramName', config => { return vscode.window.showInputBox({ placeHolder: 'Please enter the name of a markdown file in the workspace folder', value: 'readme.md' }); }); The variable can now be used in any string typed value of a launch configuration as **${command:AskForProgramName}**. ## Using a DebugConfigurationProvider If the static nature of debug contributions in the `package.json` is not sufficient, a `DebugConfigurationProvider` can be used to dynamically control the following aspects of a debug extension: * The initial debug configurations for a newly created launch.json can be generated dynamically, e.g. based on some contextual information available in the workspace. * A launch configuration can be _resolved_ (or modified) before it is used to start a new debug session. This allows for filling in default values based on information available in the workspace. Two _resolve_ methods exist: `resolveDebugConfiguration` is called before variables are substituted in the launch configuration, `resolveDebugConfigurationWithSubstitutedVariables` is called after all variables have been substituted. The former must be used if the validation logic inserts additional variables into the debug configuration. The latter must be used if the validation logic needs access to the final values of all debug configuration attributes. The `MockConfigurationProvider` in `src/extension.ts` implements `resolveDebugConfiguration` to detect the case where a debug session is started when no launch.json exists, but a Markdown file is open in the active editor. This is a typical scenario where the user has a file open in the editor and just wants to debug it without creating a launch.json. A debug configuration provider is registered for a specific debug type via `vscode.debug.registerDebugConfigurationProvider`, typically in the extension's `activate` function. To ensure that the `DebugConfigurationProvider` is registered early enough, the extension must be activated as soon as the debug functionality is used. This can be easily achieved by configuring extension activation for the `onDebug` event in the `package.json`: "activationEvents": [ "onDebug", // ... ], This catch-all `onDebug` is triggered as soon as any debug functionality is used. This works fine as long as the extension has cheap startup costs (i.e. does not spend a lot of time in its startup sequence). If a debug extension has an expensive startup (for instance because of starting a language server), the `onDebug` activation event could negatively affect other debug extensions, because it is triggered rather early and does not take a specific debug type into account. A better approach for expensive debug extensions is to use more fine-grained activation events: * `onDebugInitialConfigurations` is fired just before the `provideDebugConfigurations` method of the `DebugConfigurationProvider` is called. * `onDebugResolve:type` is fired just before the `resolveDebugConfiguration` or `resolveDebugConfigurationWithSubstitutedVariables` methods of the `DebugConfigurationProvider` for the specified type is called. **Rule of thumb:** If activation of a debug extensions is cheap, use `onDebug`. If it is expensive, use `onDebugInitialConfigurations` and/or `onDebugResolve` depending on whether the `DebugConfigurationProvider` implements the corresponding methods `provideDebugConfigurations` and/or `resolveDebugConfiguration`. ## Publishing your debugger extension Once you have created your debugger extension you can publish it to the Marketplace: * Update the attributes in the `package.json` to reflect the naming and purpose of your debugger extension. * Upload to the Marketplace as described in Publishing Extension. ## Alternative approach to develop a debugger extension As we have seen, developing a debugger extension typically involves debugging both the extension and the debug adapter in two parallel sessions. As explained above VS Code supports this nicely but development could be easier if both the extension and the debug adapter would be one program that could be debugged in one debug session. This approach is in fact easily doable as long as your debug adapter is implemented in TypeScript/JavaScript. The basic idea is to run the debug adapter directly inside the extension and to make VS Code to connect to it instead of launching a new external debug adapter per session. For this VS Code provides extension API to control how a debug adapter is created and run. A `DebugAdapterDescriptorFactory` has a method `createDebugAdapterDescriptor` that is called by VS Code when a debug session starts and a debug adapter is needed. This method must return a descriptor object (`DebugAdapterDescriptor`) that describes how the debug adapter is run. Today VS Code supports three different ways for running a debug adapter and consequently offers three different descriptor types: * `DebugAdapterExecutable`: this object describes a debug adapter as an external executable with a path and optional arguments and runtime. The executable must implement the Debug Adapter Protocol and communicate via stdin/stdout. This is VS Code's default mode of operation and VS Code uses this descriptor automatically with the corresponding values from the package.json if no `DebugAdapterDescriptorFactory` is explicitly registered. * `DebugAdapterServer`: this object describes a debug adapter running as a server that communicates via a specific local or remote port. A debug adapter implementation based on the `vscode-debugadapter` npm module supports this server mode automatically. * `DebugAdapterInlineImplementation`: this object describes a debug adapter as a JavaScript or Typescript object that implements the `vscode.DebugAdapter` interface. A debug adapter implementation based on version 1.38-pre.4 or later of the `vscode-debugadapter` npm module implements the interface automatically. Mock Debug shows examples for the three types of DebugAdapterDescriptorFactories and how they are registered for the 'mock' debug type. The run mode to use can be selected by setting the global variable `runMode` to one of the possible values `external`, `server`, or `inline`. For development, the `inline` and `server` modes are particularly useful because they allow for debugging extension and debug adapter within a single process. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/markdown-extension * Overview * Get Started * Extension Capabilities * Extension Guides * UX Guidelines * Language Extensions * Testing and Publishing * Advanced Topics * References Topics In this article Markdown extensions allow you to extend and enhance Visual Studio Code's built-in Markdown preview. This includes changing the look of the preview or adding support for new Markdown syntax. ## Changing the look of the Markdown preview with CSS Extensions can contribute CSS to change the look or layout of the Markdown preview. Stylesheets are registered using the `markdown.previewStyles` Contribution Point in the extension's `package.json`: "contributes": { "markdown.previewStyles": [ "./style.css" ] } `"markdown.previewStyles"` is a list of files relative to the extension's root folder. Contributed styles are added after the built-in Markdown preview styles but before a user's `"markdown.styles"`. The Markdown Preview GitHub Styling extension is a good example that demonstrates using a stylesheet to make the Markdown preview look like GitHub's rendered Markdown. You can review the extension's source code on GitHub. ## Adding support for new syntax with markdown-it plugins The VS Code Markdown preview supports the CommonMark specification. Extensions can add support for additional Markdown syntax by contributing a markdown-it plugin. To contribute a markdown-it plugin, first add a `"markdown.markdownItPlugins"` contribution in your extension's `package.json`: "contributes": { "markdown.markdownItPlugins": true } Then, in the extension's main `activation` function, return an object with a function named `extendMarkdownIt`. This function takes the current markdown-it instance and must return a new markdown-it instance: import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { return { extendMarkdownIt(md: any) { return md.use(require('markdown-it-emoji')); } }; } To contribute multiple markdown-it plugins, return multiple `use` statements chained together: return md.use(require('markdown-it-emoji')).use(require('markdown-it-hashtag')); Extensions that contribute markdown-it plugins are activated lazily, when a Markdown preview is shown for the first time. The markdown-emoji extension demonstrates using a markdown-it plugin to add emoji support to the markdown preview. You can review the Emoji extension's source code on GitHub. You may also want to review: * Guidelines for markdown-it plugin developers * Existing markdown-it plugins ## Adding advanced functionality with scripts For advanced functionality, extensions may contribute scripts that are executed inside of the Markdown preview. "contributes": { "markdown.previewScripts": [ "./main.js" ] } Contributed scripts are loaded asynchronously and reloaded on every content change. The Markdown Preview Mermaid Support extension demonstrates using scripts to add Mermaid diagrams and flowchart support to the markdown preview. You can review the Mermaid extension's source code on GitHub. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/testing In this article The Testing API allows Visual Studio Code extensions to discover tests in the workspace and publish results. Users can execute tests in the Test Explorer view, from decorations, and inside commands. With these new APIs, Visual Studio Code supports richer displays of outputs and diffs than was previously possible. > **Note**: The Testing API is available in VS Code version 1.59 and higher. ## Examples There are two test providers maintained by the VS Code team: * The sample test extension, which provides tests in Markdown files. * The selfhost test extension, that we use for running tests in VS Code itself. ## Discovering tests Tests are provided by the `TestController`, which requires a globally unique ID and human-readable label to create: const controller = vscode.tests.createTestController( 'helloWorldTests', 'Hello World Tests' ); To publish tests, you add `TestItem`s as children to the controller's `items` collection. `TestItem`s are the foundation of the test API in the `TestItem` interface, and are a generic type that can describe a test case, suite, or tree item as it exists in code. They can, in turn, have `children` themselves, forming a hierarchy. For example, here's a simplified version of how the sample test extension creates tests: parseMarkdown(content, { onTest: (range, numberA, mathOperator, numberB, expectedValue) => { // If this is a top-level test, add it to its parent's children. If not, // add it to the controller's top level items. const collection = parent ? parent.children : controller.items; // Create a new ID that's unique among the parent's children: const id = [numberA, mathOperator, numberB, expectedValue].join(' '); // Finally, create the test item: const test = controller.createTestItem(id, data.getLabel(), item.uri); test.range = range; collection.add(test); } // ... }); Similar to Diagnostics, it's mostly up to the extension to control when tests are discovered. A simple extension might watch the entire workspace and parse all tests in all files at activation. However, parsing everything immediately may be slow for large workspaces. Instead, you can do two things: 1. Actively discover tests for a file when it's opened in the editor, by watching `vscode.workspace.onDidOpenTextDocument`. 2. Setting `item.canResolveChildren = true` and setting the `controller.resolveHandler`. The `resolveHandler` is called if the user takes an action to demand tests be discovered, such as by expanding an item in the Test Explorer. Here's how this strategy might look in an extension that parses files lazily: // First, create the `resolveHandler`. This may initially be called with // "undefined" to ask for all tests in the workspace to be discovered, usually // when the user opens the Test Explorer for the first time. controller.resolveHandler = async test => { if (!test) { await discoverAllFilesInWorkspace(); } else { await parseTestsInFileContents(test); } }; // When text documents are open, parse tests in them. vscode.workspace.onDidOpenTextDocument(parseTestsInDocument); // We could also listen to document changes to re-parse unsaved changes: vscode.workspace.onDidChangeTextDocument(e => parseTestsInDocument(e.document)); // In this function, we'll get the file TestItem if we've already found it, // otherwise we'll create it with `canResolveChildren = true` to indicate it // can be passed to the `controller.resolveHandler` to gets its children. function getOrCreateFile(uri: vscode.Uri) { const existing = controller.items.get(uri.toString()); if (existing) { return existing; } const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri); file.canResolveChildren = true; return file; } function parseTestsInDocument(e: vscode.TextDocument) { if (e.uri.scheme === 'file' && e.uri.path.endsWith('.md')) { parseTestsInFileContents(getOrCreateFile(e.uri), e.getText()); } } async function parseTestsInFileContents(file: vscode.TestItem, contents?: string) { // If a document is open, VS Code already knows its contents. If this is being // called from the resolveHandler when a document isn't open, we'll need to // read them from disk ourselves. if (contents === undefined) { const rawContent = await vscode.workspace.fs.readFile(file.uri); contents = new TextDecoder().decode(rawContent); } // some custom logic to fill in test.children from the contents... } The implementation of `discoverAllFilesInWorkspace` can be built using VS Code' existing file watching functionality. When the `resolveHandler` is called, you should continue watching for changes so that the data in the Test Explorer stays up to date. async function discoverAllFilesInWorkspace() { if (!vscode.workspace.workspaceFolders) { return []; // handle the case of no open folders } return Promise.all( vscode.workspace.workspaceFolders.map(async workspaceFolder => { const pattern = new vscode.RelativePattern(workspaceFolder, '**/*.md'); const watcher = vscode.workspace.createFileSystemWatcher(pattern); // When files are created, make sure there's a corresponding "file" node in the tree watcher.onDidCreate(uri => getOrCreateFile(uri)); // When files change, re-parse them. Note that you could optimize this so // that you only re-parse children that have been resolved in the past. watcher.onDidChange(uri => parseTestsInFileContents(getOrCreateFile(uri))); // And, finally, delete TestItems for removed files. This is simple, since // we use the URI as the TestItem's ID. watcher.onDidDelete(uri => controller.items.delete(uri.toString())); for (const file of await vscode.workspace.findFiles(pattern)) { getOrCreateFile(file); } return watcher; }) ); } The `TestItem` interface is simple and doesn't have room for custom data. If you need to associate extra information with a `TestItem`, you can use a `WeakMap`: const testData = new WeakMap<vscode.TestItem, MyCustomData>(); // to associate data: const item = controller.createTestItem(id, label); testData.set(item, new MyCustomData()); // to get it back later: const myData = testData.get(item); It's guaranteed that the `TestItem` instances passed to all `TestController`\-related methods will be the same as the ones originally created from `createTestItem`, so you can be sure that getting the item from the `testData` map will work. For this example, let's just store the type of each item: enum ItemType { File, TestCase } const testData = new WeakMap<vscode.TestItem, ItemType>(); const getType = (testItem: vscode.TestItem) => testData.get(testItem)!; ## Running tests Tests are executed through `TestRunProfile`s. Each profile belongs to a specific execution `kind`: run, debug, or coverage. Most test extensions will have at most one profile in each of these groups, but more are allowed. For example, if your extension runs tests on multiple platforms, you could have one profile for each combination of platform and `kind`. Each profile has a `runHandler`, which is invoked when a run of that type is requested. function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { // todo } const runProfile = controller.createRunProfile( 'Run', vscode.TestRunProfileKind.Run, (request, token) => { runHandler(false, request, token); } ); const debugProfile = controller.createRunProfile( 'Debug', vscode.TestRunProfileKind.Debug, (request, token) => { runHandler(true, request, token); } ); The `runHandler` should call `controller.createTestRun` at least once, passing through the original request. The request contains the tests to `include` in the test run (which is omitted if the user asked to run all tests) and possibly tests to `exclude` from the run. The extension should use the resulting `TestRun` object to update the state of tests involved in the run. For example: async function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { const run = controller.createTestRun(request); const queue: vscode.TestItem[] = []; // Loop through all included tests, or all known tests, and add them to our queue if (request.include) { request.include.forEach(test => queue.push(test)); } else { controller.items.forEach(test => queue.push(test)); } // For every test that was queued, try to run it. Call run.passed() or run.failed(). // The `TestMessage` can contain extra information, like a failing location or // a diff output. But here we'll just give it a textual message. while (queue.length > 0 && !token.isCancellationRequested) { const test = queue.pop()!; // Skip tests the user asked to exclude if (request.exclude?.includes(test)) { continue; } switch (getType(test)) { case ItemType.File: // If we're running a file and don't know what it contains yet, parse it now if (test.children.size === 0) { await parseTestsInFileContents(test); } break; case ItemType.TestCase: // Otherwise, just run the test case. Note that we don't need to manually // set the state of parent tests; they'll be set automatically. const start = Date.now(); try { await assertTestPasses(test); run.passed(test, Date.now() - start); } catch (e) { run.failed(test, new vscode.TestMessage(e.message), Date.now() - start); } break; } test.children.forEach(test => queue.push(test)); } // Make sure to end the run after all tests have been executed: run.end(); } In addition to the `runHandler`, you can set a `configureHandler` on the `TestRunProfile`. If present, VS Code will have UI to allow the user to configure the test run, and call the handler when they do so. From here, you can open files, show a Quick Pick, or do whatever is appropriate for your test framework. > VS Code intentionally handles test configuration differently than debug or task configuration. These are traditionally editor or IDE-centric features, and are configured in special files in the `.vscode` folder. However, tests have traditionally been executed from the command line, and most test frameworks have existing configuration strategies. Therefore, in VS Code, we avoid duplication of configuration and instead leave it up to extensions to handle. ### Test Output In addition to the messages passed to `TestRun.failed` or `TestRun.errored`, you can append generic output using `run.appendOutput(str)`. This output can be displayed in a terminal using the **Test: Show Output** and through various buttons in the UI, such as the terminal icon in the Test Explorer view. Because the string is rendered in a terminal, you can use the full set of ANSI codes, including the styles available in the ansi-styles npm package. Bear in mind that, because it is in a terminal, lines must be wrapped using CRLF (`\r\n`), not just LF (`\n`), which may be the default output from some tools. ### Test Coverage Test coverage is associated with a `TestRun` via the `run.addCoverage()` method. Canonically this should be done by `runHandler`s of profiles of the `TestRunProfileKind.Coverage`, but it is possible to call it during any test run. The `addCoverage` method takes a `FileCoverage` object, which is a summary of the coverage data in that file: async function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { // ... for await (const file of readCoverageOutput()) { run.addCoverage(new vscode.FileCoverage(file.uri, file.statementCoverage)); } } The `FileCoverage` contains the overall covered and uncovered count of statements, branches, and declarations in each file. Depending on your runtime and coverage format, you might see statement coverage referred to as line coverage, or declaration coverage referred to as function or method coverage. You can add file coverage for a single URI multiple times, in which case the new information will replace the old. Once a user opens a file with coverage or expands a file in the **Test Coverage** view, VS Code requests more information for that file. It does so by calling an extension-defined `loadDetailedCoverage` method on the `TestRunProfile` with the `TestRun`, `FileCoverage`, and a `CancellationToken`. Note that the test run and file coverage instances are the same as the ones used in `run.addCoverage`, which is useful for associating data. For example, you can create a map of `FileCoverage` objects to your own data: const coverageData = new WeakMap<vscode.FileCoverage, MyCoverageDetails>(); profile.loadDetailedCoverage = (testRun, fileCoverage, token) => { return coverageData.get(fileCoverage).load(token); }; async function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { // ... for await (const file of readCoverageOutput()) { const coverage = new vscode.FileCoverage(file.uri, file.statementCoverage); coverageData.set(coverage, file); run.addCoverage(coverage); } } Alternatively you might subclass `FileCoverage` with an implementation that includes that data: class MyFileCoverage extends vscode.FileCoverage { // ... } profile.loadDetailedCoverage = async (testRun, fileCoverage, token) => { return fileCoverage instanceof MyFileCoverage ? await fileCoverage.load() : []; }; async function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { // ... for await (const file of readCoverageOutput()) { // 'file' is MyFileCoverage: run.addCoverage(file); } } `loadDetailedCoverage` is expected to return a promise to an array of `DeclarationCoverage` and/or `StatementCoverage` objects. Both objects include a `Position` or `Range` at which they can be found in the source file. `DeclarationCoverage` objects contain a name of the thing being declared (such as a function or method name) and the number of times that declaration was entered or invoked. Statements include the number of times they were executed, as well as zero or more associated branches. Refer to the type definitions in `vscode.d.ts` for more information. In many cases you might have persistent files lying around from your test run. It's best practice to put such coverage output in the system's temporary directory (which you can retrieve via `require('os').tmpdir()`), but you can also clean them up eagerly by listening to VS Code's cue that it no longer needs to retain the test run: import { promises as fs } from 'fs'; async function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { // ... run.onDidDispose(async () => { await fs.rm(coverageOutputDirectory, { recursive: true, force: true }); }); } ### Test Tags Sometime tests can only be run under certain configurations, or not at all. For these use cases, you can use Test Tags. `TestRunProfile`s can optionally have a tag associated with them and, if they do, only tests that have that tag can be run under the profile. Once again, if there is no eligible profile to run, debug, or gather coverage from a specific test, those options will not be shown in the UI. // Create a new tag with an ID of "runnable" const runnableTag = new TestTag('runnable'); // Assign it to a profile. Now this profile can only execute tests with that tag. runProfile.tag = runnableTag; // Add the "runnable" tag to all applicable tests. for (const test of getAllRunnableTests()) { test.tags = [...test.tags, runnableTag]; } Users can also filter by tags in the Test Explorer UI. ### Publish-only controllers The presence of run profiles is optional. A controller is allowed to create tests, call `createTestRun` outside of the `runHandler`, and update tests' states in the run without having a profile. The common use case for this are controllers who load their results from an external source, like CI or summary files. In this case, these controllers should usually pass the optional `name` argument to `createTestRun`, and `false` for the `persist` argument. Passing `false` here instructs VS Code not to retain the test result, like it would for runs in the editor, since these results can be reloaded from an external source externally. const controller = vscode.tests.createTestController( 'myCoverageFileTests', 'Coverage File Tests' ); vscode.commands.registerCommand('myExtension.loadTestResultFile', async file => { const info = await readFile(file); // set the controller items to those read from the file: controller.items.replace(readTestsFromInfo(info)); // create your own custom test run, then you can immediately set the state of // items in the run and end it to publish results: const run = controller.createTestRun( new vscode.TestRunRequest(), path.basename(file), false ); for (const result of info) { if (result.passed) { run.passed(result.item); } else { run.failed(result.item, new vscode.TestMessage(result.message)); } } run.end(); }); ## Migrating from the Test Explorer UI If you have an existing extension using the Test Explorer UI, we suggest you migrate to the native experience for additional features and efficiency. We've put together a repo with an example migration of the Test Adapter sample in its Git history. You can view each step by selecting the commit name, starting from `[1] Create a native TestController`. In summary, the general steps are: 1. Instead of retrieving and registering a `TestAdapter` with the Test Explorer UI's `TestHub`, call `const controller = vscode.tests.createTestController(...)`. 2. Rather than firing `testAdapter.tests` when you discover or rediscover tests, instead create and push tests into `controller.items`, for example by calling `controller.items.replace` with an array of discovered tests that are created by calling `vscode.test.createTestItem`. Note that, as tests change, you can mutate properties on the test item and update their children, and changes will be reflected automatically in VS Code's UI. 3. To load tests initially, instead of waiting for a `testAdapter.load()` method call, set `controller.resolveHandler = () => { /* discover tests */ }`. See more information around how test discovery works in Discovering Tests. 4. To run tests, you should create a Run Profile with a handler function that calls `const run = controller.createTestRun(request)`. Instead of firing a `testStates` event, pass `TestItem`s to methods on the `run` to update their state. ## Additional contribution points The `testing/item/context` menu contribution point may be used to add menu items to Tests in the Test Explorer view. Place menu items in the `inline` group to have them inline. All other menu item groups will be displayed in a context menu accessible using the mouse right-click. Additional context keys are available in the `when` clauses of your menu items: `testId`, `controllerId`, and `testItemHasUri`. For more complex `when` scenarios, where you want actions to be optionally available for different Test Items, consider using the `in` conditional operator. If you want to reveal a test in the Explorer, you can pass the test to the command `vscode.commands.executeCommand('vscode.revealTestInExplorer', testItem)`. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/custom-data-extension Custom Data format allows extension authors to easily extend VS Code's HTML / CSS language support without having to write code. The two Contribution Points for using custom data in an extension are: * `contributes.html.customData` * `contributes.css.customData` For example, by including this section in an extension's `package.json`: { "contributes": { "html": { "customData": ["./html.html-data.json"] }, "css": { "customData": ["./css.css-data.json"] } } } VS Code will load the HTML/CSS entities defined in both files and provide language support such as auto-completion and hover information for those entities. You can find the custom-data-sample at microsoft/vscode-extension-samples. 04/03/2025 --- ## Page: https://code.visualstudio.com/api/extension-guides/telemetry * Overview * Get Started * Extension Capabilities * Extension Guides * UX Guidelines * Language Extensions * Testing and Publishing * Advanced Topics * References Topics In this article Visual Studio Code collects usage data and sends it to Microsoft to help improve our products and services. Read our privacy statement and telemetry documentation to learn more. This topic has guidelines for extension authors so that their extensions can conform to VS Code telemetry requirements and best practices. > **Note**: If you don't want to send usage data to Microsoft, you can set the `telemetry.telemetryLevel` user setting to `off`. ## Telemetry module The VS Code team maintains the @vscode/extension-telemetry npm module that provides a consistent and safe way to collect telemetry within VS Code. The module reports telemetry to Azure Monitor and Application Insights and guarantees backwards compatibility against previous versions of VS Code. Follow this guide to set up Azure Monitor and get your Application Insights instrumentation key. ## Without the telemetry module Extension authors who wish not to use Application Insights can utilize their own custom solution to send telemetry. In this case, it is still required that extension authors respect the user's choice by utilizing the `isTelemetryEnabled` and `onDidChangeTelemetryEnabled` API. By doing this, users will have one centralized place to control their telemetry settings. ## Custom telemetry setting Extension may wish to give user control for extension specific telemetry independent of VS Code telemetry. In this case, we suggest that you introduce a specific extension setting. It is recommended that custom telemetry settings be tagged with `telemetry` and `usesOnlineServices` so that users can more easily query them in the Settings UI. Adding a custom telemetry setting is not an exemption from respecting a user's decision and the `isTelemetryEnabled` and `onDidChangeTelemetryEnabled` flag must always be respected. If `isTelemetryEnabled` reports false, even if your setting is enabled, telemetry must not be sent. ## telemetry.json We understand that telemetry can be a sensitive topic for many users and we aim to be as transparent as possible. The core VS Code product and most first party extensions ship with a `telemetry.json` file in their root. This allows a user to use the VS Code CLI with the `--telemetry` flag to receive a dump of all telemetry that VS Code produces. Extension authors may include a `telemetry.json` file in their root and it will also appear in the CLI dump. ## Do's and Don'ts โ๏ธ Do * Use the @vscode/extension-telemetry npm module if using application insights works for you. * Otherwise, respect the `isTelemetryEnabled` and `onDidChangeTelemetryEnabled` API. * Tag your custom telemetry setting with `telemetry` and `usesOnlineServices` if you have one. * Collect as little telemetry as possible. * Be as transparent as possible to your users about what you collect. โ Don't * Introduce a custom telemetry collection solution that does not ask for user consent. * Collect Personally identifiable information (PII). * Collect more telemetry than necessary. * Use just the `telemetry.telemetryLevel` setting, as it can sometimes be incorrect compared to `isTelemetryEnabled`. 04/03/2025