Void 代码库完全指南:AI 编辑器架构、双进程模型、LLM 消息管线与 Apply 机制深度解析
【免费下载链接】void开源AI代码编辑器,Cursor的替代方案。项目地址: https://gitcode.com/GitHub_Trending/void2/void
Void 是一款基于 VSCode 二次开发的开源 AI 代码编辑器,其核心业务代码几乎全部集中在src/vs/workbench/contrib/void/目录下。本指南以官方《Void Codebase Guide》为骨架,结合当前仓库源码逐层拆解 Void 的代码组织方式、Electron 双进程模型、Service/Action 架构、LLM 内部消息管线,以及 Fast Apply / Slow Apply 的完整实现原理,帮助你快速具备在 Void 代码库中定位与开发的能力。
Void 代码在哪里:目录速览
Void 的代码库并不可怕,绝大多数 Void 自己的代码都存放在src/vs/workbench/contrib/void/这一目录下。该目录下继续分为三个子目录,对应三种运行环境:
browser/:运行在渲染进程(浏览器进程)中的代码,例如 editCodeService.ts、void.contribution.ts、autocompleteService.ts 以及大量 React 界面组件(browser/react/src/下的 sidebar、quick-edit、diff、settings 等)。common/:可被任一进程使用的共享代码,例如 voidSettingsService.ts、voidModelService.ts、modelCapabilities.ts、sendLLMMessageService.ts、mcpService.ts 等类型定义与服务。electron-main/:运行在主进程中的代码,例如 sendLLMMessageChannel.ts、llmMessage/sendLLMMessage.ts 等。
如需构建、运行等工程化指令,请参考 HOW_TO_CONTRIBUTE.md(本文聚焦代码库本身的架构讲解)。
VSCode 运行机制速览:Electron 双进程模型
Void 继承自 VSCode,本质上是一个 Electron 应用。理解 Electron 的双进程模型是进入 Void 代码库的第一道门槛:
- 主进程(main process):负责应用内部事务,可以自由
import node_modules,拥有完整的 Node.js 能力。Void 的electron-main/目录下的代码都运行在这里。 - 浏览器进程(browser process):这里的 "browser" 指代 HTML 渲染环境,而不特指"网页浏览器"。Void 的界面、编辑器渲染都发生在这个进程。
browser/目录下的代码可以使用window等浏览器对象。
由此衍生出三个约定俗成的目录规则:
browser/目录下的代码永远运行在浏览器进程,可以使用window等浏览器对象;electron-main/目录下的代码永远运行在主进程,可以import node_modules;common/目录下的代码两个进程都可以用,但不获得任何特殊导入能力。
浏览器进程不能 import node_modules:两种绕过方案
浏览器进程被禁止直接import node_modules。Void 为此设计了两套解决方案,二者在仓库中都有实际落点:
- 把 node_module 代码打包进浏览器:React 就是这么做的。相关构建配置可参考 void/browser/react/build.js 与 tsup.config.js,React 组件会被打包成浏览器可用的产物(
browser/react/out/)。 - 在主进程实现逻辑,再通过 channel 建立主进程 ↔ 浏览器进程的通信:
sendLLMMessage正是如此。浏览器进程侧的 sendLLMMessageService.ts 通过mainProcessService.getChannel('void-channel-llmMessage')拿到 channel,把请求转交给主进程侧的 sendLLMMessageChannel.ts 执行真实网络请求,再通过onText、onFinalMessage、onError、onAbort等事件钩子把结果流式回传。
核心术语:Editor、Model、URI 与 Workbench
在 Void/VSCode 代码库中工作时,以下术语会高频出现:
- Editor(编辑器):你敲代码的那个东西。打开 10 个标签页,其实只是一个editor!Editor 内部包含标签页(即 model)。也就是说,Editor 是"容器",Model 是"内容"。
- Model(模型):文件内容的内部表示,在多个 editor 之间共享。例如按
Cmd+\新建一个分栏 editor 时,A.ts的 model 会被两个 editor 共享——两个 editor、一个 model,这正是改动能够双向同步的原因。 - URI:每个 model 对应一个 URI(如
/Users/.../my_file.txt)。在 VSCode 语境中,URI(也叫 "resource")通常就是一个路径。 - Workbench(工作台):包裹所有 editor、终端、文件系统树等 UI 的外壳。
- 类型:通常用
ITextModel表示 model,用ICodeEditor表示 editor。在多数场景下,你并不需要关心太多其他类型。
Service:单例注册与依赖注入
VSCode 按 "Service" 组织代码。一个 Service 就是一个只挂载一次(单例)的类,你可以用registerSingleton注册它,之后在任何构造器里用@<Service>装饰器注入使用。
Void 提供了一个完整的示例文件 _dummyContrib.ts,展示了标准的三件套写法,且"每次注册都一样":
// 1. 定义接口 + 装饰器 export interface IDummyService { readonly _serviceBrand: undefined; // services 需要这个,保持 undefined 即可 } export const IDummyService = createDecorator<IDummyService>('DummyService'); // 2. 实现类 class DummyService extends Disposable implements IWorkbenchContribution, IDummyService { static readonly ID = 'workbench.contrib.void.dummy' // workbenchContributions 需要,services 不需要 _serviceBrand: undefined; constructor(@ICodeEditorService codeEditorService: ICodeEditorService) { super(); } } // 3. 注册(二选一) registerSingleton(IDummyService, DummyService, InstantiationType.Eager); registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);Void 的核心服务几乎都遵循同样的模式,例如voidSettingsService(voidSettingsService.ts)以registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager)注册,voidModelService(voidModelService.ts)亦然。
Action / Command:可被用户与代码共同调用的函数
"Actions" 是你注册到 VSCode 上的函数,之后你或用户都可以调用它,它们也叫 "Commands"。两种触发方式:
- 用户触发:按
Cmd+Shift+P打开命令面板(command palette)执行; - 代码触发:通过
commandService按 ID 调用。
Void 用 Action 注册了Cmd+L、Cmd+K等按键监听。Action 最大的好处是:用户可以自由修改键位绑定。参考_dummyContrib.ts中registerAction2的写法:
registerAction2(class extends Action2 { constructor() { super({ f1: true, id: 'void.dummy', title: localize2('dummy', 'dummy: Init'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.Digit0, weight: KeybindingWeight.VoidExtension, } }); } async run(accessor: ServicesAccessor): Promise<void> { /* ... */ } });Void 还把所有 Action ID 集中定义在 actionIDs.ts 中(如VOID_ACCEPT_DIFF_ACTION_ID、VOID_REJECT_DIFF_ACTION_ID)。
内部 LLM 消息管线(Internal LLM Message Pipeline)
从你在 Void 侧边栏发出第一条消息,到请求真正到达你的模型提供商,中间经过了一整条依赖链。Void 选择在主进程发送 LLM 消息,原因有二:
- 规避本地提供商的CSP(内容安全策略)问题;
- 可以更方便地使用
node_modules中的 SDK 与 HTTP 库。
其核心调用链在仓库中可完整追踪:
- 用户在侧边栏 / 快捷编辑界面触发发送;
- 浏览器进程侧的 sendLLMMessageService.ts(
ILLMMessageService)校验modelSelection是否为空、消息是否为空,然后生成requestId(generateUuid()),把消息参数、settingsOfProvider、mcpTools一起通过 IPC channelvoid-channel-llmMessage发给主进程; - 主进程侧的 sendLLMMessageChannel.ts 与 llmMessage/sendLLMMessage.ts 真正组装 payload、向模型提供商发起请求;
- 流式结果通过
onText/onFinalMessage/onError事件回传到浏览器进程的 hooks 表,最终渲染到界面。
值得特别注意的是modelCapabilities(modelCapabilities.ts):这是必须在新模型发布时同步更新的关键文件。它定义了每个模型/提供商的静态能力描述VoidStaticModelInfo,包括:
| 字段 | 含义 |
|---|---|
contextWindow | 输入上下文窗口大小(token 数) |
reservedOutputTokenSpace | 为输出预留的 token 空间,为null时默认 4096 |
supportsSystemMessage | 系统消息支持方式:false/'system-role'/'developer-role'/'separated'(separated 表示以独立字段传递,如 Anthropic) |
specialToolFormat | 工具调用格式:'openai-style'/'anthropic-style'/'gemini-style' |
supportsFIM | 是否支持 FIM(fill-in-middle)格式,即是否可用于自动补全 |
reasoningCapabilities | 推理能力描述:是否支持推理、能否关闭、能否输出思考内容、预算/努力程度滑块、开源模型的 think 标签等 |
additionalOpenAIPayload | 追加到 OpenAI 兼容请求体中的额外字段 |
cost/downloadable | 每百万 token 价格信息与模型是否可下载(仅信息展示,不参与请求组装) |
该文件还实现了modelOptionsFallback兜底逻辑:对于未能精确匹配的模型名,通过字符串包含规则(如lower.includes('claude')、lower.includes('deepseek')、lower.includes('qwen3')等)回退到最接近的已知模型能力配置,确保任何未知模型也能以合理默认值工作。
Apply 机制:Fast Apply 与 Slow Apply
Void 有两种 Apply:Fast Apply(基于 Search/Replace)与Slow Apply(重写整个文件)。
当用户点击 Apply 且启用了 Fast Apply 时,Void 会提示 LLM 输出如下格式的 Search/Replace 块:
<<<<<<< ORIGINAL // 原始代码 ======= // 替换后的代码 >>>>>>> UPDATED这就是 Void 能在上千行的文件上快速应用改动的原因——它本质上等同于让 LLM 替用户按 Ctrl+F 做一次"查找并替换"查询。相关的提示词模板可以在 prompts.ts 中看到(searchReplaceGivenDescription_systemMessage/searchReplaceGivenDescription_userMessage),而 Search/Replace 块的解析逻辑位于 extractCodeFromResult.ts(extractSearchReplaceBlocks、ExtractedSearchReplaceBlock)。
Apply 内部实现:DiffZone、DiffArea 与流式 Diff
editCodeService(editCodeService.ts)是 Apply 的执行者。同一份代码同时服务于三条路径:LLM 调用 Edit 工具、用户点击 Apply、以及用户提交Cmd+K——三者只是 Fast/Slow Apply 模式不同。
关键术语
- DiffZone:一个
{startLine, endLine}的文本区域,Void 在其中计算并展示红色/绿色的Diff。当文件发生任何改动时,Void 会遍历该文件上的所有 DiffArea 并刷新其 Diff。 - DiffArea:更一般化的抽象,只追踪行号,像 DiffZone 一样。
- 唯一支持"流式"的 DiffArea 是 DiffZone:每个正在流式的 DiffZone 都持有一个
llmCancelToken。
这些数据结构在 editCodeServiceTypes.ts 中有完整定义:DiffZone带_streamState(isStreaming、streamRequestIdRef、当前流式行号line)与_diffOfId映射;Diff由ComputedDiff派生,分为edit/insertion/deletion三种类型;还有CtrlKZone(承载 Cmd+K 输入框)、TrackingZone(携带任意元数据)、VoidFileSnapshot(用于快照审批状态)等。
Apply 的工作流程
- 点击 Apply 时:Void 在整个文件上创建一个DiffZone,这样 LLM 产生的任何改动都会以红/绿 diff 显示出来,随后开始流式传输改动;
- LLM 调用 Edit 时:本质上是调用了 Apply;
- 提交 Cmd+K 时:与 Apply 相同,只是创建一个更小的 DiffZone(而非整个文件)。
Diff 计算与定位的核心辅助函数在 helpers/findDiffs.ts,负责把流式文本与文件内容对比、找出精确的 diff 行区间。editCodeService.ts中还包含了 Search/Replace 块的错误反馈逻辑:当ORIGINAL块在文件中找不到完全匹配、出现多次匹配、或与另一个ORIGINAL块重叠时,会生成人类可读的错误信息回传给 LLM 并中止本次 Apply,保证快速应用不会误改代码。
写文件的内部机制:voidModelService
当 Void 要修改你的代码时,它只是往一个文本 model 里写入内容。这意味着:要向文件写入内容,你只需要知道它的 URI,而不需要手动 load、save 等。为了让这套机制正常工作,背后有一些烦人的 URI/model 生命周期问题,Void 全部封装在了voidModelService(voidModelService.ts)中:
initializeModel(uri):通过ITextModelService.createModelReference(uri)创建 model 引用,并保存在_modelRefOfURI[uri.fsPath]中持有强引用,防止被垃圾回收;getModel(uri)/getModelFromFsPath(fsPath):同步取回{ model, editorModel };getModelSafe(uri):若 model 尚未初始化则先初始化再返回;saveModel(uri):调用ITextFileService.save并设置skipSaveParticipants: true,避免触发扩展的格式化参与器额外改动文件(否则撤销栈里会多出无关条目)。注释中明确说明这样可以让"我们的改动 → 保存"被当作同一次变更处理。
Void 设置内部机制:voidSettingsService
Void 有一个voidSettingsService(voidSettingsService.ts)存储所有 Void 设置——提供商、模型、全局 Void 设置等。可以把它想象成任何核心 Void 服务的隐式依赖:所有核心服务都直接或间接读取它。其状态结构VoidSettingsState包含:
settingsOfProvider:每个提供商的设置(API Key、endpoint、模型列表等);modelSelectionOfFeature:每个功能当前选择的模型;optionsOfModelSelection:每个功能/提供商/模型的选项(如推理预算);overridesOfModel:用户对模型能力的覆盖;globalSettings:全局设置;mcpUserStateOfName:MCP 服务器的用户开关状态;_modelOptions:由以上数据计算出的可选模型列表。
设置术语表
- FeatureName:
Chat|Ctrl+K|Autocomplete|Apply|SCM(源码 voidSettingsTypes.ts 中featureNames定义,含"提交信息生成器"SCM 功能); - ModelSelection:一个
{providerName, modelName}对; - ProviderName:提供商名称,如
'ollama'、'openAI'、'anthropic'、'gemini'、'deepseek'、'openRouter'、'groq'、'xAI'、'mistral'、'vLLM'、'lmStudio'、'liteLLM'、'openAICompatible'、'googleVertex'、'microsoftAzure'、'awsBedrock'; - ModelName:模型名字符串,如
'gpt-4o'、'claude-3-7-sonnet-latest'; - RefreshProvider:会被反复 ping 以更新模型列表的提供商(本地提供商
ollama、vLLM、lmStudio支持自动探测,setAutodetectedModels负责把探测结果合并进模型列表); - ChatMode:
normal|gather|agent(源码中默认值为'agent')。
持久化:加密存储与状态迁移
voidSettingsService的存储实现非常值得借鉴:所有状态通过IEncryptionService加密后,以VOID_SETTINGS_STORAGE_KEY(见 storageKeys.ts)写入IStorageService(StorageScope.APPLICATION、StorageTarget.USER)。每次状态变更都会经过_validatedModelState校验——自动重算_didFillInProviderSettings、重建可选项列表、并在用户所选模型失效时自动回退到列表第一项或null。代码中还包含多条版本迁移逻辑(如 1.0.3 增加includeToolLintErrors、1.2.5 将autoApprove从布尔改为对象、1.3.5 增加 SCM 功能等),保证旧版本用户升级后状态结构自动对齐。
全局设置默认值
源码defaultGlobalSettings给出的默认值如下:
| 设置项 | 默认值 | 含义 |
|---|---|---|
enableFastApply | true | 启用 Fast Apply(Search/Replace) |
chatMode | 'agent' | 对话模式(normal / gather / agent) |
enableAutocomplete | false | 是否启用自动补全 |
autoRefreshModels | true | 自动刷新模型列表 |
syncApplyToChat | true | Apply 跟随 Chat 的模型选择 |
syncSCMToChat | true | SCM 跟随 Chat 的模型选择 |
showInlineSuggestions | true | 显示内联建议 |
includeToolLintErrors | true | 工具 lint 错误是否纳入上下文 |
disableSystemMessage | false | 是否禁用系统消息 |
autoAcceptLLMChanges | false | 是否自动接受 LLM 的改动 |
autoApprove | {} | 按工具类型配置的自动审批 |
aiInstructions | '' | 自定义 AI 指令 |
isOnboardingComplete | false | 新手引导是否完成 |
提供商默认端点
modelCapabilities.ts中的defaultProviderSettings给出了各提供商的默认配置(本地服务端点,便于开箱即用):
- Ollama:
http://127.0.0.1:11434 - vLLM:
http://localhost:8000 - LM Studio:
http://localhost:1234 - LiteLLM:
http://localhost:4000 - OpenAI-Compatible:
endpoint为空,需自行填写 baseURL(如https://my-website.com/v1),支持headersJSON自定义请求头 - Google Vertex:默认
region: 'us-west2' - Microsoft Azure:默认
azureApiVersion: '2024-05-01-preview' - AWS Bedrock:默认
region: 'us-east-1'
审批状态(Approval State)
editCodeService的数据结构中包含了用户需要审查的全部改动信息,但这些信息不是以方便消费的格式存放的。因此 Void 专门编写了一个派生服务,把这些原始数据转换为更实用的"审批状态",供界面(如 diff 上的接受/拒绝按钮)直接消费。相关动作 ID 定义在 actionIDs.ts(VOID_ACCEPT_DIFF_ACTION_ID、VOID_REJECT_DIFF_ACTION_ID),快照结构VoidFileSnapshot与DiffAreaSnapshotEntry则定义在 editCodeServiceTypes.ts——快照只保留diffAreaSnapshotKeys中列出的关键字段(type、diffareaid、originalCode、startLine、endLine、editorId),用于在流式过程中记录"改动前的文件状态"。
构建流程
关于 Void 的构建管线,仓库内注释说明其由独立的构建仓库(void-builder)负责,Void 主仓库专注于源码。这意味着发布流程、打包、签名、自动更新等工程化环节与主仓库解耦,主仓库的 CI 相关配置可参考根目录的 CodeQL.yml 与 gulpfile.js。
VSCode 代码库学习路线
Void 团队为想要深入 VSCode 生态的开发者整理了一份分级参考清单(原文为外部链接,此处转为可操作的知识点摘要):
- 入门必读:VSCode 用户界面指南(auxbar、面板等)与 UX 设计指南(Container、View、Item 等抽象)。
- 贡献者必读:VSCode 源码组织方式——这是整份清单中最重要的一篇,解释了入口文件在哪里、
browser/与common/的含义等;VSCode 内置样式变量,即var(--vscode-{theme 名,点替换为横线})形式的 CSS 变量,以及 Webview 主题化指南。 - 杂项:VSCode 内置命令全集(不常用,仅作参考)。值得注意的是:VSCode 的仓库就是 Monaco 编辑器的源码来源——一个 "editor" 就是一个 Monaco 编辑器,它共享
ITextModel等代码。 - 扩展 API(历史遗留):Void 已经不再是一个扩展,因此这些链接不再是必需,但如果未来重新构建扩展可能会用到——包括扩展所需文件清单、扩展
package.json的 schema、"contributes"挂载机制、完整扩展 API(重点看页面底部的 cancellation tokens、events、disposables 模式)、以及package.json中的 activation events。
结语
从整体看,Void 的架构哲学非常清晰:主进程负责重活(发请求、操作文件系统),浏览器进程负责 UI 与状态,common/承载共享逻辑,Service 单例负责全局状态,Action 负责可重绑定的命令入口。无论你是想为 Void 新增一个提供商、调试 Apply 的 diff 逻辑,还是理解它如何管理模型能力,都可以从 void/ 目录出发,沿着voidSettingsService(配置)→sendLLMMessageService(请求管线)→editCodeService(代码修改)这条主线快速上手。
【免费下载链接】void开源AI代码编辑器,Cursor的替代方案。项目地址: https://gitcode.com/GitHub_Trending/void2/void
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考