File: NavigateTo\RoslynNavigateToItem.cs
Web Access
Project: ..\..\..\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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.NavigateTo
{
    /// <summary>
    /// Data about a navigate to match.  Only intended for use by C# and VB.  Carries enough rich information to
    /// rehydrate everything needed quickly on either the host or remote side.
    /// </summary>
    [DataContract]
    internal readonly struct RoslynNavigateToItem
    {
        [DataMember(Order = 0)]
        public readonly bool IsStale;
 
        [DataMember(Order = 1)]
        public readonly DocumentId DocumentId;
 
        [DataMember(Order = 2)]
        public readonly ImmutableArray<ProjectId> AdditionalMatchingProjects;
 
        [DataMember(Order = 3)]
        public readonly DeclaredSymbolInfo DeclaredSymbolInfo;
 
        /// <summary>
        /// Will be one of the values from <see cref="NavigateToItemKind"/>.
        /// </summary>
        [DataMember(Order = 4)]
        public readonly string Kind;
 
        [DataMember(Order = 5)]
        public readonly NavigateToMatchKind MatchKind;
 
        [DataMember(Order = 6)]
        public readonly bool IsCaseSensitive;
 
        [DataMember(Order = 7)]
        public readonly ImmutableArray<TextSpan> NameMatchSpans;
 
        [DataMember(Order = 8)]
        public readonly ImmutableArray<PatternMatch> Matches;
 
        public RoslynNavigateToItem(
            bool isStale,
            DocumentId documentId,
            ImmutableArray<ProjectId> additionalMatchingProjects,
            DeclaredSymbolInfo declaredSymbolInfo,
            string kind,
            NavigateToMatchKind matchKind,
            bool isCaseSensitive,
            ImmutableArray<TextSpan> nameMatchSpans,
            ImmutableArray<PatternMatch> matches)
        {
            IsStale = isStale;
            DocumentId = documentId;
            AdditionalMatchingProjects = additionalMatchingProjects;
            DeclaredSymbolInfo = declaredSymbolInfo;
            Kind = kind;
            MatchKind = matchKind;
            IsCaseSensitive = isCaseSensitive;
            NameMatchSpans = nameMatchSpans;
            Matches = matches;
        }
 
        public async Task<INavigateToSearchResult?> TryCreateSearchResultAsync(
            Solution solution, Document? activeDocument, CancellationToken cancellationToken)
        {
            if (IsStale)
            {
                // may refer to a document that doesn't exist anymore.  Bail out gracefully in that case.
                var document = solution.GetDocument(DocumentId);
                if (document == null)
                    return null;
 
                return new NavigateToSearchResult(this, document, activeDocument);
            }
            else
            {
                var document = await solution.GetRequiredDocumentAsync(
                    DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
                return new NavigateToSearchResult(this, document, activeDocument);
            }
        }
 
        private class NavigateToSearchResult : INavigateToSearchResult, INavigableItem
        {
            private static readonly char[] s_dotArray = { '.' };
 
            private readonly RoslynNavigateToItem _item;
 
            /// <summary>
            /// The <see cref="Document"/> that <see cref="_item"/> is contained within.
            /// </summary>
            private readonly Document _itemDocument;
 
            /// <summary>
            /// The document the user was editing when they invoked the navigate-to operation.
            /// </summary>
            private readonly Document? _activeDocument;
 
            private readonly string _additionalInformation;
            private readonly Lazy<string> _secondarySort;
 
            public NavigateToSearchResult(
                RoslynNavigateToItem item,
                Document itemDocument,
                Document? activeDocument)
            {
                _item = item;
                _itemDocument = itemDocument;
                _activeDocument = activeDocument;
                _additionalInformation = ComputeAdditionalInformation();
                _secondarySort = new Lazy<string>(ComputeSecondarySort);
            }
 
            private string ComputeAdditionalInformation()
            {
                // For partial types, state what file they're in so the user can disambiguate the results.
                var combinedProjectName = ComputeCombinedProjectName();
                return (_item.DeclaredSymbolInfo.IsPartial, IsNonNestedNamedType()) switch
                {
                    (true, true) => string.Format(FeaturesResources._0_dash_1, _itemDocument.Name, combinedProjectName),
                    (true, false) => string.Format(FeaturesResources.in_0_1_2, _item.DeclaredSymbolInfo.ContainerDisplayName, _itemDocument.Name, combinedProjectName),
                    (false, true) => string.Format(FeaturesResources.project_0, combinedProjectName),
                    (false, false) => string.Format(FeaturesResources.in_0_project_1, _item.DeclaredSymbolInfo.ContainerDisplayName, combinedProjectName),
                };
            }
 
            private string ComputeCombinedProjectName()
            {
                // If there aren't any additional matches in other projects, we don't need to merge anything.
                if (_item.AdditionalMatchingProjects.Length > 0)
                {
                    // First get the simple project name and flavor for the actual project we got a hit in.  If we can't
                    // figure this out, we can't create a merged name.
                    var firstProject = _itemDocument.Project;
                    var (firstProjectName, firstProjectFlavor) = firstProject.State.NameAndFlavor;
 
                    if (firstProjectName != null)
                    {
                        var solution = firstProject.Solution;
 
                        using var _ = ArrayBuilder<string>.GetInstance(out var flavors);
                        flavors.Add(firstProjectFlavor!);
 
                        // Now, do the same for the other projects where we had a match. As above, if we can't figure out the
                        // simple name/flavor, or if the simple project name doesn't match the simple project name we started
                        // with then we can't merge these.
                        foreach (var additionalProjectId in _item.AdditionalMatchingProjects)
                        {
                            var additionalProject = solution.GetRequiredProject(additionalProjectId);
                            var (projectName, projectFlavor) = additionalProject.State.NameAndFlavor;
                            if (projectName == firstProjectName)
                                flavors.Add(projectFlavor!);
                        }
 
                        flavors.RemoveDuplicates();
                        flavors.Sort();
 
                        return $"{firstProjectName} ({string.Join(", ", flavors)})";
                    }
                }
 
                // Couldn't compute a merged project name (or only had one project).  Just return the name of hte project itself.
                return _itemDocument.Project.Name;
            }
 
            string INavigateToSearchResult.AdditionalInformation => _additionalInformation;
 
            private bool IsNonNestedNamedType()
                => !_item.DeclaredSymbolInfo.IsNestedType && IsNamedType();
 
            private bool IsNamedType()
            {
                switch (_item.DeclaredSymbolInfo.Kind)
                {
                    case DeclaredSymbolInfoKind.Class:
                    case DeclaredSymbolInfoKind.Record:
                    case DeclaredSymbolInfoKind.Enum:
                    case DeclaredSymbolInfoKind.Interface:
                    case DeclaredSymbolInfoKind.Module:
                    case DeclaredSymbolInfoKind.Struct:
                    case DeclaredSymbolInfoKind.RecordStruct:
                        return true;
                    default:
                        return false;
                }
            }
 
            string INavigateToSearchResult.Kind => _item.Kind;
 
            NavigateToMatchKind INavigateToSearchResult.MatchKind => _item.MatchKind;
 
            bool INavigateToSearchResult.IsCaseSensitive => _item.IsCaseSensitive;
 
            string INavigateToSearchResult.Name => _item.DeclaredSymbolInfo.Name;
 
            ImmutableArray<TextSpan> INavigateToSearchResult.NameMatchSpans => _item.NameMatchSpans;
 
            string INavigateToSearchResult.SecondarySort => _secondarySort.Value;
 
            private string ComputeSecondarySort()
            {
                using var _ = ArrayBuilder<string>.GetInstance(out var parts);
 
                // Ensure if all else is equal, that high-pri items (e.g. from the user's current file) come first
                // before low pri items.  This only applies if things like the MatchKind are the same.  So we'll
                // still show an exact match from another file before a substring match from the current file.
                parts.Add(ComputeFolderDistance().ToString("X4"));
 
                parts.Add(_item.DeclaredSymbolInfo.ParameterCount.ToString("X4"));
                parts.Add(_item.DeclaredSymbolInfo.TypeParameterCount.ToString("X4"));
                parts.Add(_item.DeclaredSymbolInfo.Name);
 
                // For partial types, we break up the file name into pieces.  i.e. If we have
                // Outer.cs and Outer.Inner.cs  then we add "Outer" and "Outer Inner" to 
                // the secondary sort string.  That way "Outer.cs" will be weighted above
                // "Outer.Inner.cs"
                var fileName = Path.GetFileNameWithoutExtension(_itemDocument.FilePath ?? "");
                parts.AddRange(fileName.Split(s_dotArray));
 
                return string.Join(" ", parts);
 
                // How close these files are in terms of file system path.  Identical files will have distance 0. Files
                // in the same folder will have distance 1.  Files in different folders will have increasing values here
                // depending on how many folder elements they share/differ on.
                int ComputeFolderDistance()
                {
                    // No need to compute anything if there is no active document.  Consider all documents equal.
                    if (_activeDocument == null)
                        return 0;
 
                    // The result was in the active document, this get highest priority.
                    if (_activeDocument == _itemDocument)
                        return 0;
 
                    var activeFolders = _activeDocument.Folders;
                    var itemFolders = _itemDocument.Folders;
 
                    // see how many folder they have in common.
                    var commonCount = GetCommonFolderCount();
 
                    // from this, we can see how many folders then differ between them.
                    var activeDiff = activeFolders.Count - commonCount;
                    var itemDiff = itemFolders.Count - commonCount;
 
                    // Add one more to the result.  This way if they share all the same folders that we still return
                    // '1', indicating that this close to, but not as good a match as an exact file match.
                    return activeDiff + itemDiff + 1;
                }
 
                int GetCommonFolderCount()
                {
                    var activeFolders = _activeDocument.Folders;
                    var itemFolders = _itemDocument.Folders;
 
                    var maxCommon = Math.Min(activeFolders.Count, itemFolders.Count);
                    for (var i = 0; i < maxCommon; i++)
                    {
                        if (activeFolders[i] != itemFolders[i])
                            return i;
                    }
 
                    return maxCommon;
                }
            }
 
            string? INavigateToSearchResult.Summary => null;
 
            INavigableItem INavigateToSearchResult.NavigableItem => this;
 
            ImmutableArray<PatternMatch> INavigateToSearchResult.Matches => _item.Matches;
 
            #region INavigableItem
 
            Glyph INavigableItem.Glyph => GetGlyph(_item.DeclaredSymbolInfo.Kind, _item.DeclaredSymbolInfo.Accessibility);
 
            private static Glyph GetPublicGlyph(DeclaredSymbolInfoKind kind)
                => kind switch
                {
                    DeclaredSymbolInfoKind.Class => Glyph.ClassPublic,
                    DeclaredSymbolInfoKind.Constant => Glyph.ConstantPublic,
                    DeclaredSymbolInfoKind.Constructor => Glyph.MethodPublic,
                    DeclaredSymbolInfoKind.Delegate => Glyph.DelegatePublic,
                    DeclaredSymbolInfoKind.Enum => Glyph.EnumPublic,
                    DeclaredSymbolInfoKind.EnumMember => Glyph.EnumMemberPublic,
                    DeclaredSymbolInfoKind.Event => Glyph.EventPublic,
                    DeclaredSymbolInfoKind.ExtensionMethod => Glyph.ExtensionMethodPublic,
                    DeclaredSymbolInfoKind.Field => Glyph.FieldPublic,
                    DeclaredSymbolInfoKind.Indexer => Glyph.PropertyPublic,
                    DeclaredSymbolInfoKind.Interface => Glyph.InterfacePublic,
                    DeclaredSymbolInfoKind.Method => Glyph.MethodPublic,
                    DeclaredSymbolInfoKind.Module => Glyph.ModulePublic,
                    DeclaredSymbolInfoKind.Property => Glyph.PropertyPublic,
                    DeclaredSymbolInfoKind.Struct => Glyph.StructurePublic,
                    DeclaredSymbolInfoKind.RecordStruct => Glyph.StructurePublic,
                    _ => Glyph.ClassPublic,
                };
 
            private static Glyph GetGlyph(DeclaredSymbolInfoKind kind, Accessibility accessibility)
            {
                // Glyphs are stored in this order:
                //  ClassPublic,
                //  ClassProtected,
                //  ClassPrivate,
                //  ClassInternal,
 
                var rawGlyph = GetPublicGlyph(kind);
 
                switch (accessibility)
                {
                    case Accessibility.Private:
                        rawGlyph += (Glyph.ClassPrivate - Glyph.ClassPublic);
                        break;
                    case Accessibility.Internal:
                        rawGlyph += (Glyph.ClassInternal - Glyph.ClassPublic);
                        break;
                    case Accessibility.Protected:
                    case Accessibility.ProtectedOrInternal:
                    case Accessibility.ProtectedAndInternal:
                        rawGlyph += (Glyph.ClassProtected - Glyph.ClassPublic);
                        break;
                }
 
                return rawGlyph;
            }
 
            ImmutableArray<TaggedText> INavigableItem.DisplayTaggedParts
                => ImmutableArray.Create(new TaggedText(
                    TextTags.Text, _item.DeclaredSymbolInfo.Name + _item.DeclaredSymbolInfo.NameSuffix));
 
            bool INavigableItem.DisplayFileLocation => false;
 
            /// <summary>
            /// DeclaredSymbolInfos always come from some actual declaration in source.  So they're
            /// never implicitly declared.
            /// </summary>
            bool INavigableItem.IsImplicitlyDeclared => false;
 
            Document INavigableItem.Document => _itemDocument;
 
            TextSpan INavigableItem.SourceSpan => _item.DeclaredSymbolInfo.Span;
 
            bool INavigableItem.IsStale => _item.IsStale;
 
            ImmutableArray<INavigableItem> INavigableItem.ChildItems => ImmutableArray<INavigableItem>.Empty;
 
            #endregion
        }
    }
}