Roslyn:在单个源代码行中枚举确切的标记 + 琐事跨度?

Roslyn: enumerate exact token + trivia spans on a single source line?

我希望有效地实施以下方法:

IEnumerable<ColoredSpan> GetSyntaxHighlightedSpansOnLine(int lineNumber);

我有一个 DocumentSourceTextSyntaxTree 等。假设 ColoredSpan 是一些颜色和字符串(或 chars 的其他来源)的元组。对于此代码的第三行,例如:

namespace Foo
{ /* Badly formatted coment...
    which continues here... */ class Bar : public IBaz // TODO: rename classes
    {
        ...

我希望通过文本提供可枚举的结果:

"    ", "which continues here... */", " ", "class", " ", "Bar", " ",
":", " ", "public", " ", "IBaz", " ", "// TODO: rename classes", "\r\n"

注意包含空格和评论琐事,以及部分多行评论。

指出派生 CSharpSyntaxWalker 来遍历整个 AST 部分的方法,而不是有效地将遍历限制到单行的节点。在每行的基础上,这效率不高,我无法轻易找出例如哪些小节。 Roslyn "trivia"(例如多行注释)到 return。它还 returns 重叠节点(例如命名空间)。

我试过了,啦:

var lineSpan = sf.GetText().Lines[lineNumber].Span;
var nodes = syntaxTree.GetRoot()
                      .DescendantNodes()
                      .Where(x => x.Span.IntersectsWith(lineSpan))

但是这 return 是整个 AST 子树,前序遍历,这又是低效的,而且 return 是重叠节点(例如命名空间)并且不处理琐事。其他样本适用于整个 documents/scripts。我还查阅了接近零的 API 文档。

代码分析 API 是否有效地允许这样做?或者实现这个方法,我是否需要提前遍历整个AST并存储一个我自己设计的主观庞大的并行耗内存数据结构

虽然您可以从 AST 重建此数据,但更好的 API 似乎以 Microsoft.CodeAnalysis.Classification.Classifier 的形式提供。它looks expensive,但是:

对于同步结果,您需要一个 Roslyn SemanticModel 作为您突出显示的源代码,您可以通过调用它们的 [=15= 从 DocumentCompilation 中获取它] 方法。您可以在获取 SyntaxTreeSourceText 的同时获取和缓存它,即一旦您拥有文档。你还需要一个Workspace。鉴于这些,您可以按需调用Classifier.GetClassifiedSpans()

如果您无法轻松获得电流 SemanticModel,您可以改为调用 Classifier.GetClassifiedSpansAsync(),它将为您构建特定 TextSpan 的微型模型。

这两种变体几乎都能提供您所要求的可枚举性,但不完全是。

首先,它returns以字符串的形式class弱类型化(class名称、关键字、运算符等)"enum" ;这些似乎对应于 ClassificationTypeNames class 的 const 成员,因此推测它们是可靠的。您可以简单地将 ClassificationTypeNames.ClassName 等映射到颜色。

其次,由于此调用 returns 仅 classified 跨度将丢失 unclassified 跨度,例如,空格。你将不得不重建包括这些琐事在内的全套跨度,即使乏味也很简单:

IEnumerable<ColoredSpan> DescribeLine(int lineNumber)
{
    var lineSpan = sourceText.Lines[lineNumber].Span;
    var classified = Classifier.GetClassifiedSpans(semanticModel, lineSpan, workspace);
    var cursor = lineSpan.Start;

    // Presuming you need a string rather than a TextSpan.
    Func<TextSpan, string> textOf = x => sourceText.ToString(x);

    if (!classified.Any())
        yield return new ColoredSpan(defaultStyle, textOf(lineSpan));

    foreach (var overlap in classified)
    {
        var classified = overlap.TextSpan.Intersection(lineSpan).Value;

        if (classified.Start > cursor)
        {
            var unclassified = new TextSpan(cursor, classified.Start - cursor);
            cursor = classified.Start;
            yield return new ColoredSpan(defaultStyle, textOf(unclassified));
        }

        var style = StyleFromClassificationType(overlapping.ClassificationType);

        yield return new ColoredSpan(style, textOf((TextSpan)classified));

        cursor = classified.Start + classified.Length;
    }

    if (cursor < lineSpan.Start + lineSpan.Length)
    {
        var trailing = new TextSpan(cursor, lineSpan.Start + lineSpan.Length - cursor);
        yield return new ColoredSpan(defaultStyle, textOf(trailing));
    }
}

此代码假定存在 ColoredSpan(如您的问题)和一个将 ClassificationTypeNames 映射到颜色的 StyleFromClassificationType() 助手。

由于 Roslyn 目前缺少任何可能传达作者对这些 API 的意图的 API 文档,我建议在将此实现与 vim 一起使用之前测量性能和活力。

如果分析显示这过于昂贵,那么以这种格式缓存 n 最近查看的源代码行表示并在需要时重新计算,使该缓存无效 n 将相对简单=56=] 源代码更改。