Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Features/Core/Portable/CodeLens/CodeLensHelpers.cs
Original file line number Diff line number Diff line change
@@ -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<object, object> descriptorProperties)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved from later code. used by us, and then also exposed through EA for UnitTesting to use.

{
// 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ImmutableArray<ReferenceMethodDescriptor>?> FindReferenceMethodsAsync(
Guid projectGuid, string filePath, TextSpan span, DocumentId? sourceGeneratedDocumentId, CancellationToken cancellationToken);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing you need to worry about @shyamnamboodiripad This just gives us a strong api signature between this code here and the impl on the vs side.


internal interface IUnitTestingFeaturesReferencesServiceCallback
{
Task<TResult> InvokeAsync<TResult>(string targetName, IReadOnlyList<object?> arguments, CancellationToken cancellationToken);
}
Copy link
Member Author

@CyrusNajmabadi CyrusNajmabadi Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shyam you'll provide an impl of this on your side. it will be implemented as:

class YourImpl(
    IAsyncCodeLensDataPointProvider provider,
    ICodeLensCallbackService callbackService) : IUnitTestingFeaturesReferencesServiceCallback
{
    public Task<TResult> InvokeAsync<TResult>(string targetName, IReadOnlyList<object?> arguments, CancellationToken cancellationToken) => callbackService.InvokeAsync(targetName, arguments, cancellationToken);
}


internal static class UnitTestingFeaturesReferencesService
{
public static DocumentId? GetSourceGeneratorDocumentId(IDictionary<object, object> descriptorProperties)
=> CodeLensHelpers.GetSourceGeneratorDocumentId(descriptorProperties);

public static async Task<ImmutableArray<(string MethodFullyQualifedName, string MethodOutputFilePath)>> GetCallerMethodsAsync(
Guid projectGuid,
string filePath,
TextSpan span,
DocumentId? sourceGeneratedDocumentId,
IUnitTestingFeaturesReferencesServiceCallback callback,
CancellationToken cancellationToken)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this api is much lower than the prior Editor level api. So it doesn't talk about vs codelens concepts. It just talks about data, and just passes in an abstraction for the call to be made back to VS through the 'callback' parameter which hides all the codelens stuff as well.

{
var callerMethods = await callback.InvokeAsync<ImmutableArray<ReferenceMethodDescriptor>?>(
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));
}
}
77 changes: 44 additions & 33 deletions src/VisualStudio/Core/Def/CodeLens/CodeLensCallbackListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,13 +27,16 @@

namespace Microsoft.VisualStudio.LanguageServices.CodeLens;

using static CodeLensHelpers;

/// <summary>
/// This is used by new codelens API to get extra data from VS if it is needed.
/// </summary>
[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;

Expand Down Expand Up @@ -78,9 +83,15 @@ public async Task<ImmutableDictionary<Guid, string>> GetProjectVersionsAsync(Imm
public async Task<ReferenceCount?> 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;
Expand All @@ -104,9 +115,15 @@ public async Task<ImmutableDictionary<Guid, string>> GetProjectVersionsAsync(Imm
public async Task<(string projectVersion, ImmutableArray<ReferenceLocationDescriptor> 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;
Expand All @@ -125,10 +142,24 @@ public async Task<ImmutableDictionary<Guid, string>> GetProjectVersionsAsync(Imm

public async Task<ImmutableArray<ReferenceMethodDescriptor>?> 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<ImmutableArray<ReferenceMethodDescriptor>?> 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;
Expand All @@ -139,21 +170,17 @@ public async Task<ImmutableDictionary<Guid, string>> 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
Expand Down Expand Up @@ -203,40 +230,24 @@ private async Task EnsureMaxResultAsync(CancellationToken cancellationToken)
}
}

private Task<Document?> GetDocumentAsync(Solution solution, Guid projectGuid, string filePath, CodeLensDescriptorContext descriptorContext)
private async Task<Document?> GetDocumentAsync(
Solution solution, Guid projectGuid, string filePath, DocumentId? sourceGeneratorDocumentId, CancellationToken cancellationToken)
{
if (projectGuid == VSConstants.CLSID.MiscellaneousFilesProject_guid)
{
return SpecializedTasks.Default<Document>();
}
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<Document>();

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);
}
}
Loading