实战:虚拟文件系统插件
综合前面四篇的知识,实现一个完整的内存文件系统插件 memfs:文件系统挂载、资源管理器树、读写删除、文件名搜索、终端操作虚拟路径,全部打通。
项目结构
text
memfs/
├── package.json
├── tsconfig.json
├── src/
│ ├── extension.ts # 激活入口:注册与命令
│ ├── memfsProvider.ts # FileSystemProvider 实现
│ ├── memfsTree.ts # TreeView 数据提供者
│ └── search.ts # 文件名搜索实现package.json
json
{
"name": "memfs-explorer",
"displayName": "MemFS Explorer",
"description": "内存文件系统:文件树浏览、读写、搜索、终端集成",
"version": "1.0.0",
"publisher": "mypublisher",
"engines": { "vscode": "^1.80.0" },
"categories": ["Other"],
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js",
"contributes": {
"commands": [
{ "command": "memfs.newFile", "title": "MemFS: 新建文件" },
{ "command": "memfs.newFolder", "title": "MemFS: 新建文件夹" },
{ "command": "memfs.delete", "title": "MemFS: 删除" },
{ "command": "memfs.search", "title": "MemFS: 搜索文件名" },
{ "command": "memfs.openInTerminal", "title": "MemFS: 在终端打开" },
{ "command": "memfs.seed", "title": "MemFS: 生成示例数据" }
],
"menus": {
"view/title": [
{ "command": "memfs.search", "when": "view == memfsExplorer", "group": "navigation" },
{ "command": "memfs.seed", "when": "view == memfsExplorer", "group": "navigation" }
],
"view/item/context": [
{ "command": "memfs.newFile", "when": "view == memfsExplorer && viewItem == memfs-dir", "group": "1_modification" },
{ "command": "memfs.newFolder", "when": "view == memfsExplorer && viewItem == memfs-dir", "group": "1_modification" },
{ "command": "memfs.delete", "when": "view == memfsExplorer", "group": "2_destructive" },
{ "command": "memfs.openInTerminal", "when": "view == memfsExplorer && viewItem == memfs-dir", "group": "3_terminal" }
]
},
"views": {
"explorer": [
{ "id": "memfsExplorer", "name": "内存文件系统" }
]
}
},
"scripts": { "compile": "tsc -p ./" },
"devDependencies": {
"@types/vscode": "^1.80.0",
"@types/node": "^18.0.0",
"typescript": "^5.0.0"
}
}memfsProvider.ts
核心文件系统实现,覆盖读写、目录、删除、重命名、变更通知:
typescript
import * as vscode from 'vscode';
interface Entry {
stat: vscode.FileStat;
data: Uint8Array;
children: Map<string, Entry>;
}
export class MemFsProvider implements vscode.FileSystemProvider {
private root = new Map<string, Entry>();
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 entry = this.root.get(uri.path);
if (!entry) throw vscode.FileSystemError.FileNotFound(uri);
return entry.stat;
}
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
const entry = this.root.get(uri.path);
if (!entry || entry.stat.type !== vscode.FileType.Directory) {
throw vscode.FileSystemError.FileNotFound(uri);
}
return [...entry.children.entries()].map(([name, e]) => [name, e.stat.type]);
}
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const entry = this.root.get(uri.path);
if (!entry) throw vscode.FileSystemError.FileNotFound(uri);
return entry.data;
}
// ---------- 写 ----------
async writeFile(
uri: vscode.Uri,
content: Uint8Array,
options: { create: boolean; overwrite: boolean }
): Promise<void> {
const entry = this.root.get(uri.path);
if (!entry) {
if (!options.create) throw vscode.FileSystemError.FileNotFound(uri);
this.insert(uri, content, vscode.FileType.File);
return;
}
if (!options.overwrite) throw vscode.FileSystemError.FileExists(uri);
entry.data = content;
entry.stat.size = content.length;
entry.stat.mtime = Date.now();
this.fire(uri, vscode.FileChangeType.Changed);
}
async createDirectory(uri: vscode.Uri): Promise<void> {
this.insert(uri, new Uint8Array(0), vscode.FileType.Directory);
}
// ---------- 删 / 移 ----------
async delete(uri: vscode.Uri, options: { recursive: boolean }): Promise<void> {
const entry = this.root.get(uri.path);
if (!entry) throw vscode.FileSystemError.FileNotFound(uri);
const isDir = entry.stat.type === vscode.FileType.Directory;
if (isDir && entry.children.size > 0 && !options.recursive) {
throw vscode.FileSystemError.DirectoryNotEmpty(uri);
}
this.root.delete(uri.path);
this.fire(uri, vscode.FileChangeType.Deleted);
}
async rename(
oldUri: vscode.Uri,
newUri: vscode.Uri,
options: { overwrite: boolean }
): Promise<void> {
const entry = this.root.get(oldUri.path);
if (!entry) throw vscode.FileSystemError.FileNotFound(oldUri);
if (this.root.has(newUri.path) && !options.overwrite) {
throw vscode.FileSystemError.FileExists(newUri);
}
this.root.delete(oldUri.path);
this.root.set(newUri.path, entry);
this.fire(oldUri, vscode.FileChangeType.Deleted);
this.fire(newUri, vscode.FileChangeType.Created);
}
watch(_uri: vscode.Uri): vscode.Disposable {
return new vscode.Disposable(() => {});
}
// ---------- 工具 ----------
private insert(uri: vscode.Uri, data: Uint8Array, type: vscode.FileType) {
this.root.set(uri.path, {
stat: {
type,
ctime: Date.now(),
mtime: Date.now(),
size: data.length
},
data,
children: new Map()
});
this.fire(uri, vscode.FileChangeType.Created);
}
private fire(uri: vscode.Uri, type: vscode.FileChangeType) {
this.emitter.fire([{ type, uri }]);
}
// 供搜索使用的全量列表
listAll(): Array<{ uri: vscode.Uri; name: string; isDirectory: boolean }> {
const result: Array<{ uri: vscode.Uri; name: string; isDirectory: boolean }> = [];
for (const [path, entry] of this.root) {
result.push({
uri: vscode.Uri.parse(`memfs:${path}`),
name: path.split('/').pop() ?? path,
isDirectory: entry.stat.type === vscode.FileType.Directory
});
}
return result;
}
dispose() {
this.emitter.dispose();
}
}memfsTree.ts
TreeView 数据提供者,把内存文件树映射为资源管理器视图:
typescript
import * as vscode from 'vscode';
import { MemFsProvider } from './memfsProvider';
export class MemFsNode {
constructor(
public readonly uri: vscode.Uri,
public readonly name: string,
public readonly type: vscode.FileType
) {}
get isDirectory(): boolean {
return this.type === vscode.FileType.Directory;
}
}
export class MemFsTreeProvider implements vscode.TreeDataProvider<MemFsNode> {
private emitter = new vscode.EventEmitter<MemFsNode | undefined>();
readonly onDidChangeTreeData: vscode.Event<MemFsNode | undefined> =
this.emitter.event;
constructor(private provider: MemFsProvider) {
// 文件系统任何变更都刷新整棵树
provider.onDidChangeFile(() => this.emitter.fire(undefined));
}
getTreeItem(element: MemFsNode): vscode.TreeItem {
const item = new vscode.TreeItem(
element.name,
element.isDirectory
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
item.iconPath = new vscode.ThemeIcon(
element.isDirectory ? 'folder' : 'file'
);
// contextValue 供菜单 when 条件区分类型
item.contextValue = element.isDirectory ? 'memfs-dir' : 'memfs-file';
// 文件可点击打开
if (!element.isDirectory) {
item.command = {
command: 'memfs.openFile',
title: '打开文件',
arguments: [element]
};
}
return item;
}
async getChildren(element?: MemFsNode): Promise<MemFsNode[]> {
// 顶层:根目录 /;子层:对应目录
const uri = element?.uri ?? vscode.Uri.parse('memfs:/');
const entries = await this.provider.readDirectory(uri);
return entries.map(([name, type]) => new MemFsNode(
vscode.Uri.parse(`memfs:${uri.path}/${name}`),
name,
type
));
}
getParent(element: MemFsNode): MemFsNode | undefined {
const parts = element.uri.path.split('/').filter(Boolean);
parts.pop();
if (parts.length === 0) return undefined;
return new MemFsNode(
vscode.Uri.parse(`memfs:/${parts.join('/')}`),
parts[parts.length - 1],
vscode.FileType.Directory
);
}
refresh() {
this.emitter.fire(undefined);
}
}search.ts
文件名搜索:遍历内存文件系统,用 QuickPick 展示结果,选择后定位:
typescript
import * as vscode from 'vscode';
import { MemFsProvider } from './memfsProvider';
export async function searchFiles(provider: MemFsProvider) {
// 输入关键字
const keyword = await vscode.window.showInputBox({
prompt: '输入要搜索的文件名关键字',
placeHolder: '例如: index, readme, ts'
});
if (!keyword) return;
// 全量扫描并模糊匹配(不区分大小写)
const all = provider.listAll();
const matched = all.filter((f) =>
f.name.toLowerCase().includes(keyword.toLowerCase())
);
if (matched.length === 0) {
vscode.window.showInformationMessage(`没有找到包含 "${keyword}" 的文件`);
return;
}
// QuickPick 展示候选
const pick = await vscode.window.showQuickPick(
matched.map((m) => ({
label: m.isDirectory ? `$(folder) ${m.name}` : `$(file) ${m.name}`,
description: m.uri.path,
detail: m.isDirectory ? '目录' : '文件',
node: m
})),
{ placeHolder: `找到 ${matched.length} 个匹配项` }
);
if (pick) {
await revealAndOpen(pick.node.uri, pick.node.isDirectory);
}
}
// 在树中定位或直接打开
async function revealAndOpen(uri: vscode.Uri, isDirectory: boolean) {
if (isDirectory) {
// 目录:切到资源管理器并选中
await vscode.commands.executeCommand(
'workbench.view.explorer'
);
} else {
// 文件:直接打开编辑器
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
}
}extension.ts
激活入口,把文件系统、树、命令、终端全部串起来:
typescript
import * as vscode from 'vscode';
import { MemFsProvider } from './memfsProvider';
import { MemFsNode, MemFsTreeProvider } from './memfsTree';
import { searchFiles } from './search';
export function activate(context: vscode.ExtensionContext) {
// 1. 挂载内存文件系统到 memfs scheme
const provider = new MemFsProvider();
context.subscriptions.push(
vscode.workspace.registerFileSystemProvider('memfs', provider, {
isCaseSensitive: true
}),
provider
);
// 2. 生成示例数据
seedSampleData();
// 3. 注册 TreeView
const treeProvider = new MemFsTreeProvider(provider);
const treeView = vscode.window.createTreeView('memfsExplorer', {
treeDataProvider: treeProvider,
showCollapseAll: true
});
context.subscriptions.push(treeView);
// ---------- 命令 ----------
// 新建文件
context.subscriptions.push(
vscode.commands.registerCommand('memfs.newFile', async (node?: MemFsNode) => {
const dir = await pickDirectory(node, '选择新建文件的位置');
if (!dir) return;
const name = await vscode.window.showInputBox({ prompt: '文件名' });
if (!name) return;
const uri = vscode.Uri.parse(`memfs:${dir}/${name}`);
await vscode.workspace.fs.writeFile(uri, new Uint8Array(0), {
create: true,
overwrite: false
});
// 打开新文件
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
})
);
// 新建文件夹
context.subscriptions.push(
vscode.commands.registerCommand('memfs.newFolder', async (node?: MemFsNode) => {
const dir = await pickDirectory(node, '选择新建文件夹的位置');
if (!dir) return;
const name = await vscode.window.showInputBox({ prompt: '文件夹名' });
if (!name) return;
await vscode.workspace.fs.createDirectory(
vscode.Uri.parse(`memfs:${dir}/${name}`)
);
})
);
// 删除(带确认)
context.subscriptions.push(
vscode.commands.registerCommand('memfs.delete', async (node?: MemFsNode) => {
if (!node) return;
const msg = node.isDirectory
? `确定删除文件夹 ${node.name} 及其内容?`
: `确定删除文件 ${node.name}?`;
const answer = await vscode.window.showWarningMessage(
msg,
{ modal: true },
'删除'
);
if (answer === '删除') {
await vscode.workspace.fs.delete(node.uri, { recursive: true });
}
})
);
// 打开文件
context.subscriptions.push(
vscode.commands.registerCommand('memfs.openFile', async (node: MemFsNode) => {
if (!node || node.isDirectory) return;
const doc = await vscode.workspace.openTextDocument(node.uri);
await vscode.window.showTextDocument(doc, { preview: false });
})
);
// 搜索
context.subscriptions.push(
vscode.commands.registerCommand('memfs.search', () => searchFiles(provider))
);
// 在终端中打开
context.subscriptions.push(
vscode.commands.registerCommand(
'memfs.openInTerminal',
async (node?: MemFsNode) => {
const dir = await pickDirectory(node, '选择要在终端打开的目录');
if (!dir) return;
openVirtualTerminal(dir);
}
)
);
// 生成示例数据
context.subscriptions.push(
vscode.commands.registerCommand('memfs.seed', seedSampleData)
);
// ---------- 工具函数 ----------
// 解析目标目录:节点为目录用节点,否则默认根目录
function pickDirectory(
node: MemFsNode | undefined,
title: string
): Promise<string | undefined> {
if (node?.isDirectory) {
return Promise.resolve(node.uri.path);
}
const pick = vscode.window.createQuickPick();
pick.title = title;
pick.placeholder = '输入虚拟路径(如 /data)';
pick.onDidAccept(() => {
const path = pick.value.trim() || '/';
pick.hide();
pick.resolve?.();
resolvePath(path);
});
// 简化实现:直接返回输入值
let resolvePath: (p: string | undefined) => void;
const p = new Promise<string | undefined>((resolve) => {
resolvePath = resolve;
});
pick.onDidHide(() => {
if (pick.value.trim() === '' && !resolved) resolvePath(undefined);
});
let resolved = false;
pick.onDidAccept(() => {
resolved = true;
resolvePath(pick.value.trim() || '/');
pick.dispose();
});
pick.show();
return p;
}
// 终端集成:cd 到虚拟目录并执行命令
function openVirtualTerminal(dir: string) {
const term = vscode.window.createTerminal('MemFS');
term.show(true);
// cd 进入虚拟路径(memfs: 前缀),当前目录即 /dir
term.sendText(`cd memfs:${dir}`);
term.sendText('dir'); // Windows 下列出内容
}
// 示例数据
async function seedSampleData() {
const files: Array<[string, string]> = [
['/README.md', '# MemFS 示例\n\n一个内存文件系统插件。'],
['/package.json', '{\n "name": "memfs-demo",\n "version": "1.0.0"\n}'],
['/src/index.ts', 'export const hello = "memfs";\n'],
['/src/util.ts', 'export function add(a: number, b: number) {\n return a + b;\n}\n'],
['/docs/guide.md', '# 使用指南\n\n在终端输入 cd memfs:/docs 进入本目录。']
];
for (const [path, text] of files) {
// 逐级确保父目录存在
const parts = path.split('/').filter(Boolean);
let current = '';
for (let i = 0; i < parts.length - 1; i++) {
current += '/' + parts[i];
await vscode.workspace.fs.createDirectory(
vscode.Uri.parse(`memfs:${current}`)
);
}
await vscode.workspace.fs.writeFile(
vscode.Uri.parse(`memfs:${path}`),
new TextEncoder().encode(text),
{ create: true, overwrite: true }
);
}
treeProvider.refresh();
}
}
export function deactivate() {}简化目录选择
上面的 pickDirectory 逻辑稍显繁琐,可以用更直接的实现替代——统一用输入框获取虚拟路径:
typescript
async function resolveDirPath(node?: MemFsNode): Promise<string> {
// 右键在目录上时直接用该目录
if (node?.isDirectory) return node.uri.path;
// 否则弹出输入框
const input = await vscode.window.showInputBox({
prompt: '输入虚拟目录路径',
value: node?.uri.path ?? '/'
});
return input ?? '/';
}
// 命令里替换为:
const dir = await resolveDirPath(node);
if (!dir) return;终端中操作虚拟路径
终端与文件系统联动的关键:cd memfs:/xxx 可以让终端进入虚拟目录。VS Code 的集成终端支持任意 scheme 的路径切换,之后执行的命令都以虚拟目录为工作目录:
typescript
import * as vscode from 'vscode';
// 在终端中执行:进入虚拟目录 → 创建文件 → 校验内容
export function terminalWorkflow() {
const term = vscode.window.createTerminal('MemFS 演练');
term.show(true);
// 用换行分隔多命令,逐条执行
term.sendText(
'cd memfs:/docs' + '\n' + // 进入虚拟目录
'type NUL > memo.txt' + '\n' + // 新建空文件(Windows)
'echo hello > memo.txt' // 写入内容
);
// 稍后读取虚拟文件验证
setTimeout(async () => {
const content = await vscode.workspace.fs.readFile(
vscode.Uri.parse('memfs:/docs/memo.txt')
);
vscode.window.showInformationMessage(
`虚拟文件内容: ${new TextDecoder().decode(content)}`
);
}, 2000);
}终端命令与 workspace.fs 的双向印证
text
终端: cd memfs:/docs && echo hi > memo.txt
↓ (写入虚拟文件系统)
workspace.fs.readFile(memfs:/docs/memo.txt)
↓
"hi" —— 两条路径看到的是同一份数据全流程串联
text
激活插件
↓
registerFileSystemProvider('memfs') + seed 示例数据
↓
TreeView 挂载到资源管理器 → 树形展示 memfs:/ 内容
↓
用户操作(右键/命令面板/快捷按钮)
├─ 新建文件 → writeFile → 树自动刷新
├─ 新建文件夹 → createDirectory
├─ 删除 → delete(带确认)
├─ 搜索 → listAll 模糊匹配 → QuickPick 跳转
└─ 终端打开 → cd memfs:/dir → 命令行操作
↓
任何变更触发 onDidChangeFile → TreeView 刷新验证清单
text
1. 启动扩展宿主,资源管理器出现"内存文件系统"视图
2. 展开树:README.md、package.json、src/、docs/ 可见
3. 右键 src → 新建文件 a.ts → 编辑并保存
4. 右键 a.ts → 删除 → 弹确认 → 树中消失
5. 视图工具栏点搜索 → 输入 "util" → 定位到 util.ts
6. 右键 docs → 在终端打开 → cd memfs:/docs 生效
7. 终端中 echo hello > memo.txt → 树上出现 memo.txt
8. 打开 memo.txt → 内容为 hello七步验证覆盖了挂载、展示、增删改查、搜索、终端联动。一个内存文件系统插件就完成了——它的代码量不大,但把 FileSystemProvider、TreeView、workspace.fs、终端四套 API 完整地串在了一起。