65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展 Visual Studio 以提供彩色语言编辑器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2018年5月20日

CPOL

22分钟阅读

viewsIcon

26156

downloadIcon

608

在本文中,我们将探讨 Visual Studio 编辑器的实现,该编辑器允许编辑虚构的“Colorful”语言。该编辑器最低限度地实现了语法分类/着色和 IntelliSense 自动完成。

背景

最近,我对编写 Visual Studio 扩展产生了兴趣。Visual Studio 可扩展性框架提供了令人眼花缭乱的机制来实现这一目标。本文仅描述其中一种机制。

具体来说,它讨论了实现语法着色和 IntelliSense 自动完成。语法着色通过 IClassifierProvider / IClassifier 接口实现。IntelliSense 自动完成通过 ICompletionSource / ICompletionSourceProvider 接口实现。

虽然我认为本文中提出的概念有价值,但这并不是实现这些功能的唯一方法。需要明确的是,我并不认可这些特定的接口。我只是分享我所学到的知识。事实上,根据以下链接,可能有更简单的方法来实现相同的目标。

语言扩展指南
https://vscode.js.cn/docs/extensionAPI/language-support

我可能会,也可能不会在未来的文章中探讨这些指南。这将取决于我自己的持续兴趣和读者的兴趣水平。

要求

本文随附的解决方案假设您正在使用 Visual Studio 2017。虽然将解决方案移植到早期版本可能并不困难,但仍需要一些工作。

就我而言,我使用 Visual Studio 2017 Professional。我相信使用 Visual Studio 2017 Community 也可以创建 VSIX 项目。但是,我尚未测试该配置。

该解决方案还假设您已安装“Visual Studio 扩展开发”功能。

引言

本文描述了虚构“Colorful”语言编辑器的实现。该编辑器作为 Visual Studio 集成开发环境 (IDE) 的扩展实现。

要运行或测试代码,只需打开解决方案并像运行其他任何应用程序一样运行它。第二个 Visual Studio 实例将启动。标题栏中将显示“实验实例”的指示,如下所示。

在 Visual Studio 的实验实例中,打开扩展名为 .colorful 的文件将执行此解决方案中的代码。该解决方案包含文件 Sample.colorful 以提供帮助。

要真正安装它,在非实验实例中,只需双击此解决方案创建的 VSIX 文件。安装完成后,您可以导航到“工具”->“扩展和更新”以确认安装(并在以后卸载)。它应该如下所示:

如果您不熟悉创建 Visual Studio 扩展 (VSIX 项目),本文后面将提供分步指南。在描述此实现之前,有必要首先介绍一些基本概念。

VSIX

这些扩展的交付单位是 VSIX 文件。VSIX 文件是实现 Microsoft Open Packaging Conventions 的 ZIP 文件。只需将文件重命名为使用 .zip 文件扩展名,即可非常轻松地查看文件内容。

不出所料,VSIX 文件通过 Visual Studio 中的 VSIX 项目创建和构建。此项目模板位于“可扩展性”下。

部署 VSIX 文件时,它们会被复制到以下隐藏目录中:

%LocalAppData%\Microsoft\VisualStudio\<version>\Extensions

例如

C:\Users\JohnDoe\AppData\Local\Microsoft\VisualStudio\15.0_da79fc12\Extensions

托管可扩展性框架 (MEF)

托管可扩展性框架 (MEF) 提供了一种机制,用于向 Visual Studio(或其他应用程序)通告组件(称为部件)的可用性。它还提供了一种机制,用于这些应用程序与这些组件(部件)共享信息。

要通告部件的存在,我们只需使用 Export 属性 (System.ComponentModel.Composition.ExportAttribute) 装饰相关类,如下所示:

[Export(typeof(IClassifierProvider))]
internal sealed class ColorfulClassifierProvider : IClassifierProvider
{
  [Import]
  private IClassificationTypeRegistryService classificationRegistry = null;
}

