File: NavigationBar\CSharpNavigationBarItemService.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.NavigationBar;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using static Microsoft.CodeAnalysis.NavigationBar.RoslynNavigationBarItem;
 
namespace Microsoft.CodeAnalysis.CSharp.NavigationBar;
 
[ExportLanguageService(typeof(INavigationBarItemService), LanguageNames.CSharp), Shared]
internal class CSharpNavigationBarItemService : AbstractNavigationBarItemService
{
    private static readonly SymbolDisplayFormat s_typeFormat =
        SymbolDisplayFormat.CSharpErrorMessageFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeVariance);
 
    private static readonly SymbolDisplayFormat s_memberFormat =
        new(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
            memberOptions: SymbolDisplayMemberOptions.IncludeParameters |
                           SymbolDisplayMemberOptions.IncludeExplicitInterface,
            typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
            parameterOptions: SymbolDisplayParameterOptions.IncludeType |
                              SymbolDisplayParameterOptions.IncludeName |
                              SymbolDisplayParameterOptions.IncludeDefaultValue |
                              SymbolDisplayParameterOptions.IncludeParamsRefOut,
            miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                                  SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral |
                                  SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public CSharpNavigationBarItemService()
    {
    }
 
    protected override async Task<ImmutableArray<RoslynNavigationBarItem>> GetItemsInCurrentProcessAsync(
        Document document, bool supportsCodeGeneration, CancellationToken cancellationToken)
    {
        var typesInFile = await GetTypesInFileAsync(document, cancellationToken).ConfigureAwait(false);
        var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        return GetMembersInTypes(document.Project.Solution, tree, typesInFile, cancellationToken);
    }
 
    private static ImmutableArray<RoslynNavigationBarItem> GetMembersInTypes(
        Solution solution, SyntaxTree tree, IEnumerable<INamedTypeSymbol> types, CancellationToken cancellationToken)
    {
        using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken))
        {
            using var _1 = ArrayBuilder<RoslynNavigationBarItem>.GetInstance(out var items);
 
            foreach (var type in types)
            {
                using var _2 = ArrayBuilder<RoslynNavigationBarItem>.GetInstance(out var memberItems);
 
                foreach (var member in type.GetMembers())
                {
                    if (member.IsImplicitlyDeclared ||
                        member.Kind == SymbolKind.NamedType ||
                        IsAccessor(member))
                    {
                        continue;
                    }
 
                    if (member is IMethodSymbol { PartialImplementationPart: { } } methodSymbol)
                    {
                        memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol, tree, cancellationToken));
                        memberItems.AddIfNotNull(CreateItemForMember(solution, methodSymbol.PartialImplementationPart, tree, cancellationToken));
                    }
                    else if (member is IPropertySymbol { PartialImplementationPart: { } } propertySymbol)
                    {
                        memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol, tree, cancellationToken));
                        memberItems.AddIfNotNull(CreateItemForMember(solution, propertySymbol.PartialImplementationPart, tree, cancellationToken));
                    }
                    else if (member is IMethodSymbol or IPropertySymbol)
                    {
                        Debug.Assert(member is IMethodSymbol { PartialDefinitionPart: null } or IPropertySymbol { PartialDefinitionPart: null },
                            $"NavBar expected GetMembers to return partial method/property definition parts but the implementation part was returned.");
 
                        memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken));
                    }
                    else
                    {
                        memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken));
                    }
                }
 
                memberItems.Sort((x, y) =>
                {
                    var textComparison = x.Text.CompareTo(y.Text);
                    return textComparison != 0 ? textComparison : x.Grayed.CompareTo(y.Grayed);
                });
 
                var spans = GetSymbolLocation(solution, type, tree, cancellationToken);
                if (spans == null)
                    continue;
 
                items.Add(new SymbolItem(
                    type.Name,
                    text: type.ToDisplayString(s_typeFormat),
                    glyph: type.GetGlyph(),
                    isObsolete: type.IsObsolete(),
                    spans.Value,
                    childItems: memberItems.ToImmutable()));
            }
 
            items.Sort((x1, x2) => x1.Text.CompareTo(x2.Text));
            return items.ToImmutableAndClear();
        }
    }
 
    private static async Task<IEnumerable<INamedTypeSymbol>> GetTypesInFileAsync(Document document, CancellationToken cancellationToken)
    {
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        return GetTypesInFile(semanticModel, cancellationToken);
    }
 
    private static IEnumerable<INamedTypeSymbol> GetTypesInFile(SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetTypesInFile_CSharp, cancellationToken))
        {
            var types = new HashSet<INamedTypeSymbol>();
            using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var nodesToVisit);
 
            nodesToVisit.Push(semanticModel.SyntaxTree.GetRoot(cancellationToken));
 
            while (nodesToVisit.TryPop(out var node))
            {
                if (cancellationToken.IsCancellationRequested)
                    return [];
 
                var type = GetType(semanticModel, node, cancellationToken);
 
                if (type != null)
                {
                    types.Add((INamedTypeSymbol)type);
                }
 
                if (node is BaseMethodDeclarationSyntax or
                    BasePropertyDeclarationSyntax or
                    BaseFieldDeclarationSyntax or
                    StatementSyntax or
                    ExpressionSyntax)
                {
                    // quick bail out to prevent us from creating every nodes exist in current file
                    continue;
                }
 
                foreach (var child in node.ChildNodes())
                {
                    nodesToVisit.Push(child);
                }
            }
 
            return types;
        }
    }
 
    private static ISymbol? GetType(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken)
        => node switch
        {
            BaseTypeDeclarationSyntax t => semanticModel.GetDeclaredSymbol(t, cancellationToken),
            DelegateDeclarationSyntax d => semanticModel.GetDeclaredSymbol(d, cancellationToken),
            _ => null,
        };
 
    private static bool IsAccessor(ISymbol member)
    {
        if (member.Kind == SymbolKind.Method)
        {
            var method = (IMethodSymbol)member;
 
            return method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet;
        }
 
        return false;
    }
 
    private static RoslynNavigationBarItem? CreateItemForMember(
        Solution solution, ISymbol member, SyntaxTree tree, CancellationToken cancellationToken)
    {
        var location = GetSymbolLocation(solution, member, tree, cancellationToken);
        if (location == null)
            return null;
 
        return new SymbolItem(
            member.Name,
            member.ToDisplayString(s_memberFormat),
            member.GetGlyph(),
            member.IsObsolete(),
            location.Value);
    }
 
    private static SymbolItemLocation? GetSymbolLocation(
        Solution solution, ISymbol symbol, SyntaxTree tree, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        if (symbol.Kind == SymbolKind.Field)
        {
            return symbol.ContainingType.TypeKind == TypeKind.Enum
                ? GetSymbolLocation(solution, symbol, tree, static reference => GetEnumMemberSpan(reference))
                : GetSymbolLocation(solution, symbol, tree, static reference => GetFieldReferenceSpan(reference));
        }
        else
        {
            return GetSymbolLocation(solution, symbol, tree, static reference => reference.Span);
        }
    }
 
    private static TextSpan GetFieldReferenceSpan(SyntaxReference reference)
    {
        var declaringNode = reference.GetSyntax();
 
        var spanStart = declaringNode.SpanStart;
        var spanEnd = declaringNode.Span.End;
 
        var fieldDeclaration = declaringNode.GetAncestor<FieldDeclarationSyntax>();
        if (fieldDeclaration != null)
        {
            var variables = fieldDeclaration.Declaration.Variables;
 
            if (variables.FirstOrDefault() == declaringNode)
                spanStart = fieldDeclaration.SpanStart;
 
            if (variables.LastOrDefault() == declaringNode)
                spanEnd = fieldDeclaration.Span.End;
        }
 
        return TextSpan.FromBounds(spanStart, spanEnd);
    }
 
    private static TextSpan GetEnumMemberSpan(SyntaxReference reference)
    {
        var declaringNode = reference.GetSyntax();
        if (declaringNode is EnumMemberDeclarationSyntax enumMember)
        {
            var enumDeclaration = enumMember.GetAncestor<EnumDeclarationSyntax>();
 
            if (enumDeclaration != null)
            {
                var index = enumDeclaration.Members.IndexOf(enumMember);
                if (index != -1 && index < enumDeclaration.Members.SeparatorCount)
                {
                    // Cool, we have a comma, so do it
                    var start = enumMember.SpanStart;
                    var end = enumDeclaration.Members.GetSeparator(index).Span.End;
 
                    return TextSpan.FromBounds(start, end);
                }
            }
        }
 
        return declaringNode.Span;
    }
}