前段时间重构完一个项目后顺手为它开发了一个 VS Code 扩展。而 VS Code 官方文档仅提供了简略的 API 说明和一个参考插件,很多细节均未详述。查阅资料和参考其他相关扩展后,终于实现了我的需求。在此,我将对整个开发流程及相关细节进行总结,供大家参考。
前言
VS Code 原生支持 Markdown,主要通过两个内置扩展实现(以下统称内置 Markdown 扩展):
- Markdown Basics:定义了 Markdown 的语法高亮和部分代码片段。
- Markdown Language Features:提供了 Markdown 的预览功能(包括预览窗口和 Notebook 支持)。
众所周知,VS Code 基于 Electron 开发,本质上是一个结合了 Chromium 和 Node.js 的应用,前后端是分离的。因此,内置 Markdown 扩展的预览流程如下:
- 后端:通过内置若干插件的 Markdown-it,将 Markdown 转换为 HTML。
- 前端:实现了一个 Custom Editor,本质上还是一个 Webview,通过消息传递 HTML。内容被包裹到
.markdown-body
中。
自 #22836 起,VS Code 开放了扩展内置 Markdown 扩展的接口。开发者可以扩展 Markdown 预览的前后端功能:向预览界面注入自定义样式和脚本,添加自定义的 Markdown-it 插件。
声明扩展
由于开发的是 Markdown 扩展,推荐在
activationEvents
字段中限制扩展激活条件,以优化体验。"activationEvents": ["onLanguage:markdown"]VS Code 扩展通过在
package.json
文件的contributes
字段中声明使用的 API。
扩展样式
通过 markdown.previewStyles
为 Markdown 预览添加额外的样式:
"contributes": { "markdown.previewStyles": [ "./style.css" ]}
为便于适配不同主题,VS Code 会在 Webview 的 body 元素上添加一个特殊的类(以及一个数据属性)。
VS Code 将主题分为以下几类,因此你可以使用下面这些类进行样式选择:
vscode-light
:亮色主题。vscode-dark
:暗色主题。vscode-high-contrast
:高对比度主题。vscode-high-contrast-light
:亮色高对比度主题。
.vscode-dark {}.vscode-light{}.vscode-high-contrast:not(.vscode-high-contrast-light) {}.vscode-high-contrast-light{}
你也可以通过 body[data-vscode-theme-kind="vscode-dark"]
来选择相应的主题。
扩展脚本
通过 markdown.previewScripts
为 Markdown 预览添加额外的脚本:
"contributes": { "markdown.previewScripts": [ "./script.js" ]}
需要注意的是,由于安全性限制,此处并非完整的 Webview,无法访问 VS Code 的 API。
此外,为了提升性能,默认 Markdown 插件在 Webview 内容更新时不会完全重新渲染,而是采用增量更新 [1]的策略。因此,默认情况下脚本仅在首次加载时执行。相应的,你需要监听 vscode.markdown.updateContent
事件,以在 Markdown 内容更新时触发相关逻辑。
window.addEventListener('vscode.markdown.updateContent', () => { // 处理内容更新});
由于这种增量更新机制,动态内容的处理会受到一定限制。
扩展 Markdown-it
通过 markdown-it.extensions
为 Markdown 预览添加自定义的 Markdown-it 插件:
"contributes": { "markdown-it.extensions": [ "./plugin.js" ]}
如果在此前你未接触过 Markdown-it 的插件开发,可能开始会和我一样有些困惑。与 Remark 等基于 AST 的设计不同,Markdown-it 使用的是基于 Token 流的机制,在大多数情况下,其结构都是扁平化的,
不过,它的抽象模型要比 Remark 要简单,大概分两步:通过 Rules 定义 Token,再通过 Renderer 渲染。
以下是我实际用到的一个场景,覆盖了 Markdown-it 默认的代码块的渲染逻辑:
const defaultFenceRenderer = md.renderer.rules.fence
md.renderer.rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx] const info = token.info.trim()
if (info === 'markmap') { const content = tokens[idx].content return renderMarkmap(content) }
return defaultFenceRenderer(tokens, idx, options, env, self)}
官方文档 提供了 API 和架构的说明,但并没有详细的示例,可以参考这篇文章。
另外,生成的 API 文档 中的类型有些地方仍有模糊,可以直接参考 @types/markdown-it
中类型定义,很全面。
扩展语法高亮
严格来说,这并不完全属于 Markdown 扩展的范畴,但确实是一个常见的需求场景。
VS Code 借用了 TextMate 语法来描述语法高亮[2],并允许在某种语言中注入其他语言的语法高亮。
TextMate 的原始格式是 XML 的 plist 格式,而 VS Code 则使用其等价的 JSON 或 YAML 格式。为了方便复用已有的规则,可能需要在不同格式之间进行转换。可以配合以下两个工具:
- TextMate Languages:也语法高亮,但主要用它的格式转换。
- TextMate Syntax Highlighting and Intellisense:增强语法高亮和提供 Intellisense 支持。
此处常见的需求是在 Markdown 编辑界面中高亮代码块。可以参考 这个项目。简单来说,需要完成以下两步:
- 自定义语言的语法高亮规则:定义一个语言的语法规则文件。
- 在 Markdown 中注入语法高亮:将自定义语言的语法规则注入到 Markdown 的语法高亮中。
"contributes": { "languages": [{ /// 1. 自定义一个语言的语法高亮规则 "id": "LANGUAGENAME-markdown-injection" }], "grammars": [{ "language": "LANGUAGENAME-markdown-injection", "scopeName": "markdown.LANGUAGENAME.codeblock", "path": "./syntaxes/LANGUAGENAME-markdown-injection.json", "injectTo": [/// 2. 在 Markdown 的语法高亮中注入这个语言的语法高亮 "text.html.markdown" ], "embeddedLanguages": { // 设置嵌套语言 "meta.embedded.block.LANGUAGENAME": "LANGUAGENAME" } }]}
TextMate 的官方文档同样只简单说明了其语法等相关概念(好熟悉),属于 TextMate 使用手册的一部分。如果需要编写自定义的语法高亮规则,可以参考这篇文章,以及其他项目中的 tmLanguage.json
等文件。
触发刷新命令
可以通过执行 markdown.preview.refresh
命令来刷新预览。(一般用于同步设置更新、主题切换等)
import * as vscode from 'vscode'
vscode.commands.executeCommand('markdown.preview.refresh')
结语
以上总结了 Markdown 扩展开发涉及到的一些方面。主要起导航的作用,未做太多延伸,后面也许会按需扩充。
起初开发插件的时候,想用一下仰望已久的 reactive-vscode 来简化对 VS Code API 的调用,最后发现实际上没有用到,就删掉了(不过他脚手架配的挺好的🤣)。如果你有复杂的状态跟踪需求、不想用默认的事件驱动而想使用类似于 Vue 的 Composition API 的形式来开发 VS Code 插件、获得整洁的代码,还是很推荐的!
就这样吧,88!
[1] 具体来说,内置扩展通过 morphdom 对 DOM 进行 diff,自己实现了一个比较逻辑以做细节处理(如保持 details 标签的展开状态等)。
[2] 这个实践被 Sublime Text 发扬光大,也因此 tmLanguage 成为许多项目的语法高亮的标准格式。但也由于 TextMate 使用了 Oniguruma 作为正则表达式引擎,同样也被传染到了许多项目中,包括最近非常流行的语法高亮库 Shiki。当然,也出现了 oniguruma-to-es 这样的项目,来将 Oniguruma 的正则表达式转换为 JavaScript 的正则表达式。