FileSystem Providers enable the integration of virtual file systems into VSCode, allowing extensions to seamlessly integrate files from arbitrary sources such as remote servers, databases, or encrypted archives into the IDE. This functionality conceptually corresponds to Eclipse File System Contributions, but extends the possibilities with modern asynchronous operations and deeper integration into the VSCode ecosystem.
The FileSystem Provider interface forms an abstraction layer between VSCode and various data sources. Unlike traditional file systems that are based on local paths, VSCode works with URI schemas that identify different providers. A provider registers for a specific schema and takes responsibility for all operations on URIs of that schema.
The architecture follows an event-driven approach where the provider informs VSCode about changes and VSCode in turn delegates operations to the provider. This separation enables transparent integration of complex data sources without the editor itself needing to understand the specific protocols or access methods.
export interface FileSystemProvider {
// Basic metadata of a file or folder
stat(uri: vscode.Uri): vscode.FileStat | Thenable<vscode.FileStat>;
// List directory contents
readDirectory(uri: vscode.Uri): [string, vscode.FileType][] | Thenable<[string, vscode.FileType][]>;
// Read file content
readFile(uri: vscode.Uri): Uint8Array | Thenable<Uint8Array>;
// Create file or folder
createDirectory(uri: vscode.Uri): void | Thenable<void>;
// Write file
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean }): void | Thenable<void>;
// Monitor file system changes
onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]>;
}The FileStat type describes the metadata of a file or
folder and includes information such as size, modification date, and
file type. This structure corresponds to stat information in Unix
systems but has been adapted for VSCode’s needs.
The following example shows a simple implementation of a FileSystem Provider for an in-memory file system that demonstrates the basic operations:
import * as vscode from 'vscode';
export class MemoryFileSystemProvider implements vscode.FileSystemProvider {
private _onDidChangeFile: vscode.EventEmitter<vscode.FileChangeEvent[]> = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._onDidChangeFile.event;
// In-memory storage for files and folders
private data = new Map<string, { type: vscode.FileType; content?: Uint8Array; mtime: number; size: number }>();
constructor() {
// Initialize with a root folder
this.data.set('/', {
type: vscode.FileType.Directory,
mtime: Date.now(),
size: 0
});
}
// Retrieve metadata of a file or folder
stat(uri: vscode.Uri): vscode.FileStat {
const entry = this.data.get(uri.path);
if (!entry) {
throw vscode.FileSystemError.FileNotFound(uri);
}
return {
type: entry.type,
ctime: entry.mtime, // Creation time
mtime: entry.mtime, // Modification time
size: entry.size
};
}
// List directory contents
readDirectory(uri: vscode.Uri): [string, vscode.FileType][] {
const entry = this.data.get(uri.path);
if (!entry) {
throw vscode.FileSystemError.FileNotFound(uri);
}
if (entry.type !== vscode.FileType.Directory) {
throw vscode.FileSystemError.FileNotADirectory(uri);
}
// Find all entries that are direct children of the specified path
const result: [string, vscode.FileType][] = [];
const searchPath = uri.path === '/' ? '/' : uri.path + '/';
for (const [path, entry] of this.data) {
if (path.startsWith(searchPath) && path !== uri.path) {
const relativePath = path.substring(searchPath.length);
// Only direct children, no subdirectories
if (!relativePath.includes('/')) {
result.push([relativePath, entry.type]);
}
}
}
return result;
}
// Read file content
readFile(uri: vscode.Uri): Uint8Array {
const entry = this.data.get(uri.path);
if (!entry) {
throw vscode.FileSystemError.FileNotFound(uri);
}
if (entry.type !== vscode.FileType.File) {
throw vscode.FileSystemError.FileIsADirectory(uri);
}
return entry.content || new Uint8Array(0);
}
// Write file
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean }): void {
const exists = this.data.has(uri.path);
if (exists && !options.overwrite) {
throw vscode.FileSystemError.FileExists(uri);
}
if (!exists && !options.create) {
throw vscode.FileSystemError.FileNotFound(uri);
}
// Ensure the parent directory exists
const parentPath = this.getParentPath(uri.path);
if (parentPath && !this.data.has(parentPath)) {
throw vscode.FileSystemError.FileNotFound(vscode.Uri.parse(`memory:${parentPath}`));
}
// Create or update file
const now = Date.now();
this.data.set(uri.path, {
type: vscode.FileType.File,
content: content,
mtime: now,
size: content.length
});
// Trigger change event
this._onDidChangeFile.fire([{
type: exists ? vscode.FileChangeType.Changed : vscode.FileChangeType.Created,
uri: uri
}]);
}
// Create directory
createDirectory(uri: vscode.Uri): void {
if (this.data.has(uri.path)) {
throw vscode.FileSystemError.FileExists(uri);
}
const now = Date.now();
this.data.set(uri.path, {
type: vscode.FileType.Directory,
mtime: now,
size: 0
});
this._onDidChangeFile.fire([{
type: vscode.FileChangeType.Created,
uri: uri
}]);
}
// Helper method to determine the parent path
private getParentPath(path: string): string | null {
if (path === '/') {
return null;
}
const lastSlash = path.lastIndexOf('/');
return lastSlash === 0 ? '/' : path.substring(0, lastSlash);
}
}This implementation shows the essential concepts of a FileSystem
Provider. The data structure (Map) simulates a simple file
system, while the methods implement standard file system operations.
Particularly important is the correct error handling with the specific
FileSystemError types that VSCode expects for consistent
user guidance.
Registration of a FileSystem Provider occurs via a specific URI schema that uniquely identifies the provider:
export function activate(context: vscode.ExtensionContext) {
// Create provider instance
const memoryProvider = new MemoryFileSystemProvider();
// Register provider for the "memory" schema
context.subscriptions.push(
vscode.workspace.registerFileSystemProvider('memory', memoryProvider, {
isCaseSensitive: true,
isReadonly: false
})
);
// Example command to create a file in the memory file system
context.subscriptions.push(
vscode.commands.registerCommand('memoryfs.createFile', async () => {
const uri = vscode.Uri.parse('memory:/example.txt');
const encoder = new TextEncoder();
const content = encoder.encode('Hello from Memory FileSystem!');
await vscode.workspace.fs.writeFile(uri, content);
// Open file in editor
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
})
);
}The registration options define the provider’s behavior.
isCaseSensitive determines whether file names distinguish
between upper and lower case, while isReadonly indicates
whether write operations are supported.
Production FileSystem Providers often need to handle remote data sources or large amounts of data. The following example shows an extended implementation with caching and asynchronous operations:
export class RemoteFileSystemProvider implements vscode.FileSystemProvider {
private _onDidChangeFile: vscode.EventEmitter<vscode.FileChangeEvent[]> = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._onDidChangeFile.event;
// Cache for metadata and directory contents
private statCache = new Map<string, { stat: vscode.FileStat; expires: number }>();
private directoryCache = new Map<string, { entries: [string, vscode.FileType][]; expires: number }>();
private readonly cacheTimeout = 30000; // 30 seconds cache lifetime
// Asynchronous stat operation with caching
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
const cached = this.statCache.get(uri.path);
if (cached && Date.now() < cached.expires) {
return cached.stat;
}
try {
// Simulation of a remote API query
const stat = await this.fetchRemoteStat(uri.path);
// Cache result
this.statCache.set(uri.path, {
stat,
expires: Date.now() + this.cacheTimeout
});
return stat;
} catch (error) {
throw vscode.FileSystemError.FileNotFound(uri);
}
}
// Asynchronous directory listing with caching
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
const cached = this.directoryCache.get(uri.path);
if (cached && Date.now() < cached.expires) {
return cached.entries;
}
try {
const entries = await this.fetchRemoteDirectory(uri.path);
// Cache result
this.directoryCache.set(uri.path, {
entries,
expires: Date.now() + this.cacheTimeout
});
return entries;
} catch (error) {
throw vscode.FileSystemError.FileNotFound(uri);
}
}
// Asynchronous file read operations with streaming for large files
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
try {
// For large files, streaming could be implemented here
return await this.fetchRemoteFile(uri.path);
} catch (error) {
throw vscode.FileSystemError.FileNotFound(uri);
}
}
// Batch write operations for better performance
async writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean }): Promise<void> {
try {
await this.uploadRemoteFile(uri.path, content, options);
// Invalidate cache
this.invalidateCache(uri.path);
// Trigger change event
this._onDidChangeFile.fire([{
type: vscode.FileChangeType.Changed,
uri: uri
}]);
} catch (error) {
throw vscode.FileSystemError.NoPermissions(uri);
}
}
// Cache invalidation on changes
private invalidateCache(path: string): void {
this.statCache.delete(path);
// Also invalidate parent directories
const parentPath = this.getParentPath(path);
if (parentPath) {
this.directoryCache.delete(parentPath);
}
}
// Simulation of remote API calls
private async fetchRemoteStat(path: string): Promise<vscode.FileStat> {
// Simulation of an HTTP request
return new Promise((resolve) => {
setTimeout(() => {
resolve({
type: vscode.FileType.File,
ctime: Date.now(),
mtime: Date.now(),
size: 1024
});
}, 100);
});
}
private async fetchRemoteDirectory(path: string): Promise<[string, vscode.FileType][]> {
// Simulation of an API request for directory contents
return new Promise((resolve) => {
setTimeout(() => {
resolve([
['file1.txt', vscode.FileType.File],
['subdirectory', vscode.FileType.Directory]
]);
}, 150);
});
}
private async fetchRemoteFile(path: string): Promise<Uint8Array> {
// Simulation of file download
return new Promise((resolve) => {
setTimeout(() => {
const encoder = new TextEncoder();
resolve(encoder.encode(`Content of ${path}`));
}, 200);
});
}
private async uploadRemoteFile(path: string, content: Uint8Array, options: any): Promise<void> {
// Simulation of file upload
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 300);
});
}
private getParentPath(path: string): string | null {
if (path === '/') {
return null;
}
const lastSlash = path.lastIndexOf('/');
return lastSlash === 0 ? '/' : path.substring(0, lastSlash);
}
}This extended implementation shows important aspects for production-ready FileSystem Providers. Caching significantly reduces the number of remote calls, while asynchronous operations ensure a responsive user experience. Cache invalidation ensures that changes are correctly propagated.
FileSystem Providers must inform VSCode about changes in the file
system to ensure consistent display. This is done through the
onDidChangeFile event:
export class WatchableFileSystemProvider implements vscode.FileSystemProvider {
private _onDidChangeFile: vscode.EventEmitter<vscode.FileChangeEvent[]> = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._onDidChangeFile.event;
// Monitor external changes (e.g., through polling or WebSocket)
private startFileWatcher(): void {
// Simulation of external monitoring
setInterval(() => {
this.checkForExternalChanges();
}, 5000);
}
private checkForExternalChanges(): void {
// Simulation of detecting external changes
const changes: vscode.FileChangeEvent[] = [
{
type: vscode.FileChangeType.Changed,
uri: vscode.Uri.parse('memory:/watched-file.txt')
}
];
if (changes.length > 0) {
this._onDidChangeFile.fire(changes);
}
}
// Batch events for better performance
private pendingChanges: vscode.FileChangeEvent[] = [];
private fireTimeout: NodeJS.Timeout | undefined;
private scheduleChangeEvent(change: vscode.FileChangeEvent): void {
this.pendingChanges.push(change);
// Debouncing: collect events and send in batches
if (this.fireTimeout) {
clearTimeout(this.fireTimeout);
}
this.fireTimeout = setTimeout(() => {
this._onDidChangeFile.fire([...this.pendingChanges]);
this.pendingChanges = [];
this.fireTimeout = undefined;
}, 100);
}
}Event handling is particularly important for synchronization between different instances of VSCode or with external changes to the file system. Batching events improves performance when many changes occur simultaneously.
FileSystem Providers open up diverse possibilities for extensions that go beyond local files. Typical application scenarios include integration of cloud storage services, database connections as virtual file systems, encrypted archives, or remote development environments. Seamless integration into the VSCode workflow allows developers to work with these data sources as if they were local files.
The comparison to Eclipse shows that VSCode FileSystem Providers are designed to be more flexible and asynchronous. While Eclipse File System Contributions often work synchronously, the VSCode model enables better handling of network latencies and large amounts of data. The URI-based architecture also provides clearer separation between different data sources.
Implementation should always consider performance aspects, particularly caching strategies and asynchronous operations. Thoughtful error handling and consistent event handling are crucial for a professional user experience.