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.
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');
}
}The TextEditorEdit interface provides three fundamental
operations that cover all types of text manipulation:
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.
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.
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);
});
}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);
}
});
}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');
});
}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);
});
}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);
});
}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');
});
}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');
}
}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;
}
}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}`);
}
}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);
});
}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);
});
}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');
}
}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);
}