參加你附近的 ,瞭解 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 擴充套件性的資訊,請嘗試以下主題