参加你附近的 ,了解 VS Code 中的 AI 辅助开发。

自定义编辑器 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 表示。另一方面,CustomReadonlyEditorProviderCustomEditorProvider允许您提供自己的文档模型,这使它们可以用于非文本文件格式。

自定义编辑器每个资源都有一个文档模型,但该文档可能存在多个编辑器实例(视图)。例如,假设您打开一个具有CustomTextEditorProvider的文件,然后运行**视图:拆分编辑器**命令。在这种情况下,仍然只有一个TextDocument,因为工作区中仍然只有一个资源的副本,但现在该资源有两个 Webview。

CustomEditorCustomTextEditor

自定义编辑器分为两类:自定义文本编辑器和自定义编辑器。它们之间的主要区别在于它们如何定义其文档模型。

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 模式与文件名匹配,以确定自定义编辑器是否可以用于它们。例如,*.pngfilenamePattern将为所有 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文件时会发生以下情况

  1. VS Code 触发一个onCustomEditor:catCustoms.catScratch激活事件。

    如果我们的扩展尚未激活,这将激活它。在激活期间,我们的扩展必须通过调用registerCustomEditorProvider确保为catCustoms.catScratch注册一个CustomTextEditorProvider

  2. 然后 VS Code 在已注册的catCustoms.catScratchCustomTextEditorProvider上调用resolveCustomTextEditor

    此方法接受正在打开的资源的TextDocumentWebviewPanel。扩展必须填充此 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。猫抓扩展就是这样实现的

  1. 用户单击 Webview 中的**添加抓痕**按钮。这会将消息发布从 Webview 返回到扩展。

  2. 扩展接收到消息。然后它更新其内部文档模型(在猫抓示例中,只是向 JSON 添加一个新条目)。

  3. 扩展创建一个WorkspaceEdit,将更新后的 JSON 写入文档。此编辑使用vscode.workspace.applyEdit应用。

尽量将您的工作区编辑保持在更新文档所需的最少更改。另请记住,如果您正在处理 JSON 等语言,您的扩展应该尝试遵守用户现有的格式约定(空格与制表符、缩进大小等)。

TextDocument 到 Webview

TextDocument更改时,您的扩展还需要确保其 Webview 反映文档的新状态。TextDocuments 可以通过用户操作(例如撤消、重做或还原文件);通过其他扩展使用WorkspaceEdit;或者通过在 VS Code 的默认文本编辑器中打开文件的用户来更改。猫抓扩展就是这样实现的

  1. 在扩展中,我们订阅了vscode.workspace.onDidChangeTextDocument事件。此事件在TextDocument的每次更改时都会触发(包括我们的自定义编辑器所做的更改!)

  2. 当文档的更改到来时,我们会向 Webview 发布一条包含其新文档状态的消息。然后该 Webview 更新自身以渲染更新后的文档。

重要的是要记住,自定义编辑器触发的任何文件编辑都会导致onDidChangeTextDocument触发。确保您的扩展不会进入更新循环,即用户在 Webview 中进行编辑,这会触发onDidChangeTextDocument,这会导致 Webview 更新,这会导致 Webview 触发对您的扩展的另一次更新,这会触发onDidChangeTextDocument,依此类推。

另请记住,如果您正在处理 JSON 或 XML 等结构化语言,文档可能并非总是处于有效状态。您的扩展必须能够优雅地处理错误或向用户显示错误消息,以便他们了解问题所在以及如何修复它。

最后,如果更新您的 Webview 成本很高,请考虑去抖动您的 Webview 更新。

自定义编辑器

CustomEditorProviderCustomReadonlyEditorProvider允许您为二进制文件格式创建自定义编辑器。此 API 允许您完全控制文件如何向用户显示、如何对其进行编辑,并允许您的扩展挂接到save和其他文件操作。同样,如果您正在为基于文本的文件格式构建编辑器,请强烈考虑使用CustomTextEditor,因为它们实现起来要简单得多。

自定义编辑器扩展示例包含一个用于爪绘文件(只是以.pawdraw文件扩展名结尾的 jpeg 文件)的简单示例自定义二进制编辑器。让我们看看构建二进制文件自定义编辑器所需的内容。

CustomDocument

对于自定义编辑器,您的扩展负责使用CustomDocument接口实现自己的文档模型。这使您的扩展可以自由地在CustomDocument上存储其所需的任何数据,以便与您的自定义编辑器交互,但这也意味着您的扩展必须实现基本的文档操作,例如保存和备份文件数据以进行热退出。

每个打开的文件都有一个CustomDocument。用户可以为单个资源打开多个编辑器——例如通过拆分当前自定义编辑器——但所有这些编辑器都将由同一个CustomDocument支持。

