Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support goto-def taking you from an interceptor method to the location being intercepted. #73992

Merged
merged 17 commits into from
Jun 14, 2024
Merged
3 changes: 3 additions & 0 deletions src/EditorFeatures/Core/EditorFeaturesResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,9 @@ Do you want to proceed?</value>
<data name="_0_declarations" xml:space="preserve">
<value>'{0}' declarations</value>
</data>
<data name="_0_intercepted_locations" xml:space="preserve">
<value>'{0}' intercepted locations</value>
</data>
<data name="An_inline_rename_session_is_active_for_identifier_0" xml:space="preserve">
<value>An inline rename session is active for identifier '{0}'. Invoke inline rename again to access additional options. You may continue to edit the identifier being renamed at any time.</value>
<comment>For screenreaders. {0} is the identifier being renamed.</comment>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.Linq;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Navigation;

internal abstract partial class AbstractDefinitionLocationService
{
private sealed class ByteArrayComparer : IEqualityComparer<ImmutableArray<byte>>
{
public static readonly ByteArrayComparer Instance = new();

private ByteArrayComparer() { }

public bool Equals(ImmutableArray<byte> x, ImmutableArray<byte> y)
=> x.SequenceEqual(y);

public int GetHashCode(ImmutableArray<byte> obj)
=> Hash.CombineValues(obj);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@
// 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.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.GoToDefinition;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Navigation;

internal abstract class AbstractDefinitionLocationService : IDefinitionLocationService
internal abstract partial class AbstractDefinitionLocationService(
IThreadingContext threadingContext,
IStreamingFindUsagesPresenter streamingPresenter) : IDefinitionLocationService
{
private readonly IThreadingContext _threadingContext;
private readonly IStreamingFindUsagesPresenter _streamingPresenter;
private readonly IThreadingContext _threadingContext = threadingContext;
private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter;

protected AbstractDefinitionLocationService(
IThreadingContext threadingContext,
IStreamingFindUsagesPresenter streamingPresenter)
{
_threadingContext = threadingContext;
_streamingPresenter = streamingPresenter;
}
private static readonly ConditionalWeakTable<ProjectState, AsyncLazy<Dictionary<ImmutableArray<byte>, DocumentId>>> s_projectToLazyContentHashMap = new();

private static Task<INavigableLocation?> GetNavigableLocationAsync(
Document document, int position, CancellationToken cancellationToken)
Expand All @@ -40,54 +43,62 @@ protected AbstractDefinitionLocationService(

public async Task<DefinitionLocation?> GetDefinitionLocationAsync(Document document, int position, CancellationToken cancellationToken)
{
var symbolService = document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();

// We want to compute this as quickly as possible so that the symbol be squiggled and navigated to. We
// don't want to wait on expensive operations like computing source-generators or skeletons if we can avoid
// it. So first try with a frozen document, then fallback to a normal document. This mirrors how go-to-def
// works as well.
return await GetDefinitionLocationWorkerAsync(document.WithFrozenPartialSemantics(cancellationToken)).ConfigureAwait(false) ??
await GetDefinitionLocationWorkerAsync(document).ConfigureAwait(false);

async Task<DefinitionLocation?> GetDefinitionLocationWorkerAsync(Document document)
async ValueTask<DefinitionLocation?> GetDefinitionLocationWorkerAsync(Document document)
{
return await GetControlFlowTargetLocationAsync(document).ConfigureAwait(false) ??
await GetSymbolLocationAsync(document).ConfigureAwait(false);
}

async ValueTask<DefinitionLocation?> GetControlFlowTargetLocationAsync(Document document)
{
var symbolService = document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();
var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync(
document, position, cancellationToken).ConfigureAwait(false);
if (controlFlowTarget != null)
{
var location = await GetNavigableLocationAsync(
document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false);
return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan));
}
else
{
// Try to compute the referenced symbol and attempt to go to definition for the symbol.
var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync(
document, position, cancellationToken).ConfigureAwait(false);
if (symbol is null)
return null;

// if the symbol only has a single source location, and we're already on it,
// try to see if there's a better symbol we could navigate to.
var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync(
project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false);
if (remappedLocation != null)
return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span));

var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync(
symbol, position, document, cancellationToken).ConfigureAwait(false);

var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync(
symbol,
project.Solution,
_threadingContext,
_streamingPresenter,
thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (location is null)
return null;

return new DefinitionLocation(location, new DocumentSpan(document, span));
}
if (controlFlowTarget == null)
return null;

var location = await GetNavigableLocationAsync(
document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false);
return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan));
}

async ValueTask<DefinitionLocation?> GetSymbolLocationAsync(Document document)
{
// Try to compute the referenced symbol and attempt to go to definition for the symbol.
var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync(
document, position, cancellationToken).ConfigureAwait(false);
if (symbol is null)
return null;

// if the symbol only has a single source location, and we're already on it,
// try to see if there's a better symbol we could navigate to.
var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync(
project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false);
if (remappedLocation != null)
return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span));

var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync(
symbol, position, document, cancellationToken).ConfigureAwait(false);

var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync(
symbol,
project.Solution,
_threadingContext,
_streamingPresenter,
thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (location is null)
return null;

return new DefinitionLocation(location, new DocumentSpan(document, span));
}
}

Expand Down Expand Up @@ -119,24 +130,136 @@ protected AbstractDefinitionLocationService(
// instead navigate to the actual interface member.
//
// In the future we can expand this with other mappings if appropriate.
var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations();
if (interfaceImpls.Length == 0)
return null;
return await TryGetExplicitInterfaceLocationAsync().ConfigureAwait(false) ??
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
await TryGetInterceptedLocationAsync().ConfigureAwait(false);

async ValueTask<INavigableLocation?> TryGetExplicitInterfaceLocationAsync()
{
var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations();
if (interfaceImpls.Length == 0)
return null;

var title = string.Format(EditorFeaturesResources._0_implemented_members,
FindUsagesHelpers.GetDisplayName(symbol));
var title = string.Format(EditorFeaturesResources._0_implemented_members,
FindUsagesHelpers.GetDisplayName(symbol));

using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var builder);
foreach (var impl in interfaceImpls)
using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var builder);
foreach (var impl in interfaceImpls)
{
builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false));
}

var definitions = builder.ToImmutable();

return await _streamingPresenter.GetStreamingLocationAsync(
_threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false);
}

async ValueTask<INavigableLocation?> TryGetInterceptedLocationAsync()
{
builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false));
if (symbol is not IMethodSymbol method)
return null;

// Find attributes of the form: [InterceptsLocationAttribute(version: 1, data: "...")];

var attributes = method.GetAttributes();
var interceptsLocationDatas = InterceptsLocationUtilities.GetInterceptsLocationData(attributes);
if (interceptsLocationDatas.Length == 0)
return null;

var lazyContentHashToDocumentMap = s_projectToLazyContentHashMap.GetValue(
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
project.State,
static projectState => AsyncLazy.Create(
static (projectState, cancellationToken) => ComputeContentHashToDocumentMapAsync(projectState, cancellationToken),
projectState));

var contentHashToDocumentMap = await lazyContentHashToDocumentMap.GetValueAsync(cancellationToken).ConfigureAwait(false);
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved

using var _ = ArrayBuilder<DocumentSpan>.GetInstance(out var documentSpans);

foreach (var interceptsLocationData in interceptsLocationDatas)
{
if (contentHashToDocumentMap.TryGetValue(interceptsLocationData.ContentHash, out var documentId))
{
var document = solution.GetDocument(documentId);
if (document != null)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(interceptsLocationData.Position);
documentSpans.Add(new DocumentSpan(document, token.Span));
}
}
}

documentSpans.RemoveDuplicates();

if (documentSpans.Count == 0)
{
return null;
}
else if (documentSpans.Count == 1)
{
// Just one document span this mapped to. Navigate directly do that.
return await documentSpans[0].GetNavigableLocationAsync(cancellationToken).ConfigureAwait(false);
}
else
{
var title = string.Format(EditorFeaturesResources._0_intercepted_locations,
FindUsagesHelpers.GetDisplayName(method));

var definitionItem = method.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true);

var referenceItems = new List<SourceReferenceItem>();
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
var classificationOptions = ClassificationOptions.Default with { ClassifyObsoleteSymbols = false };
foreach (var documentSpan in documentSpans)
{
var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync(
documentSpan, classifiedSpans: null, classificationOptions, cancellationToken).ConfigureAwait(false);

referenceItems.Add(new SourceReferenceItem(
definitionItem, documentSpan, classifiedSpans, SymbolUsageInfo.None, additionalProperties: []));
}

// Multiple document spans this mapped to. Show them all.
return new NavigableLocation(async (options, cancellationToken) =>
{
// Can only navigate or present items on UI thread.
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

// We have multiple definitions, or we have definitions with multiple locations. Present this to the
// user so they can decide where they want to go to.
//
// We ignore the cancellation token returned by StartSearch as we're in a context where
// we've computed all the results and we're synchronously populating the UI with it.
var (context, _) = _streamingPresenter.StartSearch(title, new StreamingFindUsagesPresenterOptions(SupportsReferences: true));
try
{
await context.OnDefinitionFoundAsync(definitionItem, cancellationToken).ConfigureAwait(false);
await context.OnReferencesFoundAsync(referenceItems.AsAsyncEnumerable(), cancellationToken).ConfigureAwait(false);
}
finally
{
await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false);
}

return true;
});
}
}
}

var definitions = builder.ToImmutable();
private static async Task<Dictionary<ImmutableArray<byte>, DocumentId>> ComputeContentHashToDocumentMapAsync(ProjectState projectState, CancellationToken cancellationToken)
{
var result = new Dictionary<ImmutableArray<byte>, DocumentId>(ByteArrayComparer.Instance);

foreach (var (documentId, documentState) in projectState.DocumentStates.States)
{
var text = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false);
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
var checksum = text.GetContentHash();
result[checksum] = documentId;
}

return await _streamingPresenter.GetStreamingLocationAsync(
_threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false);
return result;
}

private static async Task<bool> IsThirdPartyNavigationAllowedAsync(
Expand Down
5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading