File: QuickInfo\Presentation\QuickInfoContentBuilder.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Shared.Collections;
 
namespace Microsoft.CodeAnalysis.QuickInfo.Presentation;
 
internal static class QuickInfoContentBuilder
{
    private static readonly FrozenDictionary<Glyph, QuickInfoGlyphElement> s_glyphToElementMap = CreateGlyphToElementMap();
 
    private static FrozenDictionary<Glyph, QuickInfoGlyphElement> CreateGlyphToElementMap()
    {
        var glyphs = (Glyph[])Enum.GetValues(typeof(Glyph));
        var result = new Dictionary<Glyph, QuickInfoGlyphElement>(capacity: glyphs.Length);
 
        foreach (var glyph in glyphs)
        {
            result.Add(glyph, new QuickInfoGlyphElement(glyph));
        }
 
        return result.ToFrozenDictionary();
    }
 
    public static async Task<QuickInfoContainerElement> BuildInteractiveContentAsync(
        QuickInfoItem quickInfoItem,
        QuickInfoContentBuilderContext context,
        CancellationToken cancellationToken)
    {
        var (symbolGlyph, addWarningGlyph) = ComputeGlyphs(quickInfoItem);
 
        using var remainingSections = TemporaryArray<QuickInfoSection>.Empty;
        var (descriptionSection, documentationCommentsSection) = ComputeSections(quickInfoItem, ref remainingSections.AsRef());
 
        // We can't declare elements in a using statement because we need to assign to its indexer below.
        // TemporaryArray<T>'s non-copyable semantics restrict this.
        var elements = TemporaryArray<QuickInfoElement>.Empty;
        try
        {
            // Add a dummy item that we'll replace with the first line below.
            elements.Add(null!);
 
            // Build the first line of QuickInfo item, the images and the Description section should be on the first line with Wrapped style
            using var firstLineElements = TemporaryArray<QuickInfoElement>.Empty;
 
            if (symbolGlyph != Glyph.None)
            {
                firstLineElements.Add(s_glyphToElementMap[symbolGlyph]);
            }
 
            if (addWarningGlyph)
            {
                firstLineElements.Add(s_glyphToElementMap[Glyph.CompletionWarning]);
            }
 
            var navigationActionFactory = context.NavigationActionFactory;
 
            if (descriptionSection is not null)
            {
                var isFirstElement = true;
 
                foreach (var element in descriptionSection.TaggedParts.ToInteractiveTextElements(navigationActionFactory))
                {
                    if (isFirstElement)
                    {
                        isFirstElement = false;
                        firstLineElements.Add(element);
                    }
                    else
                    {
                        // If the description section contains multiple paragraphs, the second and additional paragraphs
                        // are not wrapped in firstLineElements (they are normal paragraphs).
                        elements.Add(element);
                    }
                }
            }
 
            // Replace the dummy first element with the real first line elements.
            elements[0] = new QuickInfoContainerElement(QuickInfoContainerStyle.Wrapped, firstLineElements.ToImmutableAndClear());
 
            if (documentationCommentsSection is not null)
            {
                var isFirstElement = true;
 
                foreach (var element in documentationCommentsSection.TaggedParts.ToInteractiveTextElements(navigationActionFactory))
                {
                    if (isFirstElement)
                    {
                        isFirstElement = false;
 
                        // Stack the first paragraph of the documentation comments with the last line of the description
                        // to avoid vertical padding between the two.
                        var lastIndex = elements.Count - 1;
                        var lastElement = elements[lastIndex];
 
                        elements[lastIndex] = new QuickInfoContainerElement(
                            QuickInfoContainerStyle.Stacked,
                            lastElement,
                            element);
                    }
                    else
                    {
                        elements.Add(element);
                    }
                }
            }
 
            // Add the remaining sections
            if (remainingSections.Count > 0)
            {
                foreach (var section in remainingSections)
                {
                    foreach (var element in section.TaggedParts.ToInteractiveTextElements(navigationActionFactory))
                    {
                        elements.Add(element);
                    }
                }
            }
 
            // build text for RelatedSpan
            if (quickInfoItem.RelatedSpans.Length > 0)
            {
                using var textRuns = TemporaryArray<QuickInfoClassifiedTextRun>.Empty;
                var document = context.Document;
                var tabSize = context.LineFormattingOptions.TabSize;
                var spanSeparatorNeededBefore = false;
 
                var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
                foreach (var relatedSpan in quickInfoItem.RelatedSpans)
                {
                    // We don't present additive-spans (like static/reassigned-variable) any differently, so strip them
                    // out of the classifications we get back.
                    var classifiedSpans = await ClassifierHelper.GetClassifiedSpansAsync(
                        document, relatedSpan, context.ClassificationOptions, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false);
 
                    foreach (var span in IndentationHelper.GetSpansWithAlignedIndentation(text, classifiedSpans, tabSize))
                    {
                        if (spanSeparatorNeededBefore)
                        {
                            textRuns.Add(new QuickInfoClassifiedTextRun(
                                ClassificationTypeNames.WhiteSpace,
                                "\r\n",
                                QuickInfoClassifiedTextStyle.UseClassificationFont));
 
                            spanSeparatorNeededBefore = false;
                        }
 
                        textRuns.Add(new QuickInfoClassifiedTextRun(
                            span.ClassificationType,
                            text.ToString(span.TextSpan),
                            QuickInfoClassifiedTextStyle.UseClassificationFont));
                    }
 
                    spanSeparatorNeededBefore = true;
                }
 
                if (textRuns.Count > 0)
                {
                    elements.Add(new QuickInfoClassifiedTextElement(textRuns.ToImmutableAndClear()));
                }
            }
 
            // Add on-the-fly documentation
            if (quickInfoItem.OnTheFlyDocsInfo is not null)
            {
                elements.Add(new QuickInfoOnTheFlyDocsElement(context.Document, quickInfoItem.OnTheFlyDocsInfo));
            }
 
            return new QuickInfoContainerElement(
                QuickInfoContainerStyle.Stacked | QuickInfoContainerStyle.VerticalPadding,
                elements.ToImmutableAndClear());
        }
        finally
        {
            elements.Dispose();
        }
    }
 