在上面的示例中,您还会注意到一个 Import 属性 (System.ComponentModel.Composition.ImportAttribute)。此属性表示我们请求 MEF 使用主机应用程序(在本例中为 Visual Studio)提供的信息来初始化此变量。

语法分类

通过实现 IClassifier / IClassifierProvider 接口(在 Microsoft.VisualStudio.Text.Classification 命名空间中),我们可以“分类”文件中的文本跨度。例如,我们可以将一段文本分类为“关键字”。在 Visual Studio 中,默认情况下,关键字以蓝色显示。

在 Visual Studio 2017 中,可用的分类及其相关颜色可以在“工具”->“选项”->“字体和颜色”下找到,如下所示:

IntelliSense 自动完成

IntelliSense 自动完成是 Visual Studio 中一项巧妙的功能,它会弹出一个建议列表,以完成您部分输入的单词。

通过实现 ICompletionSource / ICompletionSourceProvider 接口(在 Microsoft.VisualStudio.Language.Intellisense 命名空间中),我们可以为部分单词提供建议的完成。

实现语法分类

语法分类通过 IClassifierProvider / IClassifier 接口(在 Microsoft.VisualStudio.Text.Classification 命名空间中)实现。在我们的代码中,这涉及以下五个类:

类名 描述
Colorful 此类通过 MEF 导出内容类型定义(用于“Colorful”语言)以及文件扩展名 (.colorful) 到该内容类型定义的映射。
ColorfulClassifier 此类实现 IClassifier 接口,以返回文本行中每个部分(跨度)的分类。
ColorfulClassifierProvider 此类实现 IClassifierProvider 接口,并充当工厂来创建 ColorfulClassifier 类的实例。
ColorfulKeywords 此类为“Colorful”语言提供了一个关键字(颜色名称)字典。
ColorfulTokenizer 此类将文本行中的部分(标记)分开。它将颜色名称分类为关键字,以“//”开头的文本分类为注释,标点符号(例如逗号)分类为运算符,十进制整数分类为字符串文字,其他所有内容分类为“其他”。

实现 IntelliSense 自动完成

IntelliSense 自动完成通过 ICompletionSource / ICompletionSourceProvider 接口(在 Microsoft.VisualStudio.Language.Intellisense 命名空间中)实现。在我们的代码中,这涉及以下六个类:

类名 描述
Colorful 此类通过 MEF 导出内容类型定义(用于“Colorful”语言)以及文件扩展名 (.colorful) 到该内容类型定义的映射。
ColorfulCompletionSource 此类实现 ICompletionSource 接口,为位于编辑器光标(插入符号)当前位置的部分单词提供一组过滤后的匹配颜色名称(来自 ColorfulKeywords)。
ColorfulCompletionSourceProvider 此类实现 ICompletionSourceProvider 接口,并充当工厂来创建 ColorfulCompletionSource 类的实例。
ColorfulKeywords 此类为“Colorful”语言提供了一个关键字(颜色名称)字典。
ColorfulOleCommandTarget 此类实现 IOleCommandTarget 接口(在 Microsoft.VisualStudio.OLE.Interop 命名空间中),以启动、提交和关闭 IntelliSense 自动完成会话。
ColorfulTextViewCreationListener 此类实现 IVsTextViewCreationListener(在 Microsoft.VisualStudio.Editor 命名空间中),以侦听与 Colorful 内容类型关联的文本视图的创建。对于这些文本视图,它还充当工厂来创建 ColorfulOleCommandTarget 类的实例。

Colorful 生命周期

下图描述了此解决方案中不同类实例的生命周期。

虽然大多数步骤都在预料之中,但我必须承认有几个惊喜。我惊讶地发现 ColorfulTextViewCreationListener(步骤 6)和 ColorfulOleCommandTarget(步骤 7)的实例化并没有首先出现。我更惊讶于 ColorfulClassifierProvider 的第二次实例化(步骤 5)。

理解代码

在本文的这一部分,我们将更详细地检查每个类。

Colorful

