File: FindReferences\StreamingFindUsagesPresenter.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.ComponentModel.Composition.Hosting;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.LanguageServices.Implementation.FindReferences;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.FindAllReferences;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Shell.TableManager;
using Microsoft.VisualStudio.Text.Classification;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.FindUsages;
 
[Export(typeof(IStreamingFindUsagesPresenter)), Shared]
internal sealed partial class StreamingFindUsagesPresenter : IStreamingFindUsagesPresenter
{
    public const string RoslynFindUsagesTableDataSourceIdentifier =
        nameof(RoslynFindUsagesTableDataSourceIdentifier);
 
    public const string RoslynFindUsagesTableDataSourceSourceTypeIdentifier =
        nameof(RoslynFindUsagesTableDataSourceSourceTypeIdentifier);
 
    private readonly IServiceProvider _serviceProvider;
 
    public readonly ClassificationTypeMap TypeMap;
    public readonly IEditorFormatMapService FormatMapService;
    private readonly IAsynchronousOperationListener _asyncListener;
    private readonly Lazy<IClassificationFormatMap> _lazyClassificationFormatMap;
 
    private readonly Workspace _workspace;
    private readonly IThreadingContext ThreadingContext;
    private readonly IGlobalOptionService _globalOptions;
 
    private readonly HashSet<AbstractTableDataSourceFindUsagesContext> _currentContexts = [];
    private readonly ImmutableArray<ITableColumnDefinition> _customColumns;
 
    /// <summary>
    /// Optional info bar that can be shown in the FindRefs window.  Useful, for example, for letting the user know
    /// if the results are incomplete because the solution is loading.  Only valid to read/write on the UI thread.
    /// </summary>
    private IVsInfoBarUIElement? _infoBar;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public StreamingFindUsagesPresenter(
        IThreadingContext threadingContext,
        VisualStudioWorkspace workspace,
        IGlobalOptionService globalOptions,
        Shell.SVsServiceProvider serviceProvider,
        ClassificationTypeMap typeMap,
        IEditorFormatMapService formatMapService,
        IClassificationFormatMapService classificationFormatMapService,
        IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
        [ImportMany] IEnumerable<Lazy<ITableColumnDefinition, NameMetadata>> columns)
        : this(workspace,
               threadingContext,
               serviceProvider,
               globalOptions,
               typeMap,
               formatMapService,
               classificationFormatMapService,
               GetCustomColumns(columns),
               asynchronousOperationListenerProvider)
    {
    }
 
    // Test only
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")]
    public StreamingFindUsagesPresenter(
        Workspace workspace,
        ExportProvider exportProvider)
        : this(workspace,
              exportProvider.GetExportedValue<IThreadingContext>(),
              exportProvider.GetExportedValue<Shell.SVsServiceProvider>(),
              exportProvider.GetExportedValue<IGlobalOptionService>(),
              exportProvider.GetExportedValue<ClassificationTypeMap>(),
              exportProvider.GetExportedValue<IEditorFormatMapService>(),
              exportProvider.GetExportedValue<IClassificationFormatMapService>(),
              exportProvider.GetExportedValues<ITableColumnDefinition>(),
              exportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>())
    {
    }
 
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0034:Exported parts should have [ImportingConstructor]", Justification = "Used incorrectly by tests")]
    private StreamingFindUsagesPresenter(
        Workspace workspace,
        IThreadingContext threadingContext,
        Shell.SVsServiceProvider serviceProvider,
        IGlobalOptionService optionService,
        ClassificationTypeMap typeMap,
        IEditorFormatMapService formatMapService,
        IClassificationFormatMapService classificationFormatMapService,
        IEnumerable<ITableColumnDefinition> columns,
        IAsynchronousOperationListenerProvider asyncListenerProvider)
    {
        _workspace = workspace;
        ThreadingContext = threadingContext;
        _globalOptions = optionService;
        _serviceProvider = serviceProvider;
        TypeMap = typeMap;
        FormatMapService = formatMapService;
        _asyncListener = asyncListenerProvider.GetListener(FeatureAttribute.FindReferences);
 
        _lazyClassificationFormatMap = new Lazy<IClassificationFormatMap>(() =>
        {
            ThreadingContext.ThrowIfNotOnUIThread();
            return classificationFormatMapService.GetClassificationFormatMap("tooltip");
        });
 
        _customColumns = columns.ToImmutableArray();
    }
 
