GLM 4.5 Air

VS Code Extension API: WebView & Gutter Icons Guide

Complete guide to embedding WebView with native editor and adding clickable gutter icons in VS Code extensions. Learn API limitations, implementation strategies, and code examples.

Question

VS Code Extension API: Embedding Native Editor with WebView in One Tab or Adding Clickable Gutter Icons

Question 1: Native Editor + WebView in the Same Tab

I’m developing a VS Code extension and want to create a custom view that combines:

  • A code editor (for .ts/.js files) with full workspace support, including editing, syntax highlighting, IntelliSense, go-to definition, and diagnostics
  • A WebView UI panel for visualization and controls
  • Both appearing together as a single tab/editor view, not as separate editors or split panes

Does the VS Code extension API allow embedding the native editor view inside a tab together with a WebView while preserving full language support? If this isn’t possible, is there any official documentation or issue indicating this limitation?

Question 2: Clickable Gutter/Margin Icons as Fallback

If embedding isn’t feasible, I need an alternative approach:

  • Display small icons/buttons in the editor’s left margin/gutter beside line numbers (e.g., “Run test”)
  • Allow users to click these icons to trigger extension commands

Specific questions:

  1. Does the VS Code extension API support clickable gutter/margin icons?
  2. If yes, which API (decorations, margin, commands, etc.) is used, and are there restrictions (e.g., icon display without clickability)?
  3. If not supported, what are common workarounds (e.g., listening to editor mouse events, computing click regions, using CodeLens, overlay UI, etc.)?

I appreciate any pointers, API references, or example implementations.

GLM 4.5 Air

VS Code Extension API: Embedding Native Editor with WebView in One Tab or Adding Clickable Gutter Icons

Brief Answer

The VS Code Extension API doesn’t currently support embedding a native editor view with a WebView in the same tab while preserving full language support. Official documentation indicates this is a limitation, with no direct API method available for this specific use case. However, you can add clickable icons to the editor’s gutter margin using the TextEditorDecorationType API combined with command callbacks, though with some limitations on icon placement and interaction.

Contents


Can You Embed a Native Editor with WebView in the Same Tab?

Understanding the Limitations of VS Code Editor API

The VS Code extension API doesn’t currently provide a direct way to embed a native editor view (with full IntelliSense, syntax highlighting, etc.) together with a WebView in the same tab. The editor and WebView APIs are designed to operate separately:

  • Editor API: Provides access to the built-in text editor but doesn’t allow embedding it within other UI elements
  • WebView API: Creates a separate panel or view that can display HTML content but doesn’t include native editor capabilities

The fundamental limitation is that VS Code’s text editor is a complex, native component that handles rendering, language services, and user interactions in a self-contained manner. The extension API exposes ways to interact with existing editors or create new ones, but not to embed them within custom UI elements.

Existing Workarounds and Their Limitations

While there’s no direct solution, developers have explored several workarounds:

  1. Split View Approach: Using the vscode.window.createTextEditor API to open the editor in one pane and WebView in another, then programmatically managing their layout. However, this creates separate tabs rather than a unified experience.

  2. Custom Editor Provider: Implementing a custom editor using vscode.window.registerCustomEditorProvider. This allows creating a completely custom editor view, but it requires implementing all editor features (syntax highlighting, IntelliSense, etc.) from scratch, which is complex and doesn’t leverage the native editor’s capabilities.

  3. WebView with Monaco Editor: Embedding Monaco Editor (VS Code’s underlying editor) within the WebView. While this provides editor functionality, it doesn’t integrate with VS Code’s language services, workspace features, or existing file system in the same way as the native editor.

Official Documentation and Community Solutions

The VS Code extension documentation doesn’t explicitly address this limitation, but related discussions in the GitHub repository indicate this is a known constraint:

  • Issue #85721 on VS Code GitHub discusses embedding WebView in editor, with no official solution provided
  • Issue #90463 mentions limitations of editor embedding
  • Community solutions generally involve one of the workarounds mentioned above

