diff --git a/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs b/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs
index 098b15c620951..3f35470afd51d 100644
--- a/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs
+++ b/src/Features/Core/Portable/DesignerAttribute/AbstractDesignerAttributeIncrementalAnalyzer.cs
@@ -3,15 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Composition;
-using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
-using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
@@ -22,154 +19,51 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute
[ExportWorkspaceService(typeof(IDesignerAttributeDiscoveryService)), Shared]
internal sealed partial class DesignerAttributeDiscoveryService : IDesignerAttributeDiscoveryService
{
- ///
- /// Protects mutable state in this type.
- ///
- private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1);
-
- ///
- /// Keep track of the last information we reported. We will avoid notifying the host if we recompute and these
- /// don't change.
- ///
- private readonly ConcurrentDictionary _documentToLastReportedInformation = new();
-
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DesignerAttributeDiscoveryService()
{
}
- public async ValueTask ProcessSolutionAsync(
- Solution solution,
- DocumentId? priorityDocumentId,
- IDesignerAttributeDiscoveryService.ICallback callback,
- CancellationToken cancellationToken)
- {
- using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
- {
- // Remove any documents that are now gone.
- foreach (var docId in _documentToLastReportedInformation.Keys)
- {
- if (!solution.ContainsDocument(docId))
- _documentToLastReportedInformation.TryRemove(docId, out _);
- }
-
- // Handle the priority doc first.
- var priorityDocument = solution.GetDocument(priorityDocumentId);
- if (priorityDocument != null)
- await ProcessProjectAsync(priorityDocument.Project, priorityDocument, callback, cancellationToken).ConfigureAwait(false);
-
- // Process the rest of the projects in dependency order so that their data is ready when we hit the
- // projects that depend on them.
- var dependencyGraph = solution.GetProjectDependencyGraph();
- foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken))
- {
- if (projectId != priorityDocumentId?.ProjectId)
- await ProcessProjectAsync(solution.GetRequiredProject(projectId), specificDocument: null, callback, cancellationToken).ConfigureAwait(false);
- }
- }
- }
-
- private async Task ProcessProjectAsync(
+ public async IAsyncEnumerable ProcessProjectAsync(
Project project,
- Document? specificDocument,
- IDesignerAttributeDiscoveryService.ICallback callback,
- CancellationToken cancellationToken)
+ DocumentId? priorityDocumentId,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
{
+ // Ignore projects that don't support compilation or don't even have the DesignerCategoryAttribute in it.
if (!project.SupportsCompilation)
- return;
+ yield break;
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var designerCategoryType = compilation.DesignerCategoryAttributeType();
if (designerCategoryType == null)
- return;
-
- await ScanForDesignerCategoryUsageAsync(
- project, specificDocument, callback, designerCategoryType, cancellationToken).ConfigureAwait(false);
-
- // If we scanned just a specific document in the project, now scan the rest of the files.
- if (specificDocument != null)
- await ScanForDesignerCategoryUsageAsync(project, specificDocument: null, callback, designerCategoryType, cancellationToken).ConfigureAwait(false);
- }
-
- private async Task ScanForDesignerCategoryUsageAsync(
- Project project,
- Document? specificDocument,
- IDesignerAttributeDiscoveryService.ICallback callback,
- INamedTypeSymbol designerCategoryType,
- CancellationToken cancellationToken)
- {
- // We need to reanalyze the project whenever it (or any of its dependencies) have
- // changed. We need to know about dependencies since if a downstream project adds the
- // DesignerCategory attribute to a class, that can affect us when we examine the classes
- // in this project.
- var projectVersion = await project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false);
+ yield break;
- // Now get all the values that actually changed and notify VS about them. We don't need
- // to tell it about the ones that didn't change since that will have no effect on the
- // user experience.
- var changedData = await ComputeChangedDataAsync(
- project, specificDocument, projectVersion, designerCategoryType, cancellationToken).ConfigureAwait(false);
-
- // Only bother reporting non-empty information to save an unnecessary RPC.
- if (!changedData.IsEmpty)
- await callback.ReportDesignerAttributeDataAsync(changedData, cancellationToken).ConfigureAwait(false);
-
- // Now, keep track of what we've reported to the host so we won't report unchanged files in the future. We
- // do this after the report has gone through as we want to make sure that if it cancels for any reason we
- // don't hold onto values that may not have made it all the way to the project system.
- foreach (var data in changedData)
- _documentToLastReportedInformation[data.DocumentId] = (data.Category, projectVersion);
- }
+ // If there is a priority doc, then scan that first.
+ var priorityDocument = priorityDocumentId == null ? null : project.GetDocument(priorityDocumentId);
+ if (priorityDocument is { FilePath: not null })
+ {
+ var data = await ComputeDesignerAttributeDataAsync(designerCategoryType, priorityDocument, cancellationToken).ConfigureAwait(false);
+ if (data != null)
+ yield return data.Value;
+ }
- private async Task> ComputeChangedDataAsync(
- Project project,
- Document? specificDocument,
- VersionStamp projectVersion,
- INamedTypeSymbol designerCategoryType,
- CancellationToken cancellationToken)
- {
- using var _1 = ArrayBuilder>.GetInstance(out var tasks);
+ // now process the rest of the documents.
+ using var _ = ArrayBuilder>.GetInstance(out var tasks);
foreach (var document in project.Documents)
{
- // If we're only analyzing a specific document, then skip the rest.
- if (specificDocument != null && document != specificDocument)
- continue;
-
- // If we don't have a path for this document, we cant proceed with it.
- // We need that path to inform the project system which file we're referring to.
- if (document.FilePath == null)
+ if (document == priorityDocument || document.FilePath is null)
continue;
- // If nothing has changed at the top level between the last time we analyzed this document and now, then
- // no need to analyze again.
- if (_documentToLastReportedInformation.TryGetValue(document.Id, out var existingInfo) &&
- existingInfo.projectVersion == projectVersion)
- {
- continue;
- }
-
tasks.Add(ComputeDesignerAttributeDataAsync(designerCategoryType, document, cancellationToken));
}
- using var _2 = ArrayBuilder.GetInstance(tasks.Count, out var results);
-
- // Avoid unnecessary allocation of result array.
- await Task.WhenAll((IEnumerable)tasks).ConfigureAwait(false);
-
- foreach (var task in tasks)
+ // Convert the tasks into one final stream we can read all the results from.
+ await foreach (var dataOpt in tasks.ToImmutable().StreamAsync(cancellationToken).ConfigureAwait(false))
{
- var dataOpt = await task.ConfigureAwait(false);
- if (dataOpt == null)
- continue;
-
- var data = dataOpt.Value;
- _documentToLastReportedInformation.TryGetValue(data.DocumentId, out var existingInfo);
- if (existingInfo.category != data.Category)
- results.Add(data);
+ if (dataOpt != null)
+ yield return dataOpt.Value;
}
-
- return results.ToImmutableAndClear();
}
private static async Task ComputeDesignerAttributeDataAsync(
@@ -179,18 +73,16 @@ private async Task> ComputeChangedDataAsyn
{
Contract.ThrowIfNull(document.FilePath);
- // We either haven't computed the designer info, or our data was out of date. We need
- // So recompute here. Figure out what the current category is, and if that's different
- // from what we previously stored.
var category = await DesignerAttributeHelpers.ComputeDesignerAttributeCategoryAsync(
designerCategoryType, document, cancellationToken).ConfigureAwait(false);
- return new DesignerAttributeData
- {
- Category = category,
- DocumentId = document.Id,
- FilePath = document.FilePath,
- };
+ // If there's no category (the common case) don't return anything. The host itself will see no results
+ // returned and can handle that case (for example, if a type previously had the attribute but doesn't
+ // any longer).
+ if (category == null)
+ return null;
+
+ return new DesignerAttributeData(category, document.Id, document.FilePath);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
{
diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs
index 150e957379fb3..b17014b068654 100644
--- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs
+++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeData.cs
@@ -10,24 +10,34 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute
/// Serialization typed used to pass information to/from OOP and VS.
///
[DataContract]
- internal struct DesignerAttributeData
+ internal readonly struct DesignerAttributeData
{
///
/// The category specified in a [DesignerCategory("...")] attribute.
///
[DataMember(Order = 0)]
- public string? Category;
+ public readonly string? Category;
///
/// The document this applies to.
///
[DataMember(Order = 1)]
- public DocumentId DocumentId;
+ public readonly DocumentId DocumentId;
///
/// Path for this .
///
[DataMember(Order = 2)]
- public string FilePath;
+ public readonly string FilePath;
+
+ public DesignerAttributeData(string? category, DocumentId documentId, string filePath)
+ {
+ Category = category;
+ DocumentId = documentId;
+ FilePath = filePath;
+ }
+
+ public DesignerAttributeData WithCategory(string? category)
+ => new(category, DocumentId, FilePath);
}
}
diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs
index 038f14f77fc8b..97ffc48cef825 100644
--- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs
+++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeHelpers.cs
@@ -54,6 +54,9 @@ internal static class DesignerAttributeHelpers
}
return null;
+
+ static string? GetArgumentString(TypedConstant argument)
+ => argument is { IsNull: false, Type.SpecialType: SpecialType.System_String, Value: string stringValue } ? stringValue.Trim() : null;
}
private static SyntaxNode? FindFirstNonNestedClass(
@@ -77,18 +80,5 @@ internal static class DesignerAttributeHelpers
return null;
}
-
- private static string? GetArgumentString(TypedConstant argument)
- {
- if (argument.Type == null ||
- argument.Type.SpecialType != SpecialType.System_String ||
- argument.IsNull ||
- argument.Value is not string stringValue)
- {
- return null;
- }
-
- return stringValue.Trim();
- }
}
}
diff --git a/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs
index c3bc3fbf0af0d..c98d3fd5c4998 100644
--- a/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs
+++ b/src/Features/Core/Portable/DesignerAttribute/IDesignerAttributeDiscoveryService.cs
@@ -2,21 +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.
-using System;
-using System.Collections.Immutable;
+using System.Collections.Generic;
using System.Threading;
-using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.DesignerAttribute
{
- internal partial interface IDesignerAttributeDiscoveryService : IWorkspaceService
+ internal interface IDesignerAttributeDiscoveryService : IWorkspaceService
{
- public interface ICallback
- {
- ValueTask ReportDesignerAttributeDataAsync(ImmutableArray data, CancellationToken cancellationToken);
- }
-
- ValueTask ProcessSolutionAsync(Solution solution, DocumentId? priorityDocumentId, ICallback callback, CancellationToken cancellationToken);
+ IAsyncEnumerable ProcessProjectAsync(Project project, DocumentId? priorityDocumentId, CancellationToken cancellationToken);
}
}
diff --git a/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs
index 4716273c30a92..15c8d86e9fda3 100644
--- a/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs
+++ b/src/Features/Core/Portable/DesignerAttribute/IRemoteDesignerAttributeDiscoveryService.cs
@@ -2,13 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
-using System.Collections.Immutable;
-using System.Composition;
+using System.Collections.Generic;
using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.Host.Mef;
-using Microsoft.CodeAnalysis.Remote;
namespace Microsoft.CodeAnalysis.DesignerAttribute
{
@@ -18,27 +13,6 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute
///
internal interface IRemoteDesignerAttributeDiscoveryService
{
- internal interface ICallback
- {
- ValueTask ReportDesignerAttributeDataAsync(RemoteServiceCallbackId callbackId, ImmutableArray data, CancellationToken cancellationToken);
- }
-
- ValueTask DiscoverDesignerAttributesAsync(RemoteServiceCallbackId callbackId, Checksum solutionChecksum, DocumentId? priorityDocument, CancellationToken cancellationToken);
- }
-
- [ExportRemoteServiceCallbackDispatcher(typeof(IRemoteDesignerAttributeDiscoveryService)), Shared]
- internal sealed class RemoteDesignerAttributeDiscoveryCallbackDispatcher : RemoteServiceCallbackDispatcher, IRemoteDesignerAttributeDiscoveryService.ICallback
- {
- [ImportingConstructor]
- [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public RemoteDesignerAttributeDiscoveryCallbackDispatcher()
- {
- }
-
- private new IDesignerAttributeDiscoveryService.ICallback GetCallback(RemoteServiceCallbackId callbackId)
- => (IDesignerAttributeDiscoveryService.ICallback)base.GetCallback(callbackId);
-
- public ValueTask ReportDesignerAttributeDataAsync(RemoteServiceCallbackId callbackId, ImmutableArray data, CancellationToken cancellationToken)
- => GetCallback(callbackId).ReportDesignerAttributeDataAsync(data, cancellationToken);
+ IAsyncEnumerable DiscoverDesignerAttributesAsync(Checksum solutionChecksum, ProjectId project, DocumentId? priorityDocument, CancellationToken cancellationToken);
}
}
diff --git a/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs b/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs
index a10722fc7775c..f0503dafe78db 100644
--- a/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs
+++ b/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs
@@ -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.
-#nullable disable
-
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.DesignerAttribute;
+using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
+using Microsoft.CodeAnalysis.Remote.Testing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Utilities;
@@ -19,37 +20,40 @@ namespace Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.DesignerAttri
[UseExportProvider]
public class DesignerAttributeServiceTests
{
- [Fact]
- public async Task NoDesignerTest1()
+ private static readonly TestComposition s_inProcessComposition = EditorTestCompositions.EditorFeatures;
+ private static readonly TestComposition s_outOffProcessComposition = s_inProcessComposition.WithTestHostParts(TestHost.OutOfProcess);
+
+ [Theory, CombinatorialData]
+ public async Task NoDesignerTest1(TestHost host)
{
var code = @"class Test { }";
- await TestAsync(code, category: null);
+ await TestAsync(code, category: null, host);
}
- [Fact]
- public async Task NoDesignerOnSecondClass()
+ [Theory, CombinatorialData]
+ public async Task NoDesignerOnSecondClass(TestHost host)
{
await TestAsync(
@"class Test1 { }
[System.ComponentModel.DesignerCategory(""Form"")]
-class Test2 { }", category: null);
+class Test2 { }", category: null, host);
}
- [Fact]
- public async Task NoDesignerOnStruct()
+ [Theory, CombinatorialData]
+ public async Task NoDesignerOnStruct(TestHost host)
{
await TestAsync(
@"
[System.ComponentModel.DesignerCategory(""Form"")]
-struct Test1 { }", category: null);
+struct Test1 { }", category: null, host);
}
- [Fact]
- public async Task NoDesignerOnNestedClass()
+ [Theory, CombinatorialData]
+ public async Task NoDesignerOnNestedClass(TestHost host)
{
await TestAsync(
@@ -57,42 +61,56 @@ await TestAsync(
{
[System.ComponentModel.DesignerCategory(""Form"")]
class Test2 { }
-}", category: null);
+}", category: null, host);
}
- [Fact]
- public async Task SimpleDesignerTest()
+ [Theory, CombinatorialData]
+ public async Task SimpleDesignerTest(TestHost host)
{
await TestAsync(
@"[System.ComponentModel.DesignerCategory(""Form"")]
-class Test { }", "Form");
+class Test { }", "Form", host);
}
- [Fact]
- public async Task SimpleDesignerTest2()
+ [Theory, CombinatorialData]
+ public async Task SimpleDesignerTest2(TestHost host)
{
await TestAsync(
@"using System.ComponentModel;
[DesignerCategory(""Form"")]
-class Test { }", "Form");
+class Test { }", "Form", host);
}
- private static async Task TestAsync(string codeWithMarker, string category)
+ private static async Task TestAsync(string codeWithMarker, string? category, TestHost host)
{
- using var workspace = TestWorkspace.CreateCSharp(codeWithMarker, openDocuments: false);
+ using var workspace = TestWorkspace.CreateCSharp(
+ codeWithMarker, openDocuments: false, composition: host == TestHost.OutOfProcess ? s_outOffProcessComposition : s_inProcessComposition);
+
+ var solution = workspace.CurrentSolution;
var hostDocument = workspace.Documents.First();
var documentId = hostDocument.Id;
- var document = workspace.CurrentSolution.GetDocument(documentId);
-
- var compilation = await document.Project.GetCompilationAsync();
- var actual = await DesignerAttributeHelpers.ComputeDesignerAttributeCategoryAsync(
- compilation.DesignerCategoryAttributeType(), document, CancellationToken.None);
- Assert.Equal(category, actual);
+ var service = solution.Services.GetRequiredService();
+ var stream = service.ProcessProjectAsync(solution.GetRequiredProject(documentId.ProjectId), priorityDocumentId: null, CancellationToken.None);
+
+ var items = new List();
+ await foreach (var item in stream)
+ items.Add(item);
+
+ if (category != null)
+ {
+ Assert.Equal(1, items.Count);
+ Assert.Equal(category, items.Single().Category);
+ Assert.Equal(documentId, items.Single().DocumentId);
+ }
+ else
+ {
+ Assert.Empty(items);
+ }
}
}
}
diff --git a/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs b/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs
index f8b55276c7f50..395b697978e3f 100644
--- a/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs
+++ b/src/VisualStudio/Core/Def/DesignerAttribute/VisualStudioDesignerAttributeService.cs
@@ -5,24 +5,24 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using EnvDTE;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.DesignerAttribute;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
-using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
+using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
-using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.VisualStudio.Designer.Interfaces;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
+using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.Services;
using Roslyn.Utilities;
@@ -31,7 +31,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.DesignerAttribu
{
[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared]
internal class VisualStudioDesignerAttributeService :
- ForegroundThreadAffinitizedObject, IDesignerAttributeDiscoveryService.ICallback, IEventListener