34 File Operations with vscode.workspace.fs

34.1 Introduction and Motivation

VSCode extensions frequently require access to the file system to create configuration files, set up project templates, write logs, or analyze existing files. Unlike in classic Java applications where you can work directly with java.io.File or java.nio.file.Path, VSCode operates under a sandbox principle that prevents direct file system access.

The vscode.workspace.fs API provides a controlled and platform-independent interface for file operations. This abstraction brings crucial advantages: extensions work uniformly on Windows, macOS, and Linux, and they automatically support remote workspaces where files are located on remote servers or in containers.

While you’re accustomed to accessing the file system directly in Java with File.createNewFile() or Files.write(), in VSCode extensions you must handle all operations through the provided API. This may initially seem restrictive, but ensures consistency and security across different deployment scenarios.

34.2 Uri Handling in Workspace

VSCode works exclusively with Uri objects to represent file paths. A Uri (Uniform Resource Identifier) is conceptually comparable to a java.net.URI, but optimized specifically for VSCode contexts. While in Java you might switch between String paths and Path objects, VSCode consistently expects Uri instances.

import * as vscode from 'vscode';

// Determine base Uri of an open workspace
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
    vscode.window.showErrorMessage('No workspace opened');
    return;
}

// Create Uri for a new file in the workspace root directory
const fileUri = vscode.Uri.joinPath(workspaceFolder.uri, 'example.txt');

// Uri for a file in a subdirectory
const configUri = vscode.Uri.joinPath(workspaceFolder.uri, 'config', 'settings.json');

The first important difference from Java lies in error handling: workspaceFolders can be undefined when no workspace is open. This corresponds to a state you don’t know in Eclipse – where a workspace is always present. In VSCode, however, a user can open individual files without workspace context.

The Uri.joinPath() method functionally corresponds to Paths.get().resolve() in Java, but works exclusively with forward slashes and automatically normalizes paths. You don’t need to worry about operating system-specific path separators.

34.3 File Operations: Basic CRUD Operations

34.3.1 Writing Files

Writing files requires converting string content to binary data. VSCode uses the web standard TextEncoder for this conversion:

async function writeConfigurationFile(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        throw new Error('Workspace required');
    }
    
    const configUri = vscode.Uri.joinPath(workspaceFolder.uri, '.vscode', 'launch.json');
    
    const configuration = {
        version: "0.2.0",
        configurations: [
            {
                name: "Debug TypeScript",
                type: "node",
                request: "launch",
                program: "${workspaceFolder}/dist/index.js"
            }
        ]
    };
    
    // Convert JSON string to Uint8Array
    const content = new TextEncoder().encode(JSON.stringify(configuration, null, 2));
    
    // Write file - overwrites existing files
    await vscode.workspace.fs.writeFile(configUri, content);
    
    vscode.window.showInformationMessage('Configuration created');
}

Using TextEncoder may initially seem cumbersome, especially compared to Java’s Files.writeString(). The reason lies in VSCode’s web-based architecture: all file operations work with binary data to handle different encodings and file types uniformly.

34.3.2 Reading Files

Reading occurs as the reverse of the writing process:

async function readDocumentationFile(): Promise<string> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        throw new Error('Workspace required');
    }
    
    const readmeUri = vscode.Uri.joinPath(workspaceFolder.uri, 'README.md');
    
    try {
        // Read file as Uint8Array
        const data = await vscode.workspace.fs.readFile(readmeUri);
        
        // Convert binary data to string
        const text = new TextDecoder().decode(data);
        
        return text;
    } catch (error) {
        // FileSystemError is thrown when file doesn't exist
        throw new Error(`README.md not found: ${error}`);
    }
}

Error handling differs from Java: instead of FileNotFoundException you get a FileSystemError. However, the try-catch structure remains familiar.

34.3.3 Deleting Files

async function deleteTempFiles(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        return;
    }
    
    const tempUri = vscode.Uri.joinPath(workspaceFolder.uri, 'temp.log');
    
    try {
        await vscode.workspace.fs.delete(tempUri, { 
            recursive: false,  // Only files, no directories
            useTrash: true     // Move to trash instead of permanent deletion
        });
        
        vscode.window.showInformationMessage('Temporary files removed');
    } catch (error) {
        // Ignore errors if file doesn't exist
        console.log('Temp file was not present');
    }
}

The option useTrash: true corresponds to the behavior users expect: files end up in the trash and can be restored. This is an important UX aspect that you would have to implement manually in Java applications.

34.3.4 Moving and Copying Files

VSCode conceptually separates between moving (rename) and copying (copy):

async function reorganizeProjectStructure(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        return;
    }
    
    const oldPathUri = vscode.Uri.joinPath(workspaceFolder.uri, 'old-config.json');
    const newPathUri = vscode.Uri.joinPath(workspaceFolder.uri, 'config', 'app-config.json');
    const backupUri = vscode.Uri.joinPath(workspaceFolder.uri, 'backup', 'config-backup.json');
    
    try {
        // Create backup (copy)
        await vscode.workspace.fs.copy(oldPathUri, backupUri, { 
            overwrite: false 
        });
        
        // Move and rename file
        await vscode.workspace.fs.rename(oldPathUri, newPathUri, { 
            overwrite: true 
        });
        
        vscode.window.showInformationMessage('Project structure updated');
    } catch (error) {
        vscode.window.showErrorMessage(`Reorganization failed: ${error}`);
    }
}

The term “rename” is misleading here – the operation corresponds to Files.move() in Java and can perform both renaming and moving.

34.4 Directory Operations

34.4.1 Creating Directories

async function initializeProjectStructure(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        return;
    }
    
    const directories = ['src', 'dist', 'tests', 'docs'];
    
    for (const directoryName of directories) {
        const directoryUri = vscode.Uri.joinPath(workspaceFolder.uri, directoryName);
        
        try {
            await vscode.workspace.fs.createDirectory(directoryUri);
            console.log(`Directory created: ${directoryName}`);
        } catch (error) {
            // Directory already exists - that's fine
            console.log(`Directory already exists: ${directoryName}`);
        }
    }
}

Unlike Files.createDirectories() in Java, createDirectory() doesn’t automatically create parent directories. You must build the hierarchy from top to bottom.

34.4.2 Listing Directory Contents

async function analyzeDirectoryContent(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        return;
    }
    
    try {
        const entries = await vscode.workspace.fs.readDirectory(workspaceFolder.uri);
        
        for (const [name, type] of entries) {
            switch (type) {
                case vscode.FileType.File:
                    console.log(`File: ${name}`);
                    break;
                case vscode.FileType.Directory:
                    console.log(`Directory: ${name}`);
                    break;
                case vscode.FileType.SymbolicLink:
                    console.log(`Symbolic Link: ${name}`);
                    break;
                default:
                    console.log(`Unknown Type: ${name}`);
            }
        }
    } catch (error) {
        vscode.window.showErrorMessage(`Directory not readable: ${error}`);
    }
}

The return as an array of tuples [string, FileType] differs from Java’s DirectoryStream<Path>, but provides the same functionality with explicit typing.

34.5 Existence Checking and Metadata

Checking whether a file or directory exists is done through the stat() method:

async function checkAndCreateConfiguration(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        return;
    }
    
    const configUri = vscode.Uri.joinPath(workspaceFolder.uri, 'tsconfig.json');
    
    try {
        const stat = await vscode.workspace.fs.stat(configUri);
        
        // File exists - check details
        if (stat.type === vscode.FileType.File) {
            const sizeInKB = Math.round(stat.size / 1024);
            const modifiedOn = new Date(stat.mtime);
            
            console.log(`tsconfig.json found: ${sizeInKB}KB, modified: ${modifiedOn.toLocaleString()}`);
        } else {
            vscode.window.showWarningMessage('tsconfig.json is not a regular file');
        }
    } catch (error) {
        // File doesn't exist - create default configuration
        await createDefaultTsConfig(configUri);
    }
}

async function createDefaultTsConfig(configUri: vscode.Uri): Promise<void> {
    const defaultConfiguration = {
        compilerOptions: {
            target: "ES2020",
            module: "commonjs",
            outDir: "./dist",
            rootDir: "./src",
            strict: true,
            esModuleInterop: true
        },
        include: ["src/**/*"],
        exclude: ["node_modules", "dist"]
    };
    
    const content = new TextEncoder().encode(JSON.stringify(defaultConfiguration, null, 2));
    await vscode.workspace.fs.writeFile(configUri, content);
    
    vscode.window.showInformationMessage('Default tsconfig.json created');
}