    public IClassificationFormatMap ClassificationFormatMap
        => _lazyClassificationFormatMap.Value;
 
    private static bool IsPrimary(DefinitionItem definition)
        => definition.Properties.ContainsKey(DefinitionItem.Primary);
 
    private static IEnumerable<ITableColumnDefinition> GetCustomColumns(IEnumerable<Lazy<ITableColumnDefinition, NameMetadata>> columns)
    {
        foreach (var column in columns)
        {
            // PERF: Filter the columns by metadata name before selecting our custom columns.
            //       This is done to ensure that we do not load every single table control column
            //       that implements ITableColumnDefinition, which will cause unwanted assembly loads.
            switch (column.Metadata.Name)
            {
                case StandardTableKeyNames2.SymbolKind:
                case ContainingTypeColumnDefinition.ColumnName:
                case ContainingMemberColumnDefinition.ColumnName:
                case StandardTableKeyNames.Repository:
                case StandardTableKeyNames.ItemOrigin:
                    yield return column.Value;
                    break;
            }
        }
    }
 
    public void ClearAll()
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        foreach (var context in _currentContexts)
        {
            context.Clear();
        }
    }
 
    /// <summary>
    /// Start a search that may include Containing Type, Containing Member, or Kind information about the reference
    /// </summary>
    public (FindUsagesContext context, CancellationToken cancellationToken) StartSearch(string title, StreamingFindUsagesPresenterOptions options)
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        var vsFindAllReferencesService = (IFindAllReferencesService)_serviceProvider.GetService(typeof(SVsFindAllReferences));
 
        // Get the appropriate window for FAR results to go into.
        var window = vsFindAllReferencesService.StartSearch(title);
 
        // If there is an info bar on our tool window, clear it out.
        RemoveExistingInfoBar();
 
        // Keep track of the users preference for grouping by definition if we don't already know it.
        // We need this because we disable the Definition column when we're not showing references
        // (i.e. GoToImplementation/GoToDef).  However, we want to restore the user's choice if they
        // then do another FindAllReferences.
        var desiredGroupingPriority = _globalOptions.GetOption(FindUsagesPresentationOptionsStorage.DefinitionGroupingPriority);
        if (desiredGroupingPriority < 0)
        {
            StoreCurrentGroupingPriority(window);
        }
 
        var context = options.SupportsReferences
            ? StartSearchWithReferences(window, desiredGroupingPriority, options.IncludeContainingTypeAndMemberColumns, options.IncludeKindColumn)
            : StartSearchWithoutReferences(window, options.IncludeContainingTypeAndMemberColumns, options.IncludeKindColumn);
 
        // Keep track of this context object as long as it is being displayed in the UI.
        // That way we can Clear it out if requested by a client.  When the context is
        // no longer being displayed, VS will dispose it and it will remove itself from
        // this set.
        _currentContexts.Add(context);
        return (context, context.CancellationTokenSource!.Token);
    }
 
    private AbstractTableDataSourceFindUsagesContext StartSearchWithReferences(
        IFindAllReferencesWindow window, int desiredGroupingPriority, bool includeContainingTypeAndMemberColumns, bool includeKindColumn)
    {
        // Ensure that the window's definition-grouping reflects what the user wants.
        // i.e. we may have disabled this column for a previous GoToImplementation call. 
        // We want to still show the column as long as the user has not disabled it themselves.
        var definitionColumn = window.GetDefinitionColumn();
        if (definitionColumn.GroupingPriority != desiredGroupingPriority)
        {
            SetDefinitionGroupingPriority(window, desiredGroupingPriority);
        }
 
        // If the user changes the grouping, then store their current preference.
        var tableControl = (IWpfTableControl2)window.TableControl;
        tableControl.GroupingsChanged += (s, e) => StoreCurrentGroupingPriority(window);
 
        return new WithReferencesFindUsagesContext(
            this, window, _customColumns, _globalOptions, includeContainingTypeAndMemberColumns, includeKindColumn, ThreadingContext);
    }
 
    private AbstractTableDataSourceFindUsagesContext StartSearchWithoutReferences(
        IFindAllReferencesWindow window, bool includeContainingTypeAndMemberColumns, bool includeKindColumn)
    {
        // If we're not showing references, then disable grouping by definition, as that will
        // just lead to a poor experience.  i.e. we'll have the definition entry buckets, 
        // with the same items showing underneath them.
        SetDefinitionGroupingPriority(window, 0);
        return new WithoutReferencesFindUsagesContext(
            this, window, _customColumns, _globalOptions, includeContainingTypeAndMemberColumns, includeKindColumn, ThreadingContext);
    }
 
    private void StoreCurrentGroupingPriority(IFindAllReferencesWindow window)
    {
        _globalOptions.SetGlobalOption(FindUsagesPresentationOptionsStorage.DefinitionGroupingPriority, window.GetDefinitionColumn().GroupingPriority);
    }
 
    private void SetDefinitionGroupingPriority(IFindAllReferencesWindow window, int priority)
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        using var _ = ArrayBuilder<ColumnState>.GetInstance(out var newColumns);
        var tableControl = (IWpfTableControl2)window.TableControl;
 
        foreach (var columnState in window.TableControl.ColumnStates)
        {
            var columnState2 = columnState as ColumnState2;
            if (columnState2?.Name == StandardTableColumnDefinitions2.Definition)
            {
                newColumns.Add(new ColumnState2(
                    columnState2.Name,
                    isVisible: false,
                    width: columnState2.Width,
                    sortPriority: columnState2.SortPriority,
                    descendingSort: columnState2.DescendingSort,
                    groupingPriority: priority));
            }
            else
            {
                newColumns.Add(columnState);
            }
        }
 
        tableControl.SetColumnStates(newColumns);
    }
 
    private static (Guid, string projectName) GetGuidAndProjectName(Document document)
    {
        // The FAR system needs to know the guid for the project that a def/reference is 
        // from (to support features like filtering).  Normally that would mean we could
        // only support this from a VisualStudioWorkspace.  However, we want till work 
        // in cases like Any-Code (which does not use a VSWorkspace).  So we are tolerant
        // when we have another type of workspace.  This means we will show results, but
        // certain features (like filtering) may not work in that context.
        var vsWorkspace = document.Project.Solution.Workspace as VisualStudioWorkspace;
 
        var (projectName, _) = document.Project.State.NameAndFlavor;
        projectName ??= document.Project.Name;
 
        var guid = vsWorkspace?.GetProjectGuid(document.Project.Id) ?? Guid.Empty;
 
        return (guid, projectName);
    }
 
    private void RemoveExistingInfoBar()
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        var infoBar = _infoBar;
        _infoBar = null;
        if (infoBar != null)
        {
            var infoBarHost = GetInfoBarHost();
            infoBarHost?.RemoveInfoBar(infoBar);
        }
    }
 
    private IVsInfoBarHost? GetInfoBarHost()
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        // Guid of the FindRefs window.  Defined here:
        // https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/env/ErrorList/Pkg/Guids.cs&version=GBmain&line=24
        var guid = new Guid("a80febb4-e7e0-4147-b476-21aaf2453969");
 
        if (_serviceProvider.GetService(typeof(SVsUIShell)) is not IVsUIShell uiShell ||
            uiShell.FindToolWindow((uint)__VSFINDTOOLWIN.FTW_fFindFirst, ref guid, out var windowFrame) != VSConstants.S_OK ||
            windowFrame == null ||
            windowFrame.GetProperty((int)__VSFPROPID7.VSFPROPID_InfoBarHost, out var infoBarHostObj) != VSConstants.S_OK ||
            infoBarHostObj is not IVsInfoBarHost infoBarHost)
        {
            return null;
        }
 
        return infoBarHost;
    }
 
    private async Task ReportMessageAsync(string message, NotificationSeverity severity, CancellationToken cancellationToken)
    {
        await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        RemoveExistingInfoBar();
 
        if (_serviceProvider.GetService(typeof(SVsInfoBarUIFactory)) is not IVsInfoBarUIFactory factory)
            return;
 
        var infoBarHost = GetInfoBarHost();
        if (infoBarHost == null)
            return;
 
        _infoBar = factory.CreateInfoBar(new InfoBarModel(
            message,
            severity switch
            {
                NotificationSeverity.Information => KnownMonikers.StatusInformation,
                NotificationSeverity.Error => KnownMonikers.StatusError,
                NotificationSeverity.Warning => KnownMonikers.StatusWarning,
                _ => throw ExceptionUtilities.UnexpectedValue(severity),
            },
            isCloseButtonVisible: false));
 
        infoBarHost.AddInfoBar(_infoBar);
    }
}