// 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 System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.FindUsages;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.GoToDefinition
{
// GoToDefinition
internal abstract class AbstractGoToDefinitionService : IGoToDefinitionService
{
///
/// Used to present go to definition results in
/// This is lazily created as the LSP server only calls
/// and therefore never needs to construct the presenter.
///
private readonly Lazy _streamingPresenter;
protected AbstractGoToDefinitionService(Lazy streamingPresenter)
{
_streamingPresenter = streamingPresenter;
}
public async Task> FindDefinitionsAsync(
Document document, int position, CancellationToken cancellationToken)
{
var symbolService = document.GetLanguageService();
var (symbol, _) = await symbolService.GetSymbolAndBoundSpanAsync(document, position, includeType: true, cancellationToken).ConfigureAwait(false);
// Try to compute source definitions from symbol.
var items = symbol != null
? NavigableItemFactory.GetItemsFromPreferredSourceLocations(document.Project.Solution, symbol, displayTaggedParts: null, cancellationToken: cancellationToken)
: null;
// realize the list here so that the consumer await'ing the result doesn't lazily cause
// them to be created on an inappropriate thread.
return items?.ToList();
}
public bool TryGoToDefinition(Document document, int position, CancellationToken cancellationToken)
{
// Try to compute the referenced symbol and attempt to go to definition for the symbol.
var symbolService = document.GetLanguageService();
var (symbol, _) = symbolService.GetSymbolAndBoundSpanAsync(document, position, includeType: true, cancellationToken).WaitAndGetResult(cancellationToken);
if (symbol is null)
return false;
var remapped = TryRemapIfAlreadyOnDefinition(document, position, symbol, cancellationToken);
if (remapped)
return true;
var isThirdPartyNavigationAllowed = IsThirdPartyNavigationAllowed(symbol, position, document, cancellationToken);
return GoToDefinitionHelpers.TryGoToDefinition(symbol,
document.Project,
_streamingPresenter.Value,
thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed,
cancellationToken: cancellationToken);
}
private bool TryRemapIfAlreadyOnDefinition(
Document document, int position,
ISymbol symbol, CancellationToken cancellationToken)
{
var project = document.Project;
var solution = project.Solution;
// 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 sourceLocations = symbol.Locations.WhereAsArray(loc => loc.IsInSource);
if (sourceLocations.Length != 1)
return false;
var definitionLocation = sourceLocations[0];
if (!definitionLocation.SourceSpan.IntersectsWith(position))
return false;
var definitionTree = definitionLocation.SourceTree;
var definitionDocument = solution.GetDocument(definitionTree);
if (definitionDocument != document)
return false;
// Ok, found a match. Look for better symbols we could show results for instead.
var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations();
if (interfaceImpls.Length == 0)
return false;
var definitions = interfaceImpls.SelectMany(
i => GoToDefinitionHelpers.GetDefinitions(
i, project, thirdPartyNavigationAllowed: false, cancellationToken)).ToImmutableArray();
var title = string.Format(EditorFeaturesResources._0_implemented_members,
FindUsagesHelpers.GetDisplayName(symbol));
return _streamingPresenter.Value.TryNavigateToOrPresentItemsAsync(
solution.Workspace, title, definitions).WaitAndGetResult(cancellationToken);
}
private static bool IsThirdPartyNavigationAllowed(ISymbol symbolToNavigateTo, int caretPosition, Document document, CancellationToken cancellationToken)
{
var syntaxRoot = document.GetSyntaxRootSynchronously(cancellationToken);
var syntaxFactsService = document.GetLanguageService();
var containingTypeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(syntaxRoot, caretPosition);
if (containingTypeDeclaration != null)
{
var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult(cancellationToken);
// 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;
}
}
}