自訂編輯器 API
自訂編輯器允許擴充功能建立可完全自訂的讀取/寫入編輯器,用來取代 VS Code 的標準文字編輯器,以處理特定類型的資源。它們有廣泛的使用案例,例如:
- 直接在 VS Code 中預覽資產,例如著色器 (shaders) 或 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 來表示。另一方面,CustomReadonlyEditorProvider 和 CustomEditorProvider 允許您提供自己的文件模型,這使得它們可用於非文字檔案格式。
每個資源的自訂編輯器只有一個文件模型,但該文件可能有多個編輯器執行個體(檢視)。例如,想像您開啟一個具有 CustomTextEditorProvider 的檔案,然後執行檢視:分割編輯器 (View: Split editor) 命令。在這種情況下,因為工作區中只有該資源的一個副本,所以仍然只有一個 TextDocument,但現在該資源有了兩個 Webview。
CustomEditor 與 CustomTextEditor
自訂編輯器有兩種類別:自訂文字編輯器 (custom text editors) 和自訂編輯器 (custom editors)。兩者之間的主要區別在於它們定義文件模型的方式。
CustomTextEditorProvider 使用 VS Code 的標準 TextDocument 作為其資料模型。您可以將 CustomTextEditor 用於任何基於文字的檔案類型。CustomTextEditor 的實作要容易得多,因為 VS Code 已經知道如何處理文字檔案,因此可以實作諸如儲存和熱退出 (hot exit) 備份檔案等操作。
另一方面,使用 CustomEditorProvider 時,您的擴充功能會自帶文件模型。這意味著您可以將 CustomEditor 用於影像等二進位格式,但也意味著您的擴充功能需要承擔更多責任,包括實作儲存和備份。如果您的自訂編輯器是唯讀的(例如用於預覽的自訂編輯器),您可以跳過大部分的複雜性。
在決定使用哪種類型的自訂編輯器時,決策通常很簡單:如果您處理的是基於文字的檔案格式,請使用 CustomTextEditorProvider;對於二進位檔案格式,請使用 CustomEditorProvider。
貢獻點 (Contribution point)
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中的自訂編輯器貢獻與您程式碼中自訂編輯器實作連結起來的方式。這在所有擴充功能中必須是唯一的,因此與其使用如"preview"這樣通用的viewType,請務必使用對您的擴充功能唯一的名稱,例如"viewType": "myAmazingExtension.svgPreview"。 -
displayName- 在 VS Code UI 中識別自訂編輯器的名稱。顯示名稱會在 VS Code UI 中呈現給使用者,例如在檢視:重新開啟編輯器 (View: Reopen with) 下拉選單中。
-
selector- 指定自訂編輯器在哪些檔案上啟用。selector是一個或多個 Glob 模式 的陣列。這些 Glob 模式會與檔案名稱進行比對,以確定是否可以使用該自訂編輯器。例如*.png的filenamePattern將為所有 PNG 檔案啟用該自訂編輯器。您也可以建立更具體的模式,根據檔案或目錄名稱進行比對,例如
**/translations/*.json。 -
priority- (選填) 指定何時使用該自訂編輯器。priority控制開啟資源時何時使用自訂編輯器。可能的值包括:"default"- 嘗試對每個符合自訂編輯器selector的檔案使用該自訂編輯器。如果特定檔案有多個自訂編輯器,使用者將必須選擇他們想要使用的編輯器。"option"- 預設不使用該自訂編輯器,但允許使用者切換到它,或將其配置為預設值。
自訂編輯器啟用
當使用者開啟您的其中一個自訂編輯器時,VS Code 會觸發一個 onCustomEditor:VIEW_TYPE 啟用事件。在啟用期間,您的擴充功能必須呼叫 registerCustomEditorProvider,以註冊具有預期 viewType 的自訂編輯器。
請務必注意,只有在 VS Code 需要建立自訂編輯器的執行個體時,才會呼叫 onCustomEditor。如果 VS Code 只是向使用者顯示有關可用自訂編輯器的資訊(例如使用檢視:重新開啟編輯器 (View: Reopen with) 命令),您的擴充功能將不會被啟用。
自訂文字編輯器 (Custom Text Editor)
自訂文字編輯器允許您為文字檔案建立自訂編輯器。這可以是任何格式,從純非結構化文字到 CSV、JSON 或 XML 皆可。自訂文字編輯器使用 VS Code 的標準 TextDocument 作為其文件模型。
自訂編輯器擴充功能範例包含一個簡單的貓咪塗鴉 (cat scratch) 檔案自訂文字編輯器範例(這些檔案只是副檔名為 .cscratch 的 JSON 檔案)。讓我們看看實作自訂文字編輯器的一些重要部分。
自訂文字編輯器生命週期
VS Code 負責處理自訂文字編輯器的檢視元件(Webview)和模型元件 (TextDocument) 的生命週期。當 VS Code 需要建立新的自訂編輯器執行個體時,會呼叫您的擴充功能,並在使用者關閉分頁時清除編輯器執行個體和文件模型。
為了理解這一切在實作中是如何運作的,讓我們從擴充功能的角度來看看使用者開啟和關閉自訂文字編輯器時會發生什麼事。
開啟自訂文字編輯器
使用 自訂編輯器擴充功能範例,以下是使用者首次開啟 .cscratch 檔案時發生的情況:
-
VS Code 觸發
onCustomEditor:catCustoms.catScratch啟用事件。如果我們的擴充功能尚未啟用,這將會啟用它。在啟用期間,我們的擴充功能必須確保透過呼叫
registerCustomEditorProvider,為catCustoms.catScratch註冊一個CustomTextEditorProvider。 -
VS Code 接著會針對已註冊的
catCustoms.catScratch的CustomTextEditorProvider,呼叫resolveCustomTextEditor。此方法會取得正在開啟之資源的
TextDocument和一個WebviewPanel。擴充功能必須填入此 Webview 面板的初始 HTML 內容。
一旦 resolveCustomTextEditor 傳回,我們的自訂編輯器就會顯示給使用者。Webview 內繪製的內容完全取決於我們的擴充功能。
每次開啟自訂編輯器時,都會發生相同的流程,即使是分割自訂編輯器時也是如此。自訂編輯器的每個執行個體都有其自己的 WebviewPanel,儘管如果它們是針對同一個資源,多個自訂文字編輯器將共用同一個 TextDocument。記住:將 TextDocument 視為資源的模型,而 Webview 面板則視為該模型的檢視。
關閉自訂文字編輯器
當使用者關閉自訂文字編輯器時,VS Code 會在 WebviewPanel 上觸發 WebviewPanel.onDidDispose 事件。此時,您的擴充功能應該清除與該編輯器相關聯的任何資源(事件訂閱、檔案監控器等)。
當給定資源的最後一個自訂編輯器被關閉時,如果沒有其他編輯器在使用它,也沒有其他擴充功能持有它,則該資源的 TextDocument 也將被處置 (disposed)。您可以檢查 TextDocument.isClosed 屬性,查看 TextDocument 是否已關閉。一旦 TextDocument 被關閉,使用自訂編輯器重新開啟同一個資源將會導致開啟一個新的 TextDocument。
與 TextDocument 同步變更
由於自訂文字編輯器使用 TextDocument 作為其文件模型,它們必須負責在自訂編輯器中發生編輯時更新 TextDocument,以及在 TextDocument 變更時更新自身。
從 Webview 到 TextDocument
自訂文字編輯器中的編輯可以採取多種不同的形式——點擊按鈕、變更某些文字、拖曳項目等。每當使用者在自訂文字編輯器內編輯檔案本身時,擴充功能都必須更新 TextDocument。以下是貓咪塗鴉擴充功能實作此功能的方式:
-
使用者點擊 Webview 中的新增塗鴉 (Add scratch) 按鈕。這會從 Webview 傳送訊息 回到擴充功能。
-
擴充功能接收訊息。然後它會更新其內部的文件模型(在貓咪塗鴉範例中,僅包含向 JSON 新增一個新項目)。
-
擴充功能建立一個將更新後的 JSON 寫入文件的
WorkspaceEdit。此編輯作業會使用vscode.workspace.applyEdit來套用。
請嘗試將您的工作區編輯 (workspace edit) 限制為更新文件所需的最小變更。此外,請記住,如果您正在處理 JSON 等語言,您的擴充功能應嘗試遵循使用者的現有格式慣例(空格與定位點、縮排大小等)。
從 TextDocument 到 Webview
當 TextDocument 變更時,您的擴充功能也需要確保其 Webview 反映了文件的新狀態。TextDocument 可能會因為使用者的操作而變更,例如復原 (undo)、重做 (redo) 或還原檔案;透過使用 WorkspaceEdit 的其他擴充功能;或是因為使用者在 VS Code 的預設文字編輯器中開啟了該檔案。以下是貓咪塗鴉擴充功能實作此功能的方式:
-
在擴充功能中,我們訂閱了
vscode.workspace.onDidChangeTextDocument事件。此事件會針對TextDocument的每一次變更觸發(包括我們的自訂編輯器所做的變更!)。 -
當我們有編輯器的文件發生變更時,我們會將訊息連同其新的文件狀態傳送給 Webview。接著該 Webview 會自我更新,以呈現更新後的文件。
務必記住,自訂編輯器觸發的任何檔案編輯都會導致 onDidChangeTextDocument 被觸發。請確保您的擴充功能不會陷入更新迴圈:即使用者在 Webview 中進行編輯,觸發了 onDidChangeTextDocument,導致 Webview 更新,這又觸發了 Webview 向您的擴充功能執行另一個更新,進而再次觸發 onDidChangeTextDocument,如此循環。
也請記住,如果您處理的是 JSON 或 XML 等結構化語言,文件可能並不總是處於有效狀態。您的擴充功能必須能夠妥善處理錯誤,或向使用者顯示錯誤訊息,讓他們了解問題所在以及如何修正。
最後,如果更新 Webview 的成本很高,請考慮對更新進行 防抖動 (debouncing)。
自訂編輯器
CustomEditorProvider 和 CustomReadonlyEditorProvider 允許您為二進位檔案格式建立自訂編輯器。此 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 編輯器發生情況的順序列表:
-
VS Code 觸發
onCustomEditor:catCustoms.pawDraw啟用事件。如果我們的擴充功能尚未啟用,這將會啟用它。我們還必須確保我們的擴充功能在啟用期間為
catCustoms.pawDraw註冊了CustomReadonlyEditorProvider或CustomEditorProvider。 -
VS Code 呼叫為
catCustoms.pawDraw編輯器註冊的CustomReadonlyEditorProvider或CustomEditorProvider上的openCustomDocument。在此,我們的擴充功能會獲得一個資源 URI,並必須為該資源傳回一個新的
CustomDocument。這是我們擴充功能應該為該資源建立其文件內部模型的時機。這可能涉及從磁碟讀取並解析初始資源狀態,或是初始化我們新的CustomDocument。我們的擴充功能可以透過建立一個實作
CustomDocument的新類別來定義此模型。請記住,此初始化階段完全取決於擴充功能;VS Code 不關心擴充功能在CustomDocument上儲存的任何額外資訊。 -
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 重新經歷整個 openCustomDocument 和 resolveCustomEditor 流程。
唯讀自訂編輯器
以下許多章節僅適用於支援編輯的自訂編輯器,而且聽起來可能很矛盾,許多自訂編輯器根本不需要編輯功能。以影像預覽為例,或是記憶體傾印 (memory dump) 的視覺化呈現。兩者都可以使用自訂編輯器實作,但都不需要可編輯。這就是 CustomReadonlyEditorProvider 發揮作用的地方。
CustomReadonlyEditorProvider 允許您建立不支援編輯的自訂編輯器。它們仍然可以是互動式的,但不支援復原 (undo) 和儲存 (save) 等操作。與完全可編輯的編輯器相比,實作唯讀的自訂編輯器也簡單得多。
可編輯自訂編輯器基礎
可編輯的自訂編輯器讓您可以掛鉤到標準的 VS Code 操作,例如復原、重做、儲存和熱退出。這使得可編輯的自訂編輯器非常強大,但也意味著正確實作一個編輯器比實作一個可編輯的自訂文字編輯器或唯讀自訂編輯器要複雜得多。
可編輯的自訂編輯器由 CustomEditorProvider 實作。此介面擴充了 CustomReadonlyEditorProvider,因此您必須實作基本操作,例如 openCustomDocument 和 resolveCustomEditor,以及一組編輯特定的操作。讓我們來看看 CustomEditorProvider 中與編輯相關的部分。
編輯 (Edits)
對可編輯自訂文件的變更透過編輯來表示。編輯可以是從文字變更、影像旋轉到重新排序清單的任何內容。VS Code 將編輯具體執行什麼內容完全留給您的擴充功能處理,但 VS Code 確實需要知道編輯何時發生。編輯是 VS Code 將文件標記為「髒」(dirty) 的方式,這進而啟用了自動儲存和備份。
每當使用者在您自訂編輯器的任何 Webview 中進行編輯時,您的擴充功能都必須從其 CustomEditorProvider 觸發 onDidChangeCustomDocument 事件。根據您的自訂編輯器實作,onDidChangeCustomDocument 事件可以觸發兩種類型的事件:CustomDocumentContentChangeEvent 和 CustomDocumentEditEvent。
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 會將相關聯的文件標記為「髒」。為了讓文件不再是「髒」狀態,使用者可以儲存或還原文件,或是復原/重做回文件上一次儲存的狀態。
編輯器上的 undo 和 redo 方法在該特定編輯需要被復原或重新套用時由 VS Code 呼叫。VS 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
為了實作復原/重做,您的擴充功能必須更新其關聯自訂文件的內部狀態,以及更新所有關聯的 Webview,使它們反映文件的新狀態。請記住,單一資源可能有多個 Webview。它們必須始終顯示相同的文件資料。例如,影像編輯器的多個執行個體必須始終顯示相同的像素資料,但可以允許每個編輯器執行個體擁有自己的縮放層級和 UI 狀態。
儲存 (Saving)
當使用者儲存自訂編輯器時,您的擴充功能負責將目前狀態的儲存資源寫入磁碟。您的自訂編輯器如何執行此操作,很大程度上取決於您擴充功能的 CustomDocument 類型,以及您的擴充功能在內部追蹤編輯的方式。
儲存的第一步是取得要寫入磁碟的資料流。常見的方法包括:
-
追蹤資源的狀態,以便可以快速序列化。
例如,一個基本的影像編輯器可能會維護一個像素資料的緩衝區。
-
重播自上次儲存以來的編輯以產生新檔案。
例如,一個更有效率的影像編輯器可能會追蹤自上次儲存以來的編輯,例如
crop(裁剪)、rotate(旋轉)、scale(縮放)。儲存時,它會將這些編輯套用到檔案上一次儲存的狀態,以產生新檔案。 -
向自訂編輯器的
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 可擴充性的資訊,請嘗試這些主題: