自定义编辑器 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 的文件,然后运行 View: Split editor 命令。在这种情况下,由于工作区中仍然只有一个资源的副本,因此仍然只有一个 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 中,例如 View: Reopen with 下拉菜单中。
-
selector- 指定自定义编辑器对哪些文件有效。selector是一个或多个 glob 模式 的数组。这些 glob 模式与文件名匹配,以确定是否可以将自定义编辑器用于它们。例如,*.pngfilenamePattern将为所有 PNG 文件启用自定义编辑器。您还可以创建更具体的模式来匹配文件或目录名称,例如
**/translations/*.json。 -
priority- (可选) 指定何时使用自定义编辑器。priority控制当资源打开时何时使用自定义编辑器。可能的值是"default"- 尝试为与自定义编辑器的selector匹配的每个文件使用自定义编辑器。如果有多个自定义编辑器用于给定文件,则用户必须选择他们想要使用的自定义编辑器。"option"- 默认情况下不使用自定义编辑器,但允许用户切换到它或将其配置为默认编辑器。
自定义编辑器激活
当用户打开您的自定义编辑器之一时,VS Code 会触发 onCustomEditor:VIEW_TYPE 激活事件。在激活期间,您的扩展必须调用 registerCustomEditorProvider 以使用预期的 viewType 注册自定义编辑器。
重要的是要注意,仅当 VS Code 需要创建您的自定义编辑器的实例时,才会调用 onCustomEditor。如果 VS Code 只是向用户显示有关可用自定义编辑器的信息——例如使用 View: Reopen with 命令——您的扩展将不会被激活。
自定义文本编辑器
自定义文本编辑器允许您为文本文件创建自定义编辑器。这可以是任何内容,从纯粹的非结构化文本到 CSV 到 JSON 或 XML。
自定义编辑器扩展示例 包括一个简单的自定义文本编辑器,用于猫抓文件(这些只是以 .cscratch 文件扩展名结尾的 JSON 文件)。让我们来看一下实现自定义文本编辑器的一些重要部分。
自定义文本编辑器生命周期
VS Code 处理自定义文本编辑器的视图组件(webview)和模型组件(TextDocument)的生命周期。VS Code 在需要创建新的自定义编辑器实例时调用您的扩展,并在用户关闭选项卡时清理编辑器实例和文档模型。
为了了解这在实践中是如何工作的,让我们了解一下当用户打开自定义文本编辑器时以及当用户关闭自定义文本编辑器时会发生什么。
打开自定义文本编辑器
使用 自定义编辑器扩展示例,以下是当用户首次打开 .cscratch 文件时发生的情况
-
VS Code 触发
onCustomEditor:catCustoms.catScratch激活事件。这将激活我们的扩展(如果尚未激活)。在激活期间,我们的扩展必须确保扩展通过调用
registerCustomEditorProvider为catCustoms.catScratch注册CustomTextEditorProvider。 -
然后,VS Code 调用已注册的
CustomTextEditorProvider的resolveCustomTextEditor,用于catCustoms.catScratch。此方法获取资源的
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。以下是 cat scratch 扩展实现此功能的方式
-
用户点击 webview 中的 添加 scratch 按钮。这会 发布一条消息 从 webview 发送回扩展。
-
扩展接收到消息。然后,它更新其文档的内部模型(在 cat scratch 示例中,只是将新条目添加到 JSON)。
-
扩展创建一个
WorkspaceEdit,将更新后的 JSON 写入文档。此编辑使用vscode.workspace.applyEdit应用。
尽量将您的工作区编辑保持到更新文档所需的最小更改。同时请记住,如果您正在使用 JSON 等语言,您的扩展应该尝试观察用户现有的格式约定(空格与制表符、缩进大小等)。
从 TextDocument 到 webviews
当 TextDocument 更改时,您的扩展还需要确保其 webviews 反映文档的新状态。TextDocuments 可以通过用户操作(如撤销、重做或还原文件)、其他使用 WorkspaceEdit 的扩展,或用户在 VS Code 的默认文本编辑器中打开文件来更改。以下是 cat scratch 扩展实现此功能的方式
-
在扩展中,我们订阅
vscode.workspace.onDidChangeTextDocument事件。此事件针对TextDocument的每次更改(包括我们的自定义编辑器所做的更改!)都会触发。 -
当针对我们有编辑器的文档发生更改时,我们会向 webview 发布一条消息,其中包含其新的文档状态。然后,此 webview 会更新自身以呈现更新的文档。
重要的是要记住,自定义编辑器触发的任何文件编辑都会导致 onDidChangeTextDocument 触发。确保您的扩展不会陷入更新循环,即用户在 webview 中进行编辑,这会触发 onDidChangeTextDocument,这会导致 webview 更新,这会导致 webview 触发您的扩展上的另一个更新,这会触发 onDidChangeTextDocument,依此类推。
还要记住,如果您正在使用 JSON 或 XML 等结构化语言,则文档可能并不总是处于有效状态。您的扩展必须能够优雅地处理错误,或者向用户显示错误消息,以便他们了解错误是什么以及如何修复它。
最后,如果更新您的 webviews 成本很高,请考虑 节流 对 webview 的更新。
自定义编辑器
CustomEditorProvider 和 CustomReadonlyEditorProvider 允许您为二进制文件格式创建自定义编辑器。此 API 使您能够完全控制文件向用户显示的方式、如何对其进行编辑,并让您的扩展挂接到 save 和其他文件操作。再次说明,如果您正在为基于文本的文件格式构建编辑器,强烈建议使用 CustomTextEditor,因为它们更容易实现。
custom editor extension sample 包含一个简单的自定义二进制编辑器示例,用于 paw draw 文件(它们只是以 .pawdraw 文件扩展名结尾的 jpeg 文件)。让我们看看构建二进制文件自定义编辑器需要什么。
CustomDocument
使用自定义编辑器,您的扩展负责使用 CustomDocument 接口实现其自己的文档模型。这让您的扩展可以自由地存储在 CustomDocument 上的任何所需数据,以便与您的自定义编辑器交互,但也意味着您的扩展必须实现基本文档操作,例如保存和备份文件数据以进行热退出。
每个打开的文件都有一个 CustomDocument。用户可以为单个资源打开多个编辑器——例如,通过拆分当前的自定义编辑器——但所有这些编辑器都将由相同的 CustomDocument 支持。
自定义编辑器生命周期
supportsMultipleEditorsPerDocument
默认情况下,VS Code 仅允许每个自定义文档存在一个编辑器。这种限制使正确实现自定义编辑器更容易,因为您不必担心同步多个自定义编辑器实例。
但是,如果您的扩展可以支持它,我们建议在注册自定义编辑器时设置 supportsMultipleEditorsPerDocument: true,以便可以为同一文档打开多个编辑器实例。这将使您的自定义编辑器行为更像 VS Code 的普通文本编辑器。
打开自定义编辑器 当用户打开与 customEditor 贡献点匹配的文件时,VS Code 会触发一个 onCustomEditor 激活事件,然后调用为提供的视图类型注册的提供程序。CustomEditorProvider 具有两个角色:提供自定义编辑器的文档,然后提供编辑器本身。以下是 catCustoms.pawDraw 编辑器从 custom editor extension sample 中发生的事情的有序列表
-
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 使用来自步骤 2 的
CustomDocument和一个新的WebviewPanel调用resolveCustomEditor。在这里,我们的扩展必须填充自定义编辑器的初始 html。如果需要,我们还可以保留对
WebviewPanel的引用,以便以后引用它,例如在命令内部。
一旦 resolveCustomEditor 返回,我们的自定义编辑器就会显示给用户。
如果用户使用我们的自定义编辑器在另一个编辑器组中打开相同的资源——例如,通过拆分第一个编辑器——扩展的任务就会简化。在这种情况下,VS Code 只是使用我们创建第一个编辑器时创建的相同的 CustomDocument 调用 resolveCustomEditor。
关闭自定义编辑器
假设我们有针对同一资源的两个自定义编辑器实例。当用户关闭这些编辑器时,VS Code 会发出信号给我们的扩展,以便它可以清理与编辑器关联的任何资源。
当第一个编辑器实例关闭时,VS Code 会在关闭的编辑器上的 WebviewPanel 上触发 WebviewPanel.onDidDispose 事件。此时,我们的扩展必须清理与该特定编辑器实例关联的任何资源。
当关闭第二个编辑器时,VS Code 再次触发 WebviewPanel.onDidDispose。但是现在,我们已经关闭了与 CustomDocument 关联的所有编辑器。当没有更多编辑器用于 CustomDocument 时,VS Code 会在其上调用 CustomDocument.dispose。我们扩展的 dispose 实现必须清理与文档关联的任何资源。
如果用户然后使用我们的自定义编辑器重新打开相同的资源,我们将返回到整个 openCustomDocument、resolveCustomEditor 流程,并使用一个新的 CustomDocument。
只读自定义编辑器
以下部分中的许多内容仅适用于支持编辑的自定义编辑器,虽然这听起来可能自相矛盾,但许多自定义编辑器根本不需要编辑功能。例如,考虑一个图像预览。或者内存转储的可视化渲染。两者都可以使用自定义编辑器实现,但都不需要可编辑。
CustomReadonlyEditorProvider 允许您创建不支持编辑的自定义编辑器。它们仍然可以交互,但不支持撤销和保存等操作。与完全可编辑的编辑器相比,实现只读自定义编辑器也更简单。
可编辑自定义编辑器基础
可编辑的自定义编辑器允许你挂接到标准的 VS Code 操作,例如撤销和重做、保存以及热退出。这使得可编辑的自定义编辑器非常强大,但也意味着正确实现一个比实现一个可编辑的自定义文本编辑器或只读自定义编辑器要复杂得多。
可编辑的自定义编辑器通过 CustomEditorProvider 实现。这个接口扩展了 CustomReadonlyEditorProvider,因此你必须实现基本操作,例如 openCustomDocument 和 resolveCustomEditor,以及一组编辑特定的操作。让我们来看看 CustomEditorProvider 中编辑特定的部分。
编辑
对可编辑的自定义文档的更改通过编辑来表达。编辑可以是任何内容,从文本更改到图像旋转,再到列表重新排序。VS Code 将编辑的具体内容完全交给你的扩展来决定,但 VS Code 需要知道编辑何时发生。编辑是 VS Code 标记文档为“已修改”的方式,这反过来又启用了自动保存和备份。
每当用户在你的自定义编辑器的任何 webview 中进行编辑时,你的扩展必须从其 CustomEditorProvider 触发一个 onDidChangeCustomDocument 事件。onDidChangeCustomDocument 事件可以触发两种事件类型,具体取决于你的自定义编辑器实现:CustomDocumentContentChangeEvent 和 CustomDocumentEditEvent。
CustomDocumentContentChangeEvent
CustomDocumentContentChangeEvent 是一个最基本的编辑。它的唯一功能是告诉 VS Code 文档已被编辑。
当扩展从 onDidChangeCustomDocument 触发 CustomDocumentContentChangeEvent 时,VS Code 会将关联的文档标记为“已修改”。此时,文档要变为“未修改”的唯一方法是用户保存或撤销它。使用 CustomDocumentContentChangeEvent 的自定义编辑器不支持撤销/重做。
CustomDocumentEditEvent
CustomDocumentEditEvent 是一个更复杂的编辑,允许进行撤销/重做。你应该始终尝试使用 CustomDocumentEditEvent 实现你的自定义编辑器,只有在无法实现撤销/重做时才回退到使用 CustomDocumentContentChangeEvent。
CustomDocumentEditEvent 具有以下字段
document— 编辑所针对的CustomDocument。label— 可选文本,描述所做的编辑类型(例如:“裁剪”、“插入”...)undo— 当需要撤销编辑时,VS Code 调用此函数。redo— 当需要重做编辑时,VS Code 调用此函数。
当扩展从 onDidChangeCustomDocument 触发 CustomDocumentEditEvent 时,VS Code 会将关联的文档标记为“已修改”。要使文档不再“已修改”,用户可以保存或撤销文档,或者撤销/重做回到文档的上次保存状态。
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具有显著的内存开销,因此请谨慎使用它。
获取资源的的数据后,通常应该使用 workspace FS API 将其写入磁盘。FS API 接受 UInt8Array 的数据,并且可以写入基于二进制和文本的文件。对于二进制文件数据,只需将二进制数据放入 UInt8Array 中。对于文本文件数据,使用 Buffer 将字符串转换为 UInt8Array
const writeData = Buffer.from('my text data', 'utf8');
vscode.workspace.fs.writeFile(fileUri, writeData);
后续步骤
如果你想了解更多关于 VS Code 扩展的信息,请尝试以下主题
- Extension API - 了解完整的 VS Code 扩展 API。
- Extension Capabilities - 了解其他扩展 VS Code 的方法。