VSCode 源码二次开发:给 vscode 对象扩展自定义 API namespace
VSCode 源码二次开发:给 vscode 对象扩展自定义 API namespace
做 fork 版 VSCode 二次开发时,常需要给插件开发者暴露一套自定义能力,让他们能像 vscode.window.xxx 那样调用 vscode.myExt.xxx。这需要在源码里打通「主线程(MainThread)↔ 插件宿主(ExtHost)」的 RPC 全链路。本文用一个最小示例 vscode.myExt 走一遍完整流程。
背景:插件系统为什么要分两端
VSCode 每个插件运行在**独立的扩展宿主进程(Extension Host)**里,与主进程隔离,二者通过基于 Node net 的 IPC + 代理(Proxy)模式通信:
- 插件调用的
vscode.*API,多数只是 ExtHost 端的「代理」,真正干活的逻辑在主线程; - 调用被序列化成消息,跨进程发到 MainThread 执行,结果再回传。
所以扩展一个自定义 namespace,需要成对地写 MainThread 实现(主线程,真正逻辑)和 ExtHost 代理(插件宿主,转发调用),并在 Protocol 里定义两端接口、在 .d.ts 里定义对插件暴露的类型、最后在 api.impl 里把它挂到 vscode 对象上。
整体五步骨架:类型定义 → MainThread 实现 → Protocol 接口 → ExtHost 代理 → api.impl 暴露。
1. MainThread —— 主线程实现
在 src/vs/workbench/api/browser 新增 mainThreadMyExt.ts:
import { Disposable } from 'vs/base/common/lifecycle';
import { IMyExtService } from 'vs/platform/myExt/common/myExt';
import { ExtHostMyExtShape, MainContext, MainThreadMyExtShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadMyExt)
export class MainThreadMyExt extends Disposable implements MainThreadMyExtShape {
private readonly _proxy: ExtHostMyExtShape;
constructor(
extHostContext: IExtHostContext,
@IMyExtService private readonly myExtService: IMyExtService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostMyExt);
// 把主线程 service 的事件转发给 ExtHost
this._register(this.myExtService.onMessage(e => {
this._proxy.$onDidReceiveMessage(e);
}));
}
async $showMessage(value: string): Promise<void> {
return this.myExtService.showMessage(value);
}
}
@extHostNamedCustomer 装饰器负责把这个类注册进依赖注入系统。
注册:在 src/vs/workbench/api/browser/extensionHost.contribution.ts import 一下让它生效:
import './mainThreadMyExt';
2. ExtHost —— 插件宿主端代理
在 src/vs/workbench/api/common 新增 extHostMyExt.ts:
import { IMainContext, MainThreadMyExtShape, MainContext, ExtHostMyExtShape } from 'vs/workbench/api/common/extHost.protocol';
import { Emitter, Event } from 'vs/base/common/event';
export class ExtHostMyExt implements ExtHostMyExtShape {
private _proxy: MainThreadMyExtShape;
private readonly _onDidReceiveMessage = new Emitter<any>();
readonly onDidReceiveMessage: Event<any> = this._onDidReceiveMessage.event;
constructor(mainContext: IMainContext) {
this._proxy = mainContext.getProxy(MainContext.MainThreadMyExt);
}
// 转发给主线程执行
$showMessage(value: string): Promise<void> {
return this._proxy.$showMessage(value);
}
// 接收主线程回传的事件
$onDidReceiveMessage(e: any) {
this._onDidReceiveMessage.fire(e);
return Promise.resolve();
}
}
3. Protocol —— 定义两端接口
在 src/vs/workbench/api/common/extHost.protocol.ts 增加成对接口,并在 MainContext / ExtHostContext 注册标识:
export interface MainThreadMyExtShape extends IDisposable {
$showMessage(value: string): Promise<void>;
}
export interface ExtHostMyExtShape {
$showMessage(value: string): Promise<void>;
$onDidReceiveMessage(e: any): Promise<void>;
}
MainThreadMyExtShape= MainThread 类的类型约束;ExtHostMyExtShape= ExtHost 类的类型约束。
4. 类型定义 —— 对插件暴露的 API
所有对插件可见的类型加到 src/vscode-dts/vscode.d.ts:
export namespace myExt {
export const onDidReceiveMessage: Event<any>;
export function showMessage(value: string): Thenable<void>;
}
命名约定:类型 / 接口 / 枚举不以 “I” 开头;事件方法以
on开头并接Did/Will+ 动词,如onDidReceiveMessage。
5. RpcProtocol —— 在 api.impl 挂到 vscode 对象
在 src/vs/workbench/api/common/extHost.api.impl.ts 的 createApiFactoryAndRegisterActors 里:
// 注册 ExtHost actor
const extHostMyExt = rpcProtocol.set(ExtHostContext.ExtHostMyExt, new ExtHostMyExt(rpcProtocol));
// ... 在返回给插件的工厂函数内,组装 namespace 实现:
const myExt: typeof vscode.myExt = {
get onDidReceiveMessage() {
return extHostMyExt.onDidReceiveMessage;
},
async showMessage(value: string): Promise<void> {
return extHostMyExt.$showMessage(value);
}
};
// ... 在最终 return 的 vscode 对象里挂上:
return <typeof vscode>{
// ...
myExt,
// ...
};
至此插件里就能 vscode.myExt.showMessage('hi') 了。
官方风格的精简示例
社区里有同样思路的精简版,比如给 vscode 加一个 trash.getTrashRootPath:
// vscode.d.ts
export namespace trash {
export function getTrashRootPath(): Thenable<string>;
}
// 插件中调用
vscode.trash.getTrashRootPath().then(data => {
console.log(data);
});
五步骨架完全一致,复杂度只在于「方法/事件多少」与「跨进程传输的数据结构」。
配套:私有 @types 的引用
如果团队是 fork 版且把自定义 API 类型发布到了私有 npm 源,插件项目要用上这些类型时:
- 把
package.json的@types/vscode换成你们的私有类型包@types/<your-vscode>; - 在项目
.npmrc增加私有 registry 的 scope 映射,例如@types:registry=<你的私有 npm 源>; - 删除
node_modules与 lockfile,重新安装。