File: NavigateTo\AbstractNavigateToSearchService.InProcess.cs
Web Access
Project: src\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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Storage;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.NavigateTo;
 
internal abstract partial class AbstractNavigateToSearchService
{
    private static readonly ImmutableArray<(PatternMatchKind roslynKind, NavigateToMatchKind vsKind)> s_kindPairs =
        [
            (PatternMatchKind.Exact, NavigateToMatchKind.Exact),
            (PatternMatchKind.Prefix, NavigateToMatchKind.Prefix),
            (PatternMatchKind.NonLowercaseSubstring, NavigateToMatchKind.Substring),
            (PatternMatchKind.StartOfWordSubstring, NavigateToMatchKind.Substring),
            (PatternMatchKind.CamelCaseExact, NavigateToMatchKind.CamelCaseExact),
            (PatternMatchKind.CamelCasePrefix, NavigateToMatchKind.CamelCasePrefix),
            (PatternMatchKind.CamelCaseNonContiguousPrefix, NavigateToMatchKind.CamelCaseNonContiguousPrefix),
            (PatternMatchKind.CamelCaseSubstring, NavigateToMatchKind.CamelCaseSubstring),
            (PatternMatchKind.CamelCaseNonContiguousSubstring, NavigateToMatchKind.CamelCaseNonContiguousSubstring),
            (PatternMatchKind.Fuzzy, NavigateToMatchKind.Fuzzy),
            (PatternMatchKind.LowercaseSubstring, NavigateToMatchKind.Fuzzy),
        ];
 
    private static async ValueTask SearchSingleDocumentAsync(
        Document document,
        string patternName,
        string? patternContainer,
        DeclaredSymbolInfoKindSet kinds,
        Action<RoslynNavigateToItem> onItemFound,
        CancellationToken cancellationToken)
    {
        if (cancellationToken.IsCancellationRequested)
            return;
 
        // Get the index for the file we're searching, as well as for its linked siblings.  We'll use the latter to add
        // the information to a symbol about all the project TFMs is can be found in.
        var index = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false);
        using var _ = ArrayBuilder<(TopLevelSyntaxTreeIndex, ProjectId)>.GetInstance(out var linkedIndices);
 
        foreach (var linkedDocumentId in document.GetLinkedDocumentIds())
        {
            var linkedDocument = document.Project.Solution.GetRequiredDocument(linkedDocumentId);
            var linkedIndex = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(linkedDocument, cancellationToken).ConfigureAwait(false);
            linkedIndices.Add((linkedIndex, linkedDocumentId.ProjectId));
        }
 
