自定义编辑器进阶
基础自定义编辑器面向文本文件,进阶能力解决两类痛点:二进制/富格式文件无法用文本方式读取;脏状态与崩溃恢复需要可靠的备份机制。
二进制文件支持
二进制文件(图片、数据库、图表、压缩包)无法用 TextDocument 读取,需要自定义文档:
typescript
import * as vscode from 'vscode';
// 自定义文档类
class BinaryDocument implements vscode.CustomDocument {
// 文件内容(二进制)
data: Uint8Array;
// 脏状态标记
private _isDirty = false;
constructor(
public readonly uri: vscode.Uri,
data: Uint8Array
) {
this.data = data;
}
get isDirty(): boolean {
return this._isDirty;
}
// 保存
async save(cancellation: vscode.CancellationToken): Promise<void> {
await vscode.workspace.fs.writeFile(this.uri, this.data);
this._isDirty = false;
}
// 另存为
async saveAs(
targetResource: vscode.Uri,
cancellation: vscode.CancellationToken
): Promise<void> {
await vscode.workspace.fs.writeFile(targetResource, this.data);
this._isDirty = false;
}
// 恢复备份
async revert(
cancellation: vscode.CancellationToken
): Promise<void> {
const data = await vscode.workspace.fs.readFile(this.uri);
this.data = data;
this._isDirty = false;
}
// 备份(崩溃恢复用)
async backup(
destination: vscode.Uri,
cancellation: vscode.CancellationToken
): Promise<vscode.CustomDocumentBackup> {
await vscode.workspace.fs.writeFile(destination, this.data);
return { id: destination.toString(), delete: () => {} };
}
dispose(): void {
// 清理资源
}
}CustomDocument 接口
自定义文档是二进制编辑器的数据载体:
| 方法 | 作用 |
|---|---|
save() | 保存到文件 |
saveAs(uri) | 另存为 |
revert() | 回退到磁盘版本 |
backup(destination) | 创建备份 |
dispose() | 释放资源 |
备份与恢复 CustomDocumentBackup
VS Code 在以下场景创建备份:
- 文件保存前(保证可恢复)
- 插件崩溃/窗口崩溃时
- 未保存修改时的自动备份
备份流程
用户编辑 → 标记 dirty → VS Code 调用 backup()
↓
写入备份文件(临时目录)
↓
崩溃/关闭 → 重新打开时恢复备份 → revert()实现 backup
typescript
async backup(
destination: vscode.Uri,
cancellation: vscode.CancellationToken
): Promise<vscode.CustomDocumentBackup> {
// 将当前内容写入备份位置
await vscode.workspace.fs.writeFile(destination, this.data);
// 返回备份对象
return {
id: destination.toString(),
delete: () => {
// 备份清理逻辑(可选)
}
};
}注册二进制编辑器
typescript
class BinaryEditorProvider
implements vscode.CustomEditorProvider<BinaryDocument> {
async openCustomDocument(
uri: vscode.Uri,
openContext: vscode.CustomDocumentOpenContext,
token: vscode.CancellationToken
): Promise<BinaryDocument> {
// 读取二进制数据
const data = await vscode.workspace.fs.readFile(uri);
return new BinaryDocument(uri, data);
}
resolveCustomEditor(
document: BinaryDocument,
panel: vscode.WebviewPanel
): void {
// 渲染二进制内容(如 base64 图片、图表数据)
panel.webview.html = this.getHtml(document);
}
}编辑锁定与解锁
编辑锁定:文档被外部修改或处于只读状态时,禁止编辑:
typescript
// 文档级编辑控制
class BinaryDocument {
private locked = false;
lock() {
this.locked = true;
this._onDidChange.fire();
}
unlock() {
this.locked = false;
this._onDidChange.fire();
}
// 编辑操作前检查
canEdit(): boolean {
return !this.locked;
}
}Webview 侧禁用编辑
typescript
// 插件侧向页面发送锁定状态
webviewPanel.webview.postMessage({
type: 'locked',
value: document.isLocked
});javascript
// Webview 侧接收锁定
window.addEventListener('message', (event) => {
if (event.data.type === 'locked') {
editor.readonly = event.data.value; // 禁用编辑
}
});只读状态来源
| 来源 | 处理 |
|---|---|
| 文件只读 | 打开时检查权限 |
| 外部修改冲突 | 弹出冲突提示 |
| 临时锁定 | 大操作期间禁用 |
多 Tab 联动
同一文件被多个 Tab 打开时,需要同步状态:
typescript
class BinaryEditorProvider {
// 文档 → 打开的 Webview 映射
private openPanels = new Map<string, vscode.WebviewPanel[]>();
resolveCustomEditor(document, panel) {
const key = document.uri.toString();
const panels = this.openPanels.get(key) || [];
panels.push(panel);
this.openPanels.set(key, panels);
// 关闭时移除
panel.onDidDispose(() => {
const list = this.openPanels.get(key) || [];
const index = list.indexOf(panel);
if (index > -1) list.splice(index, 1);
});
}
// 文档更新时广播到所有关联 Tab
private broadcast(document: BinaryDocument) {
const key = document.uri.toString();
const panels = this.openPanels.get(key) || [];
panels.forEach((panel) => {
panel.webview.postMessage({
type: 'update',
data: document.data
});
});
}
}多 Tab 同步要点
| 场景 | 处理 |
|---|---|
| 一个文件多个 Tab | 修改广播到所有 Tab |
| Tab 间切换 | 切换时重新渲染 |
| 关闭部分 Tab | 移除面板引用 |
| 文档变更 | 统一由文档事件触发广播 |
脏状态管理
脏状态决定「关闭时是否提示保存」:
typescript
// 触发脏状态变化
class BinaryDocument {
private readonly _onDidChange =
new vscode.EventEmitter<vscode.CustomDocumentEditEvent<BinaryDocument>>();
readonly onDidChange = this._onDidChange.event;
// 记录一次编辑
recordEdit(eventData: Uint8Array) {
const oldData = this.data;
this.data = eventData;
this._onDidChange.fire({
document: this,
label: '编辑二进制内容',
undo: () => { this.data = oldData; },
redo: () => { this.data = eventData; }
});
}
}CustomDocumentEditEvent
| 字段 | 作用 |
|---|---|
document | 关联文档 |
label | 编辑操作描述(撤销菜单显示) |
undo() | 撤销实现 |
redo() | 重做实现 |
完整示例:图片查看器
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.window.registerCustomEditorProvider(
'myExt.imageViewer',
new ImageViewerProvider(),
{ webviewOptions: { retainContextWhenHidden: true } }
)
);
}
class ImageViewerProvider
implements vscode.CustomEditorProvider<BinaryDocument> {
async openCustomDocument(
uri: vscode.Uri,
openContext: vscode.CustomDocumentOpenContext,
token: vscode.CancellationToken
): Promise<BinaryDocument> {
const data = await vscode.workspace.fs.readFile(uri);
return new BinaryDocument(uri, data);
}
resolveCustomEditor(
document: BinaryDocument,
panel: vscode.WebviewPanel
): void {
panel.webview.options = { enableScripts: false };
// 将二进制转为 Data URL 显示
const base64 = Buffer.from(document.data).toString('base64');
const mime = getMimeType(document.uri.fsPath);
panel.webview.html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
display: flex; justify-content: center;
align-items: center; height: 100vh;
background: var(--vscode-editor-background);
}
img { max-width: 95%; max-height: 95%; }
</style>
</head>
<body>
<img src="data:${mime};base64,${base64}">
</body>
</html>`;
}
onDidChangeCustomDocument() {
// 无编辑操作,空实现
}
}
function getMimeType(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase();
const map: Record<string, string> = {
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp'
};
return map[ext || ''] || 'application/octet-stream';
}常见问题
| 问题 | 处理 |
|---|---|
| 二进制乱码 | 使用 CustomDocument 而非 TextDocument |
| 崩溃后丢失修改 | 实现 backup + revert |
| 关闭不提示保存 | 正确触发 onDidChange 脏状态 |
| 多 Tab 不同步 | 广播更新到所有关联面板 |
自定义编辑器进阶能力让插件完整支持任何文件格式,从简单文本到二进制、从单 Tab 到多 Tab 联动,覆盖真实生产场景。