    private static (Glyph symbolGlyph, bool addWarningGlyph) ComputeGlyphs(QuickInfoItem quickInfoItem)
    {
        var symbolGlyph = Glyph.None;
        var addWarningGlyph = false;
 
        foreach (var glyph in quickInfoItem.Tags.GetGlyphs())
        {
            if (symbolGlyph != Glyph.None && addWarningGlyph)
            {
                break;
            }
 
            if (symbolGlyph == Glyph.None && glyph != Glyph.CompletionWarning)
            {
                symbolGlyph = glyph;
            }
            else if (!addWarningGlyph && glyph == Glyph.CompletionWarning)
            {
                addWarningGlyph = true;
            }
        }
 
        return (symbolGlyph, addWarningGlyph);
    }
 
    private static (QuickInfoSection? descriptionSection, QuickInfoSection? documentationCommentsSection) ComputeSections(
        QuickInfoItem quickInfoItem,
        ref TemporaryArray<QuickInfoSection> remainingSections)
    {
        QuickInfoSection? descriptionSection = null;
        QuickInfoSection? documentationCommentsSection = null;
 
        foreach (var section in quickInfoItem.Sections)
        {
            switch (section.Kind)
            {
                case QuickInfoSectionKinds.Description:
                    descriptionSection ??= section;
                    break;
 
                case QuickInfoSectionKinds.DocumentationComments:
                    documentationCommentsSection ??= section;
                    break;
 
                default:
                    remainingSections.Add(section);
                    break;
            }
        }
 
        return (descriptionSection, documentationCommentsSection);
    }
}