这个类最容易解释。它没有可变部分。相反,它主要用于向 Visual Studio(通过 MEF)通告此编辑器的存在。它还提供了一些全局常量。其中一个常量,内容类型名称 (ContentType),在其他模块中用于关联负责处理 Colorful 内容类型的所有部件。

[Export]
[Name(ContentType)]
[BaseDefinition("code")]
internal static ContentTypeDefinition ContentTypeDefinition = null;

这些行仅向 Visual Studio(通过 MEF)通告新内容类型 (Colorful) 的存在。新内容类型 (Colorful) 基于 Visual Studio 公开的内置内容类型之一 (code)。

[Export]
[Name(ContentType + nameof(FileExtensionToContentTypeDefinition))]
[ContentType(ContentType)]
[FileExtension(FileExtension)]
internal static FileExtensionToContentTypeDefinition FileExtensionToContentTypeDefinition = null;

这些行仅向 Visual Studio(通过 MEF)通告文件扩展名 (.colorful) 与内容类型 (Colorful) 之间的关系。

ColorfulClassifier

这个类由 ColorfulClassifierProvider 实例化,实现 IClassifier 接口。它负责对 Colorful 内容类型文档中所有文本跨度进行语法分类。

IClassifier 接口只需要两个成员

成员名称 描述
ClassificationChanged 此事件在文本跨度的语法分类意外更改后发生。解决方案提供的代码不会引发此事件。
GetClassificationSpans(SnapshotSpan) 此方法获取与指定文本跨度相交的每个文本跨度的语法分类。“快照”类型名称部分指的是所提供的跨度是文本的快照(在特定时间点存在)。
internal ColorfulClassifier(ITextBuffer buffer,
  IStandardClassificationService classifications,
  IClassificationTypeRegistryService classificationRegistry)

构造函数接受三个参数。它们如下所示:

参数名称 描述
buffer 这是包含整个 Colorful 文档的文本缓冲区。
classifications 这是一种服务,它公开许多语言通用的标准语法分类(例如关键字、空白、字符串文字等)。
classificationRegistry 这是一种服务,它公开所有已知的语法分类。它可以用于(尽管此代码不使用)访问特定于语言的语法分类。

让我们检查 GetClassifications 方法中的代码。

var list = new List<ClassificationSpan>();

在这里,我们创建了一个分类跨度列表,我们稍后将填充它。分类跨度是语法分类和文本跨度的组合。

ITextSnapshot snapshot = span.Snapshot;
string text = span.GetText();
int length = span.Length;
int index = 0;

在这里,我们简单地获取有关我们将要处理的文本跨度“快照”的一些信息。通常,Visual Studio 代码编辑器提供的文本跨度包含整行代码的所有字符。

while(index < length)
{
  int start = index;
  index = tokenizer.AdvanceWord(text, start, out IClassificationType type);

  list.Add(new ClassificationSpan(new SnapshotSpan(snapshot,
    new Span(span.Start + start, index - start)), type));
}