For official documentation, the relevant sections are:


Adding Clickable Gutter Icons in VS Code Extensions

TextEditorDecorationType API for Gutter Icons

VS Code Extension API provides a way to add decorations to the editor, including icons in the gutter margin. The TextEditorDecorationType API allows you to:

  1. Define decoration options that include gutter icons
  2. Apply these decorations to specific lines or ranges in the editor
  3. Attach commands to decorations for interactivity

Key components involved:

  • vscode.window.createTextEditorDecorationType(): Creates a decoration type with specific styling
  • TextEditorDecorationOptions: Configuration for decorations, including gutter icon paths
  • onDidClickDecoration event: Handles clicks on decorations

Implementation Steps for Clickable Gutter Icons

Here’s how to implement clickable gutter icons:

  1. Create a Decoration Type:
typescript
const decorationType = vscode.window.createTextEditorDecorationType({
  gutterIconPath: vscode.Uri.file(path.join(context.extensionPath, 'images', 'gutter-icon.png')),
  gutterIconSize: 'contain'
});
  1. Apply Decorations to Editor:
typescript
const decorationOptions: vscode.DecorationOptions[] = [];
decorationOptions.push({
  range: new vscode.Range(lineNumber, 0, lineNumber, 0),
  hoverMessage: 'Click to run test'
});
editor.setDecorations(decorationType, decorationOptions);
  1. Handle Decoration Clicks:
typescript
vscode.window.onDidClickDecoration(e => {
  if (e.decorationOptions?.hoverMessage === 'Click to run test') {
    vscode.commands.executeCommand('your.command', e.uri);
  }
});

Limitations and Considerations

While the decoration API provides gutter icons with click handling, there are several limitations:

  1. Limited Position Control: Icons are aligned to the left side of the gutter and can’t be precisely positioned between line numbers.

  2. No Native Context Menu: Clicking on gutter icons doesn’t trigger the editor’s context menu.

  3. Performance: Too many decorations can impact editor performance, especially in large files.

  4. Dynamic Updates: Updating decorations frequently can cause flickering or performance issues.

  5. Icon Size Constraints: Gutter icons are limited to small sizes (typically 16x16 or 32x32 pixels).

Alternative Approaches

If gutter decorations don’t meet your needs, consider these alternatives:

  1. CodeLens:
typescript
vscode.languages.registerCodeLensProvider(documentSelector, {
  provideCodeLenses(document) {
    return [new vscode.CodeLens(new vscode.Range(line, 0, line, 0), {
      command: 'your.command',
      title: 'Run Test'
    })];
  }
});
  1. Editor Mouse Events: Listen to editor mouse events and calculate click regions.

  2. TreeView Integration: Create a custom TreeView that shows clickable items related to code lines.

  3. Status Bar Items: Add clickable items to the status bar that can be context-aware.


Code Examples and Practical Implementation

Example: Adding Clickable Gutter Icons

Here’s a complete example of an extension that adds clickable gutter icons:

typescript
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    // Create decoration type for gutter icon
    const decorationType = vscode.window.createTextEditorDecorationType({
        gutterIconPath: context.asUri('images/gutter-icon.png'),
        gutterIconSize: 'contain'
    });

    // Store decoration options
    let decorationOptions: vscode.DecorationOptions[] = [];
    
    // Update decorations when active editor changes
    function updateDecorations() {
        const editor = vscode.window.activeTextEditor;
        if (!editor) return;
        
        // Clear existing decorations
        decorationOptions = [];
        
        // Add decoration to line 1 (for example)
        decorationOptions.push({
            range: new vscode.Range(0, 0, 0, 0),
            hoverMessage: 'Click to run test'
        });
        
        editor.setDecorations(decorationType, decorationOptions);
    }
    
    // Initial update
    updateDecorations();
    
    // Register command to run when gutter icon is clicked
    const clickDecorationHandler = vscode.window.onDidClickDecoration(e => {
        if (e.decorationOptions?.hoverMessage === 'Click to run test') {
            vscode.commands.executeCommand('your.extension.runTest', e.uri);
        }
    });
    
    // Update decorations when editor changes
    const editorChangeHandler = vscode.window.onDidChangeActiveTextEditor(updateDecorations);
    
    // Register command
    const runTestCommand = vscode.commands.registerCommand('your.extension.runTest', (uri: vscode.Uri) => {
        vscode.window.showInformationMessage('Test executed for ' + uri.fsPath);
    });
    
    // Add disposables
    context.subscriptions.push(decorationType, clickDecorationHandler, editorChangeHandler, runTestCommand);
}

