Before we start with the actual selection operations, we define a helper function that is used in all subsequent examples. This function encapsulates the necessary error handling and makes the code more readable.
function getEditorOrReturn(): vscode.TextEditor | undefined {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage('No active editor available');
return undefined;
}
return editor;
}
// Usage in all following functions:
// const editor = getEditorOrReturn();
// if (!editor) return;This defensive check is required for all editor operations, as there might not be an active editor. In all following examples, we use this function without further explanation.
Understanding cursor position and text selection forms the foundation for any VSCode extension that deals with user input. VSCode distinguishes three fundamental states, each requiring different handling.
A cursor is a simple insertion point with no selected text – the blinking vertical line indicating where the next typed text will appear. A text selection spans a contiguous range between two positions in the document, highlighted in color. Multi-selections allow editing multiple areas simultaneously, which is especially useful for repetitive changes.
This distinction is crucial because extensions must react context-sensitively. A “wrap text” function should insert quotation marks at a plain cursor and place the cursor between them. With an existing text selection, it should wrap the selected text. For multi-selections, it must process all areas in a coordinated manner.
export function analyzeCurrentSelectionState(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selection = editor.selection;
if (selection.isEmpty) {
// Cursor only, no text selection
console.log('Plain cursor at position:', selection.active.line, selection.active.character);
} else {
// Text range selected
const text = editor.document.getText(selection);
console.log('Text selection:', text.length, 'characters');
}
// Check for multi-selection
if (editor.selections.length > 1) {
console.log('Multi-selection with', editor.selections.length, 'ranges');
}
}The isEmpty property indicates the difference between a
cursor and a text selection. It is true when the start and
end positions of the selection are identical. The
selections array contains one element for simple
selections, and more for multi-selections.
VSCode provides two central properties: selection for
the primary selection and selections for all active
selections. For a simple selection, both are identical; for
multi-selections, selection contains the most recently
created selection, while selections returns an array of all
regions.
export function demonstrateSelectionAccess(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const primarySelection = editor.selection; // The main selection (Selection object)
const allSelections = editor.selections; // All selections (array of Selection objects)
console.log(`Number of selections: ${allSelections.length}`);
console.log(`Primary selection empty: ${primarySelection.isEmpty}`);
// In case of multi-selection, the primary selection is the last element
const isMultiSelection = allSelections.length > 1;
if (isMultiSelection) {
const lastSelection = allSelections[allSelections.length - 1];
console.log('Primary selection matches last element:', primarySelection === lastSelection);
}
}This distinction becomes important later when we need to differentiate between single and batch editing.
Selection objects offer four position properties that represent different aspects of a selection. Understanding these properties is crucial for extensions that extend, analyze, or react directionally to selections.
The properties start and end ensure
lexicographic order in the document – start always precedes
or equals end, regardless of how the user created the
selection. This makes them ideal for most processing operations where
direction is irrelevant.
export function explorePositionProperties(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selection = editor.selection;
// start/end: always in document order (start comes before end)
console.log(`Document range: ${selection.start.line}:${selection.start.character} to ${selection.end.line}:${selection.end.character}`);
// anchor/active: indicate actual user marking direction
console.log(`Selection started at: ${selection.anchor.line}:${selection.anchor.character}`);
console.log(`Cursor is now at: ${selection.active.line}:${selection.active.character}`);
// Direction detection for smart extensions
const isBackwardSelection = selection.anchor.isAfter(selection.active);
console.log(`Marked backward: ${isBackwardSelection}`);
}The anchor and active properties show the
actual user interaction: anchor marks the start point of
the selection, active the current cursor position. When a
user marks text backward, anchor comes after
active. This information is valuable for extensions that
aim to extend selections intelligently.
Extracting text content from selections may seem simple, but robust extensions must account for various scenarios. The biggest challenge is delivering meaningful results even for empty selections that meet user expectations.
export function extractTextWithFallback(): string {
const editor = getEditorOrReturn();
if (!editor) return '';
const selection = editor.selection;
// First priority: explicit text selection
if (!selection.isEmpty) {
return editor.document.getText(selection);
}
// Second priority: word at cursor position
const wordRange = editor.document.getWordRangeAtPosition(selection.active);
if (wordRange) {
const wordText = editor.document.getText(wordRange);
console.log(`Fallback to word: "${wordText}"`);
return wordText;
}
// Last priority: the entire line
const lineRange = editor.document.lineAt(selection.active.line).range;
const lineText = editor.document.getText(lineRange);
console.log(`Fallback to line: "${lineText.trim()}"`);
return lineText;
}This three-tier fallback hierarchy follows natural user expectations.
A comment function should comment the current line when no text is
selected, while a “wrap text in quotes” function should use the word
under the cursor. The getWordRangeAtPosition method detects
word boundaries based on the document language.
Text modifications in VSCode are handled via the
editor.edit() system, which provides transactional
properties. All changes within an edit block are executed
atomically – either all or none. We’ll start with simple operations on a
single selection.
export function wrapSingleSelectionInQuotes(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selection = editor.selection;
editor.edit(editBuilder => {
if (selection.isEmpty) {
// For empty cursor: insert empty quotes
editBuilder.insert(selection.active, '""');
// After the edit, the cursor is automatically placed after the quotes
// To move it between the quotes, handle separately
} else {
// For text selection: wrap existing text
const selectedText = editor.document.getText(selection);
editBuilder.replace(selection, `"${selectedText}"`);
}
});
}The editBuilder collects all change instructions and
applies them in a coordinated manner. insert inserts text
at a position, replace replaces a range with new text. The
system automatically recalculates the effects on subsequent
positions.
Multi-selections pose a technical challenge: if you process selections in their natural order, earlier insertions shift the positions of later selections. An insertion at the beginning of the document invalidates all subsequent position references. The solution is to process in reverse.
export function wrapMultipleSelectionsInQuotes(): void {
const editor = getEditorOrReturn();
if (!editor) return;
editor.edit(editBuilder => {
// Process in reverse to avoid position shifts
const selections = [...editor.selections].reverse();
selections.forEach(selection => {
if (selection.isEmpty) {
editBuilder.insert(selection.active, '""');
} else {
const selectedText = editor.document.getText(selection);
editBuilder.replace(selection, `"${selectedText}"`);
}
});
});
}The copy with [...editor.selections] is important to
avoid modifying the original array. The reverse processing with
reverse() ensures that changes at the document end do not
affect earlier positions. This pattern is fundamental for all
multi-selection operations.
Extensions can not only react to existing selections but also create new ones or modify existing ones. This enables “select all occurrences” functions, smart navigation, or automatic text markings. However, this should be used sparingly as it may disrupt user expectations.
export function selectCurrentWord(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const currentPosition = editor.selection.active;
const wordRange = editor.document.getWordRangeAtPosition(currentPosition);
if (wordRange) {
// New Selection object with start and end positions of the word
editor.selection = new vscode.Selection(wordRange.start, wordRange.end);
console.log(`Word selected: "${editor.document.getText(wordRange)}"`);
} else {
// Fallback: select entire line if no word is found
const lineRange = editor.document.lineAt(currentPosition.line).range;
editor.selection = new vscode.Selection(lineRange.start, lineRange.end);
console.log('Line selected since no word at cursor position');
}
}Creating new selections is done via the Selection
constructor with two Position objects. For a plain cursor,
use the same position for start and end. Validating positions is
important – they must not lie outside document boundaries.
Creating multi-selections requires special care with position validation, as invalid positions can corrupt the entire selection array.
export function createColumnCursors(startLine: number, endLine: number, column: number): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selections: vscode.Selection[] = [];
// Create a cursor for each line in the range
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
// Check document boundaries
if (lineNumber >= editor.document.lineCount) break;
const line = editor.document.lineAt(lineNumber);
// Limit position to line length
const safeColumn = Math.min(column, line.text.length);
const position = new vscode.Position(lineNumber, safeColumn);
selections.push(new vscode.Selection(position, position));
}
// Set all selections at once
editor.selections = selections;
console.log(`${selections.length} column cursors created at column ${column}`);
}The validation with Math.min(column, line.text.length)
prevents placing cursors beyond line boundaries. Setting
editor.selections replaces all existing selections with the
new ones.
Extensions can detect patterns in selections and offer specialized functionality based on them. We begin with common, easily identifiable patterns.
export function detectBasicSelectionPatterns(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selections = editor.selections;
// Pattern 1: Column cursors (all cursors at the same column)
if (selections.length > 1 && selections.every(sel => sel.isEmpty)) {
const firstColumn = selections[0].start.character;
const allAtSameColumn = selections.every(sel =>
sel.start.character === firstColumn
);
if (allAtSameColumn) {
vscode.window.showInformationMessage(
`Column cursors detected at position ${firstColumn}`
);
return;
}
}
// Pattern 2: All selections empty (cursors only)
const allEmpty = selections.every(sel => sel.isEmpty);
if (allEmpty && selections.length > 1) {
vscode.window.showInformationMessage(
`${selections.length} cursors without text selection`
);
return;
}
// Pattern 3: All selections have text
const allHaveText = selections.every(sel => !sel.isEmpty);
if (allHaveText && selections.length > 1) {
vscode.window.showInformationMessage(
`${selections.length} text selections detected`
);
return;
}
vscode.window.showInformationMessage('Mixed or no specific selection patterns');
}This basic pattern detection distinguishes between the most common scenarios and can serve as a foundation for specialized functions.
Building on the basic pattern detection, we can perform more complex analyses that yield semantic information about the selections.
export function analyzeAdvancedSelectionPatterns(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selections = editor.selections;
// Detect consecutive lines
if (selections.length > 1) {
const lines = selections.map(sel => sel.start.line).sort((a, b) => a - b);
const consecutive = lines.every((line, index) =>
index === 0 || line === lines[index - 1] + 1
);
if (consecutive) {
vscode.window.showInformationMessage(
`Consecutive lines ${lines[0] + 1}-${lines[lines.length - 1] + 1}`
);
return;
}
}
// Detect identical text contents
const textSelections = selections.filter(sel => !sel.isEmpty);
if (textSelections.length > 1) {
const texts = textSelections.map(sel => editor.document.getText(sel));
const uniqueTexts = new Set(texts);
if (uniqueTexts.size === 1) {
vscode.window.showInformationMessage(
`${texts.length} identical text selections: "${texts[0]}"`
);
return;
}
}
vscode.window.showInformationMessage('No advanced selection patterns detected');
}This advanced analysis can serve as a foundation for intelligent features such as automated find-and-replace for identical texts or table formatting for consecutive lines.
Selection extensions must handle various edge cases that regularly occur in practice. These include empty documents, very large selections, performance issues, or timing conflicts.
export function handleSelectionEdgeCases(): void {
const editor = getEditorOrReturn();
if (!editor) return; // Basic error case already covered by helper function
// Edge case 1: empty document
if (editor.document.lineCount === 0) {
vscode.window.showWarningMessage('Document is empty – no selections possible');
return;
}
const selections = editor.selections;
// Edge case 2: performance warning with many selections
if (selections.length > 1000) {
vscode.window.showWarningMessage(
`${selections.length} selections detected. Processing may take a few seconds.`
);
}
// Edge case 3: very large text amounts
const textSelections = selections.filter(sel => !sel.isEmpty);
if (textSelections.length > 0) {
const totalLength = textSelections
.map(sel => editor.document.getText(sel).length)
.reduce((sum, length) => sum + length, 0);
if (totalLength > 100000) {
vscode.window.showWarningMessage(
`Very large text selection (${totalLength} characters). Performance may be affected.`
);
}
}
// Edge case 4: position validation
const validSelections = selections.filter(sel => {
const startValid = sel.start.line < editor.document.lineCount;
const endValid = sel.end.line < editor.document.lineCount;
return startValid && endValid;
});
if (validSelections.length !== selections.length) {
console.log(`${selections.length - validSelections.length} invalid selections ignored`);
}
}A critical aspect is the timing after edit operations. Old selection objects may be invalid after document changes. If you want to programmatically set selections after an edit operation, you should add a short delay or compute the new positions based on the changes.
Task 1: Duplicate lines Implement a function
duplicateLines() that copies each selection below itself
for text selections, and duplicates the entire line for empty cursors.
Test with a single selection, then with multi-selections. Ensure that
reverse processing works correctly.
Task 2: Intelligent word selection Write a function
selectWordOrExpand() that selects the current word on first
invocation and expands the selection by one word with each further call.
Consider the original selection direction using anchor and
active.
Task 3: Column insertion Develop a function
insertTextAtColumn(text, column) that inserts text at the
given column position in all lines of the visible range. Use
programmatic creation of multi-selections. Ignore short lines that do
not reach the specified column.
Task 4: Pattern-based processing Create a function
processSelectionsByPattern() that detects the selection
pattern and reacts accordingly: for column cursors, a tabular input
should be triggered; for identical text selections, a find-and-replace
function should be offered.
| Property | Type | Description |
|---|---|---|
selection.isEmpty |
boolean | True if only cursor, no text selected |
selection.start |
Position | Lexicographically first position (always before end) |
selection.end |
Position | Lexicographically last position (always after start) |
selection.anchor |
Position | User’s starting point of selection |
selection.active |
Position | Current cursor position (end of selection) |
position.line |
number | Line number (0-based) |
position.character |
number | Column number (0-based) |
| Method | Returns | Usage |
|---|---|---|
document.getText(range) |
string | Extract text in the specified range |
document.getWordRangeAtPosition(pos) |
Range | undefined | Determine word boundaries at a position |
editor.edit(callback) |
Promise<boolean> | Perform transactional text modification |
new Selection(start, end) |
Selection | Create new selection |
position.isAfter(other) |
boolean | Position comparison for direction detection |
| Pattern | Purpose | Code Example |
|---|---|---|
| Defensive check | Ensure editor availability | const editor = getEditorOrReturn(); if (!editor) return; |
| Fallback hierarchy | Text extraction without selection | Selection → Word → Line |
| Reverse processing | Handle multi-selections safely | [...selections].reverse().forEach(...) |
| Atomic edit | Coordinated changes | Collect all operations in one edit() block |
| Position validation | Create safe positions | Math.min(column, line.text.length) |
export function selectEnclosingBrackets(): void {
const editor = getEditorOrReturn();
if (!editor) return;
const position = editor.selection.active;
const document = editor.document;
const text = document.getText();
const offset = document.offsetAt(position);
const brackets = [['(', ')'], ['[', ']'], ['{', '}']];
let bestMatch: { start: number, end: number } | null = null;
for (const [open, close] of brackets) {
let depth = 0;
let startPos = -1;
// Search backwards for opening bracket
for (let i = offset; i >= 0; i--) {
if (text[i] === close) depth++;
if (text[i] === open) {
if (depth === 0) {
startPos = i;
break;
}
depth--;
}
}
if (startPos === -1) continue;
// Search forward for closing bracket
depth = 0;
for (let i = startPos + 1; i < text.length; i++) {
if (text[i] === open) depth++;
if (text[i] === close) {
if (depth === 0) {
const match = { start: startPos + 1, end: i };
if (!bestMatch || (startPos > bestMatch.start)) {
bestMatch = match;
}
break;
}
depth--;
}
}
}
if (bestMatch) {
const startPos = document.positionAt(bestMatch.start);
const endPos = document.positionAt(bestMatch.end);
editor.selection = new vscode.Selection(startPos, endPos);
}
}export function efficientBulkEdit(replacementText: string): void {
const editor = getEditorOrReturn();
if (!editor) return;
const selections = editor.selections;
// Performance warning for large operations
if (selections.length > 500) {
vscode.window.showInformationMessage(
`Processing ${selections.length} selections – please wait...`
);
}
// Collect all changes in one atomic edit
editor.edit(editBuilder => {
// Sort in reverse order for optimal performance
const sortedSelections = [...selections].sort((a, b) =>
b.start.line - a.start.line || b.start.character - a.start.character
);
sortedSelections.forEach(selection => {
if (!selection.isEmpty) {
editBuilder.replace(selection, replacementText);
}
});
});
}