首页
/ PowerToys 中的 Monaco Editor 集成:从 WebView2 嵌入、语言定制到版本更新与安装包维护

PowerToys 中的 Monaco Editor 集成:从 WebView2 嵌入、语言定制到版本更新与安装包维护

2026-09-05 11:57:27作者:昌雅子Ethen

本文以 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 wrappingToggle 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.jssrt.js(字幕文件);
  • monacoSpecialLanguages.js:把自定义语言与"已有语言的新扩展名"注册到 Monaco 的入口模块;
  • customTokenThemeRules.js:自定义 token 的配色规则;
  • generateLanguagesJson.html:在浏览器中重新生成 monaco_languages.json 的工具页;
  • index.html:WebView2 加载的预览页面模板。

各模块(如 Registry Preview、File Preview 处理器)则把这套文件作为应用资源打包分发,运行时由 MonacoHelperAssets/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 词法定义,分别通过 setLanguageConfigurationsetMonarchTokensProvider 挂到新 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 文档(即主文档指引的详细操作步骤),并对照当前仓库源码给出落点。

添加新的语言定义

  1. Monarch 语法在 src/Monaco/customLanguages/ 下新建语言定义文件(参照 reg.js),导出一个返回 Monarch 定义的函数,例如 export function idDefinition()。记住文件名和导出函数名,后续要用;

  2. monacoSpecialLanguages.js 顶部其他 import 之后添加:

    import { idDefinition } from './customLanguages/file.js';
    
  3. registerAdditionalLanguages 函数中调用:

    registerAdditionalNewLanguage("id", [".fileExtension"], idDefinition(), monaco)
    

    id 可以是任意字符串,推荐用某个文件扩展名(如 phpreg);

  4. 若需要为新 token 定制颜色,在 customTokenThemeRules.js 中追加一条规则:

    {token: 'token-name', foreground: 'ff0000'}
    

    foreground 外还可指定 backgroundfontStyle。注意这些规则作用于所有语言,不应修改默认 token 的颜色,而应为本语言创建专属的新 token。当前仓库中的实际示例是 gitignore 的反向匹配规则:

    export const customTokenThemeRules = [
        {token: 'custom-negation.gitignore', foreground: 'c00ce0'}
    ];
    
  5. 最后执行下面 monaco_languages.json 的重新生成步骤,让托管侧的安装与映射同步到新语言。

为已有语言添加文件扩展名

  1. registerAdditionalLanguages 中添加:

    registerAdditionalLanguage("id", [".fileExtension"], "existingId", monaco)
    

    其中 existingId 可在 monaco_languages.json 中查到(例如给 php 追加扩展名时,id 设为 phpExt、existingId 设为 php);

  2. 由于运行环境的模块加载需要,把目标语言的既有 Monarch 定义复制到同文件的 languageDefinitions 函数中(原始定义位于 monacoSRC/min/vs/basic-languages/ 对应子目录);

  3. 重新生成 monaco_languages.json

重新生成 monaco_languages.json

monaco_languages.json 包含 Monaco 支持的全部扩展名与语言 id 映射,MonacoHelper 类和安装程序都依赖它注册预览处理器,因此每次更新 Monaco 或新增语言后都必须重新生成:

  1. 在本地 Web 服务器上运行 generateLanguagesJson.html(浏览器直接以 file:// 打开时会阻止所需功能,例如可用 VS Code 的 Preview Server 扩展右键选择 Launch on browser);
  2. 浏览器会自动下载新的 monaco_languages.json
  3. 用下载的文件替换源码目录中的旧文件。

版本管理与 Monaco 更新流程

查看当前版本

monaco-editor.md 说明:当前 Monaco 版本可在 loader.js 文件中名为 versionMonaco 的变量处找到。

更新步骤

更新 Monaco 需要执行以下四个步骤:

  1. 下载最新版本 Monaco(执行 npm i monaco-editor);
  2. 删除下载文件中除 min 文件夹(最小化代码)以外的所有内容,把 min 文件夹复制/覆盖到项目的 src/Monaco/monacoSRC 目录;
  3. 按上文流程生成新的 Monaco 语言 JSON 文件;
  4. 用新文件覆盖现有的 monaco_languages.json

文档给出的耗时参考:整个 Monaco 更新流程通常需要约 30 分钟。此外,由于更新类 PR 基本是"整体替换 Monaco 源码 + 少量微调",可以直接参考历史 Monaco 更新 PR 作为模板。

安装程序中的 Monaco 清单自动生成

Monaco 源文件数量庞大,不可能在 WiX 安装配置中手工逐一罗列。文档提到这一工作由脚本(Generate-Monaco-wxs.ps1)完成:自动为所有 Monaco 文件生成安装清单,避免手工维护。当前仓库中对应实现是 installer/PowerToysSetupVNext/generateMonacoWxs.ps1,其工作流程为:

  1. 定位 NuGet 包 WixToolset.Heat 提供的 heat.exe(按 x64/x86 平台选择路径),找不到时快速失败;

  2. ..\..\src\Monaco\monacoSRC 目录执行目录热(directory harvest):

    & $heatExe dir "$SourceDir" -out "$OutputFile" -cg "$ComponentGroup" -dr "$DirectoryRef" -var "$Variable" -gg -srd -nologo
    

    生成组件组 MonacoSRCHeatGenerated,输出 MonacoSRC.wxs,并用变量 var.MonacoSRCHarvestPath 参数化源路径;

  3. 后处理生成的 WXS:为每个 <Component> 注入 Software\Classes\powertoys\components 下的注册表键值,使卸载时能按组件清理;

  4. 追加 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 版本,上述目录、注册函数与生成流程即为完整的操作依据。

登录后查看全文
热门项目推荐
相关项目推荐