File: QuickInfo\CSharpSyntacticQuickInfoProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.QuickInfo;
 
[ExportQuickInfoProvider(QuickInfoProviderNames.Syntactic, LanguageNames.CSharp), Shared]
[ExtensionOrder(After = QuickInfoProviderNames.Semantic)]
internal class CSharpSyntacticQuickInfoProvider : CommonQuickInfoProvider
{
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public CSharpSyntacticQuickInfoProvider()
    {
    }
 
    protected override Task<QuickInfoItem?> BuildQuickInfoAsync(
        QuickInfoContext context,
        SyntaxToken token)
        => Task.FromResult(BuildQuickInfo(token, context.CancellationToken));
 
    protected override Task<QuickInfoItem?> BuildQuickInfoAsync(
        CommonQuickInfoContext context,
        SyntaxToken token)
        => Task.FromResult(BuildQuickInfo(token, context.CancellationToken));
 
    private static QuickInfoItem? BuildQuickInfo(SyntaxToken token, CancellationToken cancellationToken)
    {
        switch (token.Kind())
        {
            case SyntaxKind.CloseBraceToken:
                return BuildQuickInfoCloseBrace(token);
            case SyntaxKind.HashToken:
            case SyntaxKind.EndRegionKeyword:
            case SyntaxKind.EndIfKeyword:
            case SyntaxKind.ElseKeyword:
            case SyntaxKind.ElifKeyword:
            case SyntaxKind.EndOfDirectiveToken:
                return BuildQuickInfoDirectives(token, cancellationToken);
            default:
                return null;
        }
    }
 
    private static QuickInfoItem? BuildQuickInfoCloseBrace(SyntaxToken token)
    {
        // Don't show for interpolations
        if (token.Parent is InterpolationSyntax interpolation &&
            interpolation.CloseBraceToken == token)
        {
            return null;
        }
 
        // Now check if we can find an open brace.
        var parent = token.Parent!;
        var openBrace = parent.ChildNodesAndTokens().FirstOrDefault(n => n.Kind() == SyntaxKind.OpenBraceToken).AsToken();
        if (openBrace.Kind() != SyntaxKind.OpenBraceToken)
        {
            return null;
        }
 
        var spanStart = parent.SpanStart;
        var spanEnd = openBrace.Span.End;
 
        // If the parent is a scope block, check and include nearby comments around the open brace
        // LeadingTrivia is preferred
        if (IsScopeBlock(parent))
        {
            MarkInterestedSpanNearbyScopeBlock(parent, openBrace, ref spanStart, ref spanEnd);
        }
        // If the parent is a child of a property/method declaration, object/array creation, or control flow node,
        // then walk up one higher so we can show more useful context
        else if (parent.GetFirstToken() == openBrace)
        {
            // parent.Parent must be non-null, because for GetFirstToken() to have returned something it would have had to walk up to its parent
            spanStart = parent.Parent!.SpanStart;
        }
 
        // encode document spans that correspond to the text to show
        var spans = ImmutableArray.Create(TextSpan.FromBounds(spanStart, spanEnd));
        return QuickInfoItem.Create(token.Span, relatedSpans: spans);
    }
 
    private static bool IsScopeBlock(SyntaxNode node)
        => node.IsKind(SyntaxKind.Block)
            && node.Parent?.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection or SyntaxKind.GlobalStatement;
 
    private static void MarkInterestedSpanNearbyScopeBlock(SyntaxNode block, SyntaxToken openBrace, ref int spanStart, ref int spanEnd)
    {
        var searchListAbove = openBrace.LeadingTrivia.Reverse();
        if (TryFindFurthestNearbyComment(ref searchListAbove, out var nearbyComment))
        {
            spanStart = nearbyComment.SpanStart;
            return;
        }
 
        var nextToken = block.FindToken(openBrace.FullSpan.End);
        var searchListBelow = nextToken.LeadingTrivia;
        if (TryFindFurthestNearbyComment(ref searchListBelow, out nearbyComment))
        {
            spanEnd = nearbyComment.Span.End;
            return;
        }
    }
 
    private static bool TryFindFurthestNearbyComment<T>(ref T triviaSearchList, out SyntaxTrivia nearbyTrivia)
        where T : IEnumerable<SyntaxTrivia>
    {
        nearbyTrivia = default;
 
        foreach (var trivia in triviaSearchList)
        {
            if (trivia.IsSingleOrMultiLineComment())
            {
                nearbyTrivia = trivia;
            }
            else if (trivia.Kind() is not SyntaxKind.WhitespaceTrivia and not SyntaxKind.EndOfLineTrivia)
            {
                break;
            }
        }
 
        return nearbyTrivia.IsSingleOrMultiLineComment();
    }
 
    private static QuickInfoItem? BuildQuickInfoDirectives(SyntaxToken token, CancellationToken cancellationToken)
    {
        if (token.Parent is DirectiveTriviaSyntax directiveTrivia)
        {
            if (directiveTrivia is EndRegionDirectiveTriviaSyntax)
            {
                var regionStart = directiveTrivia.GetMatchingDirective(cancellationToken);
                if (regionStart is not null)
                    return QuickInfoItem.Create(token.Span, relatedSpans: [regionStart.Span]);
            }
            else if (directiveTrivia is ElifDirectiveTriviaSyntax or ElseDirectiveTriviaSyntax or EndIfDirectiveTriviaSyntax)
            {
                var matchingDirectives = directiveTrivia.GetMatchingConditionalDirectives(cancellationToken);
                var matchesBefore = matchingDirectives
                    .TakeWhile(d => d.SpanStart < directiveTrivia.SpanStart)
                    .Select(d => d.Span)
                    .ToImmutableArray();
                if (matchesBefore.Length > 0)
                    return QuickInfoItem.Create(token.Span, relatedSpans: matchesBefore);
            }
        }
 
        return null;
    }
}