FileSystemProvider 文件系统
VS Code 的文件 API 建立在 URI 之上:file:// 是磁盘文件系统,而任何自定义 scheme 都可以通过 FileSystemProvider 接入同一套 API。内存文件、远程文件、压缩包内容、数据库记录——都能以文件树的形式呈现在资源管理器中。
文件系统插件模型
workspace.registerFileSystemProvider(scheme, provider) 把一个自定义文件系统挂载到指定 scheme 上:
import * as vscode from 'vscode';
// 注册后,任何 memfs: 开头的 URI 都走 provider 处理
const provider: vscode.FileSystemProvider = { /* 实现全部接口 */ };
const disposable = vscode.workspace.registerFileSystemProvider('memfs', provider);| 参数 | 说明 |
|---|---|
scheme | URI scheme,如 memfs、ftp、zip |
provider | 实现 FileSystemProvider 接口的对象 |
options | 可选,{ isCaseSensitive, isReadonly, isVirtual } 等 |
注册后立即生效:资源管理器地址栏输入 memfs:/ 即可访问,vscode.workspace.fs 的所有方法也能操作该 scheme 的 URI。
FileSystemProvider 接口
接口要求实现读写、目录、变更通知三组能力。先看完整骨架:
import * as vscode from 'vscode';
export class MemFileSystemProvider implements vscode.FileSystemProvider {
// 文件树:path → 节点
private root = new Map<string, Node>();
// 变更事件发射器:任何修改后都要触发
private _emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> =
this._emitter.event;
// 读取文件元信息
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
const node = this.root.get(uri.path);
if (!node) {
throw vscode.FileSystemError.FileNotFound(uri);
}
return node.stat;
}
// 读取目录列表
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
const node = this.root.get(uri.path);
if (!node || node.stat.type !== vscode.FileType.Directory) {
throw vscode.FileSystemError.FileNotFound(uri);
}
return [...node.children.entries()].map(([name, child]) => [
name,
child.stat.type
]);
}
// 读取文件内容
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const node = this.root.get(uri.path);
if (!node || node.stat.type !== vscode.FileType.File) {
throw vscode.FileSystemError.FileNotFound(uri);
}
return node.data;
}
// 写入文件内容
async writeFile(
uri: vscode.Uri,
content: Uint8Array,
options: { create: boolean; overwrite: boolean }
): Promise<void> {
const node = this.root.get(uri.path);
if (!node) {
if (!options.create) {
throw vscode.FileSystemError.FileNotFound(uri);
}
this.createNode(uri, content);
} else {
if (!options.overwrite) {
throw vscode.FileSystemError.FileExists(uri);
}
node.data = content;
this._emitter.fire([{ type: vscode.FileChangeType.Changed, uri }]);
}
}
// 创建目录
async createDirectory(uri: vscode.Uri): Promise<void> {
this.createNode(uri, new Uint8Array(0), true);
}
// 删除文件/目录
async delete(uri: vscode.Uri, options: { recursive: boolean }): Promise<void> {
const parent = this.getParent(uri);
if (!parent) return;
parent.children.delete(this.baseName(uri));
this._emitter.fire([{ type: vscode.FileChangeType.Deleted, uri }]);
}
// 重命名/移动
async rename(
oldUri: vscode.Uri,
newUri: vscode.Uri,
options: { overwrite: boolean }
): Promise<void> {
const node = this.root.get(oldUri.path);
if (!node) throw vscode.FileSystemError.FileNotFound(oldUri);
const parent = this.getParent(oldUri);
parent?.children.delete(this.baseName(oldUri));
this.root.set(newUri.path, node);
this._emitter.fire([
{ type: vscode.FileChangeType.Deleted, uri: oldUri },
{ type: vscode.FileChangeType.Created, uri: newUri }
]);
}
// 文件监听:返回 Disposable 即可,事件已在 onDidChangeFile 上报
watch(_uri: vscode.Uri): vscode.Disposable {
return new vscode.Disposable(() => {});
}
// 工具方法
private baseName(uri: vscode.Uri): string {
return uri.path.split('/').pop() ?? uri.path;
}
private getParent(uri: vscode.Uri): Node | undefined {
const idx = uri.path.lastIndexOf('/');
return this.root.get(uri.path.slice(0, idx) || '/');
}
private createNode(uri: vscode.Uri, data: Uint8Array, isDir = false) {
// 创建目录时确保父目录存在;插入到 children 并触发事件
const node: Node = {
stat: {
type: isDir ? vscode.FileType.Directory : vscode.FileType.File,
ctime: Date.now(),
mtime: Date.now(),
size: data.length
},
data,
children: new Map()
};
this.root.set(uri.path, node);
this._emitter.fire([{ type: vscode.FileChangeType.Created, uri }]);
}
}
interface Node {
stat: vscode.FileStat;
data: Uint8Array;
children: Map<string, Node>;
}接口方法一览
| 方法 | 职责 | 关键错误 |
|---|---|---|
stat(uri) | 返回文件元信息 FileStat | FileNotFound |
readDirectory(uri) | 列出目录下 [名称, 类型] 对 | FileNotFound |
readFile(uri) | 返回文件字节内容 | FileNotFound、IsADirectory |
writeFile(uri, content, options) | 写入;options.create 与 options.overwrite 控制创建/覆盖语义 | FileNotFound、FileExists、NoPermissions |
createDirectory(uri) | 创建目录 | FileExists |
delete(uri, options) | 删除;目录需 options.recursive | FileNotFound、DirectoryNotEmpty |
rename(oldUri, newUri, options) | 重命名或移动 | FileNotFound、FileExists |
watch(uri) | 返回文件监听 Disposable | — |
onDidChangeFile | 事件属性:变更通知 | — |
变更通知的必要性
onDidChangeFile 是接口中唯一的"事件"成员,但它是整个机制的灵魂:任何修改文件树的操作都必须触发它,否则资源管理器、编辑器标签不会刷新。变更事件携带 { type, uri } 数组:
this._emitter.fire([
{ type: vscode.FileChangeType.Created, uri },
{ type: vscode.FileChangeType.Changed, uri },
{ type: vscode.FileChangeType.Deleted, uri }
]);虚拟文件 URI scheme 设计
scheme 是文件系统的命名空间,设计时注意几点:
| 原则 | 示例 |
|---|---|
| 短且语义化 | memfs、vfs、zipfs |
| 路径风格统一 | memfs:/folder/file.txt(斜杠开头) |
| 不依赖平台分隔符 | 内部始终用 /,由 VS Code 处理展示 |
| authority 可选 | memfs://project/data.txt 用 authority 区分命名空间 |
import * as vscode from 'vscode';
// 用 authority 区分多实例:memfs://backend/data 与 memfs://frontend/data
function memUri(namespace: string, path: string): vscode.Uri {
return vscode.Uri.parse(`memfs://${namespace}${path}`);
}
const file = memUri('backend', '/src/main.ts');
console.log(file.scheme); // memfs
console.log(file.authority); // backend
console.log(file.path); // /src/main.ts注册与注销生命周期
注册返回的 Disposable 是注销的唯一手段,务必随插件停用释放:
import * as vscode from 'vscode';
let memfsProvider: MemFileSystemProvider | undefined;
export function activate(context: vscode.ExtensionContext) {
// 初始化 provider 并注册
memfsProvider = new MemFileSystemProvider();
const disposable = vscode.workspace.registerFileSystemProvider(
'memfs',
memfsProvider,
{ isCaseSensitive: true, isReadonly: false }
);
context.subscriptions.push(disposable);
// 演示:向内存文件系统写入欢迎文件
const welcome = vscode.Uri.parse('memfs:/welcome.txt');
vscode.workspace.fs.writeFile(
welcome,
new TextEncoder().encode('欢迎来到内存文件系统')
);
}
export function deactivate() {
// context.subscriptions 自动 dispose,也可以手动注销
memfsProvider = undefined;
}注册选项 options 可配置:
| 选项 | 说明 |
|---|---|
isCaseSensitive | 路径是否大小写敏感 |
isReadonly | 只读文件系统(写入会抛错) |
isVirtual | 标记为虚拟资源(不参与某些本地优化) |
workspace.fs 操作虚拟文件
注册后,vscode.workspace.fs 的全部方法都能作用于该 scheme:
import * as vscode from 'vscode';
async function demoWorkspaceFs() {
const dir = vscode.Uri.parse('memfs:/data');
const file = vscode.Uri.parse('memfs:/data/hello.txt');
// 创建目录
await vscode.workspace.fs.createDirectory(dir);
// 写入文件
await vscode.workspace.fs.writeFile(
file,
new TextEncoder().encode('Hello MemFS')
);
// 读取
const bytes = await vscode.workspace.fs.readFile(file);
console.log(new TextDecoder().decode(bytes));
// 列目录
const entries = await vscode.workspace.fs.readDirectory(dir);
console.log(entries); // [['hello.txt', FileType.File]]
// 统计
const stat = await vscode.workspace.fs.stat(file);
console.log(stat.size); // 11
// 删除
await vscode.workspace.fs.delete(file);
}TextEncoder / TextDecoder 负责字符串与字节数组的互转,这是与 writeFile / readFile 交互的标配。
打开虚拟文件到编辑器
虚拟文件同样可以打开进编辑器,配合 vscode.window.showTextDocument:
import * as vscode from 'vscode';
export async function openMemFile(path: string) {
const uri = vscode.Uri.parse(`memfs:${path}`);
try {
const doc = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(doc, {
preview: false
});
return editor;
} catch (err) {
vscode.window.showErrorMessage(`打开失败: ${err}`);
return undefined;
}
}注意:虚拟文件默认以 plaintext 打开,若内容有特定语言(如 TS/JSON),需调用 vscode.languages.setTextDocumentLanguage 设置语言。
TreeView 联动:资源管理器展示虚拟文件系统
把虚拟文件系统接到 TreeView,用户可以像操作普通文件一样浏览、展开:
数据提供者
import * as vscode from 'vscode';
export class MemFsTreeProvider implements vscode.TreeDataProvider<Node> {
private _onDidChangeTreeData = new vscode.EventEmitter<void>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
constructor(private provider: MemFileSystemProvider) {}
// 根节点:列出 / 目录
getTreeItem(element: Node): vscode.TreeItem {
const treeItem = new vscode.TreeItem(
element.name,
element.isDirectory
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
treeItem.description = element.isDirectory ? '文件夹' : `${element.size} B`;
treeItem.iconPath = element.isDirectory
? new vscode.ThemeIcon('folder')
: new vscode.ThemeIcon('file');
treeItem.contextValue = element.isDirectory ? 'memfs-dir' : 'memfs-file';
return treeItem;
}
getChildren(element?: Node): Thenable<Node[]> {
// 顶层:读根目录;子层:读对应目录
const uri = element
? element.uri
: vscode.Uri.parse('memfs:/');
return this.provider.readDirectory(uri).then((entries) =>
entries.map(([name, type]) => new Node(uri, name, type))
);
}
refresh() {
this._onDidChangeTreeData.fire();
}
}
// 简化节点包装
class Node {
uri: vscode.Uri;
constructor(
parent: vscode.Uri,
public name: string,
public type: vscode.FileType
) {
this.uri = vscode.Uri.parse(`${parent.path}/${name}`);
}
get isDirectory() {
return this.type === vscode.FileType.Directory;
}
get size() {
return this.type === vscode.FileType.File ? this.name.length : 0;
}
}激活与联动
import * as vscode from 'vscode';
import { MemFileSystemProvider } from './memfsProvider';
import { MemFsTreeProvider } from './memFsTree';
export function activate(context: vscode.ExtensionContext) {
// 1. 创建 provider 并注册文件系统
const fsProvider = new MemFileSystemProvider();
context.subscriptions.push(
vscode.workspace.registerFileSystemProvider('memfs', fsProvider)
);
// 2. 初始化示例数据
const seed = async () => {
const src = vscode.Uri.parse('memfs:/src');
await vscode.workspace.fs.createDirectory(src);
await vscode.workspace.fs.writeFile(
vscode.Uri.parse('memfs:/src/index.ts'),
new TextEncoder().encode('export const v = 1;')
);
await vscode.workspace.fs.writeFile(
vscode.Uri.parse('memfs:/README.md'),
new TextEncoder().encode('# MemFS 示例')
);
};
seed();
// 3. 注册 TreeView
const treeProvider = new MemFsTreeProvider(fsProvider);
const treeView = vscode.window.createTreeView('memfsExplorer', {
treeDataProvider: treeProvider
});
// 4. 监听变更自动刷新树
const sub = fsProvider.onDidChangeFile(() => treeProvider.refresh());
context.subscriptions.push(treeView, sub);
// 5. 点击文件打开编辑器
context.subscriptions.push(
vscode.commands.registerCommand('memfs.openFile', (node: Node) => {
if (node && !node.isDirectory) {
vscode.workspace.openTextDocument(node.uri).then((doc) =>
vscode.window.showTextDocument(doc)
);
}
})
);
}
export function deactivate() {}package.json 声明 TreeView
{
"contributes": {
"views": {
"explorer": [
{
"id": "memfsExplorer",
"name": "内存文件系统"
}
]
},
"commands": [
{ "command": "memfs.openFile", "title": "打开文件" }
],
"menus": {
"view/item/context": [
{
"command": "memfs.openFile",
"when": "view == memfsExplorer && viewItem == memfs-file",
"group": "inline"
}
]
}
}
}联动闭环
注册 provider → memfs scheme 可用
↓
workspace.fs 写文件 → onDidChangeFile 触发
↓
TreeView refresh → 资源管理器树更新
↓
点击树节点 → openTextDocument 打开虚拟文件
↓
编辑保存 → writeFile → 再次触发变更FileSystemProvider 把"文件"抽象成接口:注册一个 scheme,读写、目录、监听、变更通知就全部打通。资源管理器、编辑器、workspace.fs 三方共享同一套文件模型,虚拟文件系统与真实磁盘文件在使用体验上几乎没有差别。