在这里,我们处理输入文本。使用 ColorfulTokenizer.AdvanceWord 方法,我们将文本行分解为单词(或标记)。每个单词是一组连续的空白字符,一组连续的非空白字符,单个标点符号字符(例如逗号),或一个完整的注释(以文本“//”开头)。

AdvanceWord 方法返回每个单词的两部分信息:它结束的从零开始的索引 (index) 及其语法分类类型 (type)。

利用这些信息,结合单词开始的从零开始的索引 (start),我们为单词创建一个新的分类跨度。我们首先创建一个 SnapshotSpan,它表示特定时间点单词的文本跨度。使用它,结合语法分类类型,我们创建一个分类跨度。最后,我们将该分类跨度添加到我们之前创建的列表中。

return list;

在构建了所有分类跨度的列表后,我们将其返回给调用方(Visual Studio)。

由于我们的语法是人造的,并且故意简单,我们可以作弊我们的实现。

一个真正的实现很可能会在后台线程中执行大部分分类逻辑。在该后台线程中,它可能会为整个文档创建一个抽象语法树 (AST)。它可能会监视文本缓冲区(在构造函数中提供)的变化,并在这些变化发生时更新 AST。

然后,GetClassificationSpans 方法将简单地找到与输入文本相交的 AST 元素。对于每个这些元素,它将创建一个分类跨度。

ColorfulClassifierProvider

这个类简单地实现了 IClassifierProvider 接口,以提供一个工厂来创建 ColorfulClassifier 实例。它由 Visual Studio(通过 MEF)实例化。

[Export(typeof(IClassifierProvider))]
[Name(nameof(ColorfulClassifierProvider))]
[ContentType(Colorful.ContentType)]
internal sealed class ColorfulClassifierProvider : IClassifierProvider

在这里,我们只是向 Visual Studio(通过 MEF)通告 Colorful 内容类型的 IClassifierProvider 实现的存在。部件的名称是可选的,并且在很大程度上是任意的,但应该具有一定的唯一性。

[Import]
private IClassificationTypeRegistryService classificationRegistry = null;

在这里,我们从 Visual Studio(通过 MEF)导入一项服务,该服务使我们能够访问所有已知的语法分类类型。我们稍后将此服务与我们创建的 ColorfulClassifier 实例共享。

[Import]
private IStandardClassificationService classifications = null;

在这里,我们从 Visual Studio(通过 MEF)导入一项服务,该服务使我们能够访问许多语言通用的标准语法分类类型(例如关键字)。我们稍后将此服务与我们创建的 ColorfulClassifier 实例共享。

public IClassifier GetClassifier(ITextBuffer buffer) =>
  buffer.Properties.GetOrCreateSingletonProperty(() =>
    new ColorfulClassifier(buffer, classifications, classificationRegistry));

IClassifierProvider 接口只有一个必需的成员:GetClassifier 方法。

在我们的实现中,我们创建了一个与 Visual Studio 作为参数提供的文本缓冲区关联的 ColorfulClassifier 实例。我们使用 GetOrCreateSingletonProperty 方法来确保每个文本缓冲区只创建一个实例。对于相同的文本缓冲区,后续调用只是返回之前创建的实例。

ColorfulCompletionSource

这个类由 ColorfulCompletionSourceProvider 实例化,实现 ICompletionSource 接口。这个接口只需要两个方法:AugmentCompletionSession,它提供一个与用户部分输入的单词匹配的单词列表,以及 Dispose,它可以用来释放非托管资源。

internal ColorfulCompletionSource(ITextBuffer buffer,
  ITextStructureNavigatorSelectorService navigatorService)

构造函数接受两个参数:包含用户正在键入的单词的文本缓冲区以及一个服务(来自 Visual Studio)以获取该文本缓冲区的导航器。

public void AugmentCompletionSession(ICompletionSession session,
  IList<CompletionSet> completionSets)

AugmentCompletionSession 方法接受两个参数:一个 IntelliSense 自动完成会话 (ICompletionSession) 和一个完成集列表 (CompletionSet)。完成集基本上是一个完成单词列表,以及一个包含部分单词的文本跨度。后者是一个“跟踪”文本跨度。“跟踪”文本跨度允许 IntelliSense 感应部分单词的变化,并随着用户继续输入而调整显示的选项。

ITrackingSpan wordToComplete = GetTrackingSpanForWordToComplete(session);

当用户开始键入部分单词时,这将触发 IntelliSense 自动完成会话。上面,我们使用自己的 GetTrackingSpanForWordToComplete 方法来获取该部分单词的“跟踪”文本跨度。

CompletionSet completionSet = new CompletionSet(Moniker, DisplayName,
  wordToComplete, completions, null);

上面,我们创建了一个完成集,将部分单词的“跟踪”文本跨度与所有可能的完整单词列表配对。

completionSets.Add(completionSet);

上面,我们将此完成集添加到可用完成集列表中;从而增强完成会话。IntelliSense 支持多个选项卡的概念,每个选项卡都有不同的完成集。这就是为什么有一个完成集列表,而不是只有一个。

让我们快速查看 GetTrackingSpanForWordToComplete,以便我们了解如何获取部分单词。

SnapshotPoint currentPoint = session.TextView.Caret.Position.BufferPosition - 1;

在这里,我们找到文本视图中光标的位置。这将帮助我们确定用户正在键入哪个部分单词。

TextExtent extent = Navigator.GetExtentOfWord(currentPoint);

接下来,我们获取用户正在键入的单词。在这种情况下,“单词”被定义为由空格或标点符号包围的连续非空格字符集。

return currentPoint.Snapshot.CreateTrackingSpan(extent.Span,
  SpanTrackingMode.EdgeInclusive);

最后,我们为单词创建一个“跟踪”文本跨度,它会随着用户继续输入而自行调整。

ColorfulCompletionSourceProvider

这个类简单地实现了 IClassifierProvider,以提供一个工厂来创建 ColorfulCompletionSource 实例。它由 Visual Studio(通过 MEF)实例化。

[Export(typeof(ICompletionSourceProvider))]
[Name(nameof(ColorfulCompletionSourceProvider))]
[ContentType(Colorful.ContentType)]
internal class ColorfulCompletionSourceProvider : ICompletionSourceProvider

在这里,我们只是向 Visual Studio(通过 MEF)通告 Colorful 内容类型的 ICompletionSourceProvider 实现的存在。部件的名称是可选的,并且在很大程度上是任意的,但应该具有一定的唯一性。

[Import]
private ITextStructureNavigatorSelectorService navigatorService = null;

在这里,我们从 Visual Studio(通过 MEF)导入一项服务,该服务使我们能够访问文本缓冲区的导航器(也来自 Visual Studio)。“导航器”允许我们执行简单的操作,例如从文本中获取一个单词。

public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer) =>
  new ColorfulCompletionSource(textBuffer, navigatorService);

