32 Cursor Position and Selection in VSCode Extensions

32.1 Helper function for defensive programming

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.

32.2 Basics: Understanding the three selection states

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.

32.3 Accessing selection data

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.

32.4 Understanding position properties

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.

32.5 Extracting text with intelligent fallback

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.

32.6 Modifying text for single selections

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.

32.7 Modifying text for multi-selections

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.

32.8 Programmatically creating selections

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.

32.9 Programmatically creating multi-selections

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.

32.10 Detecting simple selection patterns

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.

32.11 Analyzing advanced selection patterns

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.

32.12 Error handling and edge cases

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.

32.13 Practical application tasks

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.

32.14 API reference

32.14.1 Selection and Position properties

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)

32.14.2 Important methods

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

32.14.3 Common programming patterns

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)

32.15 Complete example implementations

32.15.1 Bracket selection with nesting

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);
    }
}

32.15.2 Performance-optimized batch processing

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);
            }
        });
    });
}