使用 Visual Studio 编辑器扩展性

Visual Studio 编辑器支持添加到其功能的扩展。 示例包括使用现有语言插入和修改代码的扩展。

对于新的 Visual Studio 扩展性模型的初始版本,仅支持以下功能:

  • 侦听正在打开和关闭的文本视图。
  • 侦听文本视图(编辑器)状态更改。
  • 阅读文档的文本和所选内容/插入符号位置。
  • 执行文本编辑和选择/插入符号更改。
  • 定义新的文档类型。
  • 使用新的文本视图边距扩展文本视图。

Visual Studio 编辑器通常指编辑任何类型的文本文件(称为文档)的功能。 可以打开单个文件进行编辑,打开的编辑器窗口称为 “ TextView.

编辑器对象模型在编辑器概念介绍。

开始使用

扩展代码可配置为响应各种入口点(当用户与 Visual Studio 交互时发生的情况)运行。 编辑器扩展性目前支持三个入口点:侦听器、 EditorExtensibility 服务对象和命令。

当编辑器窗口中发生某些操作时,事件侦听器将触发,以代码表示。TextView 例如,当用户在编辑器中键入内容时,会发生一个 TextViewChanged 事件。 打开或关闭编辑器窗口时, TextViewOpenedTextViewClosed 发生事件。

编辑器服务对象是类的 EditorExtensibility 一个实例,它公开实时编辑器功能,例如执行文本编辑。

用户通过单击某个项来启动命令,你可以将其放置在菜单、上下文菜单或工具栏上。

添加文本视图侦听器

有两种类型的侦听器: ITextViewChangedListenerITextViewOpenClosedListener。 这些侦听器可用于观察文本编辑器的打开、关闭和修改。

然后,创建一个新类,实现 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") },
      };
      ...

由于 ITextViewOpenClosedListenerITextViewChangedListener 都声明 TextViewExtensionConfiguration 属性,配置适用于这两个侦听器。

运行扩展时,应会看到:

每种方法都会传递一个 ITextViewSnapshot ,其中包含用户调用操作时的文本视图和文本文档的状态,以及 IDE 希望取消挂起的操作时将具有 IsCancellationRequested == true 的 CancellationToken。

定义扩展何时相关

扩展通常仅与某些受支持的文档类型和方案相关,因此必须明确定义其适用性。 可以通过多种方式使用 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# 等。

DocumentType 是分层的。 也就是说,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(wild卡) 模式匹配时才激活扩展的适用性,从而进一步限制扩展的适用性:

