|
// 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.Collections.Generic;
using System.Collections.Immutable;
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 Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Navigation;
internal abstract partial class AbstractDefinitionLocationService(
IThreadingContext threadingContext,
IStreamingFindUsagesPresenter streamingPresenter) : IDefinitionLocationService
{
private readonly IThreadingContext _threadingContext = threadingContext;
private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter;
private static Task<INavigableLocation?> GetNavigableLocationAsync(
Document document, int position, CancellationToken cancellationToken)
{
var solution = document.Project.Solution;
var workspace = solution.Workspace;
var service = workspace.Services.GetRequiredService<IDocumentNavigationService>();
return service.GetLocationForPositionAsync(
workspace, document.Id, position, cancellationToken);
}
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 ValueTask<DefinitionLocation?> GetDefinitionLocationWorkerAsync(Document document)
{
return await GetControlFlowTargetLocationAsync(document).ConfigureAwait(false) ??
await GetSymbolLocationAsync(document).ConfigureAwait(false);
}
async ValueTask<DefinitionLocation?> GetControlFlowTargetLocationAsync(Document document)
{
var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync(
document, position, cancellationToken).ConfigureAwait(false);
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 solution = project.Solution;
var regularDefinitions = await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
symbol, solution, isThirdPartyNavigationAllowed, cancellationToken).ConfigureAwait(false);
var interceptorDefinitions = await GetInterceptorDefinitionsAsync(
solution, document, span, cancellationToken).ConfigureAwait(false);
var symbolDisplayName = FindUsagesHelpers.GetDisplayName(symbol);
var title = interceptorDefinitions.Length == 0
? string.Format(EditorFeaturesResources._0_declarations, symbolDisplayName)
: string.Format(EditorFeaturesResources._0_declarations_and_interceptors, symbolDisplayName);
var allDefinitions = regularDefinitions.Concat(interceptorDefinitions);
var location = await _streamingPresenter.GetStreamingLocationAsync(
_threadingContext, solution.Workspace, title, allDefinitions, cancellationToken).ConfigureAwait(false);
if (location is null)
return null;
return new DefinitionLocation(location, new DocumentSpan(document, span));
}
async ValueTask<ImmutableArray<DefinitionItem>> GetInterceptorDefinitionsAsync(
Solution solution, Document document, TextSpan span, CancellationToken cancellationToken)
{
var semanticFacts = document.GetRequiredLanguageService<ISemanticFactsService>();
var interceptorSymbol = await semanticFacts.GetInterceptorSymbolAsync(document, span.Start, cancellationToken).ConfigureAwait(false);
return await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
interceptorSymbol, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Attempts to find a better definition for the symbol, if the user is already on the definition of it.
/// </summary>
/// <param name="project">The project context to use for finding symbols</param>
/// <param name="originalDocument">The document the user is navigating from. This may not be part of the project supplied.</param>
private async Task<INavigableLocation?> GetAlternativeLocationIfAlreadyOnDefinitionAsync(
Project project, int position, ISymbol symbol, Document originalDocument, CancellationToken cancellationToken)
{
var solution = project.Solution;
var sourceLocations = symbol.Locations.WhereAsArray(loc => loc.IsInSource);
if (sourceLocations.Length != 1)
return null;
var definitionLocation = sourceLocations[0];
if (!definitionLocation.SourceSpan.IntersectsWith(position))
return null;
var definitionTree = definitionLocation.SourceTree;
var definitionDocument = solution.GetDocument(definitionTree);
if (definitionDocument != originalDocument)
return null;
// Ok, we were already on the definition. Look for better symbols we could show results for instead. This can be
// expanded with other mappings in the future if appropriate.
return await TryGetExplicitInterfaceLocationAsync().ConfigureAwait(false) ??
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));
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()
{
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;
using var _ = ArrayBuilder<DocumentSpan>.GetInstance(out var documentSpans);
foreach (var (contentHash, position) in interceptsLocationDatas)
{
var document = await project.GetDocumentAsync(contentHash, cancellationToken).ConfigureAwait(false);
if (document != null)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (position >= 0 && position < root.FullSpan.Length)
{
var token = root.FindToken(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>(capacity: documentSpans.Count);
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;
});
}
}
}
private static async Task<bool> IsThirdPartyNavigationAllowedAsync(
ISymbol symbolToNavigateTo, int caretPosition, Document document, CancellationToken cancellationToken)
{
var syntaxRoot = document.GetRequiredSyntaxRootSynchronously(cancellationToken);
var syntaxFactsService = document.GetRequiredLanguageService<ISyntaxFactsService>();
var containingTypeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(syntaxRoot, caretPosition);
if (containingTypeDeclaration != null)
{
var semanticModel = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false);
// Allow third parties to navigate to all symbols except types/constructors
// if we are navigating from the corresponding type.
if (semanticModel.GetDeclaredSymbol(containingTypeDeclaration, cancellationToken) is ITypeSymbol containingTypeSymbol &&
(symbolToNavigateTo is ITypeSymbol || symbolToNavigateTo.IsConstructor()))
{
var candidateTypeSymbol = symbolToNavigateTo is ITypeSymbol
? symbolToNavigateTo
: symbolToNavigateTo.ContainingType;
if (Equals(containingTypeSymbol, candidateTypeSymbol))
{
// We are navigating from the same type, so don't allow third parties to perform the navigation.
// This ensures that if we navigate to a class from within that class, we'll stay in the same file
// rather than navigate to, say, XAML.
return false;
}
}
}
return true;
}
}
|