自定义编辑器生命周期

支持每个文档多个编辑器

默认情况下,VS Code 只允许每个自定义文档有一个编辑器。此限制使得正确实现自定义编辑器变得更容易,因为您不必担心多个自定义编辑器实例之间的同步。

但是,如果您的扩展支持此功能,我们建议在注册自定义编辑器时设置supportsMultipleEditorsPerDocument: true,以便可以为同一文档打开多个编辑器实例。这将使您的自定义编辑器的行为更像 VS Code 的普通文本编辑器。

打开自定义编辑器当用户打开与customEditor贡献点匹配的文件时,VS Code 会触发onCustomEditor激活事件,然后调用为提供的视图类型注册的提供程序。CustomEditorProvider有两个角色:提供自定义编辑器的文档,然后提供编辑器本身。以下是自定义编辑器扩展示例catCustoms.pawDraw编辑器发生的情况的有序列表

  1. VS Code 触发一个onCustomEditor:catCustoms.pawDraw激活事件。

    如果我们的扩展尚未激活,这将激活它。我们还必须确保我们的扩展在激活期间为catCustoms.pawDraw编辑器注册一个CustomReadonlyEditorProviderCustomEditorProvider

  2. VS Code 调用为catCustoms.pawDraw编辑器注册的CustomReadonlyEditorProviderCustomEditorProvider上的openCustomDocument

    在这里,我们的扩展被赋予一个资源 URI,并且必须为该资源返回一个新的CustomDocument。这是我们的扩展应该为该资源创建其文档内部模型的时机。这可能涉及从磁盘读取和解析初始资源状态或初始化我们的新CustomDocument

    我们的扩展可以通过创建实现CustomDocument的新类来定义此模型。请记住,此初始化阶段完全由扩展决定;VS Code 不关心扩展在CustomDocument上存储的任何额外信息。

  3. 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再次经历整个openCustomDocumentresolveCustomEditor流程。

只读自定义编辑器

以下许多部分仅适用于支持编辑的自定义编辑器,虽然这听起来可能自相矛盾,但许多自定义编辑器根本不需要编辑功能。例如,考虑一个图像预览。或者内存转储的可视化渲染。两者都可以使用自定义编辑器实现,但都不需要可编辑。这就是CustomReadonlyEditorProvider的用武之地。

CustomReadonlyEditorProvider允许您创建不支持编辑的自定义编辑器。它们仍然可以交互,但不支持撤消和保存等操作。与完全可编辑的自定义编辑器相比,实现只读自定义编辑器也简单得多。

可编辑自定义编辑器基础

可编辑自定义编辑器允许您挂接到标准 VS Code 操作,例如撤消和重做、保存和热退出。这使得可编辑自定义编辑器非常强大,但也意味着正确实现一个比实现可编辑自定义文本编辑器或只读自定义编辑器要复杂得多。

可编辑自定义编辑器由CustomEditorProvider实现。此接口扩展了CustomReadonlyEditorProvider,因此您需要实现基本操作,例如openCustomDocumentresolveCustomEditor,以及一组特定于编辑的操作。让我们看看CustomEditorProvider中特定于编辑的部分。

编辑

对可编辑自定义文档的更改通过编辑来表达。编辑可以是任何内容,从文本更改到图像旋转,再到列表重新排序。VS Code 完全由您的扩展决定编辑的具体操作,但 VS Code 需要知道何时发生编辑。编辑是 VS Code 将文档标记为“脏”的方式,这反过来又启用了自动保存和备份。

每当用户在自定义编辑器的任何 Webview 中进行编辑时,您的扩展都必须从其CustomEditorProvider触发一个onDidChangeCustomDocument事件。onDidChangeCustomDocument事件可以根据您的自定义编辑器实现触发两种事件类型:CustomDocumentContentChangeEventCustomDocumentEditEvent

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 会调用编辑器上的undoredo方法。VS Code 维护一个内部编辑堆栈,因此如果您的扩展使用三个编辑触发onDidChangeCustomDocument,我们称它们为abc

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类型以及您的扩展如何在内部跟踪编辑。

保存的第一步是获取要写入磁盘的数据流。常见的方法包括

  • 跟踪资源的状态,以便可以快速序列化。

    例如,一个基本的图像编辑器可能会维护一个像素数据缓冲区。

  • 重放上次保存以来的编辑以生成新文件。

    例如,一个更高效的图像编辑器可能会跟踪自上次保存以来的编辑,例如croprotatescale。在保存时,它会将这些编辑应用于文件的上次保存状态以生成新文件。

  • 向自定义编辑器的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 扩展性的信息,请尝试以下主题