自定义编辑器 API
自定义编辑器允许扩展创建完全可定制的读写编辑器,用于替换 VS Code 的标准文本编辑器以处理特定类型的资源。它们具有多种用例,例如
- 直接在 VS Code 中预览资产,例如着色器或 3D 模型。
- 为 Markdown 或 XAML 等语言创建所见即所得的编辑器。
- 为 CSV、JSON 或 XML 等数据文件提供替代的可视化渲染。
- 为二进制或文本文件构建完全可定制的编辑体验。
本文档概述了自定义编辑器 API 和实现自定义编辑器的基础知识。我们将了解两种类型的自定义编辑器及其区别,以及哪种编辑器适合您的用例。然后,我们将针对每种自定义编辑器类型,介绍构建良好行为的自定义编辑器的基础知识。
虽然自定义编辑器是一个功能强大的新扩展点,但实现基本的自定义编辑器实际上并不困难!但是,如果您正在开发第一个 VS Code 扩展,您可能希望考虑在熟悉 VS Code API 的基础知识之前,暂时不要深入研究自定义编辑器。自定义编辑器建立在许多 VS Code 概念之上,例如 webviews 和文本文档,因此如果您同时学习所有这些新概念,可能会有点让人不知所措。
但是,如果您已经准备好,并且正在考虑构建所有这些很酷的自定义编辑器,那么让我们开始吧!请务必下载 自定义编辑器扩展示例,以便您可以按照文档说明操作,并了解自定义编辑器 API 的组合方式。
链接
VS Code API 用法
自定义编辑器 API 基础
自定义编辑器是一种替代视图,用于替换 VS Code 的标准文本编辑器以处理特定资源。自定义编辑器有两个部分:用户与之交互的视图以及您的扩展用于与底层资源交互的文档模型。
自定义编辑器的视图端是使用 webview 实现的。这使您可以使用标准的 HTML、CSS 和 JavaScript 构建自定义编辑器的用户界面。Webviews 无法直接访问 VS Code API,但它们可以通过相互传递消息与扩展进行通信。请查看我们的 webview 文档,以了解有关 webviews 的更多信息以及使用它们的最佳实践。
自定义编辑器的另一部分是文档模型。此模型是您的扩展用于理解正在处理的资源(文件)的方式。CustomTextEditorProvider
使用 VS Code 的标准 TextDocument 作为其文档模型,对文件的所有更改都使用 VS Code 的标准文本编辑 API 表达。另一方面,CustomReadonlyEditorProvider
和 CustomEditorProvider
允许您提供自己的文档模型,这使它们可以用于非文本文件格式。
自定义编辑器每个资源只有一个文档模型,但可能存在该文档的多个编辑器实例(视图)。例如,假设您打开一个具有 CustomTextEditorProvider
的文件,然后运行 **视图:拆分编辑器** 命令。在这种情况下,仍然只有一个 TextDocument
,因为工作区中只有一份资源副本,但现在有两个 webviews 用于该资源。
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
中的自定义编辑器贡献与代码中的自定义编辑器实现关联起来的方式。这必须在所有扩展之间都是唯一的,因此,请不要使用像"preview"
这样通用的viewType
,而是确保使用一个对您的扩展来说是唯一的viewType
,例如"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
注册一个自定义编辑器。
需要注意的是,只有在 VS Code 需要创建您的自定义编辑器的实例时才会调用 onCustomEditor
。如果 VS Code 只是向用户显示有关可用自定义编辑器的某些信息(例如,使用 **视图:使用...重新打开** 命令),则不会激活您的扩展。
自定义文本编辑器
自定义文本编辑器允许您为文本文件创建自定义编辑器。这可以是任何东西,从纯文本到 CSV,再到 JSON 或 XML。自定义文本编辑器使用 VS Code 的标准 TextDocument 作为其文档模型。
自定义编辑器扩展示例 包含一个简单的示例自定义文本编辑器,用于 cat scratch 文件(它们只是以 .cscratch
文件扩展名结尾的 JSON 文件)。让我们看看实现自定义文本编辑器的一些重要部分。
自定义文本编辑器生命周期
VS Code 处理自定义文本编辑器的视图组件(webviews)和模型组件(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
。以下是猫抓扩展程序实现此功能的方式
-
用户在 webview 中单击“添加草稿”按钮。这会从 webview 回到扩展程序发布一条消息。
-
扩展程序收到消息。然后,它更新其对文档的内部模型(在猫抓示例中,这只是在 JSON 中添加一个新条目)。
-
扩展程序创建一个
WorkspaceEdit
,将更新后的 JSON 写入文档。此编辑使用vscode.workspace.applyEdit
应用。
尽量将您的工作区编辑保持在更新文档所需的最小更改。另外请记住,如果您使用的是 JSON 等语言,您的扩展程序应尝试遵守用户的现有格式约定(空格与制表符、缩进大小等)。
从TextDocument
到 webview
当TextDocument
更改时,您的扩展程序还需要确保其 webview 反映文档的新状态。TextDocument 可以通过用户操作(例如撤消、重做或还原文件)进行更改;通过使用WorkspaceEdit
的其他扩展程序进行更改;或通过在 VS Code 的默认文本编辑器中打开文件的用户进行更改。以下是猫抓扩展程序实现此功能的方式
-
在扩展程序中,我们订阅了
vscode.workspace.onDidChangeTextDocument
事件。此事件会针对TextDocument
的每次更改(包括我们的自定义编辑器进行的更改!)触发。 -
当我们有编辑器时,文档发生更改时,我们会将一条包含其新文档状态的消息发布到 webview。然后,此 webview 更新自身以呈现更新后的文档。
重要的是要记住,任何由自定义编辑器触发的文件编辑都将导致onDidChangeTextDocument
触发。确保您的扩展程序不会进入更新循环,其中用户在 webview 中进行编辑,这会触发onDidChangeTextDocument
,这会导致 webview 更新,这会导致 webview 在您的扩展程序上触发另一个更新,这会触发onDidChangeTextDocument
,等等。
还要记住,如果您使用的是 JSON 或 XML 等结构化语言,则文档可能并不总是处于有效状态。您的扩展程序必须能够优雅地处理错误,或者向用户显示错误消息,以便他们了解错误所在以及如何修复错误。
最后,如果更新 webview 很昂贵,请考虑对更新 webview 进行去抖动。
自定义编辑器
CustomEditorProvider
和CustomReadonlyEditorProvider
允许您为二进制文件格式创建自定义编辑器。此 API 使您可以完全控制向用户显示文件的方式、对其进行编辑的方式,并使您的扩展程序能够挂钩到save
和其他文件操作。同样,如果您正在为基于文本的文件格式构建编辑器,强烈建议使用CustomTextEditor
,因为它们更容易实现。
在自定义编辑器扩展程序示例中,包括一个简单的自定义二进制编辑器示例,用于 paw draw 文件(它们只是以.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 使用步骤 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
的实现必须清理与该文档关联的任何资源。
如果用户随后使用我们的自定义编辑器重新打开同一资源,我们将使用新的CustomDocument
回到整个openCustomDocument
、resolveCustomEditor
流程。
只读自定义编辑器
以下大部分部分仅适用于支持编辑的自定义编辑器,虽然听起来很矛盾,但许多自定义编辑器根本不需要编辑功能。例如,考虑一个图像预览。或者内存转储的可视化呈现。两者都可以使用自定义编辑器实现,但两者都不需要可编辑。这就是CustomReadonlyEditorProvider
的用武之地。
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 可扩展性的更多信息,请尝试以下主题