使用 Visual Studio 編輯器擴充性
Visual Studio 編輯器支援新增至其功能的擴充功能。 範例包括以現有語言插入和修改程式碼的擴充功能。
針對新 Visual Studio 擴充性模型的初始版本,僅支援下列功能:
- 接聽正在開啟和關閉的文字檢視。
- 接聽文字檢視 (編輯器) 狀態變更。
- 讀取文件的文字和選項/插入號位置。
- 執行文字編輯和選項/插入號變更。
- 定義新的文件類型。
- 使用新的文字檢視邊界擴充文字檢視。
Visual Studio 編輯器通常是指編輯任何類型的文字檔案 (即文件) 的功能。 可以開啟個別檔案進行編輯,而開啟的編輯器視窗稱為 TextView
。
編輯器物件模型會在編輯器概念中描述。
開始使用
您的擴充功能程式碼可以設定為執行,以回應各種進入點 (當使用者與 Visual Studio 互動時發生的情況)。 編輯器擴充性目前支援三個進入點:接聽程式、EditorExtensibility 服務物件,和命令。
當編輯器視窗中發生特定動作時,事件接聽程式會觸發,在程式碼中以 TextView
表示。 例如,當使用者在編輯器中輸入某個專案時,就會發生 TextViewChanged
事件。 當編輯器視窗開啟或關閉時,會發生 TextViewOpened
和 TextViewClosed
事件。
編輯器服務物件是 EditorExtensibility
類別的執行個體,該物件會公開即時編輯器功能,例如執行文字編輯。
命令是由使用者在項目上按一下來起始,您可以將其放在功能表、內容功能表,或工具列上。
新增文字檢視接聽程式
接聽程式有兩種類型:ITextViewChangedListener 和 ITextViewOpenClosedListener。 這些接聽程式可用來觀察文字編輯器的開啟、關閉和修改。
然後,建立新的類別,實作 ExtensionPart 基類和 ITextViewChangedListener
、ITextViewOpenClosedListener
,或兩者,並新增 VisualStudioContribution 屬性。
然後,根據 ITextViewChangedListener 和 ITextViewOpenClosedListener 的要求,實作 TextViewExtensionConfiguration 屬性,使接聽程式在編輯 C# 檔案時套用:
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") },
};
本文稍後列出了其他程式語言的可用文件類型和檔案類型,並且還可以在需要時定義自訂檔案類型。
假設您決定實作這兩個接聽程式,則完成的類別宣告看起來應該如下所示:
[VisualStudioContribution]
public sealed class TextViewOperationListener :
ExtensionPart, // This is the extension part base class containing infrastructure necessary to use VS services.
ITextViewOpenClosedListener, // Indicates this part listens for text view lifetime events.
ITextViewChangedListener // Indicates this part listens to text view changes.
{
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
// Indicates this part should only light up in C# files.
AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") },
};
...
由於 ITextViewOpenClosedListener 和 ITextViewChangedListener 宣告 TextViewExtensionConfiguration 屬性,因此組態會套用至這兩個接聽程式。
當您執行擴充功能時,應該會看到:
- 只要使用者開啟文字檢視,就會呼叫 ITextViewOpenClosedListener.TextViewOpenedAsync。
- 只要使用者關閉文字檢視,就會呼叫 ITextViewOpenClosedListener.TextViewClosedAsync。
- 只要使用者對文字檢視所顯示的文字文件進行文字變更時,就會呼叫 ITextViewChangedListener.TextViewChangedAsync。
這些方法中的每一個都會傳遞一個 ITextViewSnapshot,其中包含使用者叫用動作時文字檢視和文字文件的狀態,以及一個 CancellationToken,當 IDE 希望取消待完成動作時,該 CancellationToken 將會具有 IsCancellationRequested == true
。
定義您的擴充功能何時相關
您的擴充功能通常只與某些支援的文件類型和案例相關,因此請務必清楚定義其適用性。 您可以使用 AppliesTo 組態,以數種方式清楚定義擴充功能的適用性。 您可以指定擴充功能支援的程式碼語言之類的檔案類型,以及/或根據檔案名稱或路徑比對模式來進一步調整擴充功能的適用性。
使用 AppliesTo 組態指定程式設計語言
AppliesTo 組態指出擴充功能應該啟動的程式設計語言案例。 它會寫入為 AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }
,其中文件類型是 Visual Studio 內建語言的已知名稱,或 Visual Studio 擴充功能中定義的自訂名稱。
下列資料表顯示一些已知的文件類型:
DocumentType | 描述 |
---|---|
"CSharp" | C# |
"C/C++" | C、C++、標頭,和 IDL |
"TypeScript" | TypeScript 和 JavaScript 類型語言。 |
"HTML" | HTML |
"JSON" | JSON |
"text" | 文字檔,包括 “code” 的階層式子系,從 “text” 遞減。 |
"code" | C、C++、C# 等等。 |
DocumentTypes 是階層式的。 也就是說,C# 和 C++ 都是從 "code" 遞減,因此宣告 "code" 會導致您的擴充功能啟用所有程式碼語言,C#、C、C++ 等。
定義新的文件類型
您可以定義新的文件類型,例如支援自訂程式碼語言,方法是將靜態 DocumentTypeConfiguration 屬性新增至擴充功能專案中的任何類別,並以 VisualStudioContribution
屬性標記屬性。
DocumentTypeConfiguration
可讓您定義新的文件類型、指定它繼承一個或多個其他文件類型,以及指定一個或多個用來識別檔案類型的副檔名:
using Microsoft.VisualStudio.Extensibility.Editor;
internal static class MyDocumentTypes
{
[VisualStudioContribution]
internal static DocumentTypeConfiguration MarkdownDocumentType => new("markdown")
{
FileExtensions = new[] { ".md", ".mdk", ".markdown" },
BaseDocumentType = DocumentType.KnownValues.Text,
};
}
文件類型定義會與舊版 Visual Studio 擴充性所提供的內容類型定義合併,可讓您將其他副檔名對應至現有的文件類型。
文件選取器
除了 DocumentFilter.FromDocumentType 之外,DocumentFilter.FromGlobPattern 還可讓您在文件檔案路徑符合 glob (萬用字元) 模式時,進一步限制擴充功能的適用性:
[VisualStudioContribution]
public sealed class TextViewOperationListener
: ExtensionPart, ITextViewOpenClosedListener, ITextViewChangedListener
{
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = new[]
{
DocumentFilter.FromDocumentType("CSharp"),
DocumentFilter.FromGlobPattern("**/tests/*.cs"),
},
};
[VisualStudioContribution]
public sealed class TextViewOperationListener
: ExtensionPart, ITextViewOpenClosedListener, ITextViewChangedListener
{
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = new[]
{
DocumentFilter.FromDocumentType(MyDocumentTypes.MarkdownDocumentType),
DocumentFilter.FromGlobPattern("docs/*.md", relativePath: true),
},
};
pattern
參數代表在文件的絕對路徑上相符的 glob 模式。
Glob 模式可以具有下列語法:
*
比對路徑區段中的零個或多個字元?
比對路徑區段中的一個字元**
比對任意數目的路徑區段,包括沒有{}
分組條件 (例如,**/*.{ts,js}
符合所有 TypeScript 和 JavaScript 檔案)[]
在路徑區段中宣告要比對的字元範圍 (例如,example.[0-9]
要比對example.0
、example.1
、...)[!...]
否定路徑區段中要比對的字元範圍 (例如,example.[!0-9]
在example.a
上比對,example.b
,但不是example.0
)
反斜線 (\
) 在glob 模式中無效。 建立 glob 模式時,請務必將任何反斜線轉換為斜線。
存取編輯器功能
您的編輯器擴充類別繼承自 ExtensionPart。 ExtensionPart
類別會公開擴充性屬性。 使用這個屬性,您可以要求 EditorExtensibility 物件的執行個體。 您可以使用這個物件來存取即時編輯器功能,例如執行編輯。
EditorExtensibility editorService = this.Extensibility.Editor();
存取命令內的編輯器狀態
在每個 Command
中的 ExecuteCommandAsync()
都會傳遞包含叫用命令時 IDE 狀態快照集的 IClientContext
。 您可以透過 ITextViewSnapshot
介面存取使用中文件,方法是藉由呼叫非同步方法 GetActiveTextViewAsync
從 EditorExtensibility
物件取得:
using ITextViewSnapshot textView = await this.Extensibility.Editor().GetActiveTextViewAsync(clientContext, cancellationToken);
擁有 ITextViewSnapshot
之後,您就可以存取編輯器狀態。 ITextViewSnapshot
是編輯器狀態在某個時間點不可變的檢視,因此您必須使用編輯器物件模型中的其他介面來進行編輯。
從擴充功能變更文字文件
編輯,也就是在 Visual Studio 編輯器中開啟的文字文件變更,可能是由使用者互動、Visual Studio 中的執行緒,例如語言服務和其他擴充功能引起。 您的擴充功能必須準備好處理即時發生的文件文字變更。
在主要 Visual Studio IDE 處理序之外執行的擴充功能,使用非同步設計模式與 Visual Studio IDE 處理序溝通。 這表示使用非同步方法呼叫,如 C# 中的 async
關鍵詞所指示,並以方法名稱的尾碼強化 Async
。 在期望回應使用者動作之編輯器的內容中,非同步是一個顯著的優勢。 傳統的同步 API 呼叫若花費的時間超過預期,將會停止回應使用者輸入,建立直到 API 呼叫完成為止的 UI 凍結。 使用者對現代互動式應用程式的期望是文字編輯器始終保持回應,並且永遠不會阻止它們工作。 因此,非同步擴充功能對於滿足使用者期望至關重要。
若要深入了解非同步程式設計,請參閱使用 async 和 await 進行非同步程式設計。
在新的 Visual Studio 擴充性模型中,擴充功能對於使用者而言是第二個類別:它無法直接修改編輯器或文字文件。 所有狀態變更都是非同步且合作的,Visual Studio IDE 會代表擴充功能執行要求的變更。 擴充功能可以在特定版本的文件或文字檢視上要求一個或多個變更,但擴充功能的變更可能會遭到拒絕,例如,如果文件的該區域已變更。
使用 EditorExtensibility
上的 EditAsync()
方法要求編輯。
如果您熟悉舊版 Visual Studio 擴充功能,ITextDocumentEditor
與從 ITextBuffer 和 ITextDocument 變更方法的狀態幾乎相同,而且支援大部分相同的功能。
MutationResult result = await this.Extensibility.Editor().EditAsync(
batch =>
{
var editor = document.AsEditable(batch);
editor.Replace(textView.Selection.Extent, newGuidString);
},
cancellationToken);
為了避免錯位編輯,會套用來自編輯器擴充功能的編輯,如下所示:
- 擴充功能會要求根據文件的最新版本進行編輯。
- 該要求可能包含一個或多個文字編輯、插入號位置變更等等。 任何類型實作
IEditable
都可以在單 一EditAsync()
要求中變更,包括ITextViewSnapshot
和ITextDocumentSnapshot
。 編輯是由編輯器完成,其可透過AsEditable()
在特定類別上要求。 - 編輯要求將會傳送到 Visual Studio IDE,只有當被變更的物件自發出要求的版本以來未發生變更時,編輯要求才會成功。 如果文件已變更,則變更可能會遭到拒絕,要求在較新版本上重試擴充功能。 變異運算的結果會儲存在
result
。 - 編輯以不可分割方式應用,這意味著不會中斷其他正在執行的執行緒。 最佳做法是在單一
EditAsync()
呼叫的狹窄時間範圍內執行所有應該發生的變更,以減少使用者編輯所產生的非預期行為的可能性,或編輯之間發生的語言服務動作 (例如,擴充功能編輯與 Roslyn C# 交錯移動插入號)。
非同步執行
ITextViewSnapshot.GetTextDocumentAsync 會在 Visual Studio 擴充功能中開啟文字文件的複本。 由於擴充功能會在個別處理序執行,因此所有擴充功能互動都是非同步、合作的,而且有一些注意事項:
警告
如果在舊的 ITextDocument
上呼叫 ,因為 Visual Studio 用戶端可能不再快取它,如果使用者自建立之後已進行許多變更,則 GetTextDocumentAsync
可能會失敗。 基於這個理由,如果您打算稍後儲存 ITextView
以存取其文件,而且無法容忍失敗,建議您立即呼叫 GetTextDocumentAsync
。 這樣做會將該版本的文件的文字內容擷取到您的擴充功能中,確保該版本的複本在過期之前傳送到您的擴充功能。
警告
如果使用者關閉文件,GetTextDocumentAsync
或 MutateAsync
可能會失敗。
並行執行
⚠️ | 編輯器擴充功能有時會並行執行 |
---|
初始版本有已知問題,可能會導致編輯器擴充功能程式碼並行執行。 每個非同步方法會確保以正確的順序呼叫,但第一個 await
之後的接續作業可能會交錯。 如果您的擴充功能依賴執行順序,請考慮維護傳入要求的佇列來保留順序,直到修正此問題。
如需詳細資訊,請參閱 StreamJsonRpc 預設排序和並行。
使用新的邊界擴充 Visual Studio 編輯器
擴充功能可以將新的文字檢視邊界提供給 Visual Studio 編輯器。 文字檢視邊界是一個矩形的 UI 控制項,附加在文字檢視的四個端之一。
文字檢視邊界會放在邊界容器中 (請參閱 ContainerMarginPlacement.KnownValues),並在相對於其他邊界之前或之後排序 (請參閱 MarginPlacement.KnownValues)。
文字檢視邊界提供者會實作 ITextViewMarginProvider 介面、藉由實作 TextViewMarginProviderConfiguration 來設定其提供的邊界,並在啟動時,透過 CreateVisualElementAsync 提供要裝載在邊界中的 UI 控制項。
由於 VisualStudio.Extensibility 中的擴充功能可能來自 Visual Studio 的跨處理序,因此無法直接使用 WPF 做為文字檢視邊界內容的存取層。 相反地,將內容提供給文字檢視邊界需要建立 RemoteUserControl 和該控制項的對應資料範本。 雖然以下有一些簡單的範例,但建議您在建立文字檢視邊界 UI 內容時,閱讀遠端 UI 文件。
/// <summary>
/// Configures the margin to be placed to the left of built-in Visual Studio line number margin.
/// </summary>
public TextViewMarginProviderConfiguration TextViewMarginProviderConfiguration => new(marginContainer: ContainerMarginPlacement.KnownValues.BottomRightCorner)
{
Before = new[] { MarginPlacement.KnownValues.RowMargin },
};
/// <summary>
/// Creates a remotable visual element representing the content of the margin.
/// </summary>
public async Task<IRemoteUserControl> CreateVisualElementAsync(ITextViewSnapshot textView, CancellationToken cancellationToken)
{
var documentSnapshot = await textView.GetTextDocumentAsync(cancellationToken);
var dataModel = new WordCountData();
dataModel.WordCount = CountWords(documentSnapshot);
this.dataModels[textView.Uri] = dataModel;
return new MyMarginContent(dataModel);
}
除了設定邊界位置之外,文字檢視邊界提供者也可以設定使用 GridCellLength 和 GridUnitType 屬性,放置邊界之方格的大小。
文字檢視邊界通常會將與文字檢視相關的某些資料可視化 (例如,目前的行號或錯誤計數),因此大部分的文字檢視邊界提供者也會想要接聽文字檢視事件,以回應文字檢視的開啟、關閉和使用者輸入。
Visual Studio 只會建立文字檢視邊界提供者的一個執行個體,而不論使用者開啟多少個適用的文字檢視,因此如果您的邊界顯示一些具狀態的資料,您的提供者必須保留目前開啟的文字檢視的狀態。
如需詳細資訊,請參閱字數邊界範例。
尚不支援其內容必須與文字檢視行對齊的垂直文字檢視邊界。
相關內容
了解編輯器概念中的編輯器介面和類型。
檢閱簡單編輯器型擴充功能的範例程式碼:
進階使用者可能想要了解編輯器 RPC 支援。