Developers perform repetitive tasks daily that are excellently suited for automation through extensions. One of the most common operations is the context-dependent insertion of comments or wrapping selected code parts with structured templates. These scenarios occur both in code documentation and in implementing recurring patterns.
In Eclipse, you were probably familiar with Live Templates or code generators that provide similar functionalities. While VSCode offers built-in snippets, a custom extension enables automations precisely adapted to your workflow. The following example demonstrates an extension that intelligently distinguishes between two contexts: inserting a TODO comment with an empty cursor and wrapping selected text with a structured template.
This functionality illustrates a fundamental aspect of extension development: context-dependent response to user input. The extension analyzes the current state of the editor and performs different operations based on this analysis. This pattern is found in nearly all productive extensions.
The example implements a command named
extension.insertCommentOrTemplate that fulfills two
distinct functions. With an empty cursor, it inserts a structured TODO
comment at the current position. With an active text selection, it
replaces the selected text with a template that embeds the original text
in a commented block.
This dual functionality demonstrates important concepts of the VSCode API: analyzing selection states, conditional execution of different edit operations, and using template systems with placeholders. At the same time, the example shows how an extension can provide maximum utility with minimal complexity.
The technical implementation combines all concepts covered in the preceding chapters: command registration, editor access, selection analysis, and the TextEditorEdit API. Through this combination, you gain a complete understanding of the development workflow for VSCode extensions.
The extension consists of a registered command that encapsulates all logic in a single function. This architecture corresponds to the VSCode pattern for simple, focused extensions:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext): void {
// Command registration in the activate function
const command = vscode.commands.registerCommand(
'extension.insertCommentOrTemplate',
async () => {
// Access to the active editor with mandatory null check
const editor = vscode.window.activeTextEditor;
if (!editor) {
// User-friendly error message when editor is missing
vscode.window.showErrorMessage('No active editor found');
return;
}
// Analyze selection state - core logic of context-dependent processing
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
// Edit operation with conditional logic for different contexts
const success = await editor.edit(editBuilder => {
if (selection.isEmpty) {
// Scenario A: Cursor without selection - insert TODO comment
handleEmptySelection(editBuilder, selection, editor);
} else {
// Scenario B: Text selection - template-based replacement
handleTextSelection(editBuilder, selection, selectedText);
}
});
// Success message for better user experience
if (success) {
const action = selection.isEmpty ? 'Comment inserted' : 'Template applied';
vscode.window.showInformationMessage(action);
} else {
vscode.window.showErrorMessage('Operation failed');
}
}
);
// Register command for automatic deregistration on extension deactivation
context.subscriptions.push(command);
}
/**
* Handles the case of an empty cursor by inserting a TODO comment
* @param editBuilder The VSCode TextEditorEdit builder for atomic operations
* @param selection The current selection (empty, but containing position)
* @param editor The active TextEditor for context information
*/
function handleEmptySelection(
editBuilder: vscode.TextEditorEdit,
selection: vscode.Selection,
editor: vscode.TextEditor
): void {
const position = selection.active;
// Intelligent indentation based on the current line
const currentLine = editor.document.lineAt(position.line);
const indentation = currentLine.text.match(/^\s*/)?.[0] || '';
// Timestamp for better tracking
const timestamp = new Date().toISOString().substring(0, 10);
// Structured comment with context
const comment = `${indentation}// TODO (${timestamp}): Please add description\n`;
editBuilder.insert(position, comment);
}
/**
* Handles text selection through template-based replacement
* @param editBuilder The VSCode TextEditorEdit builder
* @param selection The current text selection
* @param selectedText The selected text content
*/
function handleTextSelection(
editBuilder: vscode.TextEditorEdit,
selection: vscode.Selection,
selectedText: string
): void {
// Template with placeholder for dynamic content
const template = `/* BEGIN BLOCK */\n{{TEXT}}\n/* END BLOCK */`;
// Placeholder replacement - basis for more complex template systems
const processedTemplate = template.replace('{{TEXT}}', selectedText);
// Atomic replacement of the entire selection
editBuilder.replace(selection, processedTemplate);
}The extension follows the standard lifecycle of VSCode extensions. In
the activate function, the command is registered and added
to the context.subscriptions array. This registration
ensures automatic cleanup on extension deactivation and prevents memory
leaks.
Using an anonymous async function as command handler enables the use
of await for asynchronous edit operations. This
architecture is typical for extensions based on editor operations, as
these fundamentally run asynchronously.
The core of the extension lies in analyzing the
selection.isEmpty state. This boolean property
distinguishes between a simple cursor and an active text selection. This
distinction enables the implementation of context-dependent
functionalities that intelligently adapt to user intent.
// Demonstration of selection logic
const selection = editor.selection;
if (selection.isEmpty) {
// selection.active contains the cursor position
// selection.start === selection.end === selection.active
console.log(`Cursor at line ${selection.active.line}, column ${selection.active.character}`);
} else {
// selection.start and selection.end span the selected range
const selectedText = editor.document.getText(selection);
console.log(`Selection: "${selectedText}" from ${selection.start.line}:${selection.start.character} to ${selection.end.line}:${selection.end.character}`);
}The template system uses simple string replacement for the
{{TEXT}} placeholder. This implementation can serve as a
basis for more complex template engines:
// Advanced template processing for future development
function processAdvancedTemplate(template: string, context: { [key: string]: string }): string {
let result = template;
// Replace all placeholders in format {{KEY}}
Object.entries(context).forEach(([key, value]) => {
const placeholder = `{{${key}}}`;
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
result = result.replace(regex, value);
});
return result;
}
// Application example for advanced templates
const advancedContext = {
TEXT: selectedText,
AUTHOR: 'Developer Name',
DATE: new Date().toLocaleDateString('en-US'),
PROJECT: vscode.workspace.name || 'Unknown Project'
};
const advancedTemplate = `/*
* Created by: {{AUTHOR}}
* Date: {{DATE}}
* Project: {{PROJECT}}
*
* {{TEXT}}
*/`;The handleEmptySelection function implements intelligent
indentation through analysis of the current line. This pattern ensures
that inserted code respects existing formatting:
// Detailed indentation analysis
function analyzeIndentation(editor: vscode.TextEditor, position: vscode.Position): string {
const currentLine = editor.document.lineAt(position.line);
const lineText = currentLine.text;
// Extract leading whitespace characters
const indentationMatch = lineText.match(/^(\s*)/);
const existingIndentation = indentationMatch ? indentationMatch[1] : '';
// Consider editor settings for consistent indentation
const tabSize = editor.options.tabSize as number || 4;
const insertSpaces = editor.options.insertSpaces as boolean || true;
// For empty line beginning: use standard indentation
if (existingIndentation.length === 0 && lineText.trim().length > 0) {
return insertSpaces ? ' '.repeat(tabSize) : '\t';
}
return existingIndentation;
}The extension implements multi-layered error handling, ranging from basic null checks to specific edit validations:
export async function robustCommentInsertion(): Promise<void> {
const editor = vscode.window.activeTextEditor;
// Primary validation: editor availability
if (!editor) {
vscode.window.showErrorMessage('No active editor available');
return;
}
// Secondary validation: document properties
const document = editor.document;
if (document.isUntitled && document.getText().length === 0) {
const proceed = await vscode.window.showQuickPick(
['Yes, continue', 'No, cancel'],
{ placeHolder: 'Document is empty and unsaved. Continue?' }
);
if (proceed !== 'Yes, continue') {
return;
}
}
// Tertiary validation: position validity
const selection = editor.selection;
if (selection.active.line >= document.lineCount) {
vscode.window.showErrorMessage('Invalid cursor position');
return;
}
try {
const success = await editor.edit(editBuilder => {
// Edit operation with local error handling
if (selection.isEmpty) {
handleEmptySelection(editBuilder, selection, editor);
} else {
const selectedText = document.getText(selection);
// Validation of selection
if (selectedText.length > 10000) {
throw new Error('Text selection too large for template processing');
}
handleTextSelection(editBuilder, selection, selectedText);
}
});
if (!success) {
vscode.window.showErrorMessage('Edit operation was rejected by VSCode');
}
} catch (error) {
vscode.window.showErrorMessage(`Processing error: ${error}`);
}
}A productive extension should adapt to the characteristics of the current programming language:
/**
* Determines language-specific comment syntax
* @param languageId The VSCode language ID of the active document
* @returns Object with line and block comment syntax
*/
function getLanguageCommentStyle(languageId: string): { line: string; blockStart: string; blockEnd: string } {
const commentStyles: { [key: string]: { line: string; blockStart: string; blockEnd: string } } = {
'typescript': { line: '//', blockStart: '/*', blockEnd: '*/' },
'javascript': { line: '//', blockStart: '/*', blockEnd: '*/' },
'java': { line: '//', blockStart: '/*', blockEnd: '*/' },
'python': { line: '#', blockStart: '"""', blockEnd: '"""' },
'html': { line: '<!--', blockStart: '<!--', blockEnd: '-->' },
'css': { line: '/*', blockStart: '/*', blockEnd: '*/' },
'xml': { line: '<!--', blockStart: '<!--', blockEnd: '-->' }
};
// Fallback for unknown languages
return commentStyles[languageId] || { line: '//', blockStart: '/*', blockEnd: '*/' };
}
/**
* Language-dependent comment insertion
*/
function handleLanguageSpecificComment(
editBuilder: vscode.TextEditorEdit,
selection: vscode.Selection,
editor: vscode.TextEditor
): void {
const languageId = editor.document.languageId;
const commentStyle = getLanguageCommentStyle(languageId);
const position = selection.active;
const currentLine = editor.document.lineAt(position.line);
const indentation = currentLine.text.match(/^\s*/)?.[0] || '';
const timestamp = new Date().toISOString().substring(0, 10);
// Language-specific comment
const comment = `${indentation}${commentStyle.line} TODO (${timestamp}): Please add description\n`;
editBuilder.insert(position, comment);
}A professional extension should support user configuration. VSCode provides the settings system for this:
/**
* Loads user-defined templates from VSCode settings
*/
function loadUserTemplates(): { [key: string]: string } {
const config = vscode.workspace.getConfiguration('commentExtension');
// Default templates as fallback
const defaultTemplates = {
'block': '/* BEGIN BLOCK */\n{{TEXT}}\n/* END BLOCK */',
'debug': '// DEBUG START\n{{TEXT}}\n// DEBUG END',
'todo': '// TODO ({{DATE}}): {{TEXT}}'
};
// Merge user templates with default templates
const userTemplates = config.get<{ [key: string]: string }>('templates', {});
return { ...defaultTemplates, ...userTemplates };
}
/**
* Template selection through QuickPick interface
*/
async function selectTemplate(): Promise<string | undefined> {
const templates = loadUserTemplates();
const templateNames = Object.keys(templates);
if (templateNames.length === 1) {
return templates[templateNames[0]];
}
const selectedTemplate = await vscode.window.showQuickPick(
templateNames.map(name => ({
label: name,
description: templates[name].substring(0, 50) + '...'
})),
{ placeHolder: 'Select template' }
);
return selectedTemplate ? templates[selectedTemplate.label] : undefined;
}The extension can be extended to support multiple selections:
/**
* Extended implementation with multiple selection support
*/
export async function handleMultipleSelections(): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const selections = editor.selections;
// Analysis of selections
const emptySelections = selections.filter(sel => sel.isEmpty);
const textSelections = selections.filter(sel => !sel.isEmpty);
if (selections.length > 1) {
vscode.window.showInformationMessage(
`Processing ${selections.length} selections: ${textSelections.length} with text, ${emptySelections.length} cursor-only`
);
}
const success = await editor.edit(editBuilder => {
// Processing in reverse order to preserve positions
[...selections].reverse().forEach(selection => {
if (selection.isEmpty) {
handleEmptySelection(editBuilder, selection, editor);
} else {
const selectedText = editor.document.getText(selection);
handleTextSelection(editBuilder, selection, selectedText);
}
});
});
if (success) {
vscode.window.showInformationMessage(`${selections.length} selections processed successfully`);
}
}An extended extension can extract context from the current workspace:
/**
* Gathers context information for extended templates
*/
async function gatherWorkspaceContext(): Promise<{ [key: string]: string }> {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const activeEditor = vscode.window.activeTextEditor;
const context: { [key: string]: string } = {
DATE: new Date().toLocaleDateString('en-US'),
TIME: new Date().toLocaleTimeString('en-US'),
PROJECT: workspaceFolder?.name || 'Unknown Project',
FILE: activeEditor?.document.fileName.split('/').pop() || 'Unknown File',
LANGUAGE: activeEditor?.document.languageId || 'text'
};
// Add Git information (if available)
try {
const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports;
if (gitExtension) {
const git = gitExtension.getAPI(1);
const repository = git.repositories[0];
if (repository) {
context.BRANCH = repository.state.HEAD?.name || 'main';
context.COMMIT = repository.state.HEAD?.commit?.substring(0, 8) || 'unknown';
}
}
} catch (error) {
// Git information optional - ignore on errors
}
return context;
}Future versions of the extension could integrate code analysis to generate even more intelligent templates:
/**
* Analyzes selected code for context-specific templates
*/
function analyzeCodeContext(selectedText: string, languageId: string): string {
// Simple heuristics for code recognition
const codePatterns = {
function: /function\s+(\w+)\s*\(/,
class: /class\s+(\w+)/,
variable: /(const|let|var)\s+(\w+)/,
import: /import\s+.*from\s+['"]/
};
for (const [type, pattern] of Object.entries(codePatterns)) {
if (pattern.test(selectedText)) {
return getCodeSpecificTemplate(type, selectedText);
}
}
return getDefaultTemplate();
}
function getCodeSpecificTemplate(codeType: string, code: string): string {
const templates = {
function: `/**
* Function: {{TEXT}}
* @description Automatically generated documentation
* @author {{AUTHOR}}
* @date {{DATE}}
*/
{{TEXT}}`,
class: `/**
* Class: {{TEXT}}
* @description {{TEXT}}
* @version 1.0
* @author {{AUTHOR}}
*/
{{TEXT}}`,
variable: `// Variable {{TEXT}} - add description
{{TEXT}}`,
import: `// Import: {{TEXT}}
{{TEXT}}`
};
return templates[codeType] || templates['function'];
}When working with large files, performance considerations are important:
/**
* Performance-optimized version for large selections
*/
async function optimizedTemplateApplication(
selectedText: string,
template: string
): Promise<string> {
// For large texts: streaming-based processing
if (selectedText.length > 100000) {
return processLargeTextInChunks(selectedText, template);
}
// Standard processing for normal sizes
return template.replace('{{TEXT}}', selectedText);
}
async function processLargeTextInChunks(text: string, template: string): Promise<string> {
const chunkSize = 10000;
const chunks: string[] = [];
// Split text into blocks
for (let i = 0; i < text.length; i += chunkSize) {
chunks.push(text.substring(i, i + chunkSize));
}
// Apply template to each block
const processedChunks = chunks.map(chunk =>
template.replace('{{TEXT}}', chunk)
);
return processedChunks.join('\n\n');
}