Control structures govern the program flow in extensions and are essential for implementing commands, event handlers, and API interactions. TypeScript extends JavaScript control structures with type safety and offers specific advantages over Java for extension developers: automatic Promise handling with async/await, optional chaining for safe object access, and union types for precise switch statements.
The VSCode API works heavily with asynchronous operations, conditional configuration access, and error handling. This chapter demonstrates the practical use of the most important control structures in extension contexts.
Extensions often need to react to environmental conditions: Is an editor active? Does a file exist? Does the user have specific rights? TypeScript’s if/else syntax is identical to Java but offers enhanced capabilities through optional chaining and type inference.
// Typical extension checks using modern TypeScript features
function saveCurrentFile(): void {
const editor = vscode.window.activeTextEditor;
// Optional chaining avoids null-pointer-like errors
if (editor?.document.isDirty) {
editor.document.save();
vscode.window.showInformationMessage('File saved');
} else if (!editor) {
vscode.window.showWarningMessage('No active editor');
} else {
vscode.window.showInformationMessage('File already saved');
}
}The ?. operator checks for null,
undefined, and property existence simultaneously. Compared
to Java’s verbose null-checking, this significantly reduces code
redundancy.
Conditional command availability illustrates a typical extension use case:
function registerConditionalCommands(context: vscode.ExtensionContext): void {
// Register command only if a workspace is open
if (vscode.workspace.workspaceFolders) {
const workspaceCommand = vscode.commands.registerCommand('demo.workspaceInfo', () => {
const folderCount = vscode.workspace.workspaceFolders!.length;
vscode.window.showInformationMessage(`Workspace has ${folderCount} folders`);
});
context.subscriptions.push(workspaceCommand);
}
}The exclamation mark (!) after the array access is
TypeScript’s non-null assertion – safely usable here since the
if-condition guarantees existence.
Switch statements are ideal for command routing and configuration evaluation. TypeScript’s union types make switch statements type-safe and allow the compiler to check for completeness.
// Union type for defined command categories
type CommandCategory = 'file' | 'edit' | 'debug' | 'view';
function executeCommandByCategory(category: CommandCategory, action: string): void {
switch (category) {
case 'file':
if (action === 'save') {
vscode.commands.executeCommand('workbench.action.files.save');
} else if (action === 'open') {
vscode.commands.executeCommand('workbench.action.files.openFile');
}
break;
case 'edit':
vscode.commands.executeCommand(`editor.action.${action}`);
break;
case 'debug':
vscode.commands.executeCommand(`workbench.action.debug.${action}`);
break;
case 'view':
vscode.commands.executeCommand(`workbench.action.${action}`);
break;
// TypeScript detects missing cases
default:
const exhaustiveCheck: never = category;
throw new Error(`Unknown category: ${exhaustiveCheck}`);
}
}The never type in the default case is a TypeScript
specialty: if all union type values have been handled,
category is of type never. If a case is
missing, the compiler throws a type error.
Configuration evaluation shows another practical application:
function applyLogLevel(): void {
const config = vscode.workspace.getConfiguration('demo');
const logLevel = config.get<string>('logLevel', 'info');
switch (logLevel) {
case 'verbose':
console.log('Verbose logging enabled');
// Fall-through intended
case 'info':
console.log('Info logging enabled');
break;
case 'warn':
console.warn('Only warnings and errors logged');
break;
case 'error':
console.error('Only errors logged');
break;
default:
console.log('Unknown log level, using info');
}
}VSCode extensions primarily work asynchronously. TypeScript’s async/await simplifies Promise handling compared to Java’s CompletableFuture syntax and enables sequential readability for asynchronous operations.
// Sequential file operations with error handling
async function processMultipleFiles(): Promise<void> {
const files = await vscode.workspace.findFiles('**/*.ts', '**/node_modules/**');
for (const file of files) {
try {
const document = await vscode.workspace.openTextDocument(file);
const content = document.getText();
// Asynchronous processing with await
if (content.includes('TODO')) {
await vscode.window.showInformationMessage(`TODO found in ${file.fsPath}`);
}
} catch (error) {
console.error(`Failed to process ${file.fsPath}:`, error);
}
}
}The for...of loop with await is particularly important:
Unlike forEach or map, it executes operations
sequentially. Parallel processing would require
Promise.all().
Asynchronous user interaction illustrates practical use:
async function promptUserAction(): Promise<void> {
const selection = await vscode.window.showQuickPick(
['Save All', 'Close All', 'Cancel'],
{ placeHolder: 'Choose action' }
);
// Conditional execution based on user selection
if (selection === 'Save All') {
await vscode.workspace.saveAll();
vscode.window.showInformationMessage('All files saved');
} else if (selection === 'Close All') {
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
}
// On Cancel or Escape, selection is undefined – no action needed
}Extension code must robustly handle API errors, file system issues, and user input. TypeScript’s try/catch offers typed error handling and enhanced capabilities through finally blocks.
async function safeFileOperation(fileName: string): Promise<boolean> {
let operationSuccess = false;
try {
// Multiple potentially failing operations
const uri = vscode.Uri.file(fileName);
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type === vscode.FileType.File) {
const content = await vscode.workspace.fs.readFile(uri);
console.log(`File size: ${content.length} bytes`);
operationSuccess = true;
}
} catch (error) {
// TypeScript-specific error handling
if (error instanceof vscode.FileSystemError) {
vscode.window.showErrorMessage(`File system error: ${error.message}`);
} else if (error instanceof Error) {
vscode.window.showErrorMessage(`General error: ${error.message}`);
} else {
vscode.window.showErrorMessage('Unknown error occurred');
}
} finally {
// Cleanup or logging, always executed
console.log(`File operation completed: ${operationSuccess}`);
}
return operationSuccess;
}The instanceof checks are TypeScript type guards – they
narrow the type of error within the respective if
blocks.
Extensions frequently process file lists, editor contents, or configuration arrays. TypeScript offers modern iteration methods that differ significantly from Java’s iterator pattern.
function analyzeWorkspaceFiles(): void {
const editors = vscode.window.visibleTextEditors;
// for...of for arrays with objects
for (const editor of editors) {
const { document } = editor;
console.log(`Analyzing: ${document.fileName}`);
// for...of for string iteration (line by line)
const lines = document.getText().split('\n');
for (const [index, line] of lines.entries()) {
if (line.trim().startsWith('//')) {
console.log(`Comment at line ${index + 1}: ${line.trim()}`);
}
}
}
// Classic for loop for index-based access
for (let i = 0; i < editors.length; i++) {
const position = new vscode.Position(0, 0);
editors[i].selection = new vscode.Selection(position, position);
}
}The entries() method is particularly useful: it provides
both index and value; destructuring makes its use elegant.
Conditional iteration with while appears in event-driven operations:
async function waitForDocumentReady(document: vscode.TextDocument): Promise<void> {
let attempts = 0;
const maxAttempts = 10;
while (document.isDirty && attempts < maxAttempts) {
console.log(`Waiting for document to be saved... Attempt ${attempts + 1}`);
await new Promise(resolve => setTimeout(resolve, 100)); // wait 100ms
attempts++;
}
if (attempts >= maxAttempts) {
throw new Error('Document not ready after maximum attempts');
}
}The control structures presented work together in typical extension scenarios. A command handler demonstrates the practical combination:
async function handleComplexCommand(): Promise<void> {
try {
// Conditional pre-checks
if (!vscode.window.activeTextEditor) {
vscode.window.showWarningMessage('No active editor');
return;
}
const editor = vscode.window.activeTextEditor;
const config = vscode.workspace.getConfiguration('demo');
const mode = config.get<string>('processingMode', 'safe');
// Switch for processing mode
switch (mode) {
case 'fast':
await processFast(editor);
break;
case 'thorough':
await processThorough(editor);
break;
default:
await processSafe(editor);
}
vscode.window.showInformationMessage('Processing completed');
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
vscode.window.showErrorMessage(`Processing failed: ${message}`);
}
}
async function processSafe(editor: vscode.TextEditor): Promise<void> {
const document = editor.document;
// Iteration through all lines with error handling
for (let i = 0; i < document.lineCount; i++) {
try {
const line = document.lineAt(i);
if (line.text.trim()) { // Only non-empty lines
console.log(`Processing line ${i + 1}: ${line.text.substring(0, 50)}...`);
}
} catch (lineError) {
console.warn(`Skipped line ${i + 1} due to error:`, lineError);
continue; // Continue with next line
}
}
}This structure shows the interplay of all covered control structures in a realistic extension scenario: conditional checks, switch-based mode selection, try/catch error handling, and safe iteration.