🚀 在 VS Code 中

自訂編輯器 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,例如 "viewType": "myAmazingExtension.svgPreview"

  • displayName - 在 VS Code UI 中識別自訂編輯器的名稱。

    顯示名稱會顯示在 VS Code UI 中,例如檢視:重新以...開啟下拉式選單。

  • selector - 指定自訂編輯器適用於哪些檔案。

    selector 是一個或多個 glob 模式的陣列。這些 glob 模式會與檔案名稱比對,以判斷是否可以將自訂編輯器用於它們。filenamePattern(例如 *.png)將為所有 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 會在已註冊的 CustomTextEditorProvider 上針對 catCustoms.catScratch 叫用 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 反映文件的新狀態。TextDocument 可以透過使用者動作(例如復原、重做或還原檔案)、透過其他擴充功能使用 WorkspaceEdit 或透過在 VS Code 的預設文字編輯器中開啟檔案的使用者來變更。以下是貓抓擴充功能如何實作此功能

  1. 在擴充功能中,我們訂閱 vscode.workspace.onDidChangeTextDocument 事件。每次 TextDocument 變更(包括我們的自訂編輯器所做的變更!)都會觸發此事件。

  2. 當文件發生變更且我們有該文件的編輯器時,我們會將包含其新文件狀態的訊息發佈到 webview。然後,此 webview 會更新自身以呈現更新的文件。

請務必記住,自訂編輯器觸發的任何檔案編輯都會導致 onDidChangeTextDocument 觸發。請確保您的擴充功能不會進入更新迴圈,使用者在 webview 中進行編輯,這會觸發 onDidChangeTextDocument,這會導致 webview 更新,這會導致 webview 在您的擴充功能上觸發另一次更新,這會觸發 onDidChangeTextDocument,依此類推。

另請記住,如果您正在處理 JSON 或 XML 等結構化語言,則文件可能並非始終處於有效狀態。您的擴充功能必須能夠優雅地處理錯誤,或向使用者顯示錯誤訊息,以便他們了解問題所在以及如何修正。

最後,如果更新您的 webview 成本很高,請考慮對 webview 的更新進行 debounce

自訂編輯器

CustomEditorProviderCustomReadonlyEditorProvider 可讓您為二進位檔案格式建立自訂編輯器。此 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 編輯器發生的事件的排序清單

  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 擴充性的更多資訊,請嘗試以下主題