vscode.workspace.fsVSCode 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.
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.
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.
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.
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.
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.
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.
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.
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.
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}`);
}
}
}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`);
}
}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}`);
}
}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.