| File: QuickInfo\Presentation\QuickInfoContentBuilder.cs | Web Access |
| Project: src\roslyn\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.Collections; 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 = Enum.GetValues<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); } }