自定义编辑器API

自定义编辑器允许扩展创建完全可自定义的读/写编辑器,用于代替 VS Code 针对特定类型资源的标准文本编辑器。它们有各种各样的用例,例如:

  • 直接在 VS Code 中预览资产,例如着色器或 3D 模型。
  • 为 Markdown 或 XAML 等语言创建所见即所得编辑器。
  • 为数据文件(例如 CSV、JSON 或 XML)提供替代视觉呈现。
  • 为二进制或文本文件构建完全可定制的编辑体验。

本文档概述了自定义编辑器 API 以及实现自定义编辑器的基础知识。我们将了解两种类型的自定义编辑器及其区别,以及哪一种适合您的用例。然后,对于每种自定义编辑器类型,我们将介绍构建行为良好的自定义编辑器的基础知识。

尽管自定义编辑器是一个强大的新扩展点,但实现基本的自定义编辑器实际上并不那么困难!不过,如果您正在开发第一个 VS Code 扩展,您可能需要考虑推迟深入研究自定义编辑器,直到您更熟悉 VS Code API 的基础知识。自定义编辑器基于许多 VS Code 概念(例如Web 视图和文本文档)构建,因此如果您同时学习所有这些新想法,可能会有点不知所措。

但是,如果您已经准备好并正在考虑要构建的所有很酷的自定义编辑器,那么让我们开始吧!请务必下载自定义编辑器扩展示例,以便您可以按照文档进行操作并了解自定义编辑器 API 如何组合在一起。

VS Code API 使用

自定义编辑器 API 基础知识

自定义编辑器是替代视图,用于替代特定资源的 VS Code 标准文本编辑器。自定义编辑器有两个部分:用户交互的视图和扩展用于与底层资源交互的文档模型。

自定义编辑器的视图端是使用webview实现的。这使您可以使用标准 HTML、CSS 和 JavaScript 构建自定义编辑器的用户界面。Webview 无法直接访问 VS Code API,但它们可以通过来回传递消息来与扩展进行通信。查看我们的webview 文档,了解有关 webview 的更多信息以及使用它们的最佳实践。

自定义编辑器的另一部分是文档模型。该模型是您的扩展如何理解它正在使用的资源(文件)的方式。ACustomTextEditorProvider使用 VS Code 的标准TextDocument作为其文档模型,并且对文件的所有更改都使用 VS Code 的标准文本编辑 API 来表达。CustomReadonlyEditorProvider另一方面CustomEditorProvider让您提供自己的文档模型,这使它们可以用于非文本文件格式。

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

CustomEditorCustomTextEditor

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

ACustomTextEditorProvider使用 VS Code 的标准TextDocument作为其数据模型。您可以将 a 用于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- 指定自定义编辑器对哪些文件处于活动状态。

    是一个或多个glob 模式selector的数组。这些 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作为文档模型。

定义编辑器扩展示例包括一个用于 cat 草稿文件(只是以.cscratch文件扩展名结尾的 JSON 文件)的简单示例自定义文本编辑器。让我们看一下实现自定义文本编辑器的一些重要部分。

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

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

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

打开自定义文本编辑器

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

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

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

  2. 然后,VS Code 调用resolveCustomTextEditor注册CustomTextEditorProvidercatCustoms.catScratch.

    此方法采用 表示TextDocument正在打开的资源和WebviewPanel. 扩展程序必须填写此 webview 面板的初始 HTML 内容。

返回后resolveCustomTextEditor,我们的自定义编辑器将显示给用户。webview 中绘制的内容完全取决于我们的扩展。

每次打开自定义编辑器时都会发生相同的流程,即使您拆分了自定义编辑器也是如此。自定义编辑器的每个实例都有自己的,尽管多个自定义文本编辑器如果用于相同的资源,WebviewPanel则将共享相同的。TextDocument请记住:将 视为TextDocument资源的模型,而 Web 视图面板是该模型的视图。

关闭自定义文本编辑器