[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.0example.1...)
  • [!...]若要否定要在路径段中匹配的字符范围,则为 (例如,example.[!0-9]要匹配,example.b但不example.0匹配example.a

反斜杠 (\) 在 glob 模式中无效。 创建 glob 模式时,请确保将任何反斜杠转换为斜杠。

访问编辑器功能

编辑器扩展类继承自 ExtensionPart。 该 ExtensionPart 类公开 Extensibility 属性。 使用此属性,可以请求 EditorExtensibility 对象的实例。 可以使用此对象访问实时编辑器功能,例如执行编辑。

EditorExtensibility editorService = this.Extensibility.Editor();

访问命令中的编辑器状态

ExecuteCommandAsync()Command在每次调用命令时,都会传递一个IClientContext包含 IDE 状态的快照。 可以通过接口访问活动文档,通过调用异步方法GetActiveTextViewAsyncEditorExtensibility对象获取该文档ITextViewSnapshot

using ITextViewSnapshot textView = await this.Extensibility.Editor().GetActiveTextViewAsync(clientContext, cancellationToken);

拥有 ITextViewSnapshot后,可以访问编辑器状态。 ITextViewSnapshot是编辑器状态的不可变视图,因此需要使用编辑器对象模型中的其他接口进行编辑。

从扩展对文本文档进行更改

编辑(即在 Visual Studio 编辑器中打开的文本文档的更改)可能源于用户交互、Visual Studio 中的线程(如语言服务和其他扩展)。 扩展必须准备好处理实时发生的文档文本更改。

在主 Visual Studio IDE 进程外部运行的扩展,这些扩展使用异步设计模式与 Visual Studio IDE 进程进行通信。 这意味着使用异步方法调用,如 C# 中的关键字 (keyword)指示async,并由方法名称上的后缀强化Async。 异步性在编辑器的上下文中具有显著优势,应响应用户操作。 传统的同步 API 调用(如果花费的时间超过预期)将停止响应用户输入,从而创建一个持续到 API 调用完成的 UI 冻结。 用户对新式交互式应用程序的期望是,文本编辑器始终保持响应状态,并且永远不会阻止它们正常工作。 因此,使扩展是异步的,以满足用户期望至关重要。

在异步编程中使用 async 和 await 了解有关异步编程的详细信息。

在新的 Visual Studio 扩展性模型中,扩展是相对于用户的第二类:它不能直接修改编辑器或文本文档。 所有状态更改都是异步和协作的,Visual Studio IDE 代表扩展执行请求的更改。 该扩展可以请求对文档或文本视图的特定版本进行一个或多个更改,但可能会拒绝来自扩展的更改,例如文档的该区域是否已更改。

使用 on EditAsync()EditorExtensibility方法请求编辑。

如果你熟悉旧的 Visual Studio 扩展, ITextDocumentEditor 则与从 ITextBufferITextDocument 更改状态的方法几乎相同,并支持大多数相同的功能。

MutationResult result = await this.Extensibility.Editor().EditAsync(
batch =>
{
    var editor = document.AsEditable(batch);
    editor.Replace(textView.Selection.Extent, newGuidString);
},
cancellationToken);

为了避免错放编辑,编辑编辑器扩展的编辑将按如下方式应用:

  1. 扩展请求根据文档的最新版本进行编辑。
  2. 该请求可能包含一个或多个文本编辑、插入符号位置更改等。 任何实现 IEditable 的类型都可以在单个 EditAsync() 请求(包括 ITextViewSnapshotITextDocumentSnapshot)中更改。 编辑由编辑器完成,可通过该编辑器在特定的类 AsEditable()上请求编辑。
  3. 编辑请求将发送到 Visual Studio IDE,仅当自发出请求的版本以来未更改对象时,才会成功。 如果文档已更改,可能会拒绝更改,要求扩展在较新版本上重试。 突变运算的结果存储在 result其中。
  4. 编辑以原子方式应用,这意味着不会中断其他执行线程。 最佳做法是在一次调用中执行应在较窄的时间范围内 EditAsync() 发生的所有更改,以减少用户编辑引发的意外行为的可能性,或编辑之间发生的语言服务操作(例如,扩展编辑与 Roslyn C# 交错移动插入点)。

异步执行

ITextViewSnapshot.GetTextDocumentAsync 将在 Visual Studio 扩展中打开文本文档的副本。 由于扩展在单独的进程中运行,因此所有扩展交互都是异步的、协作的,并且有一些注意事项:

注意

GetTextDocumentAsync 如果对旧 ITextDocument客户端调用,则可能会失败,因为如果用户自创建以来已进行了许多更改,则它可能不再由 Visual Studio 客户端缓存。 因此,如果计划稍后存储一个 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);
}

除了配置边距放置之外,文本视图边距提供程序还可以使用 GridCellLengthGridUnitType 属性配置应放置边距的网格单元格的大小。

文本视图边距通常可视化与文本视图相关的某些数据(例如,当前行号或错误计数),因此大多数文本视图边距提供程序也希望 侦听文本视图事件 以响应文本视图的打开、关闭和用户键入。

Visual Studio 仅创建文本视图边距提供程序的一个实例,而不考虑用户打开的适用文本视图数,因此,如果边距显示一些有状态数据,则提供程序需要保留当前打开的文本视图的状态。

有关详细信息,请参阅 字数页边距示例

尚不支持其内容与文本视图行对齐的垂直文本视图边距。

了解编辑器概念中的编辑器接口和类型。

查看基于编辑器的简单扩展的示例代码:

高级用户可能希望了解 编辑器 RPC 支持