File: RazorComponentSearchEngine.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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
 
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
 
internal class RazorComponentSearchEngine(ILoggerFactory loggerFactory) : IRazorComponentSearchEngine
{
    private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RazorComponentSearchEngine>();
 
    /// <summary>
    ///  Search for a component in a project based on its tag name and fully qualified name.
    /// </summary>
    /// <param name="tagHelper">
    ///  A <see cref="TagHelperDescriptor"/> to find the corresponding Razor component for.
    /// </param>
    /// <param name="solutionQueryOperations">
    ///  An <see cref="ISolutionQueryOperations"/> to enumerate project snapshots.
    /// </param>
    /// <param name="cancellationToken">
    ///  A token that is checked to cancel work.
    /// </param>
    /// <returns>
    ///  The corresponding <see cref="IDocumentSnapshot"/> if found, <see langword="null"/> otherwise.
    /// </returns>
    /// <remarks>
    ///  This method makes several assumptions about the nature of components. First,
    ///  it assumes that a component a given name "Name" will be located in a file
    ///  "Name.razor". Second, it assumes that the namespace the component is present in
    ///  has the same name as the assembly its corresponding tag helper is loaded from.
    ///  Implicitly, this method inherits any assumptions made by TrySplitNamespaceAndType.
    /// </remarks>
    /// <exception cref="ArgumentNullException">
    ///  Thrown if <paramref name="tagHelper"/> is <see langword="null"/>.
    /// </exception>
    public async Task<IDocumentSnapshot?> TryLocateComponentAsync(
        TagHelperDescriptor tagHelper,
        ISolutionQueryOperations solutionQueryOperations,
        CancellationToken cancellationToken)
    {
        if (tagHelper.Kind != TagHelperKind.Component)
        {
            return null;
        }
 
        var typeName = tagHelper.TypeNameIdentifier;
        var namespaceName = tagHelper.TypeNamespace;
        if (typeName == null || namespaceName == null)
        {
            _logger.LogWarning($"Could not split namespace and type for name {tagHelper.Name}.");
            return null;
        }
 
        var lookupSymbolName = RemoveGenericContent(typeName.AsMemory());
 
        foreach (var project in solutionQueryOperations.GetProjects())
        {
            foreach (var path in project.DocumentFilePaths)
            {
                // Get document and code document
                if (!project.TryGetDocument(path, out var document))
                {
                    continue;
                }
 
                // Rule out if not Razor component with correct name
                if (!document.IsPathCandidateForComponent(lookupSymbolName))
                {
                    continue;
                }
 
                var razorCodeDocument = await document.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false);
                if (razorCodeDocument is null)
                {
                    continue;
                }
 
                // Make sure we have the right namespace of the fully qualified name
                if (!razorCodeDocument.ComponentNamespaceMatches(namespaceName))
                {
                    continue;
                }
 
                return document;
            }
        }
 
        return null;
    }
 
    internal static ReadOnlyMemory<char> RemoveGenericContent(ReadOnlyMemory<char> typeName)
    {
        var genericSeparatorStart = typeName.Span.IndexOf('<');
 
        return genericSeparatorStart > 0
            ? typeName[..genericSeparatorStart]
            : typeName;
    }
}