31 The TextEditorEdit API in VSCode Extensions

31.1 Classification and Motivation

The TextEditorEdit API forms the central tool for controlled modifications to the contents of open documents in VSCode extensions. This API ensures that all editing operations are performed atomically, traceably, and reversibly.

VSCode manages the content of all open documents in its own buffer system, which is closely tied to the undo stack, change tracking, and synchronization between multiple views. Writing directly to files using Node.js APIs would bypass this system and lead to inconsistencies. The TextEditorEdit API instead ensures that all changes are correctly processed by VSCode’s change management system.

Each editing operation via editor.edit() is treated as a transactional unit. All changes made within an edit() call appear to the user as a single editing step that can be undone with one undo operation. This level of granularity meets user expectations and ensures a consistent user experience.

31.2 Basic Structure of an Edit

The edit() method of TextEditor expects a callback function that receives a TextEditorEdit builder as a parameter. This builder collects all desired changes and applies them atomically:

import * as vscode from 'vscode';

export function insertTextAtCursor(): void {
    const editor = vscode.window.activeTextEditor;
    
    if (!editor) {
        vscode.window.showWarningMessage('No active editor available');
        return;
    }
    
    editor.edit(editBuilder => {
        const currentPosition = editor.selection.active;
        editBuilder.insert(currentPosition, 'Text was inserted');
    });
}

The edit() method returns a Promise<boolean> that indicates the success of the operation. This promise resolves once all changes have been applied or an error has occurred. For simple operations, the return value can be ignored, but for critical edits, it should be checked:

export async function insertWithValidation(text: string): Promise<void> {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    const success = await editor.edit(editBuilder => {
        editBuilder.insert(editor.selection.active, text);
    });
    
    if (!success) {
        vscode.window.showErrorMessage('Text could not be inserted');
    }
}

31.3 Methods of the TextEditorEdit API

The TextEditorEdit interface provides three fundamental operations that cover all types of text manipulation:

31.3.1 insert(position: Position, value: string)

The insert() method inserts text at a specific position without overwriting existing content. The position is defined by a Position object that uses zero-based line and character indices:

export function insertTimestamp(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const currentTime = new Date().toISOString();
        editBuilder.insert(editor.selection.active, `// Generated: ${currentTime}\n`);
    });
}

With multi-cursor selections, insert() acts on the active position, which corresponds to where the cursor was last placed. More complex multi-cursor operations require separate implementations.

31.3.2 replace(location: Range, value: string)

The replace() method replaces the content of a defined range with new text. This operation effectively combines a delete() followed by an insert():

export function wrapSelectionInQuotes(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const selection = editor.selection;
        const selectedText = editor.document.getText(selection);
        
        if (selection.isEmpty) {
            editBuilder.insert(selection.active, '""');
        } else {
            editBuilder.replace(selection, `"${selectedText}"`);
        }
    });
}

The replace() method is especially suitable for transformations of existing text sections, as it more clearly expresses the semantic intent of the operation than separate delete and insert operations.

31.3.3 delete(location: Range)

The delete() method completely removes the content of a specified range:

export function deleteCurrentLine(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const currentPosition = editor.selection.active;
        const lineRange = editor.document.lineAt(currentPosition.line).range;
        editBuilder.delete(lineRange);
    });
}

export function deleteCurrentLineCompletely(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const currentPosition = editor.selection.active;
        const line = editor.document.lineAt(currentPosition.line);
        editBuilder.delete(line.rangeIncludingLineBreak);
    });
}

31.4 Typical Use Cases

31.4.1 Commenting Code

A common operation in code editors is toggling comments for lines or blocks of code:

export function toggleLineComment(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const position = editor.selection.active;
        const line = editor.document.lineAt(position.line);
        const lineText = line.text;
        
        const commentPrefix = '// ';
        const trimmedText = lineText.trimLeft();
        
        if (trimmedText.startsWith(commentPrefix)) {
            const commentIndex = lineText.indexOf(commentPrefix);
            const commentRange = new vscode.Range(
                position.line, commentIndex,
                position.line, commentIndex + commentPrefix.length
            );
            editBuilder.delete(commentRange);
        } else {
            const firstNonWhitespace = line.firstNonWhitespaceCharacterIndex;
            const insertPosition = new vscode.Position(position.line, firstNonWhitespace);
            editBuilder.insert(insertPosition, commentPrefix);
        }
    });
}

