Slidev Monaco Editor 详解:把代码块变成可编辑编辑器与 Diff 对比视图
本文基于 Slidev 官方文档 Monaco Editor 展开,系统讲解如何在幻灯片的 Markdown 代码块中通过 {monaco} 属性启用完整的 Monaco 编辑器、使用 {monaco-diff} 生成代码差异对比,以及如何调整编辑器高度、行号等细节;并结合 Slidev 源码中的代码块转换器与内置组件实现,解释这些语法背后的处理流程、monaco 相关 headmatter 配置项的默认值与生效机制,帮助你在演讲场景中直接"现场改代码"并深入理解其工程实现。
基本用法:{monaco} 属性
当你希望在演示过程中对代码进行实时修改时,只需在代码块的语言标识后追加 {monaco} 属性,该代码块就会变成一个功能完整的 Monaco 编辑器(即 VS Code 内置的浏览器版编辑器内核):
```ts {monaco}
console.log('HelloWorld')
```
语法解析发生在构建阶段的代码块转换器中。从源码 monaco.ts 可以看到,转换器用正则 ^([\w'-]+)?\s*\{(monaco[\w-]*)\}\s*(\{[^}]*\})?(.*)$ 匹配代码块 info 字符串,依次提取语言标识、Monaco 变体(monaco / monaco-diff / monaco-run 等)以及可选的属性对象:
// packages/slidev/node/syntax/codeblock/monaco.ts
const RE_MONACO = /^([\w'-]+)?\s*\{(monaco[\w-]*)\}\s*(\{[^}]*\})?(.*)$/
原始代码会先经过 lz-string 的 compressToBase64 压缩编码,然后被转换为内置组件的声明:
<Monaco v-bind="{...}" lang="ts" code-lz="压缩后的代码" />
也就是说,{monaco} 并不是在运行时"动态注入"编辑器,而是在编译期把静态代码块替换为携带压缩代码载荷的 <Monaco> 组件,真正的编辑器在客户端懒加载时才被创建(见 Monaco.vue 中 onMounted 时 await import('../setup/monaco') 的懒加载逻辑),因此 Monaco 只会在你实际使用时才被打包。
按环境启用与禁用
自 v0.48.0 起,Monaco 编辑器默认启用、按需打包。如果你希望关闭它,可以在幻灯片 frontmatter 中将 monaco 设为 false,也可以设为 dev 或 build 来按环境条件启用:
---
monaco: false # 也可以设为 `dev` 或 `build` 来条件启用
---
这与转换器中的启用判断逻辑一致(见 monaco.ts):config.monaco === true || config.monaco === mode,其中 mode 即当前运行环境(开发/构建)。frontmatter 中该字段完整的类型定义见 frontmatter.ts:monaco?: boolean | 'dev' | 'build',默认值为 true。
Diff 编辑器:{monaco-diff}
Monaco 还可以生成两段代码之间的差异视图。使用 {monaco-diff} 将代码块变成 Monaco diff 编辑器,并用 ~~~ 分隔线区分原始代码与修改后的代码:
```ts {monaco-diff}
console.log('Original text')
~~~
console.log('Modified text')
```
在源码层面,转换器检测到 monaco-diff 变体后,会用 /^\s*~~~\s*\n/m 把代码拆分为 original 与 modified 两部分:原始代码进入 code-lz 属性,修改后的代码以 diff-lz 属性单独携带(同样经过 lz-string 压缩):
// packages/slidev/node/syntax/codeblock/monaco.ts
if (monaco === 'monaco-diff') {
const [original, modified] = code.split(RE_DIFF_SEPARATOR, 2)
encoded = lz.compressToBase64(original)
diff = modified === undefined ? '' : `diff-lz="${lz.compressToBase64(modified)}"`
}
客户端 Monaco.vue 解压缩出两份代码后,分别创建 original / modified 两个 model,并通过 monaco.editor.createDiffEditor 渲染双栏对比视图;两侧内容尺寸变化都会触发高度重算,保证 diff 面板随内容自适应。如果只写了 ~~~ 而没有后半部分代码,diff-lz 属性会被省略,编辑器按普通模式渲染原始代码。
这一转换过程有集成测试覆盖:integration.test.ts 验证了形如 <<< @/snippets/snippet.ts#snippet {monaco}{propA:1} 的导入代码块同样会被转换为携带 v-bind 与 code-lz 的 <Monaco> 组件,说明 {monaco} 属性对普通代码块和导入片段都适用。
编辑器高度:{height}
默认情况下,Monaco 编辑器的高度基于初始内容固定(对应组件内的默认值 height: 'initial')。如果从一个空代码块或很小的代码块开始,又希望编辑器随你现场敲入的代码自动长高,可以设置 {height:'auto'}:
```ts {monaco} {height:'auto'}
// The editor will automatically grow as you type more code
console.log('Hello, World!')
```
也可以使用 CSS 单位指定具体高度,如 {height:'300px'} 或 {height:'100%'}。
从 Monaco.vue 的实现看,高度是一个响应式计算:
const height = computed(() => {
if (props.height === 'auto')
return `${contentHeight.value}px` // 跟随内容实时变化
if (props.height === 'initial')
return `${initialHeight.value}px` // 锁定首次渲染时的高度
return props.height // 直接透传 '300px' / '100%' 等
})
其中 contentHeight 由编辑器的 onDidContentSizeChange 事件持续更新,并在变化后调用 layout() 重新排版;initialHeight 只在首次取值。这就解释了"默认高度固定、auto 模式动态生长"的行为差异。
行号:全局 lineNumbers 与 {lines}
与其他代码块一样,Monaco 编辑器遵循全局的 lineNumbers headmatter 设置。针对单个编辑器,也可以用 {lines:true} 在保持可编辑的前提下单独开关行号:
```ts {monaco} {lines:true}
console.log('HelloWorld')
```
在组件属性上,lines 的默认值来自全局配置 configs.lineNumbers(即不显式指定时继承全局行为),最终映射为 Monaco 的 lineNumbers 选项,后者还支持 'on' | 'off' | 'relative' | 'interval' 等更细粒度的取值(见 Monaco.vue 的 props 定义)。
传递编辑器选项:editorOptions
如果需要自定义某个编辑器的行为,可以在代码块上直接内联一个符合 Monaco IEditorOptions 定义的 editorOptions 对象:
```ts {monaco} { editorOptions: { wordWrap:'on'} }
console.log('HelloWorld')
```
转换器会把代码块 info 中 {...} 形式的属性对象原样生成为 v-bind="..." 绑定到 <Monaco> 组件上,因此 height、lines、editorOptions 等都是走同一条通道。
如果希望某些选项对所有 Monaco 实例生效,则在 ./setup/monaco.ts 中通过 defineMonacoSetup 返回:
// ./setup/monaco.ts
import { defineMonacoSetup } from '@slidev/types'
export default defineMonacoSetup(() => {
return {
editorOptions: {
wordWrap: 'on'
}
}
})
优先级可以从 Monaco.vue 中 commonOptions 的展开顺序确认:内置默认值(tabSize: 2、fontSize: 11.5、禁用 minimap、fontFamily: var(--slidev-code-font-family) 等)→ setup/monaco.ts 返回的 editorOptions → 代码块上内联的 props.editorOptions,后者优先级更高,保证"单块覆盖全局"的语义。完整的配置说明参见 Configure Monaco。
TypeScript 类型与主题
类型自动加载
当 Monaco 编辑器中使用 TypeScript 并导入本地安装的依赖时,Slidev 会自动为这些依赖安装类型,让编辑器的智能提示开箱即用:
```ts {monaco}
import { ref } from 'vue'
import { useMouse } from '@vueuse/core'
const counter = ref(0)
```
前提是 vue、@vueuse/core 等包已作为本地依赖安装,Slidev 会负责剩下的类型接线;作为 SPA 部署时这些类型也会被一并打包以支持静态托管。
实现链路由两部分组成:
- 虚拟模块 monaco-types.ts:
local模式下会收集幻灯片代码块中扫描到的依赖(data.features.monaco.types),合并 frontmatter 中monacoTypesAdditionalPackages显式声明的额外包,过滤掉monacoTypesIgnorePackages命中的包,并额外把snippets/目录下的.ts/.mts/.cts文件注册为类型来源; - Vite 插件 monacoTypes.ts:将上述解析结果转成实际的
import语句,按包的dependencies递归展开,逐文件addFile注入 Monaco 的 TS 语言服务。
相关 frontmatter 配置项(类型定义见 frontmatter.ts):
---
monacoTypesAdditionalPackages:
- lodash-es
- foo
---
| 配置项 | 取值 | 默认值 | 说明 |
|---|---|---|---|
monaco |
false / true / 'dev' / 'build' |
true |
启用 Monaco,可按环境条件启用 |
monacoTypesSource |
'local' / 'cdn' / 'none' |
'local' |
类型来源;cdn 模式由 @typescript/ata 驱动,完全在客户端运行 |
monacoTypesAdditionalPackages |
string[] |
[] |
显式追加需要加载类型的包 |
monacoTypesIgnorePackages |
string[] |
[] |
忽略的包,支持模糊匹配(见 options.ts 的匹配规则) |
monacoRunUseStrict |
boolean |
true |
{monaco-run} 可运行代码是否以严格模式执行 |
关于 monacoTypesSource: 'cdn':客户端 setup/monaco.ts 会据此调用 @typescript/ata 的 setupTypeAcquisition,从 CDN 按需拉取类型并注入 TS 语言服务,整个流程运行在浏览器端。
主题与 Shiki 复用
自 v0.48.0 起,Monaco 会复用你在 Shiki 配置文件中设定的主题,由 @shikijs/monaco 完成桥接,与其余代码块保持一致的视觉风格,无需额外配置。在 setup/monaco.ts 中可以看到:先通过 shikiToMonaco(highlighter, monaco) 把 Shiki 高亮器接入 Monaco,若主题配置为对象(区分深色/浅色),则监听暗色模式切换、在 themeOption.dark/light 之间调用 monaco.editor.setTheme 动态换肤。
此外,客户端初始化还会设置 TypeScript 语言默认的编译选项(strict: true、module: ESNext、moduleResolution: NodeJs),并在编辑器获得焦点时锁定 Slidev 的全局快捷键(lockShortcuts),避免演示翻页与编辑输入互相干扰。
相关能力与源码索引
Monaco 属性族还有两个兄弟变体,与本特性共享同一套 info 字符串解析机制(转换器中 monaco === 'monaco-run' ? 'runnable' : ''):
{monaco-run}:把代码块变成可运行的编辑器,配套文档见 Configure Code Runners;{monaco-write}:允许通过Ctrl/Cmd + S把编辑内容写回本地文件,客户端在 Monaco.vue 中注册了slidev-save动作并通过 HMR 通道发送slidev:monaco-write事件,服务端插件 monacoWrite.ts 以白名单 + 路径校验(拒绝项目根目录之外的写入)落盘。
关键源码索引:
| 文件 | 作用 |
|---|---|
| packages/slidev/node/syntax/codeblock/monaco.ts | {monaco} / {monaco-diff} 等 info 字符串解析与组件生成 |
| packages/client/builtin/Monaco.vue | 编辑器/ diff 编辑器渲染、高度自适应、选项合并 |
| packages/client/setup/monaco.ts | Worker 注册、Shiki 主题桥接、ATA 类型获取 |
| packages/slidev/node/virtual/monaco-types.ts | 本地类型扫描与虚拟模块生成 |
| packages/slidev/node/vite/monacoTypes.ts | 依赖类型递归解析的 Vite 插件 |
| docs/custom/config-monaco.md | setup/monaco.ts 与全部 Monaco 配置项文档 |
综合来看,Slidev 的 Monaco 集成遵循"编译期标记 + 客户端懒加载"的架构:Markdown 属性只负责声明意图并把代码压缩编码进组件属性,真正的编辑器实例、类型服务与主题都在客户端按需初始化,配合 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 StartedRust0627
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