ICompletionSourceProvider 接口只有一个必需的成员:TryCreateCompletionSource 方法。在我们的实现中,我们创建了一个与 Visual Studio 作为参数提供的文本缓冲区关联的 ColorfulCompletionSource 实例。

ColorfulKeywords

这个类只是一个关键字(颜色名称)的单例字典。它只公开两个成员:All 属性,它提供所有关键字的列表(按字母顺序),以及 Contains 方法,它允许我们测试一个单词是否是关键字。Contains 方法不区分大小写。

ColorfulTokenizer

这个类包含将简单的文本行解析成单独的单词(或标记)并为每个单词提供语法分类的逻辑。由于实现细节不重要,我们将简单地描述它的作用而不是它如何作用。

这个类中的主要方法是 AdvanceWord 方法。它接受三个参数:要处理的文本行、该文本中当前的零基索引,以及一个输出参数,它返回最近处理的单词的语法分类。除了返回语法分类(通过 out 参数)之外,它还返回下一个单词的零基索引。

通过重复调用此方法,直到返回的索引达到文本的长度,就可以找到并分类该文本中的所有标记(单词)。

出于此方法的目的,“单词”是:连续的空白字符序列、连续的非空白字符序列、单个标点符号字符,或一个完整的注释(即以“//”开头的字符序列)。

语法分类是 IStandardClassificationService(在 Microsoft.VisualStudio.Language.StandardClassification 命名空间中)的以下成员之一,每个成员都是 IClassificationType(在 Microsoft.VisualStudio.Text.Classification.IClassificationType 命名空间中)的一个实例:

成员名称 描述
关键字 包含关键字/颜色名称(例如 Blue)的连续非空白字符序列。
运算符 单个标点符号(例如逗号)。
其他 未分类为 OperatorKeywordStringLiteral 的连续非空白字符序列。
StringLiteral 仅由字符“0”到“9”组成的连续非空白字符序列。我们选择 StringLiteral 分类而不是 NumberLiteral,因为 NumberLiteral(默认情况下)与 Other 的颜色相同。
WhiteSpace 连续的空白字符序列。

ColorfulOleCommandTarget

这个类由 ColorfulTextViewCreationListener 实例化,实现了一堆(在我看来)hacky 行为,这些行为对于根据用户界面行为(例如用户键入字符)启动和关闭 IntelliSense 自动完成会话是必要的。

基本上,我们需要启动“时光机”并与 OLE(对象链接和嵌入)交互,以便我们可以监视文本视图。

internal ColorfulOleCommandTarget(IVsTextView vsTextView, ITextView textView,
  ICompletionBroker completionBroker, SVsServiceProvider serviceProvider)

构造函数接受四个参数。它们如下所示:

参数名称 描述
vsTextView 此类别监视的 Visual Studio 文本视图。
textView 此类别监视的 WPF 文本视图。WPF 文本视图是从 Visual Studio 文本视图中获取的。
completionBroker 负责 IntelliSense 自动完成的中央完成代理。
serviceProvider 中央 Visual Studio 服务提供商。
vsTextView.AddCommandFilter(this, out nextCommandTarget);

在这里,我们将自己添加到我们正在监视的 Visual Studio 文本视图的 OLE 命令过滤器链表中。

IOleCommandTarget 需要两个方法:执行 OLE 命令的 Exec 方法,以及查询 OLE 命令状态的 QueryStatus 方法。

QueryStatus

此方法查询 OLE 命令的状态。

public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds,
  OLECMD[] prgCmds, IntPtr pCmdText) =>
  nextCommandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);

