在编辑器中处理文本

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

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

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

命令 由用户启动,方法是单击一个可在菜单、上下文菜单或工具栏上放置的项。

添加文本视图侦听器

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

然后,创建一个新类,实现 ExtensionPart 基类和 ITextViewChangedListenerITextViewOpenClosedListener或两者,并添加 VisualStudioContribution 属性。

然后,按照 ITextViewChangedListenerITextViewOpenClosedListener的要求,实现 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,其中包含用户调用操作时的文本视图和文本文档的状态,以及一个 CancellationToken,当 IDE 希望取消挂起的操作时,该令牌将具有 IsCancellationRequested == true

定义扩展何时相关

扩展通常仅与某些受支持的文档类型和方案相关,因此必须明确定义其适用性。 可以通过多种方式使用 AppliesTo 配置)来明确定义扩展的适用性。 可以通过基于文件名或路径的模式进行匹配来指定扩展名支持的代码语言等文件类型,以及/或进一步优化扩展的适用性。

使用 AppliesTo 配置项指定编程语言

AppliesTo 配置指示需要激活扩展的编程语言方案。 它编写为 AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") },其中文档类型是内置于 Visual Studio 中的语言或 Visual Studio 扩展中定义的自定义语言的常见名称。

下表显示了一些已知的文档类型:

文档类型 描述
“CSharp” C#
“C/C++” C、C++、标头和 IDL
“TypeScript” TypeScript 和 JavaScript 类型语言。
“HTML” HTML
"JSON" JSON
"text" 文本文件,包括派生自“text”的“code”分层后代。
"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.FromDocumentTypeDocumentFilter.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.0example.1、...上匹配)
  • [!...] 表示不匹配路径段中某个范围内的字符(例如,example.[!0-9] 匹配 example.aexample.b,但不匹配 example.0

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

访问编辑器功能

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

EditorExtensibility editorService = this.Extensibility.Editor();

在命令中访问编辑器状态

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

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

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