自定义编辑器允许插件创建一个基于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/*.json
priority
:可选,标示自定义编辑器何时启用,可取值有两个: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
:编辑发生的CustomDocument
label
:一个可选的描述性文案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);