diff --git a/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs b/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs new file mode 100644 index 0000000000000..1c85665b80764 --- /dev/null +++ b/src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.CodeLens; + +internal static class CodeLensHelpers +{ + public static DocumentId? GetSourceGeneratorDocumentId(IDictionary descriptorProperties) + { + // Undocumented contract here:' + // https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/CodeSense/Framework/Roslyn/Roslyn/Editor/CodeElementTag.cs&version=GBmain&_a=contents&line=84&lineStyle=plain&lineEnd=85&lineStartColumn=1&lineEndColumn=96 + if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && + TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) + { + var projectId = ProjectId.CreateFromSerialized(projectIdGuid); + return DocumentId.CreateFromSerialized(projectId, documentIdGuid); + } + + return null; + + bool TryGetGuid(string key, out Guid guid) + { + guid = Guid.Empty; + return descriptorProperties.TryGetValue(key, out var guidStringUntyped) && + guidStringUntyped is string guidString && + Guid.TryParse(guidString, out guid); + } + } +} diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs new file mode 100644 index 0000000000000..d5a4dba881e06 --- /dev/null +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingFeaturesReferencesService.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeLens; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting; + +internal interface IUnitTestingCodeLensContext +{ + Task?> FindReferenceMethodsAsync( + Guid projectGuid, string filePath, TextSpan span, DocumentId? sourceGeneratedDocumentId, CancellationToken cancellationToken); +} + +internal interface IUnitTestingFeaturesReferencesServiceCallback +{ + Task InvokeAsync(string targetName, IReadOnlyList arguments, CancellationToken cancellationToken); +} + +internal static class UnitTestingFeaturesReferencesService +{ + public static DocumentId? GetSourceGeneratorDocumentId(IDictionary descriptorProperties) + => CodeLensHelpers.GetSourceGeneratorDocumentId(descriptorProperties); + + public static async Task> GetCallerMethodsAsync( + Guid projectGuid, + string filePath, + TextSpan span, + DocumentId? sourceGeneratedDocumentId, + IUnitTestingFeaturesReferencesServiceCallback callback, + CancellationToken cancellationToken) + { + var callerMethods = await callback.InvokeAsync?>( + nameof(IUnitTestingCodeLensContext.FindReferenceMethodsAsync), + [projectGuid, filePath, span, sourceGeneratedDocumentId], + cancellationToken).ConfigureAwait(false); + + if (!callerMethods.HasValue || callerMethods.Value.IsEmpty) + { + return []; + } + + return callerMethods.Value.SelectAsArray(m => ( + MethodFullyQualifiedName: m.FullName, + MethodOutputFilePath: m.OutputFilePath)); + } +} diff --git a/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs b/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs index 94504a47b241a..79c944b0b1bdc 100644 --- a/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs +++ b/src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs @@ -12,8 +12,10 @@ using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.ExternalAccess.UnitTesting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Language.CodeLens; using Microsoft.VisualStudio.Language.CodeLens.Remoting; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; @@ -25,13 +27,16 @@ namespace Microsoft.VisualStudio.LanguageServices.CodeLens; +using static CodeLensHelpers; + /// /// This is used by new codelens API to get extra data from VS if it is needed. /// [Export(typeof(ICodeLensCallbackListener))] [ContentType(ContentTypeNames.CSharpContentType)] [ContentType(ContentTypeNames.VisualBasicContentType)] -internal sealed class CodeLensCallbackListener : ICodeLensCallbackListener, ICodeLensContext +internal sealed class CodeLensCallbackListener : + ICodeLensCallbackListener, ICodeLensContext, IUnitTestingCodeLensContext { private const int DefaultMaxSearchResultsValue = 99; @@ -78,9 +83,15 @@ public async Task> GetProjectVersionsAsync(Imm public async Task GetReferenceCountAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, ReferenceCount? previousCount, CancellationToken cancellationToken) { + if (descriptorContext.ApplicableSpan is null) + return null; + var solution = _workspace.CurrentSolution; var (documentId, node) = await GetDocumentIdAndNodeAsync( - solution, descriptor, descriptorContext, cancellationToken).ConfigureAwait(false); + solution, descriptor.ProjectGuid, descriptor.FilePath, + descriptorContext.ApplicableSpan.Value.ToTextSpan(), + GetSourceGeneratorDocumentId(descriptorContext.Properties), + cancellationToken).ConfigureAwait(false); if (documentId == null) { return null; @@ -104,9 +115,15 @@ public async Task> GetProjectVersionsAsync(Imm public async Task<(string projectVersion, ImmutableArray references)?> FindReferenceLocationsAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, CancellationToken cancellationToken) { + if (descriptorContext.ApplicableSpan is null) + return null; + var solution = _workspace.CurrentSolution; var (documentId, node) = await GetDocumentIdAndNodeAsync( - solution, descriptor, descriptorContext, cancellationToken).ConfigureAwait(false); + solution, descriptor.ProjectGuid, descriptor.FilePath, + descriptorContext.ApplicableSpan.Value.ToTextSpan(), + GetSourceGeneratorDocumentId(descriptorContext.Properties), + cancellationToken).ConfigureAwait(false); if (documentId == null) { return null; @@ -125,10 +142,24 @@ public async Task> GetProjectVersionsAsync(Imm public async Task?> FindReferenceMethodsAsync( CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, CancellationToken cancellationToken) + { + if (descriptorContext.ApplicableSpan is null) + return null; + + return await FindReferenceMethodsAsync( + descriptor.ProjectGuid, + descriptor.FilePath, + descriptorContext.ApplicableSpan.Value.ToTextSpan(), + GetSourceGeneratorDocumentId(descriptorContext.Properties), + cancellationToken).ConfigureAwait(false); + } + + public async Task?> FindReferenceMethodsAsync( + Guid projectGuid, string filePath, TextSpan span, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken) { var solution = _workspace.CurrentSolution; var (documentId, node) = await GetDocumentIdAndNodeAsync( - solution, descriptor, descriptorContext, cancellationToken).ConfigureAwait(false); + solution, projectGuid, filePath, span, sourceGeneratorDocumentId, cancellationToken).ConfigureAwait(false); if (documentId == null) { return null; @@ -139,21 +170,17 @@ public async Task> GetProjectVersionsAsync(Imm } private async Task<(DocumentId?, SyntaxNode?)> GetDocumentIdAndNodeAsync( - Solution solution, CodeLensDescriptor descriptor, CodeLensDescriptorContext descriptorContext, CancellationToken cancellationToken) + Solution solution, Guid projectGuid, string filePath, TextSpan textSpan, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken) { - if (descriptorContext.ApplicableSpan is null) - { - return default; - } - var document = await GetDocumentAsync(solution, descriptor.ProjectGuid, descriptor.FilePath, descriptorContext).ConfigureAwait(false); + var document = await GetDocumentAsync( + solution, projectGuid, filePath, sourceGeneratorDocumentId, cancellationToken).ConfigureAwait(false); if (document == null) { return default; } var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var textSpan = descriptorContext.ApplicableSpan.Value.ToTextSpan(); // TODO: This check avoids ArgumentOutOfRangeException but it's not clear if this is the right solution // https://github.com/dotnet/roslyn/issues/44639 @@ -203,40 +230,24 @@ private async Task EnsureMaxResultAsync(CancellationToken cancellationToken) } } - private Task GetDocumentAsync(Solution solution, Guid projectGuid, string filePath, CodeLensDescriptorContext descriptorContext) + private async Task GetDocumentAsync( + Solution solution, Guid projectGuid, string filePath, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken) { if (projectGuid == VSConstants.CLSID.MiscellaneousFilesProject_guid) - { - return SpecializedTasks.Default(); - } + return null; foreach (var candidateId in solution.GetDocumentIdsWithFilePath(filePath)) { if (_workspace.GetProjectGuid(candidateId.ProjectId) == projectGuid) { var currentContextId = _workspace.GetDocumentIdInCurrentContext(candidateId); - return Task.FromResult(solution.GetDocument(currentContextId)); + return solution.GetDocument(currentContextId); } } // If we couldn't find the document the usual way we did so, then maybe it's source generated; let's try locating it // with the DocumentId we have directly - if (TryGetGuid("RoslynDocumentIdGuid", out var documentIdGuid) && - TryGetGuid("RoslynProjectIdGuid", out var projectIdGuid)) - { - var projectId = ProjectId.CreateFromSerialized(projectIdGuid); - var documentId = DocumentId.CreateFromSerialized(projectId, documentIdGuid); - return _workspace.CurrentSolution.GetDocumentAsync(documentId, includeSourceGenerated: true).AsTask(); - } - - return SpecializedTasks.Default(); - - bool TryGetGuid(string key, out Guid guid) - { - guid = Guid.Empty; - return descriptorContext.Properties.TryGetValue(key, out var guidStringUntyped) && - guidStringUntyped is string guidString && - Guid.TryParse(guidString, out guid); - } + return await solution.GetDocumentAsync( + sourceGeneratorDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); } }