自定义编辑器允许插件创建一个基于VS Code标准文本编辑器的高度定制化的私人编辑器,这个编辑器可以用于特殊类型的资源,实际应用方面也有不少实际的应用:
- 资源预览,例如3D模型的shader
- 创建所见即所得的编辑器,如
Markdown或XAML - 提供
CVS、JSON、XML文件数据的可视化渲染 - 为二进制或文本文件提供编辑能力
本文提供自定义编辑器API的概括性描述,并且提供了一个自定义编辑器实现的基础知识。我们会关注两种类型的自定义编辑器,告诉你它们的差异以及适用场景。
尽管自定义编辑器功能非常强大,但实现一个基础的自定义编辑器却并不复杂。实现自定义编辑器对于初学者来说,最复杂的是要接触大量的概念,诸如webview以及text document等,这点对于初学者来说可能是比较头疼的。
VS Code提供了官方的示例 vscode-extension-samples 也是很好的学习资料,可以了解API的使用方法
本文涉及到的API:
API基础用法
自定义编辑器是替代VS Code的标准文本编辑器来显示特定资源的另一种视图,它包含两个部分:用户交互的视图和插件中用于与底层资源交互的文档模型
视图部分采用webview实现,开发者采用HTML、CSS、JavaScript来构建用户交互,webview可以通过VS Code API直接和组件相互传递信息,可以查看我们上一篇文章 VS Code插件开发教程(8) Webview 来获取个更多关于webview的信息
文档模型部分,模型的作用是让插件了解它所使用的资源。CustomTextEditorProvider利用VS Code的标准 纯文本文档 作为它的文档模型,所有的文件变化都是用VS Code的标准文本编辑API。CustomReadonlyEditorProvider和CustomEditorProvider会让你创建自己的文档模型用于哪些非文本文件
自定义编辑器对每一种资源都有一个单独文档模型,不过对于同一个文档可能有多个编辑器实例,例如使用View: Split editor功能将同一个文档一分为二
CustomEditor/CustomTextEditor概览
有两类自定义编辑器:自定义文本编辑器和自定义编辑器。二者之间最大的不同是它们定义文档模型的方式
一个CustomTextEditorProvider利用VS Code的标准 纯文本文档 作为数据模型,你可以对任何基于文本的文件类型使用CustomTextEditor。CustomTextEditor使用起来颇为容易,因为VS Code已经知道如何处理文本文件,可以实现多种操作,如保存和备份文件热退出。
CustomEditorProvider则是需要插件用自己的文档模型,这意味着你可以利用CustomEditor来处理独特的二进制文件如图片等,当然这也意味着插件需要做更多的工作,例如实现保存和后退(如果你的编辑器只用来读取预览文件,这些复杂的功能也可以省略)。
Contribution point
我们需要利用customEditors来配置自定义编辑器所需要的信息,例如告知VS Code将哪种文件和自定义编辑器相关联,如下示例:
"contributes": {
"customEditors": [{
"viewType": "catEdit.catScratch",
"displayName": "Cat Scratch",
"selector": [{
"filenamePattern": "*.cscratch"
}],
"priority": "default"
}]
}
customEditors字段的取值是个数组,每个插件可以注册若干自定义编辑器,接下来我们将其逐个字段做介绍:
viewType:自定义编辑器的唯一标识符,VS Code据此来将package.json中注册的自定义编辑器和具体的代码对应,该取值必须是跨插件唯一的displayName:在VS Code's UI中用来展示自定义编辑器时使用,例如View: Reopen with命令的下拉框中显示selector:定义那种文件会激活自定义编辑器,取值是一个glob patterns数组,既可以定义具体的文件名称,如*.png匹配全部的PNG文件,也可以定义文件夹或路径,如**/translations/*.jsonpriority:可选,标示自定义编辑器何时启用,可取值有两个:default:根据selector为每个匹配的文件启用自定义编辑器,如果匹配成功了多个编辑器,则交由用户选择使用哪个option:默认不启用自定义编辑器,而是让用户决定
自定义编辑器的启动
当一个用户打开了自定义编辑器,VS Code会触发onCustomEditor:VIEW_TYPE激活事件,然后插件必须在代码里通过调用registerCustomEditorProvider方法来用指定的viewType注册自定义编辑器。onCustomEditor:VIEW_TYPE只在VS Code创建了编辑器实例的时候才会被触发,用View: Reopen with展示编辑器列表的时候不会触发。
自定义文本编辑器
生命周期
自定义编辑器的生命周期分为可视组件(webview)生命周期和模型组件(TextDocument)生命周期,都由VS Code负责控制,下面我们来梳理下从打开到关闭自定义文本编辑器的整个生命周期流程:
打开自定义文本编辑器
以 官方示例 为例,首次打开.cscratch文件将会发生下列事情:
-
VS Code触发onCustomEditor:catCustoms.catScratch事件,在激活期间,插件务必调用registerCustomEditorProvider为catCustoms.catScratch注册CustomTextEditorProvider -
VS Code为catCustoms.catScratch调用CustomTextEditorProvider中的resolveCustomTextEditor,该方法接受两个参数TextDocument和WebviewPanel,插件需要为webview填充好初始化所必须的HTML内容
一旦resolveCustomTextEditor返回,自定义编辑器就展示给了用户,webview中渲染的内容也完全渲染了
关闭自定义文本编辑器
当用户关闭了一个自定义文本编辑器时,VS Code会触发webview的WebviewPanel.onDidDispose事件,插件需要清理与此编辑器相关的资源如事件监听、文件监视器等
当指定资源的最后一个自定义编辑器关闭,此资源的TextDocument将会被销毁,可以通过读取TextDocument.isClosed获知TextDocument是否被关闭,一旦关闭了,则打开相关资源时会再重新启动一个TextDocument
利用TextDocument同步修改
自定义文本编辑器以TextDocument作为文档模型,所以编辑器需要做好和TextDocument之间的同步,下面是示例代码中的相关实现:
从webview到TextDocument
- 用户在
webview中点击Add scratch按钮,给插件发送信息 - 插件接收到信息,更新
JSON数据 - 插件创建
WorkspaceEdit将更新完毕的JSON文件利用vscode.workspace.applyEdit写入磁盘文件
从TextDocument到webview
当一个TextDocument发生了变化,插件需要确保在webview中如实反映其中的变化,下面是示例代码中的相关实现:
- 插件监听
vscode.workspace.onDidChangeTextDocument事件,任何对TextDocument的改变都会触发该事件 - 当
TextDocument发生改变,像webview传递信息,webview内容更新
任何对磁盘文件的编辑都会触发onDidChangeTextDocument,一定要留意不要陷入更新的死循环。另外还需要对错误异常做好兼容,如处理结构化数据(JSON、XML)时的异常,务必做好兜底处理以及错误信息提示,让用户知道发生了错误以及如何修复。
最后,需要留意,如果webview更新过较频繁或更新代价较高,需要考虑节流措施
自定义编辑器
CustomEditorProvider和CustomReadonlyEditorProvider让开发者具备为二进制文件创建编辑器的能力,赋予了开发者对文件的完全控制(如果是纯文本文档,优先考虑 CustomTextEditor)
官方示例中包含一个简单的自定义二进制文件(其实就是一个以.pawdraw结尾的jpeg文件)编辑器示例,接下来我们一起看下如何创建一个二进制文件编辑器
CustomDocument
对于自定义编辑器来说,插件需要用CustomDocument接口来实现自己的文档模型,相较于TextDocument需要我们自己实现诸如保存等操作。每个打开的文件都有一个CustomDocument,即便同一个文件被多个编辑器打开,其背后也是同一个CustomDocument
自定义编辑器生命周期
默认情况下,不能同时针对一个文件打开多个自定义编辑器,这个限制会让自定义编辑器的实现因不必考虑多编辑器之间的同步而更加简单,不过VS Code也允许通过设置supportsMultipleEditorsPerDocument: true来支持多编辑器模式,这和VS Code的普通文本编辑器行为是一致的。
打开自定义编辑器
当打开了一个符合customEditor配置规则的文件时,自定义编辑器会被打开,VS Code会触发 onCustomEditor 激活事件。针对 官方示例github.com/microsoft/v… ,下面是整个生命周期流程:
-
VS Code触发onCustomEditor:catCustoms.pawDraw激活事件。在激活状态下,插件需要为catCustoms.pawDraw注册CustomReadonlyEditorProvider或CustomEditorProvider -
VS Code调用CustomReadonlyEditorProvider或CustomEditorProvider中的openCustomDocument方法。该方法可以获取到打开的资源uri,并且一定要给这个资源返回一个新的CustomDocument,这里就是插件实现自己文档模型的关键所在,这里可能会涉及到资源初始状态的读取和解析 -
VS Code调用resolveCustomEditor,传递给该方法一个在第二步生成的CustomDocument以及一个WebviewPanel。在该函数中,我们必须为自定义编辑器的webview赋初始的HTML内容。如果有需要的话,可以保存针对WebviewPanel的引用以便接下来继续使用。
一旦resolveCustomEditor执行完毕,自定义编辑器就展示给了用户
关闭自定义编辑器
假设同一个资源正在被两自定义编辑器打开,当用户关闭第一个编辑器时,VS Code触发WebviewPanel.onDidDispose事件,此时插件需要做好编辑器相关资源的清理工作。当第二个编辑器被关闭时,VS Code再次触发WebviewPanel.onDidDispose事件,此时我们已经关闭了所有与CustomDocument相关的编辑器,VS Code会触发CustomDocument.dispose,此时需要做好文档相关资源的清理工作。
只读型自定义编辑器
其实不少自定义编辑器根本不需要编辑功能,例如实现图片预览或内存快照文件的可视化,都不需要用到编辑功能,这就是CustomReadonlyEditorProvider的用武之地。CustomReadonlyEditorProvider让你可以创建一个不支持编辑的自定义编辑器,可以用来展示内容,但不支持撤销或保存等操作。和支持编辑功能的自定义编辑器相比,只读型实现起来更为简单。
可编辑型自定义编辑器入门
可编辑的自定义编辑器让你可以hook到标准的VS Code操作,如撤消、重做,保存和热退出。这使得可编辑自定义编辑器非常强大,但也意味着比只读自定义编辑器复杂得多,需要实现openCustomDocument、resolveCustomEditor等基本操作。
编辑
可编辑文档的任何变化都用“编辑”来表达,一个编辑可以指文本变化、图片旋转或列表重排等,当用户做了一个编辑操作时,插件需要在CustomEditorProvider中触发onDidChangeCustomDocument事件。onDidChangeCustomDocument事件可按两种不同的类型被触发:CustomDocumentContentChangeEvent和CustomDocumentEditEvent
-
CustomDocumentContentChangeEvent:最基础的编辑,告知VS Code一个文档被编辑了。当插件触发了CustomDocumentContentChangeEvent,VS Code会将对应的文档标记为dirty,用户可以通过保存或恢复操作来让文档恢复到non-dirty状态。使用CustomDocumentContentChangeEvent的自定义编辑器不支持撤销和恢复(undo/redo) -
CustomDocumentEditEvent:一个更加复杂的编辑,支持撤销和恢复(undo/redo)。使用场景更多(因为很少需要不支持撤销和恢复),该事件有四个字段:document:编辑发生的CustomDocumentlabel:一个可选的描述性文案undo:一个函数,当undone时被VS Code调用redo:一个函数,当undone时被VS Code调用 当插件触发了CustomDocumentEditEvent,VS Code会将对应的文档标记为dirty,执行保存、恢复或撤销可以让文档恢复到non-dirty状态。undo和redo被VS Code调用,VS Code维护着一个内部的编辑栈,如果插件触发三次onDidChangeCustomDocument(假设称之为a、b、c)
onDidChangeCustomDocument(a); onDidChangeCustomDocument(b); onDidChangeCustomDocument(c);下面是用户操作和对应的函数调用
undo — c.undo() undo — b.undo() redo — b.redo() redo — c.redo() redo — no op, no more edits为了实现
undo和redo,你的编辑器要维护一个内部的状态,包括要更新所有的相关webview,留意一个资源可能有多个webview。例如多个图片编辑器实例必须展示相同的像素数据,但是允许每个编辑器有自己的缩放倍数和UI状态
保存
当一个用户保存了一个自定义编辑器,你的插件负责将被保存的资源即时状态写入到磁盘,具体的行为很大程度上有赖于插件的CustomDocument类型以及你如何追踪编辑操作。
基本的保存操作包含以下要点:
- 追踪资源的状态,以便可以迅速的序列化。例如一个基础的图片编辑器需要维护像素数据
- 重放自上次保存以来的编辑以生成新文件。一个更有效率的图片编辑器会追踪自上次编辑以来的操作,诸如剪裁、旋转、缩放等。待到保存的时候,它可以施加这些编辑在文件的上一次保存的状态上以便生成新的文件
- 需要留意的是,自定义编辑器可以被保存,即便他们还未可见。基于这个原因,推荐插件不要依赖
WebviewPanel去实现保存。如果没法做到,你可以用WebviewPanelOptions.retainContextWhenHidden以确保webview在不可见状态时依然处于激活状态。
获取到资源的数据之后,你通常会用到 文件API 去写磁盘,文件API处理UInt8Array类型的数据,可以输出二进制或文本文件。对于二进制文件数据,简单的讲二进制数据放到UInt8Array中。对于纯文本文件数据,用Buffer将字符串转换为UInt8Array,如:
const writeData = Buffer.from('my text data', 'utf8');
vscode.workspace.fs.writeFile(fileUri, writeData);