File: SolutionExplorer\SymbolTree\RootSymbolTreeItemSourceProvider.cs
Web Access
Project: src\src\VisualStudio\Core\Impl\Microsoft.VisualStudio.LanguageServices.Implementation.csproj (Microsoft.VisualStudio.LanguageServices.Implementation)
// 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.ComponentModel;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindReferences;
using Microsoft.CodeAnalysis.GoOrFind;
using Microsoft.CodeAnalysis.GoToBase;
using Microsoft.CodeAnalysis.GoToImplementation;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Threading;
using Microsoft.Internal.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer;
 
/// <summary>
/// Source provider responsible for hearing about C#/VB files and attaching the root 'symbol tree' node.
/// Users can then expand that node to get access to the symbols within the file.  Note: this tree is 
/// built lazily (one level at a time), and only uses syntax so it can be done extremely quickly.
/// </summary>
[Export(typeof(IAttachedCollectionSourceProvider))]
[Name(nameof(RootSymbolTreeItemSourceProvider))]
[Order(Before = HierarchyItemsProviderNames.Contains)]
[AppliesToProject("CSharp | VB")]
internal sealed partial class RootSymbolTreeItemSourceProvider : AttachedCollectionSourceProvider<IVsHierarchyItem>
{
    /// <summary>
    /// Mapping from filepath to the collection sources made for it.  Is a multi dictionary because the same
    /// file may appear in multiple projects, but each will have its own collection soure to represent the view
    /// of that file through that project.
    /// </summary>
    /// <remarks>Lock this instance when reading/writing as it is used over different threads.</remarks>
    private readonly Dictionary<string, List<RootSymbolTreeItemCollectionSource>> _filePathToCollectionSources = new(
        StringComparer.OrdinalIgnoreCase);
 
    /// <summary>
    /// Queue of notifications we've heard about for changed document file paths.  We'll then go update the
    /// symbol tree item for each of these documents so that it is up to date.  Note: if the symbol tree has
    /// never been expanded,  this will bail immediately to avoid doing unnecessary work.
    /// </summary>
    private readonly AsyncBatchingWorkQueue<string> _updateSourcesQueue;
    private readonly Workspace _workspace;
 
    private readonly IGoOrFindNavigationService _goToBaseNavigationService;
    private readonly IGoOrFindNavigationService _goToImplementationNavigationService;
    private readonly IGoOrFindNavigationService _findReferencesNavigationService;
 
    public readonly SolutionExplorerNavigationSupport NavigationSupport;
    public readonly IThreadingContext ThreadingContext;
    public readonly IAsynchronousOperationListener Listener;
 
    public readonly IContextMenuController ContextMenuController;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public RootSymbolTreeItemSourceProvider(
        IThreadingContext threadingContext,
        VisualStudioWorkspace workspace,
        GoToBaseNavigationService goToBaseNavigationService,
        GoToImplementationNavigationService goToImplementationNavigationService,
        FindReferencesNavigationService findReferencesNavigationService,
        IAsynchronousOperationListenerProvider listenerProvider)
    {
        ThreadingContext = threadingContext;
        _workspace = workspace;
        _goToBaseNavigationService = goToBaseNavigationService;
        _goToImplementationNavigationService = goToImplementationNavigationService;
        _findReferencesNavigationService = findReferencesNavigationService;
        Listener = listenerProvider.GetListener(FeatureAttribute.SolutionExplorer);
        NavigationSupport = new(workspace, threadingContext, listenerProvider);
 
        _updateSourcesQueue = new AsyncBatchingWorkQueue<string>(
            DelayTimeSpan.Medium,
            UpdateCollectionSourcesAsync,
            // Ignore case as we're comparing file paths here.
            StringComparer.OrdinalIgnoreCase,
            this.Listener,
            this.ThreadingContext.DisposalToken);
 
        this._workspace.RegisterWorkspaceChangedHandler(
            e =>
            {
                var oldPath = e.OldSolution.GetDocument(e.DocumentId)?.FilePath;
                var newPath = e.NewSolution.GetDocument(e.DocumentId)?.FilePath;
 
                if (oldPath != null)
                    _updateSourcesQueue.AddWork(oldPath);
 
                if (newPath != null)
                    _updateSourcesQueue.AddWork(newPath);
            },
            options: new WorkspaceEventOptions(RequiresMainThread: false));
 
        this.ContextMenuController = new SymbolItemContextMenuController(this);
    }
 