当用户关闭自定义文本编辑器时,VS Code 会WebviewPanel.onDidDisposeWebviewPanel. 此时,您的扩展程序应该清理与该编辑器关联的所有资源(事件订阅、文件观察器等)

当给定资源的最后一个自定义编辑器关闭时,TextDocument如果没有其他编辑器使用该资源并且没有其他扩展保留该资源,则该资源的 也将被释放。您可以检查该TextDocument.isClosed物业是否TextDocument已关闭。一旦TextDocument关闭,使用自定义编辑器打开相同的资源将导致TextDocument打开新的资源。

与 TextDocument 同步更改

由于自定义文本编辑器使用 aTextDocument作为其文档模型,因此它们负责TextDocument在自定义编辑器中发生编辑时更新 ,并在发生更改时更新自身TextDocument

从网页视图到TextDocument

自定义文本编辑器中的编辑可以采取多种不同的形式——单击按钮、更改一些文本、拖动一些项目。每当用户在自定义文本编辑器中编辑文件本身时,扩展程序必须更新TextDocument. 以下是猫抓痕扩展的实现方式:

  1. 用户单击Web 视图中的“添加暂存”按钮。会将消息从 web 视图发送回扩展程序。

  2. 分机接收消息。然后,它更新文档的内部模型(在猫抓示例中仅包含向 JSON 添加新条目)。

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

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

TextDocument到 网页浏览量

TextDocument更改时,您的扩展程序还需要确保其 Web 视图反映文档的新状态。文本文档可以通过用户操作进行更改,例如撤消、重做或恢复文件;通过其他扩展使用WorkspaceEdit; 或者由在 VS Code 的默认文本编辑器中打开文件的用户执行。以下是猫抓痕扩展的实现方式:

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

  2. 当我们有编辑器的文档发生更改时,我们会向 Web 视图发布一条消息及其新文档状态。然后,该 Web 视图会更新自身以呈现更新后的文档。

重要的是要记住,自定义编辑器触发的任何文件编辑都会导致onDidChangeTextDocument触发。确保您的扩展程序不会进入更新循环,其中用户在 webview 中进行编辑,这会触发onDidChangeTextDocument,这会导致 webview 更新,这会导致 webview 触发您的扩展程序上的另一个更新,从而触发onDidChangeTextDocument,等等。

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

最后,如果更新 Web 视图的成本很高,请考虑取消对 Web 视图的更新。

自定义编辑器

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

定义编辑器扩展示例包括一个用于 paw 绘制文件(只是以.pawdraw文件扩展名结尾的 jpeg 文件)的简单示例自定义二进制编辑器。让我们看一下为二进制文件构建自定义编辑器的过程。

定制文件

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

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

自定义编辑器生命周期

支持每个文档多个编辑器

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

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

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

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

    如果我们的扩展尚未激活,这将激活它。我们还必须确保我们的扩展程序在激活期间注册 aCustomReadonlyEditorProviderCustomEditorProviderfor catCustoms.pawDraw

  2. VS Code 需要openCustomDocument我们的CustomReadonlyEditorProviderCustomEditorProvider注册的catCustoms.pawDraw编辑器。

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

    我们的扩展可以通过创建一个实现CustomDocument. 请记住,这个初始化阶段完全取决于扩展;VS Code 不关心CustomDocument.

  3. VS Code 使用resolveCustomEditor步骤CustomDocument2 中的 和新的WebviewPanel.

    这里我们的扩展必须填写自定义编辑器的初始 html。如果需要,我们还可以保留对 的引用,WebviewPanel以便稍后可以引用它,例如在命令内部。

返回后resolveCustomEditor,我们的自定义编辑器将显示给用户。

如果用户使用我们的自定义编辑器在另一个编辑器组中打开相同的资源(例如通过拆分第一个编辑器),则扩展的工作会得到简化。在这种情况下,VS Code 仅调用我们在打开第一个编辑器时创建的resolveCustomEditor相同内容。CustomDocument

关闭自定义编辑器

假设我们为同一资源打开了两个自定义编辑器实例。当用户关闭这些编辑器时,VS Code 会向我们的扩展发出信号,以便它可以清理与编辑器关联的任何资源。