我们在这里没有什么可提供的,所以我们只是让链中的下一个 OLE 命令目标响应状态。

Exec

if (VsShellUtilities.IsInAutomationFunction(ServiceProvider))
  return nextCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);

如果这是一个自动化功能(例如测试自动化),我们只是置身事外,让链中的下一个 OLE 命令目标处理该命令。

char? typedChar = GetTypedChar(pguidCmdGroup, nCmdID, pvaIn);

命令可以采用命令代码或键入字符的形式。上面,我们获取键入的字符(如果有)。

if (HandleCommit(nCmdID, typedChar))
  return VSConstants.S_OK;

上面,我们检查用户是否已从 IntelliSense 弹出窗口提交选择(例如,通过按 RETURN 键)或关闭弹出窗口(例如,通过按 ESCAPE 键)。如果出现这些情况,我们只需返回 S_OK 代码,表示我们已处理该命令。

int result = nextCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt,
  pvaIn, pvaOut);

我们允许链中的下一个命令目标处理该命令。这是必要的,因为我们希望在进一步处理之前,所有键入的字符都出现在文本视图中。

return ErrorHandler.Succeeded(result) ?
  HandleCompletion(nCmdID, typedChar, result) : result;

如果下一个命令目标成功处理了命令,我们检查是否需要启动 IntelliSense 自动完成会话。如果启动了,我们返回 S_OK;否则,我们返回从链中下一个命令目标接收到的结果。

虽然我们不会检查这个类中的每个底层方法,因为一些实现非常明显,但我们会检查那些可能更难理解的方法。这些包括:HandleCommitHandleCompletionTriggerCompletion

HandleCommit

此方法检查用户是否已从 IntelliSense 弹出窗口提交选择(例如,通过按 RETURN 键)或关闭弹出窗口(例如,通过按 ESCAPE 键)。

if (!HasCompletionSession() || (!IsCommitCommand(commandId) && !IsCommitChar(typedChar)))
  return false;

如果我们没有 IntelliSense 自动完成会话,我们只需返回。如果用户没有通过命令(RETURNTAB)或通过键入字符(空格或标点符号)启动提交,我们只需返回。

if (!completionSession.SelectedCompletionSet.SelectionStatus.IsSelected)
{
  completionSession.Dismiss();
  return false;
}

如果没有选择,我们只需关闭 IntelliSense 自动完成会话。

completionSession.Commit();
return true;

否则,我们提交用户在 IntelliSense 自动完成会话期间所做的选择。提交会话会导致部分单词的剩余字符添加到文本视图中。

HandleCompletion

此方法响应来自用户界面的命令(例如,键入的字符)。如有必要,它会启动 IntelliSense 自动完成会话。此外,如有必要,它会启动对该 IntelliSense 自动完成会话显示的先前选择的进一步过滤。