    private async ValueTask UpdateCollectionSourcesAsync(
        ImmutableSegmentedList<string> updatedFilePaths, CancellationToken cancellationToken)
    {
        using var _ = Microsoft.CodeAnalysis.PooledObjects.ArrayBuilder<RootSymbolTreeItemCollectionSource>.GetInstance(out var sources);
 
        lock (_filePathToCollectionSources)
        {
            foreach (var filePath in updatedFilePaths)
            {
                if (_filePathToCollectionSources.TryGetValue(filePath, out var pathSources))
                    sources.AddRange(pathSources);
            }
        }
 
        // Update all the affected documents in parallel.
        await Parallel.ForEachAsync(
            sources,
            cancellationToken,
            async (source, cancellationToken) =>
            {
                await source.UpdateIfEverExpandedAsync(cancellationToken)
                    .ReportNonFatalErrorUnlessCancelledAsync(cancellationToken)
                    .ConfigureAwait(false);
            }).ConfigureAwait(false);
    }
 
    protected override IAttachedCollectionSource? CreateCollectionSource(IVsHierarchyItem item, string relationshipName)
    {
        if (item == null ||
            item.IsDisposed ||
            item.HierarchyIdentity == null ||
            item.HierarchyIdentity.NestedHierarchy == null ||
            relationshipName != KnownRelationships.Contains)
        {
            return null;
        }
 
        var hierarchy = item.HierarchyIdentity.NestedHierarchy;
        var itemId = item.HierarchyIdentity.NestedItemID;
 
        if (hierarchy.GetProperty(itemId, (int)__VSHPROPID7.VSHPROPID_ProjectTreeCapabilities, out var capabilitiesObj) != VSConstants.S_OK ||
            capabilitiesObj is not string capabilities)
        {
            return null;
        }
 
        if (!capabilities.Contains(nameof(VisualStudio.ProjectSystem.ProjectTreeFlags.SourceFile)) ||
            !capabilities.Contains(nameof(VisualStudio.ProjectSystem.ProjectTreeFlags.FileOnDisk)))
        {
            return null;
        }
 
        // Important: currentFilePath is mutable state captured *AND UPDATED* in the local function  
        // OnItemPropertyChanged below.  It allows us to know the file path of the item *prior* to
        // it being changed *when* we hear the update about it having changed (since hte event doesn't
        // contain the old value).  
        if (item.CanonicalName is not string currentFilePath)
            return null;
 
        var source = new RootSymbolTreeItemCollectionSource(this, item);
        lock (_filePathToCollectionSources)
        {
            _filePathToCollectionSources.MultiAdd(currentFilePath, source);
        }
 
        // Register to hear about if this hierarchy is disposed. We'll stop watching it if so.
        item.PropertyChanged += OnItemPropertyChanged;
 
        return source;
 
        void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(ISupportDisposalNotification.IsDisposed) && item.IsDisposed)
            {
                // We are notified when the IVsHierarchyItem is removed from the tree via its INotifyPropertyChanged
                // event for the IsDisposed property. When this fires, we remove the filePath->sourcce mapping we're holding.
                lock (_filePathToCollectionSources)
                {
                    _filePathToCollectionSources.MultiRemove(currentFilePath, source);
                }
 
                item.PropertyChanged -= OnItemPropertyChanged;
            }
            else if (e.PropertyName == nameof(IVsHierarchyItem.CanonicalName))
            {
                var newPath = item.CanonicalName;
                if (newPath != currentFilePath)
                {
                    lock (_filePathToCollectionSources)
                    {
 
                        // Unlink the oldPath->source mapping, and add a new line for the newPath->source.
                        _filePathToCollectionSources.MultiRemove(currentFilePath, source);
                        _filePathToCollectionSources.MultiAdd(newPath, source);
 
                        // Keep track of the 'newPath'.
                        currentFilePath = newPath;
                    }
 
                    // If the filepath changes for an item (which can happen when it is renamed), place a notification
                    // in the queue to update it in the future.  This will ensure all the items presented for it have hte
                    // right document id.  Also reset the state of the source.  The filepath could change to something
                    // no longer valid (like .cs to .txt), or vice versa.
                    source.Reset();
                    _updateSourcesQueue.AddWork(newPath);
                }
            }
        }
    }
}