31.4.2 Multiple Coordinated Changes

Complex edit operations can involve multiple changes simultaneously. All operations within an edit() call are treated as an atomic unit:

export function addFunctionWrapper(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const selection = editor.selection;
        const selectedText = editor.document.getText(selection);
        
        editBuilder.replace(selection, `function wrapper() {\n    ${selectedText.replace(/\n/g, '\n    ')}\n}`);
        
        const headerPosition = new vscode.Position(selection.start.line, 0);
        editBuilder.insert(headerPosition, '// Auto-generated wrapper function\n');
    });
}

31.4.3 Template Insertion with Placeholders

Extensions can insert predefined code templates with placeholders:

export function insertClassTemplate(className: string): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const template = `class ${className} {
    private _value: any;
    
    constructor(value: any) {
        this._value = value;
    }
    
    public getValue(): any {
        return this._value;
    }
    
    public setValue(value: any): void {
        this._value = value;
    }
}`;
        
        editBuilder.insert(editor.selection.active, template);
    });
}

31.5 Important Concepts and Limitations

31.5.1 Position and Range Coordinates

VSCode uses a zero-based coordinate system for positions. Position(0, 0) represents the first character of the first line. This convention differs from some editors that use one-based line numbers:

export function demonstrateCoordinates(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const documentStart = new vscode.Position(0, 0);
        editBuilder.insert(documentStart, '// File header\n');
        
        const lineEndPosition = editor.document.lineAt(2).range.end;
        editBuilder.insert(lineEndPosition, ' // End of line comment');
        
        const multiLineRange = new vscode.Range(
            new vscode.Position(5, 0),
            new vscode.Position(7, 0)
        );
        editBuilder.delete(multiLineRange);
    });
}

31.5.2 Avoiding Overlapping Changes

All changes within an edit() call must be disjoint. Overlapping ranges result in undefined behavior or errors:

export function demonstrateRangeConflicts(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const range1 = new vscode.Range(0, 0, 0, 5);
        const range2 = new vscode.Range(0, 10, 0, 15);
        editBuilder.replace(range1, 'Text A');
        editBuilder.replace(range2, 'Text B');
    });
}

31.5.3 Document State and Timing

All changes must be based on the current state of the document. Ranges that become invalid between their calculation and edit execution result in errors:

export async function handleDocumentChanges(): Promise<void> {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    const currentLineCount = editor.document.lineCount;
    const lastLineRange = editor.document.lineAt(currentLineCount - 1).range;
    
    const success = await editor.edit(editBuilder => {
        editBuilder.insert(lastLineRange.end, '\n// Added by extension');
    });
    
    if (!success) {
        vscode.window.showWarningMessage('Edit failed - document was changed meanwhile');
    }
}

31.6 Error Handling and Return Values

The edit() method can fail for various reasons: invalid ranges, interim document changes, read-only files, or resource limitations. Robust extensions must handle these cases:

export async function robustEditOperation(text: string): Promise<boolean> {
    const editor = vscode.window.activeTextEditor;
    
    if (!editor) {
        vscode.window.showErrorMessage('No active editor available');
        return false;
    }
    
    if (editor.document.isUntitled && editor.document.isDirty === false) {
        const answer = await vscode.window.showWarningMessage(
            'File is not saved. Proceed anyway?',
            'Yes', 'No'
        );
        if (answer !== 'Yes') {
            return false;
        }
    }
    
    try {
        const success = await editor.edit(editBuilder => {
            const position = editor.selection.active;
            if (position.line >= editor.document.lineCount) {
                throw new Error('Position out of bounds');
            }
            
            editBuilder.insert(position, text);
        });
        
        if (!success) {
            vscode.window.showErrorMessage('Edit operation was rejected by VSCode');
            return false;
        }
        
        return true;
        
    } catch (error) {
        vscode.window.showErrorMessage(`Edit error: ${error}`);
        return false;
    }
}