if (typedChar.HasValue && Char.IsLetterOrDigit(typedChar.Value))

这检查用户是否键入了一个字母或数字。如果尚未启动 IntelliSense 自动完成会话,我们会在发生这种情况时启动它。

if (!TriggerCompletion())
  return result;

如果用户键入了一个字母或数字,我们尝试启动 IntelliSense 自动完成会话。如果我们无法启动 IntelliSense 自动完成会话(不太可能),我们只需返回链中下一个 OLE 命令接收到的结果。

if ((commandId != (uint)VSConstants.VSStd2KCmdID.BACKSPACE &&
  commandId != (uint)VSConstants.VSStd2KCmdID.DELETE) ||
  !HasCompletionSession())
  return result;

如果用户没有键入字母或数字,并且我们有一个完成会话,我们可能仍有工作要做。如果用户删除了或退格了,我们需要调整 IntelliSense 自动完成会话显示的选项。否则,我们只需返回链中下一个 OLE 命令目标接收到的结果。

completionSession.Filter();

我们调整 IntelliSense 自动完成会话显示的选项。

return VSConstants.S_OK;

我们指示我们已处理该命令。

TriggerCompletion

此方法创建 IntelliSense 自动完成会话,从而显示相关的弹出列表。

if (HasCompletionSession())
  return true;

如果我们已经创建了 IntelliSense 自动完成会话,只需返回。

SnapshotPoint? caretPoint = TextView.Caret.Position.Point.GetPoint(
  IsNotProjection, PositionAffinity.Predecessor);
if (!caretPoint.HasValue)
  return false;

基本上,这只是获取插入符号(光标)的位置。如果没有可用的位置(不太可能),我们只需返回。

ITrackingPoint trackingPoint = caretPoint.Value.Snapshot.CreateTrackingPoint(
  caretPoint.Value.Position, PointTrackingMode.Positive);

我们为光标创建一个“跟踪”位置(点)。如果有什么导致位置移动,这个“跟踪”点会跟踪这个变化。

completionSession = CompletionBroker.CreateCompletionSession(
  TextView, trackingPoint, true);

我们要求负责 IntelliSense 自动完成的中央完成代理为我们创建一个完成会话。

completionSession.Dismissed += OnSessionDismissed;

我们订阅 Dismissed 事件。这样,如果 IntelliSense 自动完成会话在我们不知情的情况下结束,我们仍然会知道它发生了。

completionSession.Start();
return true;

我们启动 IntelliSense 自动完成会话并返回。

ColorfulTextViewCreationListener

这个类简单地实现了 IVsTextViewCreationListener,以提供一个工厂来创建 ColorfulOleCommandTarget 实例。它由 Visual Studio(通过 MEF)实例化。当创建包含 Colorful 内容的可编辑文本视图时,它充当工厂。

[Export(typeof(IVsTextViewCreationListener))]
[Name(nameof(ColorfulTextViewCreationListener))]
[ContentType(Colorful.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Editable)]
public class ColorfulTextViewCreationListener : IVsTextViewCreationListener

在这里,我们只是向 Visual Studio(通过 MEF)通告 Colorful 内容类型的 IVsTextViewCreationListener 实现的存在。我们表示我们对任何可编辑的文本视图感兴趣。部件的名称是可选的,并且在很大程度上是任意的,但应该具有一定的唯一性。

[Import]
private IVsEditorAdaptersFactoryService adapterService = null;

在这里,我们从 Visual Studio(通过 MEF)导入一项服务,该服务使我们能够访问适配器(也来自 Visual Studio)。适配器允许我们从 Visual Studio 文本视图获取 WPF 文本视图。

[Import]
private ICompletionBroker completionBroker = null;

在这里,我们从 Visual Studio(通过 MEF)导入负责 IntelliSense 自动完成的中央代理。

[Import]
private SVsServiceProvider serviceProvider = null;

在这里,我们从 Visual Studio(通过 MEF)导入 Visual Studio 的中央服务提供商。

