File: Protocol\DocumentSymbols\DocumentSymbolService.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
 
namespace Microsoft.CodeAnalysis.Razor.Protocol.DocumentSymbols;
 
internal class DocumentSymbolService(IDocumentMappingService documentMappingService) : IDocumentSymbolService
{
    private readonly IDocumentMappingService _documentMappingService = documentMappingService;
 
    public SumType<DocumentSymbol[], SymbolInformation[]>? GetDocumentSymbols(RazorFileKind fileKind, Uri razorDocumentUri, RazorCSharpDocument csharpDocument, SumType<DocumentSymbol[], SymbolInformation[]> csharpSymbols)
    {
        if (csharpSymbols.TryGetFirst(out var documentSymbols))
        {
            return RemapDocumentSymbols(fileKind, csharpDocument, documentSymbols);
        }
        else if (csharpSymbols.TryGetSecond(out var symbolInformations))
        {
            using var _ = ListPool<SymbolInformation>.GetPooledObject(out var mappedSymbols);
 
            foreach (var symbolInformation in symbolInformations)
            {
                // SymbolInformation is obsolete, but things still return it so we have to handle it
#pragma warning disable CS0618 // Type or member is obsolete
                if (symbolInformation.Name == RenderMethodSignature(fileKind))
                {
                    symbolInformation.Name = RenderMethodDisplay(fileKind);
                    symbolInformation.Location.Range = LspFactory.DefaultRange;
                    symbolInformation.Location.Uri = razorDocumentUri;
                    mappedSymbols.Add(symbolInformation);
                }
                else if (_documentMappingService.TryMapToRazorDocumentRange(csharpDocument, symbolInformation.Location.Range, out var newRange))
                {
                    symbolInformation.Location.Range = newRange;
                    symbolInformation.Location.Uri = razorDocumentUri;
                    mappedSymbols.Add(symbolInformation);
                }
#pragma warning restore CS0618 // Type or member is obsolete
            }
 
            return mappedSymbols.ToArray();
        }
        else
        {
            Debug.Fail("Unsupported response type");
            throw new InvalidOperationException();
        }
    }
 
    private DocumentSymbol[]? RemapDocumentSymbols(RazorFileKind fileKind, RazorCSharpDocument csharpDocument, DocumentSymbol[]? documentSymbols)
    {
        if (documentSymbols is null)
        {
            return null;
        }
 
        using var _ = ListPool<DocumentSymbol>.GetPooledObject(out var mappedSymbols);
 
        foreach (var documentSymbol in documentSymbols)
        {
            if (TryRemapRanges(csharpDocument, documentSymbol))
            {
                documentSymbol.Children = RemapDocumentSymbols(fileKind, csharpDocument, documentSymbol.Children);
 
                mappedSymbols.Add(documentSymbol);
            }
            else if (documentSymbol.Children is [_, ..] &&
                RemapDocumentSymbols(fileKind, csharpDocument, documentSymbol.Children) is [_, ..] mappedChildren)
            {
                // This range didn't map, but some/all of its children did, so we promote them to this level so we don't
                // lose any information.
                mappedSymbols.AddRange(mappedChildren);
            }
        }
 
        return mappedSymbols.ToArray();
 
        bool TryRemapRanges(RazorCSharpDocument csharpDocument, DocumentSymbol documentSymbol)
        {
            if (documentSymbol.Detail == RenderMethodSignature(fileKind))
            {
                // Special case BuildRenderTree to always map to the top of the document
                documentSymbol.Detail = RenderMethodDisplay(fileKind);
                documentSymbol.Range = LspFactory.DefaultRange;
                documentSymbol.SelectionRange = LspFactory.DefaultRange;
 
                return true;
            }
            else if (_documentMappingService.TryMapToRazorDocumentRange(csharpDocument, documentSymbol.Range, out var newRange) &&
                _documentMappingService.TryMapToRazorDocumentRange(csharpDocument, documentSymbol.SelectionRange, out var newSelectionRange))
            {
                documentSymbol.Range = newRange;
                documentSymbol.SelectionRange = newSelectionRange;
 
                return true;
            }
 
            return false;
        }
    }
 
    private static string RenderMethodSignature(RazorFileKind fileKind)
        => fileKind == RazorFileKind.Legacy
            ? "ExecuteAsync()"
            : "BuildRenderTree(RenderTreeBuilder __builder)";
 
    private static string RenderMethodDisplay(RazorFileKind fileKind)
        => fileKind == RazorFileKind.Legacy
            ? "ExecuteAsync()"
            : "BuildRenderTree()"; // We hide __builder because it can be misleading to users: https://github.com/dotnet/razor/issues/11960
 
}