This pattern corresponds to Files.exists() and Files.size() in Java, but with async/await syntax and combined in a single API call.

34.6 Error Handling and Edge Cases

VSCode file operations can have various error conditions that you should handle proactively:

async function robustFileOperation(): Promise<void> {
    // 1. Workspace validation
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        vscode.window.showWarningMessage(
            'This function requires an open workspace'
        );
        return;
    }
    
    const targetUri = vscode.Uri.joinPath(workspaceFolder.uri, 'generated', 'output.txt');
    
    try {
        // 2. Ensure directory exists
        const directoryUri = vscode.Uri.joinPath(workspaceFolder.uri, 'generated');
        await vscode.workspace.fs.createDirectory(directoryUri);
        
        // 3. Backup existing file
        try {
            await vscode.workspace.fs.stat(targetUri);
            // File exists - create backup with timestamp
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
            const backupUri = vscode.Uri.joinPath(
                directoryUri, 
                `output-backup-${timestamp}.txt`
            );
            await vscode.workspace.fs.copy(targetUri, backupUri, { overwrite: false });
        } catch {
            // File doesn't exist - that's OK
        }
        
        // 4. Write new file
        const content = `Generated on: ${new Date().toLocaleString()}\n`;
        await vscode.workspace.fs.writeFile(
            targetUri, 
            new TextEncoder().encode(content)
        );
        
        vscode.window.showInformationMessage('File successfully created');
        
    } catch (error) {
        // 5. Specific error handling
        if (error instanceof vscode.FileSystemError) {
            switch (error.code) {
                case 'FileNotFound':
                    vscode.window.showErrorMessage('Directory or file not found');
                    break;
                case 'NoPermissions':
                    vscode.window.showErrorMessage('No permission for file operation');
                    break;
                default:
                    vscode.window.showErrorMessage(`File error: ${error.message}`);
            }
        } else {
            vscode.window.showErrorMessage(`Unexpected error: ${error}`);
        }
    }
}

34.7 Best Practices and Performance Considerations

34.7.1 Optimizing Batch Operations

When you need to process multiple files, you should parallelize operations but limit the number of concurrent operations:

async function processMultipleProjectFiles(): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        return;
    }
    
    const fileNames = ['package.json', 'tsconfig.json', 'README.md', '.gitignore'];
    
    // Sequential processing for critical operations
    for (const fileName of fileNames) {
        const fileUri = vscode.Uri.joinPath(workspaceFolder.uri, fileName);
        await processFile(fileUri);
    }
    
    // Parallel processing for independent operations
    const promises = fileNames.map(fileName => {
        const fileUri = vscode.Uri.joinPath(workspaceFolder.uri, fileName);
        return analyzeFileMetadata(fileUri);
    });
    
    const results = await Promise.allSettled(promises);
    
    // Evaluate results
    for (let i = 0; i < results.length; i++) {
        const result = results[i];
        if (result.status === 'rejected') {
            console.log(`Error with ${fileNames[i]}: ${result.reason}`);
        }
    }
}

async function processFile(uri: vscode.Uri): Promise<void> {
    // Critical file operation that needs order
    console.log(`Processing: ${uri.fsPath}`);
}

async function analyzeFileMetadata(uri: vscode.Uri): Promise<void> {
    // Independent analysis, can be done in parallel
    try {
        const stat = await vscode.workspace.fs.stat(uri);
        console.log(`${uri.fsPath}: ${stat.size} bytes`);
    } catch {
        console.log(`${uri.fsPath}: not present`);
    }
}

34.7.2 Integration with Existing VSCode Workflows

Combine file operations with VSCode events for reactive extensions:

export function activate(context: vscode.ExtensionContext) {
    // File Watcher for automatic processing
    const watcher = vscode.workspace.createFileSystemWatcher('**/*.json');
    
    watcher.onDidCreate(async (uri) => {
        await handleNewJsonFile(uri);
    });
    
    watcher.onDidChange(async (uri) => {
        await validateJsonFile(uri);
    });
    
    context.subscriptions.push(watcher);
}

async function handleNewJsonFile(uri: vscode.Uri): Promise<void> {
    try {
        const content = await vscode.workspace.fs.readFile(uri);
        const text = new TextDecoder().decode(content);
        const json = JSON.parse(text);
        
        // Offer automatic formatting
        const formatted = JSON.stringify(json, null, 2);
        if (formatted !== text) {
            const response = await vscode.window.showInformationMessage(
                'Format JSON file?',
                'Yes', 'No'
            );
            
            if (response === 'Yes') {
                await vscode.workspace.fs.writeFile(
                    uri, 
                    new TextEncoder().encode(formatted)
                );
            }
        }
    } catch (error) {
        console.log(`JSON validation failed for ${uri.fsPath}: ${error}`);
    }
}

34.8 Application Example: Project Template Generator

The following complete example demonstrates the combination of various file operations in a practical scenario:

interface ProjectTemplate {
    name: string;
    description: string;
    files: { [path: string]: string };
    directories: string[];
}

const typescriptTemplate: ProjectTemplate = {
    name: "TypeScript Project",
    description: "Basic setup for TypeScript development",
    directories: ["src", "dist", "tests"],
    files: {
        "package.json": JSON.stringify({
            name: "new-project",
            version: "1.0.0",
            scripts: {
                build: "tsc",
                test: "jest"
            },
            devDependencies: {
                typescript: "^4.0.0",
                "@types/node": "^16.0.0"
            }
        }, null, 2),
        "tsconfig.json": JSON.stringify({
            compilerOptions: {
                target: "ES2020",
                module: "commonjs",
                outDir: "./dist",
                rootDir: "./src",
                strict: true
            }
        }, null, 2),
        "src/index.ts": "console.log('Hello, TypeScript!');\n",
        "README.md": "# New TypeScript Project\n\nGenerated with VSCode Extension\n"
    }
};

async function createProjectTemplate(template: ProjectTemplate): Promise<void> {
    const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
    if (!workspaceFolder) {
        vscode.window.showErrorMessage('Workspace required for project template');
        return;
    }
    
    // Get project name from user
    const projectName = await vscode.window.showInputBox({
        prompt: 'Enter project name',
        placeHolder: 'my-project',
        validateInput: (value) => {
            if (!value || value.trim().length === 0) {
                return 'Project name required';
            }
            if (!/^[a-z0-9-]+$/.test(value)) {
                return 'Only lowercase letters, numbers and hyphens allowed';
            }
            return null;
        }
    });
    
    if (!projectName) {
        return; // User cancelled
    }
    
    try {
        // 1. Create project directory
        const projectUri = vscode.Uri.joinPath(workspaceFolder.uri, projectName);
        await vscode.workspace.fs.createDirectory(projectUri);
        
        // 2. Create subdirectories
        for (const directory of template.directories) {
            const directoryUri = vscode.Uri.joinPath(projectUri, directory);
            await vscode.workspace.fs.createDirectory(directoryUri);
        }
        
        // 3. Create files with personalized content
        for (const [relativePath, content] of Object.entries(template.files)) {
            const fileUri = vscode.Uri.joinPath(projectUri, relativePath);
            
            // Replace placeholders in content
            const personalizedContent = content
                .replace(/new-project/g, projectName)
                .replace(/New TypeScript Project/g, `${projectName} - TypeScript Project`);
            
            const bytes = new TextEncoder().encode(personalizedContent);
            await vscode.workspace.fs.writeFile(fileUri, bytes);
        }
        
        // 4. Success message and offer workspace switch
        const response = await vscode.window.showInformationMessage(
            `Project "${projectName}" created`,
            'Open Folder', 'OK'
        );
        
        if (response === 'Open Folder') {
            await vscode.commands.executeCommand('vscode.openFolder', projectUri);
        }
        
    } catch (error) {
        vscode.window.showErrorMessage(`Project creation failed: ${error}`);
    }
}

// Command registration in the activate function
export function activate(context: vscode.ExtensionContext) {
    const command = vscode.commands.registerCommand(
        'extension.createTypescriptProject', 
        () => createProjectTemplate(typescriptTemplate)
    );
    
    context.subscriptions.push(command);
}

This example shows the integration of user interaction, validation, structured file layout, and error handling in a realistic use case. It also demonstrates how you can transfer familiar concepts from Java development (Template pattern, validation, batch operations) to the VSCode extension world.