        ProcessIndex(
            DocumentKey.ToDocumentKey(document), document, patternName, patternContainer, kinds,
            index, linkedIndices, onItemFound, cancellationToken);
    }
 
    private static void ProcessIndex(
        DocumentKey documentKey,
        Document? document,
        string patternName,
        string? patternContainer,
        DeclaredSymbolInfoKindSet kinds,
        TopLevelSyntaxTreeIndex index,
        ArrayBuilder<(TopLevelSyntaxTreeIndex, ProjectId)>? linkedIndices,
        Action<RoslynNavigateToItem> onItemFound,
        CancellationToken cancellationToken)
    {
        if (cancellationToken.IsCancellationRequested)
            return;
 
        var containerMatcher = patternContainer != null
            ? PatternMatcher.CreateDotSeparatedContainerMatcher(patternContainer, includeMatchedSpans: true)
            : null;
 
        using var nameMatcher = PatternMatcher.CreatePatternMatcher(patternName, includeMatchedSpans: true, allowFuzzyMatching: true);
        using var _1 = containerMatcher;
 
        foreach (var declaredSymbolInfo in index.DeclaredSymbolInfos)
        {
            if (cancellationToken.IsCancellationRequested)
                return;
 
            // Namespaces are never returned in nav-to as they're too common and have too many locations.
            if (declaredSymbolInfo.Kind == DeclaredSymbolInfoKind.Namespace)
                continue;
 
            using var nameMatches = TemporaryArray<PatternMatch>.Empty;
            using var containerMatches = TemporaryArray<PatternMatch>.Empty;
 
            if (kinds.Contains(declaredSymbolInfo.Kind) &&
                nameMatcher.AddMatches(declaredSymbolInfo.Name, ref nameMatches.AsRef()) &&
                containerMatcher?.AddMatches(declaredSymbolInfo.FullyQualifiedContainerName, ref containerMatches.AsRef()) != false)
            {
                if (cancellationToken.IsCancellationRequested)
                    return;
 
                // See if we have a match in a linked file.  If so, see if we have the same match in
                // other projects that this file is linked in.  If so, include the full set of projects
                // the match is in so we can display that well in the UI.
                //
                // We can only do this in the case where the solution is loaded and thus we can examine
                // the relationship between this document and the other documents linked to it.  In the
                // case where the solution isn't fully loaded and we're just reading in cached data, we
                // don't know what other files we're linked to and can't merge results in this fashion.
                var additionalMatchingProjects = GetAdditionalProjectsWithMatch(
                    document, declaredSymbolInfo, linkedIndices);
 
                var result = ConvertResult(
                    documentKey, document, declaredSymbolInfo, nameMatches, containerMatches, additionalMatchingProjects);
                onItemFound(result);
            }
        }
    }
 
    private static RoslynNavigateToItem ConvertResult(
        DocumentKey documentKey,
        Document? document,
        DeclaredSymbolInfo declaredSymbolInfo,
        in TemporaryArray<PatternMatch> nameMatches,
        in TemporaryArray<PatternMatch> containerMatches,
        ImmutableArray<ProjectId> additionalMatchingProjects)
    {
        var matchKind = GetNavigateToMatchKind(nameMatches);
 
        // A match is considered to be case sensitive if all its constituent pattern matches are
        // case sensitive. 
        var isCaseSensitive = nameMatches.All(m => m.IsCaseSensitive) && containerMatches.All(m => m.IsCaseSensitive);
        var kind = GetItemKind(declaredSymbolInfo);
 
        using var matchedSpans = TemporaryArray<TextSpan>.Empty;
        foreach (var match in nameMatches)
            matchedSpans.AddRange(match.MatchedSpans);
 
        using var allPatternMatches = TemporaryArray<PatternMatch>.Empty;
        allPatternMatches.AddRange(containerMatches);
        allPatternMatches.AddRange(nameMatches);
 
        // If we were not given a Document instance, then we're finding matches in cached data
        // and thus could be 'stale'.
        return new RoslynNavigateToItem(
            isStale: document == null,
            documentKey,
            additionalMatchingProjects,
            declaredSymbolInfo,
            kind,
            matchKind,
            isCaseSensitive,
            matchedSpans.ToImmutableAndClear(),
            allPatternMatches.ToImmutableAndClear());
    }
 
    private static ImmutableArray<ProjectId> GetAdditionalProjectsWithMatch(
        Document? document,
        DeclaredSymbolInfo declaredSymbolInfo,
        ArrayBuilder<(TopLevelSyntaxTreeIndex, ProjectId)>? linkedIndices)
    {
        if (document == null || linkedIndices is null || linkedIndices.Count == 0)
            return [];
 
        using var result = TemporaryArray<ProjectId>.Empty;
 
        foreach (var (index, projectId) in linkedIndices)
        {
            // See if the index for the other file also contains this same info.  If so, merge the results so the
            // user only sees them as a single hit in the UI.
            if (index.DeclaredSymbolInfoSet.Contains(declaredSymbolInfo))
                result.Add(projectId);
        }
 
        return result.ToImmutableAndClear();
    }
 
    private static string GetItemKind(DeclaredSymbolInfo declaredSymbolInfo)
    {
        switch (declaredSymbolInfo.Kind)
        {
            case DeclaredSymbolInfoKind.Class:
            case DeclaredSymbolInfoKind.Record:
                return NavigateToItemKind.Class;
            case DeclaredSymbolInfoKind.RecordStruct:
                return NavigateToItemKind.Structure;
            case DeclaredSymbolInfoKind.Constant:
                return NavigateToItemKind.Constant;
            case DeclaredSymbolInfoKind.Delegate:
                return NavigateToItemKind.Delegate;
            case DeclaredSymbolInfoKind.Enum:
                return NavigateToItemKind.Enum;
            case DeclaredSymbolInfoKind.EnumMember:
                return NavigateToItemKind.EnumItem;
            case DeclaredSymbolInfoKind.Event:
                return NavigateToItemKind.Event;
            case DeclaredSymbolInfoKind.Field:
                return NavigateToItemKind.Field;
            case DeclaredSymbolInfoKind.Interface:
                return NavigateToItemKind.Interface;
            case DeclaredSymbolInfoKind.Constructor:
            case DeclaredSymbolInfoKind.ExtensionMethod:
            case DeclaredSymbolInfoKind.Method:
                return NavigateToItemKind.Method;
            case DeclaredSymbolInfoKind.Module:
                return NavigateToItemKind.Module;
            case DeclaredSymbolInfoKind.Indexer:
            case DeclaredSymbolInfoKind.Property:
                return NavigateToItemKind.Property;
            case DeclaredSymbolInfoKind.Struct:
                return NavigateToItemKind.Structure;
            default:
                throw ExceptionUtilities.UnexpectedValue(declaredSymbolInfo.Kind);
        }
    }
 
    private static NavigateToMatchKind GetNavigateToMatchKind(in TemporaryArray<PatternMatch> nameMatches)
    {
        // work backwards through the match kinds.  That way our result is as bad as our worst match part.  For
        // example, say the user searches for `Console.Write` and we find `Console.Write` (exact, exact), and
        // `Console.WriteLine` (exact, prefix).  We don't want the latter hit to be considered an `exact` match, and
        // thus as good as `Console.Write`.
 
        for (var i = s_kindPairs.Length - 1; i >= 0; i--)
        {
            var (roslynKind, vsKind) = s_kindPairs[i];
            foreach (var match in nameMatches)
            {
                if (match.Kind == roslynKind)
                    return vsKind;
            }
        }
 
        return NavigateToMatchKind.Regular;
    }
 
    private readonly struct DeclaredSymbolInfoKindSet
    {
        private readonly ImmutableArray<bool> _lookupTable;
 
        public DeclaredSymbolInfoKindSet(IEnumerable<string> navigateToItemKinds)
        {
            // The 'Contains' method implementation assumes that the DeclaredSymbolInfoKind type is unsigned.
            Debug.Assert(Enum.GetUnderlyingType(typeof(DeclaredSymbolInfoKind)) == typeof(byte));
 
            var lookupTable = new bool[Enum.GetValues(typeof(DeclaredSymbolInfoKind)).Length];
            foreach (var navigateToItemKind in navigateToItemKinds)
            {
                switch (navigateToItemKind)
                {
                    case NavigateToItemKind.Class:
                        lookupTable[(int)DeclaredSymbolInfoKind.Class] = true;
                        lookupTable[(int)DeclaredSymbolInfoKind.Record] = true;
                        break;
                    case NavigateToItemKind.Constant:
                        lookupTable[(int)DeclaredSymbolInfoKind.Constant] = true;
                        break;
 
                    case NavigateToItemKind.Delegate:
                        lookupTable[(int)DeclaredSymbolInfoKind.Delegate] = true;
                        break;
 
                    case NavigateToItemKind.Enum:
                        lookupTable[(int)DeclaredSymbolInfoKind.Enum] = true;
                        break;
 
                    case NavigateToItemKind.EnumItem:
                        lookupTable[(int)DeclaredSymbolInfoKind.EnumMember] = true;
                        break;
 
                    case NavigateToItemKind.Event:
                        lookupTable[(int)DeclaredSymbolInfoKind.Event] = true;
                        break;
 
                    case NavigateToItemKind.Field:
                        lookupTable[(int)DeclaredSymbolInfoKind.Field] = true;
                        break;
 
                    case NavigateToItemKind.Interface:
                        lookupTable[(int)DeclaredSymbolInfoKind.Interface] = true;
                        break;
 
                    case NavigateToItemKind.Method:
                        lookupTable[(int)DeclaredSymbolInfoKind.Constructor] = true;
                        lookupTable[(int)DeclaredSymbolInfoKind.ExtensionMethod] = true;
                        lookupTable[(int)DeclaredSymbolInfoKind.Method] = true;
                        break;
 
                    case NavigateToItemKind.Module:
                        lookupTable[(int)DeclaredSymbolInfoKind.Module] = true;
                        break;
 
                    case NavigateToItemKind.Property:
                        lookupTable[(int)DeclaredSymbolInfoKind.Indexer] = true;
                        lookupTable[(int)DeclaredSymbolInfoKind.Property] = true;
                        break;
 
                    case NavigateToItemKind.Structure:
                        lookupTable[(int)DeclaredSymbolInfoKind.Struct] = true;
                        lookupTable[(int)DeclaredSymbolInfoKind.RecordStruct] = true;
                        break;
 
                    default:
                        // Not a recognized symbol info kind
                        break;
                }
            }
 
            _lookupTable = ImmutableArray.CreateRange(lookupTable);
        }
 
        public bool Contains(DeclaredSymbolInfoKind item)
        {
            return (int)item < _lookupTable.Length
                && _lookupTable[(int)item];
        }
    }
}