自定义编辑器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来构建自定义编辑器的用户界面。Webview不能直接访问VS Code API,但它们可以通过来回传递消息与扩展进行通信。查看我们的webview文档以获取更多关于webview的信息以及使用它们的最佳实践。

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

自定义编辑器每个资源有一个单一的文档模型,但可能有多个该文档的编辑器实例(视图)。例如,假设你打开一个具有CustomTextEditorProvider的文件,然后运行View: Split editor命令。在这种情况下,仍然只有一个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中的自定义编辑器贡献与代码中的自定义编辑器实现联系起来的方式。这在所有扩展中必须是唯一的,因此不要使用通用的viewType,例如"preview",确保使用对您的扩展唯一的类型,例如"viewType": "myAmazingExtension.svgPreview"

  • displayName - 在VS Code的用户界面中标识自定义编辑器的名称。

    显示名称会在VS Code用户界面中展示给用户,例如视图:重新打开方式下拉菜单。

  • selector - 指定自定义编辑器对哪些文件有效。

    selector 是一个包含一个或多个 glob 模式的数组。这些 glob 模式与文件名进行匹配,以确定自定义编辑器是否可以用于它们。例如,*.png 这样的 filenamePattern 将为所有 PNG 文件启用自定义编辑器。

    您还可以创建更具体的模式来匹配文件或目录名称,例如 **/translations/*.json

  • priority - (可选) 指定何时使用自定义编辑器。

    priority 控制当资源打开时何时使用自定义编辑器。可能的值为:

    • "default" - Try to use the custom editor for every file that matches the custom editor's selector. If there are multiple custom editors for a given file, the user will have to select which custom editor they want to use.
    • "option" - Do not use the custom editor by default but allow users to switch to it or configure it as their default.

自定义编辑器激活

当用户打开你的自定义编辑器时,VS Code 会触发一个 onCustomEditor:VIEW_TYPE 激活事件。在激活期间,你的扩展必须调用 registerCustomEditorProvider 来注册一个具有预期 viewType 的自定义编辑器。

需要注意的是,onCustomEditor 仅在 VS Code 需要创建自定义编辑器实例时才会被调用。如果 VS Code 只是向用户展示有关可用自定义编辑器的一些信息——例如使用 View: Reopen with 命令——你的扩展将不会被激活。

自定义文本编辑器

自定义文本编辑器允许您为文本文件创建自定义编辑器。这可以是任何内容,从普通的非结构化文本到CSV,再到JSON或XML。自定义文本编辑器使用VS Code的标准TextDocument作为其文档模型。

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

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

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

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

打开自定义文本编辑器

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

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

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

  2. 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。以下是猫抓扩展实现此功能的方式:

  1. 用户点击网页视图中的添加草稿按钮。这将从网页视图发送消息回扩展程序。

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

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

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

TextDocument到webviews

TextDocument发生变化时,您的扩展程序还需要确保其webviews反映文档的新状态。TextDocuments可以通过用户操作(如撤销、重做或恢复文件)进行更改;通过其他扩展使用WorkspaceEdit;或者通过在VS Code的默认文本编辑器中打开文件的用户进行更改。以下是猫抓扩展程序实现此功能的方式:

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

  2. 当我们收到一个文档的更改,并且我们有一个编辑器时,我们会向webview发送一条消息,包含其新的文档状态。然后,这个webview会更新自身以渲染更新后的文档。

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

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

最后,如果更新您的webviews成本较高,考虑对您的webview更新进行防抖处理。

自定义编辑器

CustomEditorProviderCustomReadonlyEditorProvider 允许您为二进制文件格式创建自定义编辑器。此 API 使您能够完全控制文件如何显示给用户、如何进行编辑,并允许您的扩展程序挂钩到 save 和其他文件操作。再次强调,如果您正在为基于文本的文件格式构建编辑器,强烈建议使用 CustomTextEditor,因为它们实现起来要简单得多。

自定义编辑器扩展示例 包含了一个简单的自定义二进制编辑器示例,用于处理 paw draw 文件(这些文件只是以 .pawdraw 文件扩展名结尾的 jpeg 文件)。让我们来看看如何为二进制文件构建一个自定义编辑器。

自定义文档

使用自定义编辑器,您的扩展程序负责使用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 在我们的 CustomReadonlyEditorProviderCustomEditorProvider 上调用 openCustomDocument,这些提供者是为 catCustoms.pawDraw 编辑器注册的。

    在这里,我们的扩展被赋予了一个资源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.onDidDispose 事件。此时,我们的扩展必须清理与该特定编辑器实例相关的所有资源。

当第二个编辑器关闭时,VS Code 再次触发 WebviewPanel.onDidDispose。然而,现在我们也关闭了与 CustomDocument 相关的所有编辑器。当没有更多编辑器用于 CustomDocument 时,VS Code 会调用其上的 CustomDocument.dispose。我们扩展的 dispose 实现必须清理与文档相关的任何资源。

如果用户随后使用我们的自定义编辑器重新打开相同的资源,我们将通过整个openCustomDocumentresolveCustomEditor流程,并使用一个新的CustomDocument

只读自定义编辑器

以下许多部分仅适用于支持编辑的自定义编辑器,虽然听起来可能有些矛盾,但许多自定义编辑器根本不需要编辑功能。例如,考虑一个图像预览。或者内存转储的可视化渲染。两者都可以使用自定义编辑器实现,但都不需要可编辑。这就是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来实现你的自定义编辑器,只有在无法实现撤销/重做的情况下才回退到使用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。这些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扩展性的信息,请尝试以下主题: