现已推出!阅读有关 11 月份的新功能和修复的信息。

自定义编辑器 API

自定义编辑器允许扩展创建完全可自定义的读/写编辑器,用于替代 VS Code 的标准文本编辑器来处理特定类型的资源。它们具有广泛的用例,例如

  • 直接在 VS Code 中预览着色器或 3D 模型等资源。
  • 为 Markdown 或 XAML 等语言创建所见即所得编辑器。
  • 为 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,例如 "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 的自定义编辑器。

请务必注意,仅当 VS Code 需要创建你的自定义编辑器的实例时,才会调用 onCustomEditor。如果 VS Code 只是向用户显示有关可用自定义编辑器的一些信息(例如,通过 视图:重新打开方式 命令),则不会激活你的扩展。

自定义文本编辑器

自定义文本编辑器使你可以为文本文件创建自定义编辑器。它可以是从纯粹的非结构化文本到 CSV 到 JSON 或 XML 的任何内容。自定义文本编辑器使用 VS Code 的标准 TextDocument 作为其文档模型。

这个自定义编辑器扩展示例包含一个简单的自定义文本编辑器示例,用于猫抓文件(本质上是以.cscratch文件扩展名结尾的 JSON 文件)。让我们来看看实现自定义文本编辑器的一些重要部分。

自定义文本编辑器的生命周期

VS Code 处理自定义文本编辑器的视图组件(Webview)和模型组件 (TextDocument) 的生命周期。当需要创建新的自定义编辑器实例时,VS Code 会调用您的扩展程序;当用户关闭选项卡时,则会清理编辑器实例和文档模型。

要理解这一切如何在实践中运作,让我们从扩展程序的角度来了解当用户打开自定义文本编辑器以及当用户关闭自定义文本编辑器时会发生什么。

打开自定义文本编辑器

使用自定义编辑器扩展示例,以下是用户首次打开 .cscratch 文件时发生的情况

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

    如果扩展程序尚未激活,则会激活它。在激活期间,扩展程序必须确保通过调用 registerCustomEditorProvidercatCustoms.catScratch 注册一个 CustomTextEditorProvider

  2. 然后,VS Code 在注册的 catCustoms.catScratchCustomTextEditorProvider 上调用 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。以下是猫抓扩展程序如何实现此目的的:

  1. 用户单击 Webview 中的 “添加抓痕” 按钮。这会将一条消息从 Webview 发回给扩展程序。

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

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

尽量使您的工作区编辑仅限于更新文档所需的最小更改。另请记住,如果您正在使用诸如 JSON 之类的语言,则您的扩展程序应尝试遵守用户现有的格式约定(空格与制表符、缩进大小等)。

TextDocument 到 Webview

TextDocument 更改时,您的扩展程序还需要确保其 Webview 反映文档的新状态。TextDocument 可以通过用户操作(例如撤消、重做或还原文件);通过其他扩展程序使用 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 提供支持。

自定义编辑器的生命周期

supportsMultipleEditorsPerDocument

默认情况下,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 确实具有显著的内存开销,因此请谨慎使用它。

获取资源数据后,您通常应该使用 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 扩展性的更多信息,请尝试以下主题