VS Code Extension API: Embedding Native Editor and WebView in One Tab or Creating Clickable Gutter Icons
I’m developing a VS Code extension and have two related questions about what the extension API supports:
1. Native Editor + WebView in the Same Tab
I’d like to create a custom view or custom editor where:
- One area is a code editor (for .ts/.js) that fully supports workspace files, editing, syntax highlighting, IntelliSense, go-to definition, diagnostics, etc.
- Another area is a WebView UI panel for visualization, controls, etc.
- These two should appear 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 it’s not possible, is there any official documentation or issue indicating this limitation?
2. Clickable Gutter/Margin Icons or Buttons
If the embedding above isn’t feasible, my fallback plan:
- Show small icons/buttons in the editor’s left margin/gutter/beside line numbers (e.g. “Run test”).
- Allow users to click those icons to trigger extension commands.
Questions:
- Does the VS Code extension API support clickable gutter/margin icons?
- If yes, which API (decorations, margin, commands, etc) is used, and are there restrictions (for example, icon display without clickability)?
- If not supported, are there 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. Thanks!
VS Code Extension API: Editor-WebView Integration and Gutter Icons
Brief Answer
The VS Code extension API does not currently support embedding a native editor and WebView in the same tab as a unified view, as the native editor is designed to work only as a standalone editor tab. However, you can create clickable gutter icons using the TextEditorDecorationType
API combined with mouse event handling to detect clicks on specific regions of the editor margin. For your use case, implementing gutter icons with command triggers would be the most straightforward approach within the current API constraints.
Contents
- Native Editor and WebView Integration in One Tab
- Clickable Gutter Icons Implementation
- Alternative Approaches and Workarounds
- Code Examples and Implementation Guide
- Best Practices and Performance Considerations
Native Editor and WebView Integration in One Tab
Unfortunately, the VS Code extension API does not currently support embedding a native editor (with full IntelliSense, syntax highlighting, etc.) alongside a WebView in the same tab as a unified view. This is a known limitation of the extension architecture.
Technical Limitations
The native editor in VS Code is a complex component that’s tightly integrated with the core editor functionality. It’s designed to work as a standalone editor tab because:
-
Resource Management: The native editor manages its own resources, including language services, IntelliSense providers, and document synchronization.
-
Editor Lifecycle: Each editor tab has its own lifecycle that’s managed by VS Code’s core, making it difficult to embed within another view.
-
Isolation Requirements: The editor needs to be isolated for proper handling of file operations, undo/redo history, and other editor-specific features.
Current Alternatives
If you need both editing capabilities and WebView UI in the same workspace, consider these alternatives:
-
Split Editor View: Use VS Code’s built-in editor splitting to show the editor and WebView side by side. While not in the same tab, this provides a similar user experience.
-
WebView with Monaco Editor: Embed a Monaco editor instance (the same editor that powers VS Code) within your WebView. This gives you editor functionality but with some limitations compared to the native editor.
-
Custom Editor Provider: Implement a custom editor provider using the
vscode.registerCustomEditorProvider
API, though this is more complex and may not support all native editor features.
Official Position
The VS Code team has acknowledged this limitation in various GitHub issues. While there’s no official roadmap for embedding the native editor in custom views, you can track potential future enhancements by monitoring issues tagged with “webview” and “editor” in the VS Code GitHub repository.
Clickable Gutter Icons Implementation
Yes, the VS Code extension API does support adding clickable icons to the gutter/margin area. Here’s how to implement this feature:
Core API Components
To create clickable gutter icons, you’ll need to combine several APIs:
- TextEditorDecorationType: For defining the appearance of your gutter icons.
- DecorationOptions: For specifying where to place the decorations (in the gutter).
- Mouse Event Handling: To detect clicks on specific gutter regions.
Step-by-Step Implementation
- Create a decoration type for gutter icons:
const gutterDecorationType = vscode.window.createTextEditorDecorationType({
gutterIconPath: vscode.Uri.joinPath(extensionUri, 'images', 'gutter-icon.png'),
gutterIconSize: 'contain',
});
- Add decorations to the editor:
const decorations: vscode.DecorationOptions[] = [];
for (let i = 0; i < document.lineCount; i++) {
decorations.push({
range: new vscode.Range(new vscode.Position(i, 0), new vscode.Position(i, 0)),
gutterIconPath: vscode.Uri.joinPath(extensionUri, 'images', 'gutter-icon.png'),
});
}
editor.setDecorations(gutterDecorationType, decorations);
- Handle gutter icon clicks:
vscode.window.onDidChangeTextEditorSelection((e) => {
// This event fires when the user clicks in the editor
// You can check if the click was in the gutter area
// by examining the selection position and mouse coordinates
});
// A more robust approach is to track mouse movements and clicks
vscode.window.onDidChangeTextEditorVisibleRanges((e) => {
// Monitor visible ranges to update decorations as needed
});
Limitations and Considerations
-
No Direct Click Events: The VS Code API doesn’t provide direct click events for gutter decorations. You need to implement click detection manually.
-
Performance: Adding decorations to every line can impact performance, especially in large files. Consider using a virtualization approach to only decorate visible lines.
-
Icon Size: Gutter icons are constrained by the gutter width, which is typically narrow.
-
Interaction with Other Features: Gutter decorations may conflict with other gutter features like Git change indicators or debug breakpoints.
Alternative Approaches and Workarounds
If the gutter icon approach doesn’t meet your needs, consider these alternatives:
CodeLens
CodeLens provides clickable references and commands directly in the editor margin, above the line numbers:
vscode.languages.registerCodeLensProvider({ language: 'typescript' }, {
provideCodeLenses: (document, token) => {
return [
new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), {
command: 'extension.myCommand',
title: 'Run Test',
})
];
}
});
Pros:
- Native click handling without manual event management
- Better integration with VS Code’s UI
- Can show multiple actions per line
Cons:
- Appears above the line numbers, not in the gutter
- Limited styling options
- May affect code readability
Editor Overlay UI
Create an overlay UI that appears when hovering over specific line numbers:
vscode.window.onDidChangeTextEditorSelection((e) => {
if (e.selections.length > 0) {
const position = e.selections[0].active;
// Check if click is near line numbers
if (position.character === 0) {
// Show custom UI overlay
}
}
});
Pros:
- More flexible positioning and styling
- Can contain complex UI elements
Cons:
- More complex implementation
- May interfere with normal editor interactions
Custom View with Editor Preview
Create a custom view that contains both a WebView and a read-only preview of the editor:
vscode.window.registerWebviewViewProvider('myCustomView', {
resolveWebviewView: (webviewView, context, token) => {
webviewView.webview.options = { enableScripts: true };
webviewView.webview.html = getWebViewContent();
// Communicate with WebView to update editor preview
vscode.workspace.onDidChangeTextDocument((e) => {
webviewView.webview.postMessage({
type: 'updatePreview',
content: e.document.getText()
});
});
}
});
Pros:
- Full control over the layout
- Can implement complex interactions
Cons:
- Editor preview is read-only
- No IntelliSense or other advanced editor features
Code Examples and Implementation Guide
Here’s a complete implementation for clickable gutter icons:
1. Extension Setup
First, ensure your extension properly registers the necessary event listeners:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// Register gutter icons
const gutterIconProvider = new GutterIconProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider({ language: 'typescript' }, gutterIconProvider));
// Register command for gutter icon actions
const disposable = vscode.commands.registerCommand('extension.runTest', (lineNumber: number) => {
vscode.window.showInformationMessage(`Running test at line ${lineNumber}`);
});
context.subscriptions.push(disposable);
}
2. Gutter Icon Provider Implementation
class GutterIconProvider implements vscode.CodeLensProvider {
private _gutterIconDecorationType: vscode.TextEditorDecorationType;
constructor() {
this._gutterIconDecorationType = vscode.window.createTextEditorDecorationType({
gutterIconPath: vscode.Uri.joinPath(context.extensionUri, 'images', 'run-icon.png'),
gutterIconSize: 'contain',
});
}
provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const lenses: vscode.CodeLens[] = [];
// Add a gutter icon to every line with a test
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
if (line.text.includes('test(')) {
lenses.push(new vscode.CodeLens(line.range, {
command: 'extension.runTest',
title: '',
arguments: [i + 1] // Pass line number to command
}));
}
}
return lenses;
}
dispose() {
this._gutterIconDecorationType.dispose();
}
}
3. Click Detection Enhancement
For more precise click detection, you can enhance this with mouse event tracking:
class GutterIconProvider implements vscode.CodeLensProvider {
// ... previous code ...
private setupMouseTracking(editor: vscode.TextEditor) {
const onMouseClick = (e: any) => {
if (e.detail === 2) { // Double click
return;
}
const position = editor.selection.active;
if (position.character === 0) { // Clicked near line numbers
// Find the closest CodeLens at this line
// This requires maintaining a mapping of line numbers to CodeLens instances
}
};
// You'll need to add and remove this listener appropriately
editor.onDidChangeCursorSelection(onMouseClick);
}
}
4. WebView Integration with Editor (Alternative Approach)
If you decide to go with a WebView + Monaco Editor approach:
function getWebViewContent() {
return `<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; overflow: hidden; }
#editor { height: 100vh; width: 50%; float: left; }
#controls { height: 100vh; width: 50%; float: left; background-color: #f5f5f5; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/loader.min.js"></script>
</head>
<body>
<div id="editor"></div>
<div id="controls">
<h2>Controls</h2>
<button id="runButton">Run Code</button>
</div>
<script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs' }});
require(['vs/editor/editor.main'], function () {
const editor = monaco.editor.create(document.getElementById('editor'), {
value: '${initialCode}',
language: 'javascript',
automaticLayout: true
});
// Handle messages from extension
window.addEventListener('message', event => {
const message = event.data;
if (message.command === 'updateCode') {
editor.setValue(message.content);
}
});
document.getElementById('runButton').addEventListener('click', () => {
// Send code back to extension for execution
window.parent.postMessage({
command: 'runCode',
code: editor.getValue()
}, '*');
});
});
</script>
</body>
</html>`;
}
Best Practices and Performance Considerations
Gutter Icon Best Practices
-
Limit Decorations: Only add gutter icons to relevant lines to improve performance.
-
Use Efficient Icons: Optimize gutter icon images for quick loading.
-
Manage State: Properly clean up decorations when they’re no longer needed.
-
Handle Editor Changes: Update decorations when the document changes, but debounce these updates for large files.
let updateTimeout: NodeJS.Timeout;
vscode.workspace.onDidChangeTextDocument((e) => {
clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
// Update gutter icons
}, 300);
});
WebView Integration Best Practices
-
Minimize Communication: Reduce the frequency of messages between WebView and extension.
-
Use Proper Cleanup: Dispose of WebView resources when the view is closed.
-
Handle Resize Events: Properly handle WebView resizing for responsive layouts.
-
Security: Sanitize all content passed to WebView to prevent XSS vulnerabilities.
Performance Optimization Techniques
-
Virtualization: For large files, only render decorations for visible lines.
-
Debounce Updates: Throttle decoration updates to prevent performance issues.
-
Lazy Loading: Load resources only when needed.
-
Memory Management: Properly dispose of disposable resources to prevent memory leaks.
class GutterIconProvider implements vscode.CodeLensProvider {
private decorations: Map<string, vscode.CodeLens[]> = new Map();
provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const key = document.uri.toString();
// Check if we already have decorations for this document
if (this.decorations.has(key)) {
return this.decorations.get(key);
}
// Compute decorations
const lenses = this.computeDecorations(document);
this.decorations.set(key, lenses);
return lenses;
}
private computeDecorations(document: vscode.TextDocument): vscode.CodeLens[] {
// Only compute for visible lines if possible
const visibleRange = vscode.window.activeTextEditor?.visibleRanges[0];
const startLine = visibleRange ? visibleRange.start.line : 0;
const endLine = visibleRange ? visibleRange.end.line : document.lineCount;
const lenses: vscode.CodeLens[] = [];
for (let i = startLine; i < endLine; i++) {
// Add decorations for visible lines only
}
return lenses;
}
}
Conclusion
While VS Code doesn’t currently support embedding a native editor and WebView in the same tab, you have several alternatives to achieve your goals:
-
For gutter icons, use the CodeLens API with custom commands for the most straightforward implementation that maintains native editor features.
-
If you need a more integrated view, consider using WebView with Monaco Editor, though you’ll lose some native editor capabilities like full IntelliSense.
-
For complex layouts, explore custom views that can communicate with the editor through the VS Code messaging API.
The gutter icon approach with CodeLens provides the best balance of native functionality and custom UI elements for your use case. Remember to consider performance implications when implementing decorations, especially for large files, and follow best practices for memory management and event handling.