Example: WebView Integration as Alternative

While you can’t embed a native editor with WebView in the same tab, you can create a WebView that integrates with the current editor:

typescript
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    // Create WebView panel
    const panel = vscode.window.createWebviewPanel(
        'customEditorWebView',
        'Custom Editor View',
        vscode.ViewColumn.Beside,
        {}
    );
    
    // Set WebView content (with Monaco Editor embedded)
    panel.webview.html = getWebViewContent();
    
    // Handle messages from WebView
    panel.webview.onDidReceiveMessage(message => {
        switch (message.command) {
            case 'getEditorContent':
                const editor = vscode.window.activeTextEditor;
                if (editor) {
                    panel.webview.postMessage({
                        command: 'editorContent',
                        content: editor.document.getText()
                    });
                }
                break;
        }
    });
}

function getWebViewContent() {
    return `
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Custom Editor View</title>
        <script src="https://unpkg.com/monaco-editor@0.34.1/min/vs/loader.js"></script>
        <style>
            body, html { height: 100%; margin: 0; padding: 0; }
            #editor { width: 100%; height: 100%; }
        </style>
    </head>
    <body>
        <div id="editor" data-do-not-activate="true"></div>
        <script>
            require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.34.1/min/vs' }});
            require(['vs/editor/editor.main'], function () {
                const editor = monaco.editor.create(document.getElementById('editor'), {
                    value: '',
                    language: 'javascript',
                    theme: 'vs-dark'
                });
                
                // Handle content from VS Code extension
                window.addEventListener('message', event => {
                    if (event.data.command === 'editorContent') {
                        editor.setValue(event.data.content);
                    }
                });
                
                // Send content to VS Code extension when changed
                editor.onDidChangeModelContent(() => {
                    vscode.postMessage({
                        command: 'updateEditorContent',
                        content: editor.getValue()
                    });
                });
            });
        </script>
    </body>
    </html>
    `;
}

Conclusion and Best Practices

Key Takeaways

  1. Native Editor + WebView in Same Tab: VS Code Extension API doesn’t currently support embedding a native editor with WebView in the same tab while preserving full language support. The closest alternatives are split views or custom editors with Monaco Editor.

  2. Clickable Gutter Icons: VS Code Extension API does support adding clickable icons to the editor’s gutter through the TextEditorDecorationType API, with some limitations on positioning and performance.

  3. API Selection: For gutter icons, use TextEditorDecorationType with onDidClickDecoration events. For more complex UI elements, consider CodeLens or TreeView alternatives.

Practical Recommendations

  1. For gutter icons:

    • Use the decoration API for simple, line-specific actions
    • Consider performance implications when adding many decorations
    • Provide visual feedback when icons are clicked
  2. For editor + WebView integration:

    • Consider the split view approach if separate panes are acceptable
    • Use Monaco Editor in WebView if you need embedded editor functionality
    • Implement proper message passing between WebView and extension
  3. When choosing between approaches:

    • Evaluate the trade-offs between native features and customization
    • Consider your users’ workflow and expectations
    • Prototype different solutions to find the best fit for your use case

For more information, refer to the VS Code Extension API documentation, WebView guide, Custom Editor API, and Decoration API documentation available on the VS Code website.