当第一个编辑器实例关闭时,VS Code 会在关闭的编辑器WebviewPanel.onDidDispose上触发事件。WebviewPanel此时,我们的扩展必须清理与该特定编辑器实例关联的所有资源。

当第二个编辑器关闭时,VS Code 再次触发WebviewPanel.onDidDispose。但是现在我们还关闭了与CustomDocument. 当 不再有编辑器时CustomDocument,VS Code 会对其调用CustomDocument.dispose。我们的扩展的实现dispose必须清理与文档关联的所有资源。

如果用户随后使用我们的自定义编辑器重新打开相同的资源,我们将使用openCustomDocumentresolveCustomEditorCustomDocument.

只读自定义编辑器

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

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

可编辑的自定义编辑器基础知识

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

可编辑的自定义编辑器是由CustomEditorProvider. 该接口扩展了CustomReadonlyEditorProvider,因此您必须实现基本操作,例如openCustomDocumentresolveCustomEditor,以及一组编辑特定操作。我们来看看编辑的具体部分CustomEditorProvider

编辑

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

每当用户在自定义编辑器的任何 Web 视图中进行编辑时,您的扩展程序都必须onDidChangeCustomDocument从其CustomEditorProvider. 该onDidChangeCustomDocument事件可以触发两种事件类型,具体取决于您的自定义编辑器实现:CustomDocumentContentChangeEventCustomDocumentEditEvent

自定义文档内容更改事件

ACustomDocumentContentChangeEvent是一个简单的编辑。它的唯一功能是告诉 VS Code 文档已被编辑。

当扩展触发CustomDocumentContentChangeEventfrom时onDidChangeCustomDocument,VS Code 会将关联文档标记为脏文档。此时,使文档变得不脏的唯一方法是用户保存或恢复它。使用的自定义编辑器CustomDocumentContentChangeEvent不支持撤消/重做。

自定义文档编辑事件

ACustomDocumentEditEvent是更复杂的编辑,允许撤消/重做。您应该始终尝试使用来实现自定义编辑器,并且只有在无法实现撤消/重做时才CustomDocumentEditEvent回退到使用。CustomDocumentContentChangeEvent

ACustomDocumentEditEvent有以下字段:

  • documentCustomDocument编辑的目的。
  • label— 描述编辑类型的可选文本(例如:“裁剪”、“插入”...)
  • undo— 需要撤消编辑时 VS Code 调用的函数。
  • redo— 需要重做编辑时 VS Code 调用的函数。

当扩展触发CustomDocumentEditEventfrom时onDidChangeCustomDocument,VS Code 会将关联文档标记为脏文档。为了使文档不再脏,用户可以保存或恢复文档,或者撤消/重做回到文档上次保存的状态。

当需要撤消或重新应用特定编辑时,VS Code 会调用编辑器上的undo和方法。redoVS 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

要实现撤消/重做,您的扩展必须更新其关联的自定义文档的内部状态,并更新文档的所有关联的 Web 视图,以便它们反映文档的新状态。请记住,单个资源可能有多个 Web 视图。这些必须始终显示相同的文档数据。例如,图像编辑器的多个实例必须始终显示相同的像素数据,但可以允许每个编辑器实例拥有自己的缩放级别和 UI 状态。

保存

当用户保存自定义编辑器时,您的扩展负责将保存的资源以其当前状态写入磁盘。您的自定义编辑器如何执行此操作很大程度上取决于您的扩展程序的CustomDocument类型以及您的扩展程序内部跟踪编辑的方式。

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

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

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

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

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

  • 要求WebviewPanel使用自定义编辑器来保存文件数据。

    请记住,即使自定义编辑器不可见,也可以保存它们。因此,建议您的扩展的实现save不依赖于WebviewPanel. 如果这是不可能的,您可以使用该WebviewPanelOptions.retainContextWhenHidden设置,以便 web 视图即使在隐藏时也保持活动状态。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 扩展性的更多信息,请尝试以下主题: