From d7c8b19320f8f0db9ca417e35aa5b8ecf8e6f7a4 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 12:15:47 -0700 Subject: [PATCH 01/17] Always retrun true --- .../AnalyzerItem/AnalyzerItemSource.cs | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index c3107210dc93..60d267752f57 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer { - internal class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged + internal sealed class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged { private readonly AnalyzersFolderItem _analyzersFolder; private readonly IAnalyzersCommandHandler _commandHandler; @@ -28,6 +28,8 @@ internal class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyCh public event PropertyChangedEventHandler PropertyChanged; + public object SourceItem => _analyzersFolder; + public AnalyzerItemSource(AnalyzersFolderItem analyzersFolder, IAnalyzersCommandHandler commandHandler) { _analyzersFolder = analyzersFolder; @@ -128,27 +130,8 @@ private void NotifyPropertyChanged(string propertyName) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - public bool HasItems - { - get - { - if (_analyzerItems != null) - { - return _analyzerItems.Count > 0; - } - - var project = _analyzersFolder.Workspace - .CurrentSolution - .GetProject(_analyzersFolder.ProjectId); - - if (project != null) - { - return project.AnalyzerReferences.Count > 0; - } - - return false; - } - } + // Defer actual determination and computation of the items until later. + public bool HasItems => true; public IEnumerable Items { @@ -180,14 +163,6 @@ public IEnumerable Items } } - public object SourceItem - { - get - { - return _analyzersFolder; - } - } - private ImmutableHashSet GetAnalyzersWithLoadErrors() { if (_analyzersFolder.Workspace is VisualStudioWorkspaceImpl) From db32141a2557bca9cc2e17e99c9d6c2aad19bdee Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 12:35:59 -0700 Subject: [PATCH 02/17] Move work OOP --- .../AnalyzerItem/AnalyzerItemSource.cs | 297 ++++++++---------- 1 file changed, 138 insertions(+), 159 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index 60d267752f57..70cf03785c5d 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -2,212 +2,191 @@ // 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.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.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Remote; +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; +using VSLangProj140; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; -namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer +internal sealed class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged { - internal sealed 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 object SourceItem => _analyzersFolder; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly AsyncBatchingWorkQueue _workQueue; + private readonly IThreadingContext _threadingContext; - public AnalyzerItemSource(AnalyzersFolderItem analyzersFolder, IAnalyzersCommandHandler commandHandler) - { - _analyzersFolder = analyzersFolder; - _commandHandler = commandHandler; + private IReadOnlyCollection _analyzerReferences; - _analyzersFolder.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged; - } + public event PropertyChangedEventHandler PropertyChanged; - 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; + public object SourceItem => _analyzersFolder; + + // Defer actual determination and computation of the items until later. + public bool HasItems => !_cancellationTokenSource.IsCancellationRequested; - _analyzerItems.BeginBulkOperation(); + public IEnumerable Items => _items; - var itemsToRemove = _analyzerItems - .Where(item => !_analyzerReferences.Contains(item.AnalyzerReference)) - .ToArray(); + private void NotifyPropertyChanged(string propertyName) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - var referencesToAdd = GetFilteredAnalyzers(_analyzerReferences, project) - .Where(r => !_analyzerItems.Any(item => item.AnalyzerReference == r)) - .ToArray(); + 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 item in itemsToRemove) - { - _analyzerItems.Remove(item); - } + 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; - foreach (var reference in referencesToAdd) - { - _analyzerItems.Add(new AnalyzerItem(_analyzersFolder, reference, _commandHandler.AnalyzerContextMenuController)); - } + _cancellationTokenSource.Cancel(); + _items.Clear(); + NotifyPropertyChanged(nameof(HasItems)); + NotifyPropertyChanged(nameof(Items)); + return; + } - var sorted = _analyzerItems.OrderBy(item => item.AnalyzerReference.Display).ToArray(); - for (var i = 0; i < sorted.Length; i++) - { - _analyzerItems.Move(_analyzerItems.IndexOf(sorted[i]), i); - } + // If nothing changed wrt analyzer references, then there's nothing we need to do. + if (project.AnalyzerReferences == _analyzerReferences) + return; - _analyzerItems.EndBulkOperation(); + // Set the new set of analyzer references we're going to have AnalyzerItems for. + _analyzerReferences = project.AnalyzerReferences; - NotifyPropertyChanged(nameof(HasItems)); - } - } + var computedItems = await GetItemsWithAnalyzersOrGeneratorsAsync( + project, cancellationToken).ConfigureAwait(false); - private void NotifyPropertyChanged(string propertyName) + try { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + _items.BeginBulkOperation(); - // Defer actual determination and computation of the items until later. - public bool HasItems => true; + _items.Clear(); + _items.AddRange(computedItems); - public IEnumerable Items + return; + } + finally { - 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); - } - } - - Logger.Log( - FunctionId.SolutionExplorer_AnalyzerItemSource_GetItems, - KeyValueLogMessage.Create(m => m["Count"] = _analyzerItems.Count)); - - return _analyzerItems; - } + _items.EndBulkOperation(); + NotifyPropertyChanged(nameof(Items)); } - private ImmutableHashSet GetAnalyzersWithLoadErrors() + async Task> GetItemsWithAnalyzersOrGeneratorsAsync( + Project project, + CancellationToken cancellationToken) { - if (_analyzersFolder.Workspace is VisualStudioWorkspaceImpl) + // Can only remote AnalyzerFileReferences over to the oop side. Ignore all other kinds in the VS process. + var analyzerFileReferences = project.AnalyzerReferences.OfType().ToImmutableArray(); + var analyzerFileReferencePaths = analyzerReferences + .OfType() + .SelectAsArray(r => r.FullPath); + + var client = await RemoteHostClient.TryGetClientAsync(this.Workspace, cancellationToken).ConfigureAwait(false); + if (client is not null) { - /* - 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 result = await client.TryInvokeAsync>( + project, + (service, solutionChecksum, cancellationToken) => service.GetSourceGeneratorIdentitiesAsync( + 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 []; + + return result.Value; } - return ImmutableHashSet.Empty; + + _analyzerItems.Add(new AnalyzerItem(_analyzersFolder, reference, _commandHandler.AnalyzerContextMenuController)); } + } - private ImmutableArray GetFilteredAnalyzers(IEnumerable analyzerReferences, Project project) + private ImmutableArray GetFilteredAnalyzers(IEnumerable analyzerReferences, Project project) + { + // 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) { - var analyzersWithLoadErrors = GetAnalyzersWithLoadErrors(); - - // 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) + // 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) { - // 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)) - { - continue; - } - - builder.Add(analyzerReference); + continue; } - return builder.ToImmutableAndFree(); + builder.Add(analyzerReference); } + + return builder.ToImmutableAndFree(); } } From b563736250b3c043885a4b4d3bad79b29b9a26d3 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 12:51:31 -0700 Subject: [PATCH 03/17] in progress --- .../AnalyzerItem/AnalyzerItem.cs | 20 ++---- .../AnalyzerItem/AnalyzerItemSource.cs | 68 +++++++------------ .../AnalyzerItemSourceProvider.cs | 44 +++++------- .../AnalyzersFolderItem.cs | 2 +- .../AnalyzerFileReferenceExtensions.cs | 16 +++++ .../IRemoteSourceGenerationService.cs | 8 +++ 6 files changed, 71 insertions(+), 87 deletions(-) create mode 100644 src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItem.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItem.cs index 1af836885084..a937acffd58c 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 70cf03785c5d..c1b85f4e2358 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -2,7 +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. -using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; @@ -12,16 +11,14 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -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.Shell; using Roslyn.Utilities; -using VSLangProj140; namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; @@ -34,11 +31,10 @@ internal sealed class AnalyzerItemSource : IAttachedCollectionSource, INotifyPro private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly AsyncBatchingWorkQueue _workQueue; - private readonly IThreadingContext _threadingContext; - private IReadOnlyCollection _analyzerReferences; + private IReadOnlyCollection? _analyzerReferences; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged = null!; private Workspace Workspace => _analyzersFolder.Workspace; private ProjectId ProjectId => _analyzersFolder.ProjectId; @@ -115,7 +111,7 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) // Set the new set of analyzer references we're going to have AnalyzerItems for. _analyzerReferences = project.AnalyzerReferences; - var computedItems = await GetItemsWithAnalyzersOrGeneratorsAsync( + var references = await GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( project, cancellationToken).ConfigureAwait(false); try @@ -123,7 +119,8 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) _items.BeginBulkOperation(); _items.Clear(); - _items.AddRange(computedItems); + foreach (var analyzerReference in references) + _items.Add(new AnalyzerItem(_analyzersFolder, analyzerReference, _commandHandler.AnalyzerContextMenuController)); return; } @@ -133,60 +130,43 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) NotifyPropertyChanged(nameof(Items)); } - async Task> GetItemsWithAnalyzersOrGeneratorsAsync( + async Task> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( Project project, CancellationToken cancellationToken) { // Can only remote AnalyzerFileReferences over to the oop side. Ignore all other kinds in the VS process. - var analyzerFileReferences = project.AnalyzerReferences.OfType().ToImmutableArray(); - var analyzerFileReferencePaths = analyzerReferences + var analyzerFileReferences = project.AnalyzerReferences .OfType() - .SelectAsArray(r => r.FullPath); + .Where(static r => r.FullPath != null) + .ToImmutableArray(); var client = await RemoteHostClient.TryGetClientAsync(this.Workspace, cancellationToken).ConfigureAwait(false); if (client is not null) { - var result = await client.TryInvokeAsync>( + var result = await client.TryInvokeAsync>( project, - (service, solutionChecksum, cancellationToken) => service.GetSourceGeneratorIdentitiesAsync( - solutionChecksum, project.Id, analyzerFileReference.FullPath, cancellationToken), + (service, solutionChecksum, cancellationToken) => service.HasAnalyzersOrSourceGeneratorsAsync( + solutionChecksum, project.Id, analyzerFileReferences.SelectAsArray(static r => r.FullPath), cancellationToken), cancellationToken).ConfigureAwait(false); // If the call fails, the OOP substrate will have already reported an error if (!result.HasValue) return []; - return result.Value; - } - - - _analyzerItems.Add(new AnalyzerItem(_analyzersFolder, reference, _commandHandler.AnalyzerContextMenuController)); - } - } + Contract.ThrowIfTrue(result.Value.Length != analyzerFileReferences.Length); + using var _ = ArrayBuilder.GetInstance(analyzerFileReferences.Length, out var builder); + for (var i = 0; i < analyzerFileReferences.Length; i++) + { + var hasAnalyzersOrGenerators = result.Value[i]; + if (hasAnalyzersOrGenerators) + builder.Add(analyzerFileReferences[i]); + } - private ImmutableArray GetFilteredAnalyzers(IEnumerable analyzerReferences, Project project) - { - // 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) - { - // 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) - { - continue; + return builder.ToImmutableAndClear(); } - builder.Add(analyzerReference); + // Couldn't make a remote call. Fall back to processing in the VS host. + return analyzerFileReferences.WhereAsArray(r => r.HasAnalyzersOrSourceGenerators(project.Language)); } - - return builder.ToImmutableAndFree(); } } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSourceProvider.cs index 248921a775a5..0ddc809f17ac 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 5b3271191878..1997963ed90b 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs @@ -64,7 +64,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/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs new file mode 100644 index 000000000000..504f78c099cb --- /dev/null +++ b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs @@ -0,0 +1,16 @@ +// 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 AnalyzerFileReferenceExtensions +{ + public static bool HasAnalyzersOrSourceGenerators( + this AnalyzerFileReference 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 dca987d9e20c..e18038393cb4 100644 --- a/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs +++ b/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs @@ -57,6 +57,14 @@ ValueTask HasGeneratorsAsync( /// ValueTask> GetSourceGeneratorIdentitiesAsync( Checksum solutionChecksum, ProjectId projectId, string analyzerReferenceFullPath, CancellationToken cancellationToken); + + /// + /// Returns an array of s (each one corresponding to the in the provided array) + /// specifying if the corresponding analyzer reference has any analyzers or source generators. + /// + ValueTask> HasAnalyzersOrSourceGeneratorsAsync( + Checksum solutionChecksum, ProjectId projectId, ImmutableArray analyzerReferenceFullPaths, CancellationToken cancellationToken); } /// From fb3c8073e41264293cd2b44147608bac3e1fcd2a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 12:59:29 -0700 Subject: [PATCH 04/17] Add remote site --- ...tensions.cs => AnalyzerReferenceExtensions.cs} | 4 ++-- .../RemoteSourceGenerationService.cs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) rename src/Workspaces/Core/Portable/Shared/Extensions/{AnalyzerFileReferenceExtensions.cs => AnalyzerReferenceExtensions.cs} (82%) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs similarity index 82% rename from src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs rename to src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs index 504f78c099cb..c7bc1cb30888 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerFileReferenceExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs @@ -6,10 +6,10 @@ namespace Microsoft.CodeAnalysis.Shared.Extensions; -internal static class AnalyzerFileReferenceExtensions +internal static class AnalyzerReferenceExtensions { public static bool HasAnalyzersOrSourceGenerators( - this AnalyzerFileReference analyzerFileReference, + this AnalyzerReference analyzerFileReference, string language) => !analyzerFileReference.GetAnalyzers(language).IsDefaultOrEmpty || !analyzerFileReference.GetGenerators(language).IsDefaultOrEmpty; diff --git a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs index 03f218e62d26..7c5c53cd712d 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs @@ -149,4 +149,19 @@ public ValueTask> GetSourceGeneratorIden return ValueTaskFactory.FromResult(SourceGeneratorIdentity.GetIdentities(analyzerReference, project.Language)); }, cancellationToken); } + + public ValueTask> HasAnalyzersOrSourceGeneratorsAsync( + Checksum solutionChecksum, + ProjectId projectId, + ImmutableArray analyzerReferenceFullPaths, + CancellationToken cancellationToken) + { + return RunServiceAsync(solutionChecksum, solution => + { + var project = solution.GetRequiredProject(projectId); + + return new ValueTask>(analyzerReferenceFullPaths + .SelectAsArray(fullPath => project.AnalyzerReferences.First(a => a.FullPath == fullPath).HasAnalyzersOrSourceGenerators(project.Language))); + }, cancellationToken); + } } From a36f423ddc9b61fbb2a19b3f95d9a8425e40cce9 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:04:23 -0700 Subject: [PATCH 05/17] Update tests --- .../AnalyzerItem/AnalyzerItemSource.cs | 1 + .../AnalyzerItemsSourceTests.vb | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index c1b85f4e2358..fb61607264ae 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -54,6 +54,7 @@ public AnalyzerItemSource( _cancellationTokenSource.Token); this.Workspace.WorkspaceChanged += OnWorkspaceChanged; + _workQueue.AddWork(); } public object SourceItem => _analyzersFolder; diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb index bcf2f3deda1c..0b6811bb1323 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb @@ -2,15 +2,16 @@ ' 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.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 NotInheritable Class AnalyzerItemsSourceTests - Public Sub Ordering() + Public Async Function Ordering() As Task Dim workspaceXml = @@ -24,7 +25,12 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Dim project = workspace.Projects.Single() Dim analyzerFolder = New AnalyzersFolderItem(workspace, project.Id, Nothing, Nothing) - Dim analyzerItemsSource = New AnalyzerItemSource(analyzerFolder, New FakeAnalyzersCommandHandler) + 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 +39,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 From 88a5d34fa23929de29e34e4645fe2b42c78fdbf6 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:14:07 -0700 Subject: [PATCH 06/17] cleanup --- .../AnalyzerItem/AnalyzerItemSource.cs | 59 ++++++++++--------- .../IRemoteSourceGenerationService.cs | 9 ++- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index fb61607264ae..fa0205d1e375 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -120,7 +120,7 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) _items.BeginBulkOperation(); _items.Clear(); - foreach (var analyzerReference in references) + foreach (var analyzerReference in references.OrderBy(static r => r.Display)) _items.Add(new AnalyzerItem(_analyzersFolder, analyzerReference, _commandHandler.AnalyzerContextMenuController)); return; @@ -131,43 +131,44 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) NotifyPropertyChanged(nameof(Items)); } - async Task> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( + async Task> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( Project project, CancellationToken cancellationToken) { - // Can only remote AnalyzerFileReferences over to the oop side. Ignore all other kinds in the VS process. - var analyzerFileReferences = project.AnalyzerReferences - .OfType() - .Where(static r => r.FullPath != null) - .ToImmutableArray(); - var client = await RemoteHostClient.TryGetClientAsync(this.Workspace, cancellationToken).ConfigureAwait(false); - if (client is not null) + + // 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.HasAnalyzersOrSourceGenerators(project.Language)).ToImmutableArray(); + + using var connection = client.CreateConnection(callbackTarget: null); + + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var reference in project.AnalyzerReferences) { - var result = await client.TryInvokeAsync>( - project, - (service, solutionChecksum, cancellationToken) => service.HasAnalyzersOrSourceGeneratorsAsync( - solutionChecksum, project.Id, analyzerFileReferences.SelectAsArray(static r => r.FullPath), cancellationToken), - cancellationToken).ConfigureAwait(false); - - // If the call fails, the OOP substrate will have already reported an error - if (!result.HasValue) - return []; - - Contract.ThrowIfTrue(result.Value.Length != analyzerFileReferences.Length); - using var _ = ArrayBuilder.GetInstance(analyzerFileReferences.Length, out var builder); - for (var i = 0; i < analyzerFileReferences.Length; i++) + // Can only remote AnalyzerFileReferences over to the oop side. + if (reference is AnalyzerFileReference analyzerFileReference) { - var hasAnalyzersOrGenerators = result.Value[i]; - if (hasAnalyzersOrGenerators) - builder.Add(analyzerFileReferences[i]); + 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); } - - return builder.ToImmutableAndClear(); } - // Couldn't make a remote call. Fall back to processing in the VS host. - return analyzerFileReferences.WhereAsArray(r => r.HasAnalyzersOrSourceGenerators(project.Language)); + return builder.ToImmutableAndClear(); } } } diff --git a/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs b/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs index e18038393cb4..d0821c325762 100644 --- a/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs +++ b/src/Workspaces/Core/Portable/SourceGeneration/IRemoteSourceGenerationService.cs @@ -59,12 +59,11 @@ ValueTask> GetSourceGeneratorIdentitiesA Checksum solutionChecksum, ProjectId projectId, string analyzerReferenceFullPath, CancellationToken cancellationToken); /// - /// Returns an array of s (each one corresponding to the in the provided array) - /// specifying if the corresponding analyzer reference has any analyzers or source generators. + /// Returns whether or not the the with + /// equal to has any analyzers or source generators. /// - ValueTask> HasAnalyzersOrSourceGeneratorsAsync( - Checksum solutionChecksum, ProjectId projectId, ImmutableArray analyzerReferenceFullPaths, CancellationToken cancellationToken); + ValueTask HasAnalyzersOrSourceGeneratorsAsync( + Checksum solutionChecksum, ProjectId projectId, string analyzerReferenceFullPath, CancellationToken cancellationToken); } /// From 3e978eaec3388220cced6061efb42c87c864460d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:16:40 -0700 Subject: [PATCH 07/17] cleanup --- .../SourceGeneration/RemoteSourceGenerationService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs index 7c5c53cd712d..76272ef37761 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs @@ -143,25 +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( + public ValueTask HasAnalyzersOrSourceGeneratorsAsync( Checksum solutionChecksum, ProjectId projectId, - ImmutableArray analyzerReferenceFullPaths, + string analyzerReferenceFullPath, CancellationToken cancellationToken) { return RunServiceAsync(solutionChecksum, solution => { var project = solution.GetRequiredProject(projectId); + var analyzerReference = project.AnalyzerReferences + .First(r => r.FullPath == analyzerReferenceFullPath); - return new ValueTask>(analyzerReferenceFullPaths - .SelectAsArray(fullPath => project.AnalyzerReferences.First(a => a.FullPath == fullPath).HasAnalyzersOrSourceGenerators(project.Language))); + return ValueTaskFactory.FromResult(analyzerReference.HasAnalyzersOrSourceGenerators(project.Language)); }, cancellationToken); } } From 20e7d2c89f34a4b56b095ce6ab7541e543962fb7 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:23:40 -0700 Subject: [PATCH 08/17] cleanup --- .../AnalyzerItem/AnalyzerItemSource.cs | 3 +- .../AnalyzersFolderItem.cs | 3 ++ .../AnalyzersFolderItemSource.cs | 36 +++++++++---------- .../AnalyzerItemsSourceTests.vb | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index fa0205d1e375..c2ebdad02779 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -115,6 +115,7 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) var references = await GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( project, cancellationToken).ConfigureAwait(false); + await _analyzersFolder.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); try { _items.BeginBulkOperation(); @@ -139,7 +140,7 @@ async Task> GetAnalyzerReferencesWithAnalyzers // 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.HasAnalyzersOrSourceGenerators(project.Language)).ToImmutableArray(); + return project.AnalyzerReferences.Where(r => r is not AnalyzerFileReference || r.HasAnalyzersOrSourceGenerators(project.Language)).ToImmutableArray(); using var connection = client.CreateConnection(callbackTarget: null); diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItem.cs index 1997963ed90b..b2542ef39e49 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; diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs index a2de65442c3c..dd06db2b13a5 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs @@ -6,28 +6,39 @@ 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 { - private readonly IVsHierarchyItem _projectHierarchyItem; + private readonly IThreadingContext _threadingContext; private readonly Workspace _workspace; private readonly ProjectId _projectId; - private readonly ObservableCollection _folderItems; + private readonly IVsHierarchyItem _projectHierarchyItem; private readonly IAnalyzersCommandHandler _commandHandler; + private readonly ObservableCollection _folderItems; - public AnalyzersFolderItemSource(Workspace workspace, ProjectId projectId, IVsHierarchyItem projectHierarchyItem, IAnalyzersCommandHandler commandHandler) + public AnalyzersFolderItemSource( + IThreadingContext threadingContext, + Workspace workspace, + ProjectId projectId, + IVsHierarchyItem projectHierarchyItem, + IAnalyzersCommandHandler commandHandler) { + _threadingContext = threadingContext; _workspace = workspace; _projectId = projectId; _projectHierarchyItem = projectHierarchyItem; _commandHandler = commandHandler; - _folderItems = []; - - Update(); + _folderItems = [new AnalyzersFolderItem( + _threadingContext, + _workspace, + _projectId, + _projectHierarchyItem, + _commandHandler.AnalyzerFolderContextMenuController)]; } public bool HasItems => true; @@ -35,17 +46,4 @@ public AnalyzersFolderItemSource(Workspace workspace, ProjectId projectId, IVsHi 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)); - } } diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb index 0b6811bb1323..4c916333dd04 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb @@ -10,7 +10,7 @@ Imports Roslyn.Test.Utilities Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SolutionExplorer Public NotInheritable Class AnalyzerItemsSourceTests - + Public Async Function Ordering() As Task Dim workspaceXml = From 841fcbb80db5c9434ff1d434740af110156b1bb2 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:24:44 -0700 Subject: [PATCH 09/17] cleanup --- .../AnalyzersFolderItemSource.cs | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs index dd06db2b13a5..55d53a62b8c4 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSource.cs @@ -4,46 +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 IThreadingContext _threadingContext; - private readonly Workspace _workspace; - private readonly ProjectId _projectId; - private readonly IVsHierarchyItem _projectHierarchyItem; - private readonly IAnalyzersCommandHandler _commandHandler; - private readonly ObservableCollection _folderItems; - - public AnalyzersFolderItemSource( - IThreadingContext threadingContext, - Workspace workspace, - ProjectId projectId, - IVsHierarchyItem projectHierarchyItem, - IAnalyzersCommandHandler commandHandler) - { - _threadingContext = threadingContext; - _workspace = workspace; - _projectId = projectId; - _projectHierarchyItem = projectHierarchyItem; - _commandHandler = commandHandler; - - _folderItems = [new AnalyzersFolderItem( - _threadingContext, - _workspace, - _projectId, - _projectHierarchyItem, - _commandHandler.AnalyzerFolderContextMenuController)]; - } + private readonly ObservableCollection _folderItems = [new AnalyzersFolderItem( + threadingContext, + workspace, + projectId, + projectHierarchyItem, + commandHandler.AnalyzerFolderContextMenuController)]; public bool HasItems => true; public IEnumerable Items => _folderItems; - public object SourceItem => _projectHierarchyItem; + public object SourceItem => projectHierarchyItem; } From 006ec2d57f185dcc0befcd58d0708c5b8af3a0c3 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:32:41 -0700 Subject: [PATCH 10/17] Cleanup --- .../AnalyzersFolderItemSourceProvider.cs | 8 ++++++-- .../Core/Test/SolutionExplorer/AnalyzerItemTests.vb | 7 ++++--- .../Test/SolutionExplorer/AnalyzerItemsSourceTests.vb | 3 ++- .../Test/SolutionExplorer/AnalyzersFolderItemTests.vb | 6 +++--- .../SolutionExplorer/AnalyzersFolderProviderTests.vb | 9 +++++---- .../Test/SolutionExplorer/SourceGeneratorItemTests.vb | 2 +- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersFolderItem/AnalyzersFolderItemSourceProvider.cs index 41e7eb2dc1ab..2b66c8a56c5a 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/Test/SolutionExplorer/AnalyzerItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemTests.vb index 488f6983675c..937376086056 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 4c916333dd04..a36cdeb68f6e 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzerItemsSourceTests.vb @@ -2,6 +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.Shared.Utilities Imports Microsoft.CodeAnalysis.Shared.TestHooks Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer @@ -24,7 +25,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 listenerProvider = workspace.GetService(Of IAsynchronousOperationListenerProvider) Dim analyzerItemsSource = New AnalyzerItemSource( analyzerFolder, New FakeAnalyzersCommandHandler(), listenerProvider) diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/AnalyzersFolderItemTests.vb index 24764b596cfc..596bed22efab 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 2f4af6c64eb5..201a9b3837ef 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 2a5bd45acc49..d7ad32d3c717 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)) From d012fb4bd88d6e4ae320a917607ceca7a3e8bc4d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:35:35 -0700 Subject: [PATCH 11/17] Update src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs --- .../Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs index c7bc1cb30888..7d6bec82610e 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/AnalyzerReferenceExtensions.cs @@ -8,9 +8,7 @@ namespace Microsoft.CodeAnalysis.Shared.Extensions; internal static class AnalyzerReferenceExtensions { - public static bool HasAnalyzersOrSourceGenerators( - this AnalyzerReference analyzerFileReference, - string language) + public static bool HasAnalyzersOrSourceGenerators(this AnalyzerReference analyzerFileReference, string language) => !analyzerFileReference.GetAnalyzers(language).IsDefaultOrEmpty || !analyzerFileReference.GetGenerators(language).IsDefaultOrEmpty; } From 5bc5d6407884920423ffd7ebfa4708715b71eaca Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:43:27 -0700 Subject: [PATCH 12/17] Docs --- .../AnalyzerItem/AnalyzerItemSource.cs | 13 ++++--------- .../BaseDiagnosticAndGeneratorItemSource.cs | 5 ++++- .../SourceGeneratedFileItemSource.cs | 14 +++++++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index c2ebdad02779..c4b4059a80f6 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; -internal sealed class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged +internal sealed class AnalyzerItemSource : IAttachedCollectionSource { private readonly AnalyzersFolderItem _analyzersFolder; private readonly IAnalyzersCommandHandler _commandHandler; @@ -34,8 +34,6 @@ internal sealed class AnalyzerItemSource : IAttachedCollectionSource, INotifyPro private IReadOnlyCollection? _analyzerReferences; - public event PropertyChangedEventHandler PropertyChanged = null!; - private Workspace Workspace => _analyzersFolder.Workspace; private ProjectId ProjectId => _analyzersFolder.ProjectId; @@ -64,9 +62,6 @@ public AnalyzerItemSource( public IEnumerable Items => _items; - private void NotifyPropertyChanged(string propertyName) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { switch (e.Kind) @@ -99,9 +94,10 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) this.Workspace.WorkspaceChanged -= OnWorkspaceChanged; _cancellationTokenSource.Cancel(); + + // 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. _items.Clear(); - NotifyPropertyChanged(nameof(HasItems)); - NotifyPropertyChanged(nameof(Items)); return; } @@ -129,7 +125,6 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) finally { _items.EndBulkOperation(); - NotifyPropertyChanged(nameof(Items)); } async Task> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs index d7dd9e6a64d1..6ad886878ecf 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,6 +103,9 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) this.Workspace.WorkspaceChanged -= OnWorkspaceChanged; _cancellationTokenSource.Cancel(); + + // 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. _items.Clear(); return; } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSource.cs index 02becf337f9c..96bc24025d51 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 = []; From 37a1bdbb7dbfbf9b15a779b981b9561428460d9c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:44:49 -0700 Subject: [PATCH 13/17] Docs --- .../Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index c4b4059a80f6..f37af6f71b2e 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -52,6 +52,8 @@ public AnalyzerItemSource( _cancellationTokenSource.Token); this.Workspace.WorkspaceChanged += OnWorkspaceChanged; + + // Kick off the initial work to determine the starting set of items. _workQueue.AddWork(); } From 72f2df3dbde005df6f0db681561e0dc6f8ca42b4 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:51:05 -0700 Subject: [PATCH 14/17] Fix --- .../SourceGeneratedFileItemSourceProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs index 1d1b2f3fc69b..4693459ce834 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs @@ -29,6 +29,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; } From cb6a128d6dbc50301eea87a31fddd17294861c9d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:51:17 -0700 Subject: [PATCH 15/17] Simplify --- .../SourceGeneratedFileItemSourceProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/SourceGeneratedFileItems/SourceGeneratedFileItemSourceProvider.cs index 4693459ce834..a08d9aa9f0bc 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; From dc818fa375e2593d61e4459f8c63d950fc6186da Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 13:57:51 -0700 Subject: [PATCH 16/17] fix --- .../Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb b/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb index d7ad32d3c717..7074c37fd513 100644 --- a/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb +++ b/src/VisualStudio/Core/Test/SolutionExplorer/SourceGeneratorItemTests.vb @@ -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 From ca9442210a6fb20355bb11d88b6bb2cf7e41a263 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 19 Aug 2024 19:24:34 -0700 Subject: [PATCH 17/17] uiSwitch --- .../AnalyzerItem/AnalyzerItemSource.cs | 10 +++++++++- .../BaseDiagnosticAndGeneratorItemSource.cs | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs index f37af6f71b2e..1451dca714dc 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerItem/AnalyzerItemSource.cs @@ -99,7 +99,14 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) // 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. - _items.Clear(); + + 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(); + } + return; } @@ -113,6 +120,7 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) var references = await GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( project, cancellationToken).ConfigureAwait(false); + // 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 { diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs index 6ad886878ecf..b90add845e4e 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs @@ -106,7 +106,13 @@ private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) // 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. - _items.Clear(); + 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; } @@ -118,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();