diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItem.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItem.cs index 1af836885084e..a937acffd58cd 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItem.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItem.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Imaging; @@ -11,10 +9,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; -internal partial class AnalyzerItem( +internal sealed partial class AnalyzerItem( AnalyzersFolderItem analyzersFolder, AnalyzerReference analyzerReference, - IContextMenuController contextMenuController) : BaseItem(GetNameText(analyzerReference)) + IContextMenuController contextMenuController) + : BaseItem(GetNameText(analyzerReference)) { public AnalyzersFolderItem AnalyzersFolder { get; } = analyzersFolder; public AnalyzerReference AnalyzerReference { get; } = analyzerReference; @@ -37,14 +36,7 @@ public void Remove() => this.AnalyzersFolder.RemoveAnalyzer(this.AnalyzerReference.FullPath); private static string GetNameText(AnalyzerReference analyzerReference) - { - if (analyzerReference is UnresolvedAnalyzerReference) - { - return analyzerReference.FullPath; - } - else - { - return analyzerReference.Display; - } - } + => analyzerReference is UnresolvedAnalyzerReference unresolvedAnalyzerReference + ? unresolvedAnalyzerReference.FullPath + : analyzerReference.Display; } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index c3107210dc93c..1451dca714dc9 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -2,237 +2,179 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.SourceGeneration; using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.Shell; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; -namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer +internal sealed class AnalyzerItemSource : IAttachedCollectionSource { - internal class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged - { - private readonly AnalyzersFolderItem _analyzersFolder; - private readonly IAnalyzersCommandHandler _commandHandler; - private IReadOnlyCollection _analyzerReferences; - private BulkObservableCollection _analyzerItems; + private readonly AnalyzersFolderItem _analyzersFolder; + private readonly IAnalyzersCommandHandler _commandHandler; - public event PropertyChangedEventHandler PropertyChanged; + private readonly BulkObservableCollection _items = []; - public AnalyzerItemSource(AnalyzersFolderItem analyzersFolder, IAnalyzersCommandHandler commandHandler) - { - _analyzersFolder = analyzersFolder; - _commandHandler = commandHandler; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly AsyncBatchingWorkQueue _workQueue; - _analyzersFolder.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged; - } + private IReadOnlyCollection? _analyzerReferences; - private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) - { - switch (e.Kind) - { - case WorkspaceChangeKind.SolutionAdded: - case WorkspaceChangeKind.SolutionChanged: - case WorkspaceChangeKind.SolutionReloaded: - UpdateAnalyzers(); - break; - - case WorkspaceChangeKind.SolutionRemoved: - case WorkspaceChangeKind.SolutionCleared: - _analyzersFolder.Workspace.WorkspaceChanged -= Workspace_WorkspaceChanged; - break; - - case WorkspaceChangeKind.ProjectAdded: - case WorkspaceChangeKind.ProjectReloaded: - case WorkspaceChangeKind.ProjectChanged: - if (e.ProjectId == _analyzersFolder.ProjectId) - { - UpdateAnalyzers(); - } - - break; - - case WorkspaceChangeKind.ProjectRemoved: - if (e.ProjectId == _analyzersFolder.ProjectId) - { - _analyzersFolder.Workspace.WorkspaceChanged -= Workspace_WorkspaceChanged; - } - - break; - } - } + private Workspace Workspace => _analyzersFolder.Workspace; + private ProjectId ProjectId => _analyzersFolder.ProjectId; - private void UpdateAnalyzers() - { - if (_analyzerItems == null) - { - // The set of AnalyzerItems hasn't been realized yet. Just signal that HasItems - // may have changed. + public AnalyzerItemSource( + AnalyzersFolderItem analyzersFolder, + IAnalyzersCommandHandler commandHandler, + IAsynchronousOperationListenerProvider listenerProvider) + { + _analyzersFolder = analyzersFolder; + _commandHandler = commandHandler; - NotifyPropertyChanged(nameof(HasItems)); - return; - } + _workQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.Idle, + ProcessQueueAsync, + listenerProvider.GetListener(FeatureAttribute.SourceGenerators), + _cancellationTokenSource.Token); - var project = _analyzersFolder.Workspace - .CurrentSolution - .GetProject(_analyzersFolder.ProjectId); + this.Workspace.WorkspaceChanged += OnWorkspaceChanged; - if (project != null && - project.AnalyzerReferences != _analyzerReferences) - { - _analyzerReferences = project.AnalyzerReferences; + // Kick off the initial work to determine the starting set of items. + _workQueue.AddWork(); + } - _analyzerItems.BeginBulkOperation(); + public object SourceItem => _analyzersFolder; - var itemsToRemove = _analyzerItems - .Where(item => !_analyzerReferences.Contains(item.AnalyzerReference)) - .ToArray(); + // Defer actual determination and computation of the items until later. + public bool HasItems => !_cancellationTokenSource.IsCancellationRequested; - var referencesToAdd = GetFilteredAnalyzers(_analyzerReferences, project) - .Where(r => !_analyzerItems.Any(item => item.AnalyzerReference == r)) - .ToArray(); + public IEnumerable Items => _items; - foreach (var item in itemsToRemove) - { - _analyzerItems.Remove(item); - } + private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) + { + switch (e.Kind) + { + case WorkspaceChangeKind.SolutionAdded: + case WorkspaceChangeKind.SolutionChanged: + case WorkspaceChangeKind.SolutionReloaded: + case WorkspaceChangeKind.SolutionRemoved: + case WorkspaceChangeKind.SolutionCleared: + _workQueue.AddWork(); + break; + + case WorkspaceChangeKind.ProjectAdded: + case WorkspaceChangeKind.ProjectReloaded: + case WorkspaceChangeKind.ProjectChanged: + case WorkspaceChangeKind.ProjectRemoved: + if (e.ProjectId == this.ProjectId) + _workQueue.AddWork(); + + break; + } + } - foreach (var reference in referencesToAdd) - { - _analyzerItems.Add(new AnalyzerItem(_analyzersFolder, reference, _commandHandler.AnalyzerContextMenuController)); - } + private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) + { + // If the project went away, then shut ourselves down. + var project = this.Workspace.CurrentSolution.GetProject(this.ProjectId); + if (project is null) + { + this.Workspace.WorkspaceChanged -= OnWorkspaceChanged; - var sorted = _analyzerItems.OrderBy(item => item.AnalyzerReference.Display).ToArray(); - for (var i = 0; i < sorted.Length; i++) - { - _analyzerItems.Move(_analyzerItems.IndexOf(sorted[i]), i); - } + _cancellationTokenSource.Cancel(); - _analyzerItems.EndBulkOperation(); + // Note: mutating _items will be picked up automatically by clients who are bound to the collection. We do + // not need to notify them through some other mechanism. - NotifyPropertyChanged(nameof(HasItems)); + if (_items.Count > 0) + { + // Go back to UI thread to update the observable collection. Otherwise, it enqueue its own UI work that we cannot track. + await _analyzersFolder.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + _items.Clear(); } - } - private void NotifyPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return; } - public bool HasItems - { - get - { - if (_analyzerItems != null) - { - return _analyzerItems.Count > 0; - } - - var project = _analyzersFolder.Workspace - .CurrentSolution - .GetProject(_analyzersFolder.ProjectId); + // If nothing changed wrt analyzer references, then there's nothing we need to do. + if (project.AnalyzerReferences == _analyzerReferences) + return; - if (project != null) - { - return project.AnalyzerReferences.Count > 0; - } + // Set the new set of analyzer references we're going to have AnalyzerItems for. + _analyzerReferences = project.AnalyzerReferences; - return false; - } - } + var references = await GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( + project, cancellationToken).ConfigureAwait(false); - public IEnumerable Items + // Go back to UI thread to update the observable collection. Otherwise, it enqueue its own UI work that we cannot track. + await _analyzersFolder.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + try { - get - { - if (_analyzerItems == null) - { - _analyzerItems = []; - - var project = _analyzersFolder.Workspace - .CurrentSolution - .GetProject(_analyzersFolder.ProjectId); - - if (project != null) - { - _analyzerReferences = project.AnalyzerReferences; - var initialSet = GetFilteredAnalyzers(_analyzerReferences, project) - .OrderBy(ar => ar.Display) - .Select(ar => new AnalyzerItem(_analyzersFolder, ar, _commandHandler.AnalyzerContextMenuController)); - _analyzerItems.AddRange(initialSet); - } - } + _items.BeginBulkOperation(); - Logger.Log( - FunctionId.SolutionExplorer_AnalyzerItemSource_GetItems, - KeyValueLogMessage.Create(m => m["Count"] = _analyzerItems.Count)); + _items.Clear(); + foreach (var analyzerReference in references.OrderBy(static r => r.Display)) + _items.Add(new AnalyzerItem(_analyzersFolder, analyzerReference, _commandHandler.AnalyzerContextMenuController)); - return _analyzerItems; - } + return; } - - public object SourceItem + finally { - get - { - return _analyzersFolder; - } + _items.EndBulkOperation(); } - private ImmutableHashSet GetAnalyzersWithLoadErrors() + async Task> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( + Project project, + CancellationToken cancellationToken) { - if (_analyzersFolder.Workspace is VisualStudioWorkspaceImpl) - { - /* - var vsProject = vsWorkspace.DeferredState?.ProjectTracker.GetProject(_analyzersFolder.ProjectId); - var vsAnalyzersMap = vsProject?.GetProjectAnalyzersMap(); - - if (vsAnalyzersMap != null) - { - return vsAnalyzersMap.Where(kvp => kvp.Value.HasLoadErrors).Select(kvp => kvp.Key).ToImmutableHashSet(); - } - */ - } + var client = await RemoteHostClient.TryGetClientAsync(this.Workspace, cancellationToken).ConfigureAwait(false); - return ImmutableHashSet.Empty; - } + // If we can't make a remote call. Fall back to processing in the VS host. + if (client is null) + return project.AnalyzerReferences.Where(r => r is not AnalyzerFileReference || r.HasAnalyzersOrSourceGenerators(project.Language)).ToImmutableArray(); - private ImmutableArray GetFilteredAnalyzers(IEnumerable analyzerReferences, Project project) - { - var analyzersWithLoadErrors = GetAnalyzersWithLoadErrors(); + using var connection = client.CreateConnection(callbackTarget: null); - // Filter out analyzer dependencies which have no diagnostic analyzers, but still retain the unresolved analyzers and analyzers with load errors. - var builder = ArrayBuilder.GetInstance(); - foreach (var analyzerReference in analyzerReferences) + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var reference in project.AnalyzerReferences) { - // Analyzer dependency: - // 1. Must be an Analyzer file reference (we don't understand other analyzer dependencies). - // 2. Mush have no diagnostic analyzers. - // 3. Must have no source generators. - // 4. Must have non-null full path. - // 5. Must not have any assembly or analyzer load failures. - if (analyzerReference is AnalyzerFileReference && - analyzerReference.GetAnalyzers(project.Language).IsDefaultOrEmpty && - analyzerReference.GetGenerators(project.Language).IsDefaultOrEmpty && - analyzerReference.FullPath != null && - !analyzersWithLoadErrors.Contains(analyzerReference.FullPath)) + // Can only remote AnalyzerFileReferences over to the oop side. + if (reference is AnalyzerFileReference analyzerFileReference) { - continue; + var result = await connection.TryInvokeAsync( + project, + (service, solutionChecksum, cancellationToken) => service.HasAnalyzersOrSourceGeneratorsAsync( + solutionChecksum, project.Id, analyzerFileReference.FullPath, cancellationToken), + cancellationToken).ConfigureAwait(false); + + // If the call fails, the OOP substrate will have already reported an error + if (!result.HasValue) + return []; + + if (result.Value) + builder.Add(analyzerFileReference); + } + else if (reference.HasAnalyzersOrSourceGenerators(project.Language)) + { + builder.Add(reference); } - - builder.Add(analyzerReference); } - return builder.ToImmutableAndFree(); + return builder.ToImmutableAndClear(); } } } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSourceProvider.cs index 248921a775a57..0ddc809f17ac7 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSourceProvider.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSourceProvider.cs @@ -2,40 +2,28 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Utilities; -namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer -{ - [Export(typeof(IAttachedCollectionSourceProvider))] - [Name(nameof(AnalyzerItemSourceProvider))] - [Order] - [AppliesToProject("(CSharp | VB) & !CPS")] // in the CPS case, the Analyzers items are created by the project system - internal sealed class AnalyzerItemSourceProvider : AttachedCollectionSourceProvider - { - [Import(typeof(AnalyzersCommandHandler))] - private readonly IAnalyzersCommandHandler _commandHandler = null; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public AnalyzerItemSourceProvider() - { - } +namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; - protected override IAttachedCollectionSource CreateCollectionSource(AnalyzersFolderItem analyzersFolder, string relationshipName) - { - if (relationshipName == KnownRelationships.Contains) - { - return new AnalyzerItemSource(analyzersFolder, _commandHandler); - } - - return null; - } - } +[Export(typeof(IAttachedCollectionSourceProvider))] +[Name(nameof(AnalyzerItemSourceProvider)), Order] +[AppliesToProject("(CSharp | VB) & !CPS")] // in the CPS case, the Analyzers items are created by the project system +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class AnalyzerItemSourceProvider( + [Import(typeof(AnalyzersCommandHandler))] IAnalyzersCommandHandler commandHandler, + IAsynchronousOperationListenerProvider listenerProvider) + : AttachedCollectionSourceProvider +{ + protected override IAttachedCollectionSource? CreateCollectionSource(AnalyzersFolderItem analyzersFolder, string relationshipName) + => relationshipName == KnownRelationships.Contains + ? new AnalyzerItemSource(analyzersFolder, commandHandler, listenerProvider) + : null; } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs index 5b3271191878b..b2542ef39e496 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.Imaging.Interop; @@ -13,11 +14,13 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; internal sealed partial class AnalyzersFolderItem( + IThreadingContext threadingContext, Workspace workspace, ProjectId projectId, IVsHierarchyItem parentItem, IContextMenuController contextMenuController) : BaseItem(SolutionExplorerShim.Analyzers) { + public readonly IThreadingContext ThreadingContext = threadingContext; public Workspace Workspace { get; } = workspace; public ProjectId ProjectId { get; } = projectId; public IVsHierarchyItem ParentItem { get; } = parentItem; @@ -64,7 +67,7 @@ public void AddAnalyzer(string path) /// /// Remove an analyzer with the given path from this folder. /// - public void RemoveAnalyzer(string path) + public void RemoveAnalyzer(string? path) { var vsproject = GetVSProject(); if (vsproject == null) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs index a2de65442c3c3..55d53a62b8c42 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs @@ -4,48 +4,30 @@ using System.Collections; using System.Collections.ObjectModel; -using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.VisualStudio.Shell; namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; -internal sealed class AnalyzersFolderItemSource : IAttachedCollectionSource +internal sealed class AnalyzersFolderItemSource( + IThreadingContext threadingContext, + Workspace workspace, + ProjectId projectId, + IVsHierarchyItem projectHierarchyItem, + IAnalyzersCommandHandler commandHandler) + : IAttachedCollectionSource { - private readonly IVsHierarchyItem _projectHierarchyItem; - private readonly Workspace _workspace; - private readonly ProjectId _projectId; - private readonly ObservableCollection _folderItems; - private readonly IAnalyzersCommandHandler _commandHandler; - - public AnalyzersFolderItemSource(Workspace workspace, ProjectId projectId, IVsHierarchyItem projectHierarchyItem, IAnalyzersCommandHandler commandHandler) - { - _workspace = workspace; - _projectId = projectId; - _projectHierarchyItem = projectHierarchyItem; - _commandHandler = commandHandler; - - _folderItems = []; - - Update(); - } + private readonly ObservableCollection _folderItems = [new AnalyzersFolderItem( + threadingContext, + workspace, + projectId, + projectHierarchyItem, + commandHandler.AnalyzerFolderContextMenuController)]; public bool HasItems => true; public IEnumerable Items => _folderItems; - public object SourceItem => _projectHierarchyItem; - - internal void Update() - { - // Don't create the item a 2nd time. - if (_folderItems.Any()) - return; - - _folderItems.Add(new AnalyzersFolderItem( - _workspace, - _projectId, - _projectHierarchyItem, - _commandHandler.AnalyzerFolderContextMenuController)); - } + public object SourceItem => projectHierarchyItem; } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSourceProvider.cs index 41e7eb2dc1ab9..2b66c8a56c5a9 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSourceProvider.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSourceProvider.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -21,13 +22,16 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplore [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] [method: ImportingConstructor] internal sealed class AnalyzersFolderItemSourceProvider( + IThreadingContext threadingContext, VisualStudioWorkspace workspace, [Import(typeof(AnalyzersCommandHandler))] IAnalyzersCommandHandler commandHandler) : AttachedCollectionSourceProvider { + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly Workspace _workspace = workspace; private readonly IAnalyzersCommandHandler _commandHandler = commandHandler; + private IHierarchyItemToProjectIdMap? _projectMap; - private readonly Workspace _workspace = workspace; protected override IAttachedCollectionSource? CreateCollectionSource(IVsHierarchyItem item, string relationshipName) { @@ -46,7 +50,7 @@ internal sealed class AnalyzersFolderItemSourceProvider( if (hierarchyMapper != null && hierarchyMapper.TryGetProjectId(item.Parent, targetFrameworkMoniker: null, projectId: out var projectId)) { - return new AnalyzersFolderItemSource(_workspace, projectId, item, _commandHandler); + return new AnalyzersFolderItemSource(_threadingContext, _workspace, projectId, item, _commandHandler); } return null; diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs index d7dd9e6a64d17..b90add845e4e1 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs @@ -26,7 +26,7 @@ internal abstract partial class BaseDiagnosticAndGeneratorItemSource : IAttached private static readonly DiagnosticDescriptorComparer s_comparer = new(); private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService; - private readonly BulkObservableCollection _items = new(); + private readonly BulkObservableCollection _items = []; private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly AsyncBatchingWorkQueue _workQueue; @@ -103,7 +103,16 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) this.Workspace.WorkspaceChanged -= OnWorkspaceChanged; _cancellationTokenSource.Cancel(); - _items.Clear(); + + // Note: mutating _items will be picked up automatically by clients who are bound to the collection. We do + // not need to notify them through some other mechanism. + if (_items.Count > 0) + { + // Go back to UI thread to update the observable collection. Otherwise, it enqueue its own UI work that we cannot track. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + _items.Clear(); + } + return; } @@ -115,6 +124,7 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) if (_items.SequenceEqual([.. newDiagnosticItems, .. newSourceGeneratorItems])) return; + // Go back to UI thread to update the observable collection. Otherwise, it enqueue its own UI work that we cannot track. await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); _items.BeginBulkOperation(); diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSource.cs index 02becf337f9cf..96bc24025d51c 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSource.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.Internal.VisualStudio.PlatformUI; @@ -19,16 +18,21 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; -internal sealed class SourceGeneratedFileItemSource(SourceGeneratorItem parentGeneratorItem, Workspace workspace, IAsynchronousOperationListener asyncListener, IThreadingContext threadingContext) : Shell.IAttachedCollectionSource, ISupportExpansionEvents +internal sealed class SourceGeneratedFileItemSource( + SourceGeneratorItem parentGeneratorItem, + IThreadingContext threadingContext, + Workspace workspace, + IAsynchronousOperationListener asyncListener) + : Shell.IAttachedCollectionSource, ISupportExpansionEvents { private readonly SourceGeneratorItem _parentGeneratorItem = parentGeneratorItem; + private readonly IThreadingContext _threadingContext = threadingContext; private readonly Workspace _workspace = workspace; private readonly IAsynchronousOperationListener _asyncListener = asyncListener; - private readonly IThreadingContext _threadingContext = threadingContext; /// - /// The returned collection of items. Can only be mutated on the UI thread, as other parts of WPF are subscribed to the change - /// events and expect that. + /// The returned collection of items. Can only be mutated on the UI thread, as other parts of WPF are subscribed to + /// the change events and expect that. /// private readonly BulkObservableCollectionWithInit _items = []; diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs index 1d1b2f3fc69b8..a08d9aa9f0bcd 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs @@ -4,7 +4,6 @@ using System; using System.ComponentModel.Composition; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; @@ -29,6 +28,6 @@ internal sealed class SourceGeneratedFileItemSourceProvider( protected override IAttachedCollectionSource? CreateCollectionSource(SourceGeneratorItem item, string relationshipName) => relationshipName == KnownRelationships.Contains - ? new SourceGeneratedFileItemSource(item, workspace, _asyncListener, threadingContext) + ? new SourceGeneratedFileItemSource(item, threadingContext, workspace, _asyncListener) : null; } diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemTests.vb index 488f6983675c6..937376086056b 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemTests.vb @@ -2,13 +2,14 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports Microsoft.CodeAnalysis.Editor.[Shared].Utilities Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.VisualStudio.LanguageServices.Implementation Imports Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer - <[UseExportProvider]> + Public Class AnalyzerItemTests @@ -23,7 +24,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Using workspace = EditorTestWorkspace.Create(workspaceXml) Dim project = workspace.Projects.Single() - Dim analyzerFolder = New AnalyzersFolderItem(workspace, project.Id, Nothing, Nothing) + Dim analyzerFolder = New AnalyzersFolderItem(workspace.GetService(Of IThreadingContext), workspace, project.Id, Nothing, Nothing) Dim analyzer = New AnalyzerItem(analyzerFolder, project.AnalyzerReferences.Single(), Nothing) Assert.Equal(expected:="Goo", actual:=analyzer.Text) @@ -42,7 +43,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Using workspace = EditorTestWorkspace.Create(workspaceXml) Dim project = workspace.Projects.Single() - Dim analyzerFolder = New AnalyzersFolderItem(workspace, project.Id, Nothing, Nothing) + Dim analyzerFolder = New AnalyzersFolderItem(workspace.GetService(Of IThreadingContext), workspace, project.Id, Nothing, Nothing) Dim analyzer = New AnalyzerItem(analyzerFolder, project.AnalyzerReferences.Single(), Nothing) Dim browseObject = DirectCast(analyzer.GetBrowseObject(), AnalyzerItem.BrowseObject) diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb index bcf2f3deda1c7..a36cdeb68f6e4 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb @@ -2,15 +2,17 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. -Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities +Imports Microsoft.CodeAnalysis.Shared.TestHooks Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer +Imports Roslyn.Test.Utilities Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer - <[UseExportProvider]> - Public Class AnalyzerItemsSourceTests - - Public Sub Ordering() + + Public NotInheritable Class AnalyzerItemsSourceTests + + Public Async Function Ordering() As Task Dim workspaceXml = @@ -23,8 +25,13 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Using workspace = EditorTestWorkspace.Create(workspaceXml) Dim project = workspace.Projects.Single() - Dim analyzerFolder = New AnalyzersFolderItem(workspace, project.Id, Nothing, Nothing) - Dim analyzerItemsSource = New AnalyzerItemSource(analyzerFolder, New FakeAnalyzersCommandHandler) + Dim analyzerFolder = New AnalyzersFolderItem(workspace.GetService(Of IThreadingContext), workspace, project.Id, Nothing, Nothing) + Dim listenerProvider = workspace.GetService(Of IAsynchronousOperationListenerProvider) + Dim analyzerItemsSource = New AnalyzerItemSource( + analyzerFolder, New FakeAnalyzersCommandHandler(), listenerProvider) + + Dim waiter = listenerProvider.GetWaiter(FeatureAttribute.SourceGenerators) + Await waiter.ExpeditedWaitAsync() Dim analyzers = analyzerItemsSource.Items.Cast(Of AnalyzerItem)().ToArray() @@ -33,7 +40,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Assert.Equal(expected:="Beta", actual:=analyzers(1).Text) Assert.Equal(expected:="Gamma", actual:=analyzers(2).Text) End Using - End Sub + End Function End Class End Namespace diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderItemTests.vb index 24764b596cfc3..596bed22efabb 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderItemTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderItemTests.vb @@ -2,7 +2,7 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. -Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.VisualStudio.LanguageServices.Implementation Imports Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer @@ -23,7 +23,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Using workspace = EditorTestWorkspace.Create(workspaceXml) Dim project = workspace.Projects.Single() - Dim analyzerFolder = New AnalyzersFolderItem(workspace, project.Id, Nothing, Nothing) + Dim analyzerFolder = New AnalyzersFolderItem(workspace.GetService(Of IThreadingContext), workspace, project.Id, Nothing, Nothing) Assert.Equal(expected:=SolutionExplorerShim.Analyzers, actual:=analyzerFolder.Text) End Using @@ -41,7 +41,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Using workspace = EditorTestWorkspace.Create(workspaceXml) Dim project = workspace.Projects.Single() - Dim analyzerFolder = New AnalyzersFolderItem(workspace, project.Id, Nothing, Nothing) + Dim analyzerFolder = New AnalyzersFolderItem(workspace.GetService(Of IThreadingContext), workspace, project.Id, Nothing, Nothing) Dim browseObject = DirectCast(analyzerFolder.GetBrowseObject(), AnalyzersFolderItem.BrowseObject) Assert.Equal(expected:=SolutionExplorerShim.Analyzers, actual:=browseObject.GetComponentName()) diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderProviderTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderProviderTests.vb index 2f4af6c64eb54..201a9b3837eff 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderProviderTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderProviderTests.vb @@ -3,6 +3,7 @@ ' See the LICENSE file in the project root for more information. Imports System.Collections.ObjectModel +Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.Internal.VisualStudio.PlatformUI Imports Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer @@ -15,12 +16,11 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer <[UseExportProvider]> Public Class AnalyzersFolderProviderTests - Public Sub CreateCollectionSource_NullItem() Using environment = New TestEnvironment() Dim provider As IAttachedCollectionSourceProvider = - New AnalyzersFolderItemSourceProvider(environment.Workspace, Nothing) + New AnalyzersFolderItemSourceProvider(environment.ExportProvider.GetExportedValue(Of IThreadingContext), environment.Workspace, Nothing) Dim collectionSource = provider.CreateCollectionSource(Nothing, KnownRelationships.Contains) @@ -32,7 +32,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Public Sub CreateCollectionSource_NullHierarchyIdentity() Using environment = New TestEnvironment() Dim provider As IAttachedCollectionSourceProvider = - New AnalyzersFolderItemSourceProvider(environment.Workspace, Nothing) + New AnalyzersFolderItemSourceProvider(environment.ExportProvider.GetExportedValue(Of IThreadingContext), environment.Workspace, Nothing) Dim hierarchyItem = New MockHierarchyItem With {.HierarchyIdentity = Nothing} @@ -63,7 +63,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer } } - Dim provider As IAttachedCollectionSourceProvider = New AnalyzersFolderItemSourceProvider(environment.Workspace, New FakeAnalyzersCommandHandler) + Dim provider As IAttachedCollectionSourceProvider = New AnalyzersFolderItemSourceProvider( + environment.ExportProvider.GetExportedValue(Of IThreadingContext), environment.Workspace, New FakeAnalyzersCommandHandler) Dim collectionSource = provider.CreateCollectionSource(hierarchyItem, KnownRelationships.Contains) diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb index 2a5bd45acc492..7074c37fd513b 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb @@ -270,7 +270,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Return New LegacyDiagnosticItemSource( workspace.GetService(Of IThreadingContext), - New AnalyzerItem(New AnalyzersFolderItem(workspace, projectId, Nothing, Nothing), analyzerReference, Nothing), + New AnalyzerItem(New AnalyzersFolderItem(workspace.GetService(Of IThreadingContext), workspace, projectId, Nothing, Nothing), analyzerReference, Nothing), New FakeAnalyzersCommandHandler, workspace.GetService(Of IDiagnosticAnalyzerService), workspace.GetService(Of IAsynchronousOperationListenerProvider)) @@ -279,7 +279,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Private Shared Function CreateSourceGeneratedFilesItemSource(workspace As EditorTestWorkspace, generatorItem As SourceGeneratorItem) As Shell.IAttachedCollectionSource Dim asyncListener = workspace.GetService(Of IAsynchronousOperationListenerProvider).GetListener(FeatureAttribute.SourceGenerators) - Return New SourceGeneratedFileItemSource(generatorItem, workspace, asyncListener, workspace.GetService(Of IThreadingContext)()) + Return New SourceGeneratedFileItemSource(generatorItem, workspace.GetService(Of IThreadingContext), workspace, asyncListener) End Function Private Shared Function WaitForGeneratorsAndItemSourcesAsync(workspace As EditorTestWorkspace) As Task diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs new file mode 100644 index 0000000000000..7d6bec82610ee --- /dev/null +++ b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs @@ -0,0 +1,14 @@ +// 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 Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.CodeAnalysis.Shared.Extensions; + +internal static class AnalyzerReferenceExtensions +{ + public static bool HasAnalyzersOrSourceGenerators(this AnalyzerReference analyzerFileReference, string language) + => !analyzerFileReference.GetAnalyzers(language).IsDefaultOrEmpty || + !analyzerFileReference.GetGenerators(language).IsDefaultOrEmpty; +} diff --git a/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs b/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs index dca987d9e20cb..d0821c325762e 100644 --- a/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs +++ b/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs @@ -57,6 +57,13 @@ ValueTask HasGeneratorsAsync( /// ValueTask> GetSourceGeneratorIdentitiesAsync( Checksum solutionChecksum, ProjectId projectId, string analyzerReferenceFullPath, CancellationToken cancellationToken); + + /// + /// Returns whether or not the the with + /// equal to has any analyzers or source generators. + /// + ValueTask HasAnalyzersOrSourceGeneratorsAsync( + Checksum solutionChecksum, ProjectId projectId, string analyzerReferenceFullPath, CancellationToken cancellationToken); } /// diff --git a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs index 03f218e62d26b..76272ef377613 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs @@ -143,10 +143,25 @@ public ValueTask> GetSourceGeneratorIden { var project = solution.GetRequiredProject(projectId); var analyzerReference = project.AnalyzerReferences - .OfType() .First(r => r.FullPath == analyzerReferenceFullPath); return ValueTaskFactory.FromResult(SourceGeneratorIdentity.GetIdentities(analyzerReference, project.Language)); }, cancellationToken); } + + public ValueTask HasAnalyzersOrSourceGeneratorsAsync( + Checksum solutionChecksum, + ProjectId projectId, + string analyzerReferenceFullPath, + CancellationToken cancellationToken) + { + return RunServiceAsync(solutionChecksum, solution => + { + var project = solution.GetRequiredProject(projectId); + var analyzerReference = project.AnalyzerReferences + .First(r => r.FullPath == analyzerReferenceFullPath); + + return ValueTaskFactory.FromResult(analyzerReference.HasAnalyzersOrSourceGenerators(project.Language)); + }, cancellationToken); + } }