31.6.1 Special Error Conditions

Certain situations require special attention in error handling:

export async function handleSpecialCases(): Promise<void> {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    if (editor.document.lineCount > 10000) {
        const proceed = await vscode.window.showWarningMessage(
            'Large document detected. Operation may be slow.',
            'Continue', 'Cancel'
        );
        if (proceed !== 'Continue') return;
    }
    
    const languageId = editor.document.languageId;
    if (languageId === 'binary' || languageId === 'hex') {
        vscode.window.showErrorMessage('Operation not supported for binary files');
        return;
    }
    
    const editPromise = editor.edit(editBuilder => {
        editBuilder.insert(editor.selection.active, 'Safe text');
    });
    
    const timeoutPromise = new Promise<boolean>((_, reject) => {
        setTimeout(() => reject(new Error('Edit timeout')), 5000);
    });
    
    try {
        const success = await Promise.race([editPromise, timeoutPromise]);
        if (!success) {
            vscode.window.showErrorMessage('Edit operation failed');
        }
    } catch (error) {
        vscode.window.showErrorMessage(`Edit operation aborted: ${error}`);
    }
}

31.7 Best Practices

31.7.1 Logical Grouping of Changes

Use a single edit() call for logically related changes. This improves both performance and user experience:

// SUBOPTIMAL: Multiple separate edit operations
export async function addImportsSuboptimal(imports: string[]): Promise<void> {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    for (const importStatement of imports) {
        await editor.edit(editBuilder => {
            editBuilder.insert(new vscode.Position(0, 0), `${importStatement}\n`);
        });
    }
}

// OPTIMAL: One coordinated edit operation
export function addImportsOptimal(imports: string[]): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        const importBlock = imports.map(imp => `${imp}\n`).join('');
        editBuilder.insert(new vscode.Position(0, 0), importBlock);
    });
}

31.7.2 Defensive Validation

Implement thorough validation before critical edit operations:

export function safeTextReplacement(searchText: string, replaceText: string): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    if (!searchText || searchText.length === 0) {
        vscode.window.showErrorMessage('Search text must not be empty');
        return;
    }
    
    const document = editor.document;
    const fullText = document.getText();
    
    const searchIndex = fullText.indexOf(searchText);
    if (searchIndex === -1) {
        vscode.window.showInformationMessage(`Text "${searchText}" not found`);
        return;
    }
    
    editor.edit(editBuilder => {
        const startPosition = document.positionAt(searchIndex);
        const endPosition = document.positionAt(searchIndex + searchText.length);
        const replaceRange = new vscode.Range(startPosition, endPosition);
        
        editBuilder.replace(replaceRange, replaceText);
    });
}

31.7.3 Integration with VSCode Features

Use VSCode-specific features for better integration:

export async function smartIndentedInsert(text: string): Promise<void> {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    const position = editor.selection.active;
    const line = editor.document.lineAt(position.line);
    
    const indentation = line.text.match(/^\s*/)?.[0] || '';
    const indentedText = text
        .split('\n')
        .map((line, index) => index === 0 ? line : indentation + line)
        .join('\n');
    
    const success = await editor.edit(editBuilder => {
        editBuilder.insert(position, indentedText);
    });
    
    if (success) {
        await vscode.commands.executeCommand('editor.action.formatDocument');
    }
}

31.8 Distinction from WorkspaceEdit

The TextEditorEdit API is specifically designed for changes in the active editor. For cross-file operations or changes to unopened files, use WorkspaceEdit:

export function demonstrateApiChoices(): void {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;
    
    editor.edit(editBuilder => {
        editBuilder.insert(editor.selection.active, 'Only in active editor');
    });
    
    const workspaceEdit = new vscode.WorkspaceEdit();
    workspaceEdit.insert(editor.document.uri, editor.selection.active, 'Via WorkspaceEdit');
    vscode.workspace.applyEdit(workspaceEdit);
}