自定义编辑器 API
自定义编辑器允许扩展创建完全可自定义的读/写编辑器,用于替换 VS Code 标准文本编辑器来处理特定类型的资源。它们有多种用例,例如
- 直接在 VS Code 中预览资产,例如着色器或 3D 模型。
- 为 Markdown 或 XAML 等语言创建所见即所得 (WYSIWYG) 编辑器。
- 为 CSV、JSON 或 XML 等数据文件提供替代的可视化渲染。
- 为二进制或文本文件构建完全可自定义的编辑体验。
本文档提供了自定义编辑器 API 的概述以及实现自定义编辑器的基础知识。我们将探讨两种类型的自定义编辑器及其区别,以及哪种类型适合你的用例。然后,针对这两种自定义编辑器类型,我们将介绍构建一个良好自定义编辑器的基本知识。
虽然自定义编辑器是一个强大的新扩展点,但实现一个基本的自定义编辑器实际上并不难!不过,如果你正在开发你的第一个 VS Code 扩展,你可能需要等到更熟悉 VS Code API 的基础知识后再深入研究自定义编辑器。自定义编辑器建立在许多 VS Code 概念之上——例如 Webview 和文本文档——因此如果同时学习所有这些新概念可能会有点让人不知所措。
但如果你觉得准备好了,并且正在思考你将构建的所有很酷的自定义编辑器,那就开始吧!务必下载自定义编辑器扩展示例,以便你可以对照文档进行操作,并了解自定义编辑器 API 是如何结合起来的。
链接
VS Code API 用法
自定义编辑器 API 基础
自定义编辑器是一种替代视图,用于替换 VS Code 标准文本编辑器来显示特定资源。自定义编辑器有两个部分:用户交互的视图以及你的扩展用于与底层资源交互的文档模型。
自定义编辑器的视图端是使用 Webview 实现的。这允许你使用标准的 HTML、CSS 和 JavaScript 构建自定义编辑器的用户界面。Webview 不能直接访问 VS Code API,但它们可以通过来回传递消息与扩展进行通信。查看我们的 Webview 文档,了解有关 Webview 的更多信息以及使用它们的最佳实践。
自定义编辑器的另一部分是文档模型。此模型是你的扩展理解其正在处理的资源(文件)的方式。CustomTextEditorProvider
使用 VS Code 标准的 TextDocument 作为其文档模型,并且对文件的所有更改都使用 VS Code 标准的文本编辑 API 来表达。另一方面,CustomReadonlyEditorProvider
和 CustomEditorProvider
允许你提供自己的文档模型,这使得它们可以用于非文本文件格式。
自定义编辑器每个资源有一个文档模型,但该文档可能有多个编辑器实例(视图)。例如,假设你打开一个具有 CustomTextEditorProvider
的文件,然后运行 视图: 分割编辑器 命令。在这种情况下,仍然只有一个 TextDocument
,因为工作区中该资源仍然只有一个副本,但现在该资源有两个 Webview。
CustomEditor
对比 CustomTextEditor
自定义编辑器分为两类:自定义文本编辑器和自定义编辑器。它们之间的主要区别在于它们如何定义其文档模型。
CustomTextEditorProvider
使用 VS Code 标准的 TextDocument
作为其数据模型。你可以将 CustomTextEditor
用于任何基于文本的文件类型。CustomTextEditor
的实现要容易得多,因为 VS Code 已经知道如何处理文本文件,因此可以实现保存和文件备份等操作以实现热退出。
另一方面,使用 CustomEditorProvider
时,你的扩展会带来自己的文档模型。这意味着你可以将 CustomEditor
用于图像等二进制格式,但也意味着你的扩展需要负责更多工作,包括实现保存和备份。如果你的自定义编辑器是只读的,例如用于预览的自定义编辑器,则可以跳过许多复杂性。
在决定使用哪种类型的自定义编辑器时,决定通常很简单:如果你处理的是基于文本的文件格式,请使用 CustomTextEditorProvider
;对于二进制文件格式,请使用 CustomEditorProvider
。
贡献点
customEditors
贡献点 是你的扩展告诉 VS Code 它提供哪些自定义编辑器的方式。例如,VS Code 需要知道你的自定义编辑器适用于哪些类型的文件,以及如何在任何 UI 中识别你的自定义编辑器。
以下是 自定义编辑器扩展示例 的基本 customEditor
贡献点示例
"contributes": {
"customEditors": [
{
"viewType": "catEdit.catScratch",
"displayName": "Cat Scratch",
"selector": [
{
"filenamePattern": "*.cscratch"
}
],
"priority": "default"
}
]
}
customEditors
是一个数组,因此你的扩展可以贡献多个自定义编辑器。让我们分解自定义编辑器条目本身
-
viewType
- 自定义编辑器的唯一标识符。这是 VS Code 将
package.json
中的自定义编辑器贡献点与代码中的自定义编辑器实现关联起来的方式。它必须在所有扩展中是唯一的,因此不要使用通用的viewType
,例如"preview"
,请务必使用对你的扩展唯一的标识符,例如"viewType": "myAmazingExtension.svgPreview"
-
displayName
- 在 VS Code UI 中标识自定义编辑器的名称。显示名称显示在 VS Code UI 中,例如 视图: 重新打开方式 下拉菜单。
-
selector
- 指定自定义编辑器对哪些文件激活。selector
是一个包含一个或多个 glob 模式 的数组。这些 glob 模式用于匹配文件名,以确定自定义编辑器是否可用于它们。像*.png
这样的filenamePattern
将启用自定义编辑器用于所有 PNG 文件。你还可以创建更具体的模式来匹配文件或目录名称,例如
**/translations/*.json
。 -
priority
- (可选)指定何时使用自定义编辑器。priority
控制资源打开时何时使用自定义编辑器。可能的值是"default"
- 尝试对每个与自定义编辑器的selector
匹配的文件使用该自定义编辑器。如果给定文件有多个自定义编辑器,用户将不得不选择他们想要使用的自定义编辑器。"option"
- 默认不使用自定义编辑器,但允许用户切换到它或将其配置为默认。
自定义编辑器激活
当用户打开你的自定义编辑器之一时,VS Code 会触发 onCustomEditor:VIEW_TYPE
激活事件。在激活期间,你的扩展必须调用 registerCustomEditorProvider
以注册具有预期 viewType
的自定义编辑器。
值得注意的是,onCustomEditor
仅在 VS Code 需要创建你的自定义编辑器实例时被调用。如果 VS Code 只是向用户显示有关可用自定义编辑器的一些信息——例如使用 视图: 重新打开方式 命令——你的扩展将不会被激活。
自定义文本编辑器
自定义文本编辑器允许你为文本文件创建自定义编辑器。这可以是任何内容,从普通非结构化文本到 CSV、JSON 或 XML。自定义文本编辑器使用 VS Code 标准的 TextDocument 作为其文档模型。
自定义编辑器扩展示例 中包含一个简单的猫抓文件(实际是扩展名为 .cscratch
的 JSON 文件)自定义文本编辑器示例。让我们来看看实现自定义文本编辑器的一些重要部分。
自定义文本编辑器生命周期
VS Code 处理自定义文本编辑器的视图组件(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 内部绘制的内容完全取决于我们的扩展。
每次打开自定义编辑器时,即使是分割自定义编辑器,也会发生相同的流程。自定义编辑器的每个实例都有自己的 WebviewPanel
,但如果多个自定义文本编辑器是针对同一资源,它们将共享相同的 TextDocument
。记住:将 TextDocument
视为资源的模型,而 Webview 面板是该模型的视图。
关闭自定义文本编辑器
当用户关闭自定义文本编辑器时,VS Code 会在该编辑器的 WebviewPanel
上触发 WebviewPanel.onDidDispose
事件。此时,你的扩展应该清理与该编辑器关联的任何资源(事件订阅、文件监视器等)。
当给定资源的最后一个自定义编辑器关闭时,该资源的 TextDocument
也将被释放,前提是没有其他编辑器正在使用它,也没有其他扩展持有它。你可以检查 TextDocument.isClosed
属性以查看 TextDocument
是否已关闭。一旦 TextDocument
关闭,使用自定义编辑器再次打开同一资源将导致打开一个新的 TextDocument
。
与 TextDocument 同步更改
由于自定义文本编辑器使用 TextDocument
作为其文档模型,因此它们负责在自定义编辑器中发生编辑时更新 TextDocument
,并在 TextDocument
更改时更新自身。
从 Webview 到 TextDocument
自定义文本编辑器中的编辑可以采取许多不同的形式——点击按钮、更改文本、拖拽项目等。无论用户何时在自定义文本编辑器内部编辑文件本身,扩展都必须更新 TextDocument
。猫抓扩展的实现方式如下
-
用户点击 Webview 中的 添加草稿 按钮。这会 将消息发送 从 Webview 返回到扩展。
-
扩展接收到消息。然后它更新其内部文档模型(在猫抓示例中,这仅包括向 JSON 添加一个新条目)。
-
扩展创建一个
WorkspaceEdit
,将更新后的 JSON 写入文档。此编辑使用vscode.workspace.applyEdit
应用。
尝试将工作区编辑限制在更新文档所需的最小更改范围内。还要记住,如果你正在使用 JSON 等语言,你的扩展应尝试遵循用户现有的格式约定(空格 vs 制表符、缩进大小等)。
从 TextDocument
到 Webviews
当 TextDocument
发生变化时,你的扩展还需要确保其 Webview 反映文档的新状态。TextDocument 可以通过用户操作(如撤销、重做或还原文件)、其他扩展使用 WorkspaceEdit
,或用户在 VS Code 默认文本编辑器中打开文件来更改。猫抓扩展的实现方式如下
-
在扩展中,我们订阅
vscode.workspace.onDidChangeTextDocument
事件。每次对TextDocument
的更改都会触发此事件(包括我们的自定义编辑器所做的更改!)。 -
当我们有编辑器处理的文档发生更改时,我们会向 Webview 发送一条消息,其中包含其新的文档状态。然后此 Webview 会更新自身以渲染更新后的文档。
务必记住,自定义编辑器触发的任何文件编辑都会导致 onDidChangeTextDocument
触发。确保你的扩展不会陷入更新循环,即用户在 Webview 中进行编辑,触发 onDidChangeTextDocument
,导致 Webview 更新,进而导致 Webview 触发对你的扩展的另一次更新,再次触发 onDidChangeTextDocument
,依此类推。
还要记住,如果你正在使用 JSON 或 XML 等结构化语言,文档可能并非总是处于有效状态。你的扩展必须能够优雅地处理错误,或向用户显示错误消息,以便他们了解问题所在以及如何修复。
最后,如果更新你的 Webview 成本很高,请考虑对 Webview 的更新进行去抖 (debouncing) 处理。
自定义编辑器
CustomEditorProvider
和 CustomReadonlyEditorProvider
允许你为二进制文件格式创建自定义编辑器。此 API 使你可以完全控制文件如何显示给用户、如何进行编辑,并允许你的扩展挂接到 save
和其他文件操作。再次强调,如果你正在为基于文本的文件格式构建编辑器,强烈建议使用 CustomTextEditor
,因为它们的实现要简单得多。
自定义编辑器扩展示例 中包含一个简单的爪印图文件(实际是扩展名为 .pawdraw
的 jpeg 文件)自定义二进制编辑器示例。让我们来看看构建二进制文件自定义编辑器需要做些什么。
CustomDocument
使用自定义编辑器时,你的扩展负责使用 CustomDocument
接口实现自己的文档模型。这使得你的扩展可以自由地在 CustomDocument
上存储与自定义编辑器交互所需的任何数据,但也意味着你的扩展必须实现基本的文档操作,例如保存和备份文件数据以实现热退出。
每个打开的文件有一个 CustomDocument
。用户可以为一个资源打开多个编辑器——例如通过分割当前自定义编辑器——但所有这些编辑器都将由同一个 CustomDocument
支持。
自定义编辑器生命周期
supportsMultipleEditorsPerDocument
默认情况下,VS Code 仅允许每个自定义文档有一个编辑器。此限制使得正确实现自定义编辑器变得更容易,因为你不必担心同步多个自定义编辑器实例。
但是,如果你的扩展能够支持,我们建议在注册自定义编辑器时设置 supportsMultipleEditorsPerDocument: true
,以便可以为同一个文档打开多个编辑器实例。这将使你的自定义编辑器的行为更像 VS Code 的普通文本编辑器。
打开自定义编辑器 当用户打开与 customEditor
贡献点匹配的文件时,VS Code 会触发一个 onCustomEditor
激活事件,然后调用为提供的视图类型注册的提供程序。CustomEditorProvider
有两个角色:为自定义编辑器提供文档,然后提供编辑器本身。以下是 自定义编辑器扩展示例 中 catCustoms.pawDraw
编辑器发生情况的有序列表
-
VS Code 触发一个
onCustomEditor:catCustoms.pawDraw
激活事件。如果我们的扩展尚未激活,这将激活它。我们还必须确保我们的扩展在激活期间注册一个针对
catCustoms.pawDraw
的CustomReadonlyEditorProvider
或CustomEditorProvider
。 -
VS Code 在针对
catCustoms.pawDraw
编辑器注册的CustomReadonlyEditorProvider
或CustomEditorProvider
上调用openCustomDocument
。在在这里,我们的扩展会获得一个资源 URI,并且必须返回该资源的新
CustomDocument
。这是我们的扩展应该为该资源创建其内部文档模型的时间点。这可能涉及从磁盘读取和解析初始资源状态,或初始化我们的新CustomDocument
。我们的扩展可以通过创建一个实现
CustomDocument
的新类来定义此模型。记住,此初始化阶段完全取决于扩展;VS Code 不关心扩展在CustomDocument
上存储的任何附加信息。 -
VS Code 调用
resolveCustomEditor
,传入步骤 2 中的CustomDocument
和一个新的WebviewPanel
。在这里,我们的扩展必须填充自定义编辑器的初始 HTML。如果需要,我们还可以保留对
WebviewPanel
的引用,以便以后可以引用它,例如在命令内部。
一旦 resolveCustomEditor
返回,我们的自定义编辑器就会显示给用户。
如果用户使用我们的自定义编辑器在另一个编辑器组中打开同一资源(例如通过分割第一个编辑器),扩展的工作会简化。在这种情况下,VS Code 只会调用 resolveCustomEditor
,传入我们在打开第一个编辑器时创建的同一个 CustomDocument
。
关闭自定义编辑器
假设我们针对同一资源打开了两个自定义编辑器实例。当用户关闭这些编辑器时,VS Code 会通知我们的扩展,以便它可以清理与该编辑器关联的任何资源。
当第一个编辑器实例关闭时,VS Code 会在关闭的编辑器的 WebviewPanel
上触发 WebviewPanel.onDidDispose
事件。此时,我们的扩展必须清理与该特定编辑器实例关联的任何资源。
当第二个编辑器关闭时,VS Code 再次触发 WebviewPanel.onDidDispose
。然而,此时我们也关闭了与 CustomDocument
关联的所有编辑器。当一个 CustomDocument
没有更多编辑器时,VS Code 会在其上调用 CustomDocument.dispose
。我们的扩展对 dispose
的实现必须清理与文档关联的任何资源。
如果用户随后使用我们的自定义编辑器重新打开同一资源,我们将重新经历整个 openCustomDocument
、resolveCustomEditor
流程,并使用一个新的 CustomDocument
。
只读自定义编辑器
以下许多部分仅适用于支持编辑的自定义编辑器,虽然这听起来有些矛盾,但许多自定义编辑器根本不需要编辑功能。例如,考虑一个图像预览或内存转储的可视化渲染。两者都可以使用自定义编辑器实现,但都不需要可编辑。这就是 CustomReadonlyEditorProvider
的用武之地。
CustomReadonlyEditorProvider
允许你创建不支持编辑的自定义编辑器。它们仍然可以交互,但不支持撤销和保存等操作。与完全可编辑的自定义编辑器相比,实现只读自定义编辑器也简单得多。
可编辑自定义编辑器基础
可编辑自定义编辑器允许你挂接到 VS Code 标准操作,如撤销和重做、保存和热退出。这使得可编辑自定义编辑器非常强大,但也意味着正确实现它比实现可编辑自定义文本编辑器或只读自定义编辑器要复杂得多。
可编辑自定义编辑器由 CustomEditorProvider
实现。此接口扩展了 CustomReadonlyEditorProvider
,因此你必须实现诸如 openCustomDocument
和 resolveCustomEditor
等基本操作,以及一组特定于编辑的操作。让我们看看 CustomEditorProvider
中与编辑相关的部分。
编辑
对可编辑自定义文档的更改通过编辑来表达。编辑可以是任何内容,从文本更改到图像旋转,再到重新排列列表。VS Code 完全由你的扩展决定编辑的具体功能,但 VS Code 需要知道何时发生编辑。编辑是 VS Code 将文档标记为“脏”(dirty)的方式,这反过来又启用自动保存和备份。
每当用户在自定义编辑器的任何 Webview 中进行编辑时,你的扩展必须从其 CustomEditorProvider
中触发一个 onDidChangeCustomDocument
事件。根据你的自定义编辑器实现,onDidChangeCustomDocument
事件可以触发两种事件类型:CustomDocumentContentChangeEvent
和 CustomDocumentEditEvent
。
CustomDocumentContentChangeEvent
CustomDocumentContentChangeEvent
是一个极简的编辑。它的唯一功能是告诉 VS Code 文档已被编辑。
当扩展从 onDidChangeCustomDocument
触发 CustomDocumentContentChangeEvent
时,VS Code 会将相关文档标记为“脏”(dirty)。此时,文档变为非“脏”的唯一方法是用户保存或还原它。使用 CustomDocumentContentChangeEvent
的自定义编辑器不支持撤销/重做。
CustomDocumentEditEvent
CustomDocumentEditEvent
是一个更复杂的编辑,支持撤销/重做。你应该始终尝试使用 CustomDocumentEditEvent
来实现自定义编辑器,并且只有在无法实现撤销/重做时才回退到使用 CustomDocumentContentChangeEvent
。
CustomDocumentEditEvent
具有以下字段
document
— 此编辑所针对的CustomDocument
。label
— 可选文本,描述进行了哪种类型的编辑(例如:“裁剪”、“插入”等)undo
— 当需要撤销此编辑时,VS Code 调用的函数。redo
— 当需要重做此编辑时,VS Code 调用的函数。
当扩展从 onDidChangeCustomDocument
触发 CustomDocumentEditEvent
时,VS Code 会将相关文档标记为“脏”(dirty)。要使文档不再“脏”,用户可以保存或还原文档,或通过撤销/重做回到文档的上次保存状态。
当需要撤销或重新应用特定编辑时,VS Code 会调用编辑器上的 undo
和 redo
方法。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
要实现撤销/重做,你的扩展必须更新相关自定义文档的内部状态,并更新该文档的所有相关 Webview,以便它们反映文档的新状态。请记住,单个资源可能有多个 Webview。它们必须始终显示相同的文档数据。例如,一个图像编辑器的多个实例必须始终显示相同的像素数据,但可以允许每个编辑器实例具有自己的缩放级别和 UI 状态。
保存
当用户保存自定义编辑器时,你的扩展负责将其当前状态的已保存资源写入磁盘。自定义编辑器如何执行此操作在很大程度上取决于你的扩展的 CustomDocument
类型以及你的扩展如何在内部跟踪编辑。
保存的第一步是获取要写入磁盘的数据流。常见方法包括
-
跟踪资源的状态,以便快速序列化。
例如,一个基本的图像编辑器可能会维护一个像素数据缓冲区。
-
重放自上次保存以来的编辑以生成新文件。
例如,一个更高效的图像编辑器可能会跟踪自上次保存以来的编辑,例如
crop
、rotate
、scale
。保存时,它会将这些编辑应用到文件的最后保存状态以生成新文件。 -
向自定义编辑器请求
WebviewPanel
以保存文件数据。请记住,即使自定义编辑器不可见,也可以保存它们。因此,建议你的扩展实现的
save
方法不依赖于WebviewPanel
。如果无法避免,你可以使用WebviewPanelOptions.retainContextWhenHidden
设置,以便 webview 在隐藏时仍保持活动状态。retainContextWhenHidden
会带来显著的内存开销,因此请谨慎使用。
获取资源数据后,通常应使用 工作区 FS API 将其写入磁盘。FS API 接受 UInt8Array
类型的数据,可以写入二进制文件和文本文件。对于二进制文件数据,只需将二进制数据放入 UInt8Array
。对于文本文件数据,使用 Buffer
将字符串转换为 UInt8Array
const writeData = Buffer.from('my text data', 'utf8');
vscode.workspace.fs.writeFile(fileUri, writeData);
下一步
如果你想了解更多关于 VS Code 扩展性的信息,请尝试以下主题