PowerToys 中的 Monaco Editor 集成:从 WebView2 嵌入、语言定制到版本更新与安装包维护
本文以 PowerToys 仓库中 doc/devdocs/common/monaco-editor.md 文档为主体,系统讲解 VS Code 同款编辑器 Monaco 在 PowerToys 中的集成方案:它通过 WebView2 嵌入到 WinUI 3 应用中,为 Registry Preview、文件预览(File Preview)和 Peek 提供带语法高亮的代码文件预览能力。读完本文,你可以掌握 Monaco 资源的目录布局、自定义语言与文件扩展名的注册方式、monaco_languages.json 的生成流程,以及 Monaco 升级和安装包清单自动生成的完整操作路径。
Monaco 在 PowerToys 中的角色与使用位置
Monaco 是驱动 Visual Studio Code 的文本编辑器。在 PowerToys 中,它作为一个组件被集成进来,为桌面应用提供语法高亮、行号、智能编辑等高级文本展示能力。根据 Monaco Editor 文档,Monaco 主要被用于以下三处:
- Registry Preview 模块——用于预览和展示注册表文件(
.reg); - File Preview 预览处理器——在文件资源管理器中预览代码文件时提供语法高亮;
- Peek 模块——用于文件内容的快速预览。
从源码结构看,这三处能力共享同一套运行时资源:src/common/FilePreviewCommon 中的 MonacoHelper 类是预览各方的公共入口,而实际渲染则发生在各模块内的 WebView2 控件中加载的 HTML 页面里。
技术实现:WebView2 虚拟主机与运行时资源定位
Monaco 通过 WebView2 嵌入到 PowerToys 的 WinUI 3 应用中,从而让桌面应用复用 Monaco 的 Web 端能力。理解这一集成,需要掌握两个关键机制:虚拟主机与资源目录定位。
虚拟主机 PowerToysLocalMonaco
MonacoHelper.cs 定义了常量 VirtualHostName = "PowerToysLocalMonaco"。WebView2 的 SetVirtualHostNameToFolderMapping 机制会把本地的 Monaco 资源目录映射为虚拟主机 powertoyslocalmonaco,前端页面因此可以像访问网站一样引用资源。这一点与 index.html 中的引用方式完全对应:
<script src="http://[[PT_URL]]/monacoSRC/min/vs/loader.js"></script>
<script src="http://[[PT_URL]]/monacoSpecialLanguages.js" type="module"></script>
其中 [[PT_URL]]、[[PT_THEME]]、[[PT_CODE]] 等双中括号占位符会在运行时由托管代码替换为实际值。从 index.html 的注释可以看到完整的参数约定:
| 占位符 | 含义 |
|---|---|
[[PT_CODE]] |
文件内容的 Base64 编码 |
[[PT_THEME]] |
vs(浅色)或 vs-dark(深色) |
[[PT_LANG]] |
文件对应的 Monaco 语言 id |
[[PT_WRAP]] |
是否自动换行 |
[[PT_MINIMAP]] |
是否显示小地图 |
[[PT_CONTEXTMENU]] |
是否启用 Monaco 内置右键菜单(Peek 中因内置菜单不工作而禁用,改用自定义菜单) |
页面加载后的流程是:Base64 解码得到代码文本 → 加载 vs/editor/editor.main → 调用 registerAdditionalLanguages 注册 PowerToys 的定制语言 → 基于 customTokenThemeRules 定义一个继承系统主题的自定义主题 → 以 readOnly: true 创建编辑器。此外页面还注册了 Toggle text wrapping、Toggle minimap 等自定义右键菜单动作,并通过遍历 MenuRegistry._menuItems 移除剪切、格式化等不适用于只读预览的菜单项(见 index.html)。
运行时目录定位与语言映射
MonacoHelper 通过 GetRuntimeMonacoDirectory 定位运行时资源:在可执行文件目录下查找 Assets/Monaco(若可执行文件位于 WinUI3Apps 子目录会先纠正路径),找不到则抛出 DirectoryNotFoundException。
扩展名到语言的映射由 monaco_languages.json 驱动。GetLanguage 会把扩展名转为小写后遍历 JSON 中 list 数组各项的 extensions 字段做精确匹配,命中则返回对应语言 id,未命中或发生异常时回退到 plaintext。值得注意的是,同一份 monaco_languages.json 不仅被运行时的 MonacoHelper 使用,还被安装程序引用以注册预览处理器的文件扩展名——这正是后文更新 Monaco 后必须重新生成该文件的原因。
源码目录结构与关键文件
Monaco 相关源文件统一位于 src/Monaco 目录,核心结构如下:
- monacoSRC/min/vs/:从 monaco-editor npm 包中保留下来的最小化(minified)编辑器主程序,含
editor/(编辑器主程序)、basic-languages/(各语言 Monarch 定义)、language/(CSS/HTML/JSON/TS 智能服务)和入口loader.js; - customLanguages/:PowerToys 自行编写的语言定义,当前包含 reg.js(注册表文件)、
gitignore.js、srt.js(字幕文件); - monacoSpecialLanguages.js:把自定义语言与"已有语言的新扩展名"注册到 Monaco 的入口模块;
- customTokenThemeRules.js:自定义 token 的配色规则;
- generateLanguagesJson.html:在浏览器中重新生成
monaco_languages.json的工具页; - index.html:WebView2 加载的预览页面模板。
各模块(如 Registry Preview、File Preview 处理器)则把这套文件作为应用资源打包分发,运行时由 MonacoHelper 从 Assets/Monaco 读取。
语言注册机制:monacoSpecialLanguages.js 深入解读
monacoSpecialLanguages.js 是理解 PowerToys 定制机制的核心文件,它对外暴露异步函数 registerAdditionalLanguages(monaco),内部分两类操作:
1. 为已有语言追加扩展名——registerAdditionalLanguage(id, extensions, originalId, monaco):
// 摘自 monacoSpecialLanguages.js 的 registerAdditionalLanguages
registerAdditionalLanguage("cppExt", [".ino", ".pde"], "cpp", monaco);
registerAdditionalLanguage("xmlExt", [".wsdl", ".projitems", ".csproj", ".fsproj", ".shproj", ".vcxproj", ".vbproj", ".resx", ".resw"], "xml", monaco);
registerAdditionalLanguage("txtExt", [".sln", ".log", ".vsconfig", ".env", ".ahk", ".ion"], "txt", monaco);
registerAdditionalLanguage("iniExt", [".inf", ".gitconfig", ".gitattributes", ".editorconfig"], "ini", monaco);
其实现是:用新的 id 和扩展名数组调用 monaco.languages.register,然后从 vs/basic-languages/<originalId>/<originalId> 中取出原语言的 conf(编辑配置)和 Monarch 词法定义,分别通过 setLanguageConfiguration 和 setMonarchTokensProvider 挂到新 id 上。这样 .ino/.pde 获得与 C++ 完全相同的高亮,.log/.sln 等则获得与纯文本相同的处理——这正对应文档中"若想让 LOG 文件像 TXT 一样被预览,就把 LOG 加到 TXT 语言定义"的示例。
一个值得注意的细节:originalId == "txt" 时直接 return,不为 txt 系语言加载 Monarch 定义(纯文本无需词法分析)。
2. 注册全新语言——registerAdditionalNewLanguage(id, extensions, definition, monaco):
registerAdditionalNewLanguage("reg", [".reg"], regDefinition(), monaco);
registerAdditionalNewLanguage("gitignore", [".gitignore"], gitignoreDefinition(), monaco);
registerAdditionalNewLanguage("srt", [".srt"], srtDefinition(), monaco);
definition 是自定义文件中导出的 Monarch 词法定义。以 reg.js 为例,它定义了 .reg 文件的完整 token 规则:
export function regDefinition() {
return {
tokenPostfix: '.reg',
tokenizer: {
root: [
// Header (case-sensitive)
[/Windows Registry Editor Version 5.00/, 'comment'],
[/REGEDIT4/, 'comment'],
// Keys
[/\[\-.*\]/, 'invalid'],
[/\\.*[^\]]/, 'keyword'],
// Values
[/@/, "keyword"],
[/hex\({0,1}[0-9,a,b]\)|hex|dword(?=\:)/, "type"],
// Hive names (case in-sensitive)
[/HKEY_CLASSES_ROOT/, 'type'],
[/HKEY_LOCAL_MACHINE/, 'type'],
// ...(更多 hive 与符号规则)
]
}
}
};
可以看到它区分了头注释(Windows Registry Editor Version 5.00)、键路径(keyword)、默认值标记(@)、数据类型(hex(4)/dword:/hex(0))以及大小写不敏感的 hive 名(HKEY_*),使 Registry Preview 中对 .reg 的预览具备真正的语义高亮。
另外,文件中的 languageDefinitions() 函数把 cpp、xml、razor、vb、ini、shell 等语言的压缩源码原样内嵌为 AMD define 模块——从源码结构看,这是为了在虚拟主机环境下保证 require('vs/basic-languages/...') 能被同步解析,避免加载时序问题。
自定义 Monaco:新增语言与扩展名操作指南
以下内容整合自 FilePreviewCommon 文档(即主文档指引的详细操作步骤),并对照当前仓库源码给出落点。
添加新的语言定义
-
用 Monarch 语法在 src/Monaco/customLanguages/ 下新建语言定义文件(参照
reg.js),导出一个返回 Monarch 定义的函数,例如export function idDefinition()。记住文件名和导出函数名,后续要用; -
在 monacoSpecialLanguages.js 顶部其他 import 之后添加:
import { idDefinition } from './customLanguages/file.js'; -
在
registerAdditionalLanguages函数中调用:registerAdditionalNewLanguage("id", [".fileExtension"], idDefinition(), monaco)id可以是任意字符串,推荐用某个文件扩展名(如php、reg); -
若需要为新 token 定制颜色,在 customTokenThemeRules.js 中追加一条规则:
{token: 'token-name', foreground: 'ff0000'}除
foreground外还可指定background与fontStyle。注意这些规则作用于所有语言,不应修改默认 token 的颜色,而应为本语言创建专属的新 token。当前仓库中的实际示例是 gitignore 的反向匹配规则:export const customTokenThemeRules = [ {token: 'custom-negation.gitignore', foreground: 'c00ce0'} ]; -
最后执行下面
monaco_languages.json的重新生成步骤,让托管侧的安装与映射同步到新语言。
为已有语言添加文件扩展名
-
在
registerAdditionalLanguages中添加:registerAdditionalLanguage("id", [".fileExtension"], "existingId", monaco)其中
existingId可在 monaco_languages.json 中查到(例如给 php 追加扩展名时,id 设为phpExt、existingId 设为php); -
由于运行环境的模块加载需要,把目标语言的既有 Monarch 定义复制到同文件的
languageDefinitions函数中(原始定义位于 monacoSRC/min/vs/basic-languages/ 对应子目录); -
重新生成
monaco_languages.json。
重新生成 monaco_languages.json
monaco_languages.json 包含 Monaco 支持的全部扩展名与语言 id 映射,MonacoHelper 类和安装程序都依赖它注册预览处理器,因此每次更新 Monaco 或新增语言后都必须重新生成:
- 在本地 Web 服务器上运行 generateLanguagesJson.html(浏览器直接以
file://打开时会阻止所需功能,例如可用 VS Code 的 Preview Server 扩展右键选择Launch on browser); - 浏览器会自动下载新的
monaco_languages.json; - 用下载的文件替换源码目录中的旧文件。
版本管理与 Monaco 更新流程
查看当前版本
monaco-editor.md 说明:当前 Monaco 版本可在 loader.js 文件中名为 versionMonaco 的变量处找到。
更新步骤
更新 Monaco 需要执行以下四个步骤:
- 下载最新版本 Monaco(执行
npm i monaco-editor); - 删除下载文件中除
min文件夹(最小化代码)以外的所有内容,把min文件夹复制/覆盖到项目的 src/Monaco/monacoSRC 目录; - 按上文流程生成新的 Monaco 语言 JSON 文件;
- 用新文件覆盖现有的 monaco_languages.json。
文档给出的耗时参考:整个 Monaco 更新流程通常需要约 30 分钟。此外,由于更新类 PR 基本是"整体替换 Monaco 源码 + 少量微调",可以直接参考历史 Monaco 更新 PR 作为模板。
安装程序中的 Monaco 清单自动生成
Monaco 源文件数量庞大,不可能在 WiX 安装配置中手工逐一罗列。文档提到这一工作由脚本(Generate-Monaco-wxs.ps1)完成:自动为所有 Monaco 文件生成安装清单,避免手工维护。当前仓库中对应实现是 installer/PowerToysSetupVNext/generateMonacoWxs.ps1,其工作流程为:
-
定位 NuGet 包
WixToolset.Heat提供的heat.exe(按 x64/x86 平台选择路径),找不到时快速失败; -
对
..\..\src\Monaco\monacoSRC目录执行目录热(directory harvest):& $heatExe dir "$SourceDir" -out "$OutputFile" -cg "$ComponentGroup" -dr "$DirectoryRef" -var "$Variable" -gg -srd -nologo生成组件组
MonacoSRCHeatGenerated,输出MonacoSRC.wxs,并用变量var.MonacoSRCHarvestPath参数化源路径; -
后处理生成的 WXS:为每个
<Component>注入Software\Classes\powertoys\components下的注册表键值,使卸载时能按组件清理; -
追加
RemoveMonacoSRCFolders组件,为 heat 发现的每个目录生成<RemoveFolder ... On="uninstall"/>条目,保证卸载时目录被移除。
这套机制正是文档所述"简化 Monaco 在 PowerToys 内维护与更新"的落地方式:升级 Monaco 时只需替换 monacoSRC 内容并重新生成语言 JSON,安装清单由 heat 自动重新收割,无需人工增删文件条目。
小结
PowerToys 对 Monaco 的集成体现了一套可复用的"Web 编辑器嵌入桌面应用"工程范式:WebView2 虚拟主机解决资源引用问题,index.html 占位符模板实现主题/内容/行为的运行时注入,monacoSpecialLanguages.js + customLanguages/ 提供轻量的语言扩展点,monaco_languages.json 作为唯一事实来源打通前端高亮与托管侧的扩展名注册,heat 脚本则把百余个静态文件纳入安装包而免去手工维护。若你计划为 PowerToys 预览能力添加新语言、新扩展名或升级 Monaco 版本,上述目录、注册函数与生成流程即为完整的操作依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00