Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ internal async Task<ImmutableArray<CodeDefinitionWindowLocation>> GetContextFrom
if (symbol != null)
{
var symbolNavigationService = workspace.Services.GetRequiredService<ISymbolNavigationService>();
var definitionItem = symbol.ToNonClassifiedDefinitionItem(document.Project.Solution, includeHiddenLocations: false);
var definitionItem = await symbol.ToNonClassifiedDefinitionItemAsync(
document.Project.Solution, includeHiddenLocations: false, cancellationToken).ConfigureAwait(false);
var result = await symbolNavigationService.GetExternalNavigationSymbolLocationAsync(definitionItem, cancellationToken).ConfigureAwait(false);

if (result != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public static async Task<bool> TryPresentLocationOrNavigateIfOneAsync(
if (presenter == null)
return null;

// Find the first location from a non-generated document to navigate to automatically.
// This way the user gets taken to their user code while still being able to see all the results
// (including generated code) in the tool window.
var preferredLocation = GetPreferredNonGeneratedLocation(builder);

var navigableItems = builder.SelectAsArray(t => t.item);
return new NavigableLocation(async (options, cancellationToken) =>
{
Expand All @@ -145,4 +150,24 @@ public static async Task<bool> TryPresentLocationOrNavigateIfOneAsync(
return true;
});
}

/// <summary>
/// Finds the first navigable location from a non-generated document in the list of definition items.
/// This allows Go To Definition to automatically navigate to user code when there are multiple locations
/// including generated code.
/// </summary>
private static INavigableLocation? GetPreferredNonGeneratedLocation(
ArrayBuilder<(DefinitionItem item, INavigableLocation location)> builder)
{
foreach (var (item, location) in builder)
{
foreach (var sourceSpan in item.SourceSpans)
{
if (!sourceSpan.IsGeneratedCode)
return location;
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ await TryGetInterceptedLocationAsync().ConfigureAwait(false) ??
var title = string.Format(EditorFeaturesResources._0_intercepted_locations,
FindUsagesHelpers.GetDisplayName(method));

var definitionItem = method.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true);
var definitionItem = await method.ToNonClassifiedDefinitionItemAsync(
solution, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);

var referenceItems = new List<SourceReferenceItem>(capacity: documentSpans.Count);
var classificationOptions = ClassificationOptions.Default with { ClassifyObsoleteSymbols = false };
Expand Down
3 changes: 2 additions & 1 deletion src/EditorFeatures/Core/Peek/PeekableItemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public async Task<IEnumerable<IPeekableItem>> GetPeekableItemsAsync(
var originatingProject = solution.GetProject(symbol.ContainingAssembly, cancellationToken);
project = originatingProject ?? project;

var definitionItem = symbol.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true);
var definitionItem = await symbol.ToNonClassifiedDefinitionItemAsync(
solution, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);

var symbolNavigationService = solution.Services.GetService<ISymbolNavigationService>();
var result = await symbolNavigationService.GetExternalNavigationSymbolLocationAsync(definitionItem, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ private static async Task TestSymbolFoundAsync(string inputLine, string code)
AssertEx.NotNull(expectedSymbol);

// Compare the definition found to the definition for the test symbol
var expectedDefinition = expectedSymbol.ToNonClassifiedDefinitionItem(workspace.CurrentSolution, includeHiddenLocations: true);
var expectedDefinition = await expectedSymbol.ToNonClassifiedDefinitionItemAsync(
workspace.CurrentSolution, includeHiddenLocations: true, CancellationToken.None);

Assert.Equal(expectedDefinition.IsExternal, definition.IsExternal);
AssertEx.SetEqual(expectedDefinition.NameDisplayParts, definition.NameDisplayParts);
Expand Down
3 changes: 2 additions & 1 deletion src/Features/Core/Portable/DocumentSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ namespace Microsoft.CodeAnalysis;
/// <summary>
/// Represents a <see cref="TextSpan"/> location in a <see cref="Document"/>.
/// </summary>
internal readonly record struct DocumentSpan(Document Document, TextSpan SourceSpan);
internal readonly record struct DocumentSpan(
Document Document, TextSpan SourceSpan, bool IsGeneratedCode = false);
36 changes: 23 additions & 13 deletions src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,32 @@ internal static class DefinitionItemFactory
memberOptions: SymbolDisplayMemberOptions.IncludeContainingType,
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining);

public static DefinitionItem ToNonClassifiedDefinitionItem(
public static Task<DefinitionItem> ToNonClassifiedDefinitionItemAsync(
this ISymbol definition,
Solution solution,
bool includeHiddenLocations)
=> ToNonClassifiedDefinitionItem(definition, solution, FindReferencesSearchOptions.Default, includeHiddenLocations);
bool includeHiddenLocations,
CancellationToken cancellationToken)
=> ToNonClassifiedDefinitionItemAsync(definition, solution, FindReferencesSearchOptions.Default, includeHiddenLocations, cancellationToken);

public static DefinitionItem ToNonClassifiedDefinitionItem(
public static Task<DefinitionItem> ToNonClassifiedDefinitionItemAsync(
this ISymbol definition,
Solution solution,
FindReferencesSearchOptions options,
bool includeHiddenLocations)
=> ToNonClassifiedDefinitionItem(definition, definition.Locations, solution, options, isPrimary: false, includeHiddenLocations);
bool includeHiddenLocations,
CancellationToken cancellationToken)
=> ToNonClassifiedDefinitionItemAsync(definition, definition.Locations, solution, options, isPrimary: false, includeHiddenLocations, cancellationToken);

private static DefinitionItem ToNonClassifiedDefinitionItem(
private static async Task<DefinitionItem> ToNonClassifiedDefinitionItemAsync(
ISymbol definition,
ImmutableArray<Location> locations,
Solution solution,
FindReferencesSearchOptions options,
bool isPrimary,
bool includeHiddenLocations)
bool includeHiddenLocations,
CancellationToken cancellationToken)
{
var sourceLocations = GetSourceLocations(definition, locations, solution, includeHiddenLocations);
var sourceLocations = await GetSourceLocationsAsync(
solution, definition, locations, includeHiddenLocations, cancellationToken).ConfigureAwait(false);

return ToDefinitionItem(
definition,
Expand All @@ -67,7 +71,7 @@ public static async ValueTask<DefinitionItem> ToClassifiedDefinitionItemAsync(
bool includeHiddenLocations,
CancellationToken cancellationToken)
{
var sourceLocations = GetSourceLocations(definition, definition.Locations, solution, includeHiddenLocations);
var sourceLocations = await GetSourceLocationsAsync(solution, definition, definition.Locations, includeHiddenLocations, cancellationToken).ConfigureAwait(false);
var classifiedSpans = await ClassifyDocumentSpansAsync(classificationOptions, sourceLocations, cancellationToken).ConfigureAwait(false);
return ToDefinitionItem(definition, sourceLocations, classifiedSpans, solution, options, isPrimary);
}
Expand All @@ -85,7 +89,7 @@ public static async ValueTask<DefinitionItem> ToClassifiedDefinitionItemAsync(
var definition = group.Symbols.First();
var locations = group.Symbols.SelectManyAsArray(s => s.Locations);

var sourceLocations = GetSourceLocations(definition, locations, solution, includeHiddenLocations);
var sourceLocations = await GetSourceLocationsAsync(solution, definition, locations, includeHiddenLocations, cancellationToken).ConfigureAwait(false);
var classifiedSpans = await ClassifyDocumentSpansAsync(classificationOptions, sourceLocations, cancellationToken).ConfigureAwait(false);
return ToDefinitionItem(definition, sourceLocations, classifiedSpans, solution, options, isPrimary);
}
Expand Down Expand Up @@ -221,7 +225,12 @@ internal static ImmutableArray<AssemblyLocation> GetMetadataLocations(ISymbol de
return [];
}

private static ImmutableArray<DocumentSpan> GetSourceLocations(ISymbol definition, ImmutableArray<Location> locations, Solution solution, bool includeHiddenLocations)
private static async Task<ImmutableArray<DocumentSpan>> GetSourceLocationsAsync(
Solution solution,
ISymbol definition,
ImmutableArray<Location> locations,
bool includeHiddenLocations,
CancellationToken cancellationToken)
{
// Assembly, module, global namespace and preprocessing symbol locations include all source documents; displaying all of them is not useful.
// We could consider creating a definition item that points to the project source instead.
Expand All @@ -238,7 +247,8 @@ private static ImmutableArray<DocumentSpan> GetSourceLocations(ISymbol definitio
(includeHiddenLocations || location.IsVisibleSourceLocation()) &&
solution.GetDocument(location.SourceTree) is { } document)
{
source.Add(new DocumentSpan(document, location.SourceSpan));
var isGeneratedCode = await document.IsGeneratedCodeAsync(cancellationToken).ConfigureAwait(false);
source.Add(new DocumentSpan(document, location.SourceSpan, isGeneratedCode));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,23 +159,27 @@ private DefinitionItem GetDefinition(int definitionId)
}

[DataContract]
internal readonly struct SerializableDocumentSpan(DocumentId documentId, TextSpan sourceSpan)
internal readonly struct SerializableDocumentSpan(
DocumentId documentId, TextSpan sourceSpan, bool isGeneratedCode)
{
[DataMember(Order = 0)]
public readonly DocumentId DocumentId = documentId;

[DataMember(Order = 1)]
public readonly TextSpan SourceSpan = sourceSpan;

[DataMember(Order = 2)]
public readonly bool IsGeneratedCode = isGeneratedCode;

public static SerializableDocumentSpan Dehydrate(DocumentSpan documentSpan)
=> new(documentSpan.Document.Id, documentSpan.SourceSpan);
=> new(documentSpan.Document.Id, documentSpan.SourceSpan, documentSpan.IsGeneratedCode);

public async ValueTask<DocumentSpan> RehydrateAsync(Solution solution, CancellationToken cancellationToken)
{
var document = solution.GetDocument(DocumentId) ??
await solution.GetSourceGeneratedDocumentAsync(DocumentId, cancellationToken).ConfigureAwait(false);
await solution.GetSourceGeneratedDocumentAsync(DocumentId, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(document);
return new DocumentSpan(document, SourceSpan);
return new DocumentSpan(document, SourceSpan, IsGeneratedCode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ await context.SetSearchTitleAsync(
}
else if (baseSymbol.Locations.Any(static l => l.IsInMetadata))
{
var definitionItem = baseSymbol.ToNonClassifiedDefinitionItem(
solution, FindReferencesSearchOptions.Default, includeHiddenLocations: true);
var definitionItem = await baseSymbol.ToNonClassifiedDefinitionItemAsync(
solution, FindReferencesSearchOptions.Default, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);
await context.OnDefinitionFoundAsync(definitionItem, cancellationToken).ConfigureAwait(false);
found = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public static async Task<ImmutableArray<DefinitionItem>> GetDefinitionsAsync(
// So, if we only have a single location to go to, this does no unnecessary work. And,
// if we do have multiple locations to show, it will just be done in the BG, unblocking
// this command thread so it can return the user faster.
var definitionItem = symbol.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true);
var definitionItem = await symbol.ToNonClassifiedDefinitionItemAsync(
solution, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);

if (thirdPartyNavigationAllowed)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ private static async ValueTask AddInheritanceMemberItemsForMembersAsync(
targetSymbol = symbolInSource ?? targetSymbol;

// Right now the targets are not shown in a classified way.
var definition = ToSlimDefinitionItem(targetSymbol, solution);
var definition = await ToSlimDefinitionItemAsync(solution, targetSymbol, cancellationToken).ConfigureAwait(false);
if (definition == null)
return null;

Expand Down Expand Up @@ -711,15 +711,17 @@ private static async Task<ImmutableArray<INamedTypeSymbol>> GetDerivedTypesAndIm
/// Otherwise, create the full non-classified DefinitionItem. Because in such case we want to display all the locations to the user
/// by reusing the FAR window.
/// </summary>
private static DefinitionItem? ToSlimDefinitionItem(ISymbol symbol, Solution solution)
private static async Task<DefinitionItem?> ToSlimDefinitionItemAsync(
Solution solution, ISymbol symbol, CancellationToken cancellation)
{
var locations = symbol.Locations;
if (locations.Length > 1)
{
return symbol.ToNonClassifiedDefinitionItem(
return await symbol.ToNonClassifiedDefinitionItemAsync(
solution,
FindReferencesSearchOptions.Default with { UnidirectionalHierarchyCascade = true },
includeHiddenLocations: false);
includeHiddenLocations: false,
cancellation).ConfigureAwait(false);
}

if (locations is [var location])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ internal static class StackTraceExplorerUtilities
{
var method = await TryGetBestMatchAsync(project, fullyQualifiedTypeName, methodNode, methodArguments, methodTypeArguments, cancellationToken).ConfigureAwait(false);
if (method is not null)
{
return GetDefinition(method);
}
return await GetDefinitionAsync(method).ConfigureAwait(false);
}
else
{
Expand All @@ -81,9 +79,7 @@ internal static class StackTraceExplorerUtilities
{
var method = await TryGetBestMatchAsync(project, fullyQualifiedTypeName, methodNode, methodArguments, methodTypeArguments, cancellationToken).ConfigureAwait(false);
if (method is not null)
{
return GetDefinition(method);
}
return await GetDefinitionAsync(method).ConfigureAwait(false);
}

return null;
Expand All @@ -92,18 +88,19 @@ internal static class StackTraceExplorerUtilities
// Local Functions
//

DefinitionItem GetDefinition(IMethodSymbol method)
Task<DefinitionItem> GetDefinitionAsync(IMethodSymbol method)
{
ISymbol symbol = method;
if (symbolPart == StackFrameSymbolPart.ContainingType)
{
symbol = method.ContainingType;
}

return symbol.ToNonClassifiedDefinitionItem(
return symbol.ToNonClassifiedDefinitionItemAsync(
solution,
FindReferencesSearchOptions.Default with { UnidirectionalHierarchyCascade = true },
includeHiddenLocations: true);
includeHiddenLocations: true,
cancellationToken);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public override async ValueTask OnReferencesFoundAsync(IAsyncEnumerable<SourceRe
Text = text,
};

if (documentSpan is var (document, _))
if (documentSpan is var (document, _, _))
{
result.DocumentName = document.Name;
result.ProjectName = document.Project.Name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ public async Task<bool> TrySymbolNavigationNotifyAsync(ISymbol symbol, Project p
{
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var definitionItem = symbol.ToNonClassifiedDefinitionItem(project.Solution, includeHiddenLocations: true);
var definitionItem = await symbol.ToNonClassifiedDefinitionItemAsync(
project.Solution, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);
definitionItem.Properties.TryGetValue(DefinitionItem.RQNameKey1, out var rqName);

var result = await TryGetNavigationAPIRequiredArgumentsAsync(definitionItem, rqName, cancellationToken).ConfigureAwait(true);
Expand Down
Loading