public void VsTextViewCreated(IVsTextView vsTextView)

IVsTextViewCreationListener 接口只有一个必需的成员:VsTextViewCreated 方法。

ITextView textView = adapterService.GetWpfTextView(vsTextView);

首先,我们从 Visual Studio 文本视图获取一个 WPF 文本视图。ColorfulOleCommandTarget 中的大部分逻辑都需要一个 WPF 文本视图。

textView.Properties.GetOrCreateSingletonProperty(() =>
  new ColorfulOleCommandTarget(vsTextView, textView,
    completionBroker, serviceProvider));

上面,我们为指定的文本视图创建了一个 ColorfulOleCommandTarget 实例,并共享 IntelliSense 的中央完成代理和 Visual Studio 的中央服务提供商。我们使用 GetOrCreateSingletonProperty 来确保每个文本视图只创建一个实例。对于相同的文本缓冲区,后续调用只是返回之前创建的实例。

VSIX 项目分步指南

创建这类 VSIX 项目有点麻烦。我将尽力在此处逐步介绍。

要创建 VSIX 项目,请导航到“文件”->“新建项目”。选择“Visual C#”->“可扩展性”。您应该会看到“VSIX 项目”模板,如下所示:

如果您没有看到它,请单击“打开 Visual Studio 安装程序”。如果向下滚动一点,您应该在此类别中找到“其他工具集”和“Visual Studio 扩展开发”,如下所示:

创建解决方案后,您会注意到它内容很少,并且引用也很少。这很不幸。

要解决此问题,您需要首先向项目中添加一个项。具体来说,对于此示例,您需要添加一个“编辑器分类器”,如下所示:

添加后,您会看到一些新文件,更重要的是,您会看到大量新引用。

虽然模板源文件有助于提供一个起点,但它们对其他方面用处不大。我最终删除了很多。

我真正希望包含的一个引用是 Microsoft.VisualStudio.Language.StandardClassification。这是引用任何标准分类所必需的。由于几乎每个真正的编辑器分类器都应该引用这些,所以这个引用被省略对我来说是个谜。您需要自己添加它。

对于实现 IntelliSense 自动完成的解决方案,可用的支持不多。您基本上是靠自己。这很可惜,因为需要手动添加大量引用。

有效语法着色所需的其他引用包括:

  • Microsoft.VisualStudio.Language.StandardClassification

有效 IntelliSense 自动完成所需的其他引用包括:

  • Microsoft.VisualStudio.Editor
  • Microsoft.VisualStudio.Language.Intellisense
  • Microsoft.VisualStudio.OLE.Interop
  • Microsoft.VisualStudio.Shell.15.0
  • Microsoft.VisualStudio.Shell.Framework
  • Microsoft.VisualStudio.TextManager.Interop

其他阅读

虽然有点难以找到,但 Microsoft 文档中实际上散布着一些关于这些主题的不错解释。如需进一步阅读,请查看以下内容:

开始开发 Visual Studio 扩展
https://docs.microsoft.com/zh-cn/visualstudio/extensibility/starting-to-develop-visual-studio-extensions

编辑器内部
https://docs.microsoft.com/zh-cn/visualstudio/extensibility/inside-the-editor

语言扩展指南
https://vscode.js.cn/docs/extensionAPI/language-support

托管可扩展性框架 (MEF)
https://docs.microsoft.com/zh-cn/dotnet/framework/mef/

IntelliSense
https://vscode.js.cn/docs/editor/intellisense

开放打包约定
https://zh.wikipedia.org/wiki/开放打包约定

对象链接与嵌入
https://zh.wikipedia.org/wiki/对象链接与嵌入

相关链接

一位读者建议以下链接会有帮助。虽然我个人还没有机会尝试,但它似乎很切题。

可扩展性工具
https://marketplace.visualstudio.com/items?itemName=MadsKristensen.ExtensibilityTools

历史

  • 2018/5/20 - 上传原始版本
  • 2018/6/17 - 添加了可扩展性工具链接
© . All rights reserved.