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

Fix goto-def on partial methods in LSP #68559

Merged
merged 2 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/EditorFeatures/Core.Wpf/Peek/PeekableItemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public async Task<IEnumerable<IPeekableItem>> GetPeekableItemsAsync(

var solution = project.Solution;
symbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, solution, cancellationToken).ConfigureAwait(false) ?? symbol;
symbol = await GoToDefinitionHelpers.TryGetPreferredSymbolAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
symbol = await GoToDefinitionFeatureHelpers.TryGetPreferredSymbolAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
if (symbol is null)
return ImmutableArray<IPeekableItem>.Empty;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ protected AbstractAsyncGoToDefinitionService(
using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var builder);
foreach (var impl in interfaceImpls)
{
builder.AddRange(await GoToDefinitionHelpers.GetDefinitionsAsync(
builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false));
}

Expand Down
94 changes: 2 additions & 92 deletions src/EditorFeatures/Core/GoToDefinition/GoToDefinitionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,109 +3,18 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.GoToDefinition
{
internal static class GoToDefinitionHelpers
{
public static async Task<ImmutableArray<DefinitionItem>> GetDefinitionsAsync(
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 to common location. not tied to 'EditorFeatures' (e.g. VS) impl of goto-def.

ISymbol? symbol,
Solution solution,
bool thirdPartyNavigationAllowed,
CancellationToken cancellationToken)
{
symbol = await TryGetPreferredSymbolAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
if (symbol is null)
return ImmutableArray.Create<DefinitionItem>();

using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var definitions);

// Going to a symbol may end up actually showing the symbol in the Find-Usages window.
// This happens when there is more than one location for the symbol (i.e. for partial
// symbols) and we don't know the best place to take you to.
//
// The FindUsages window supports showing the classified text for an item. It does this
// in two ways. Either the item can pass along its classified text (and the window will
// defer to that), or the item will have no classified text, and the window will compute
// it in the BG.
//
// Passing along the classified information is valuable for OOP scenarios where we want
// all that expensive computation done on the OOP side and not in the VS side.
//
// However, Go To Definition is all in-process, and is also synchronous. So we do not
// want to fetch the classifications here. It slows down the command and leads to a
// measurable delay in our perf tests.
//
// 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);

if (thirdPartyNavigationAllowed)
{
var factory = solution.Services.GetService<IDefinitionsAndReferencesFactory>();
if (factory != null)
{
var thirdPartyItem = await factory.GetThirdPartyDefinitionItemAsync(solution, definitionItem, cancellationToken).ConfigureAwait(false);
definitions.AddIfNotNull(thirdPartyItem);
}
}

definitions.Add(definitionItem);
return definitions.ToImmutable();
}

public static async Task<ISymbol?> TryGetPreferredSymbolAsync(
Solution solution, ISymbol? symbol, CancellationToken cancellationToken)
{
// VB global import aliases have a synthesized SyntaxTree.
// We can't go to the definition of the alias, so use the target type.

var alias = symbol as IAliasSymbol;
if (alias != null)
{
if (alias.Target is INamespaceSymbol ns && ns.IsGlobalNamespace)
return null;
}

// VB global import aliases have a synthesized SyntaxTree.
// We can't go to the definition of the alias, so use the target type.

if (alias != null)
{
var sourceLocations = NavigableItemFactory.GetPreferredSourceLocations(
solution, symbol, cancellationToken);

if (sourceLocations.All(l => solution.GetDocument(l.SourceTree) == null))
symbol = alias.Target;
}

var definition = await SymbolFinder.FindSourceDefinitionAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

symbol = definition ?? symbol;

// If it is a partial method declaration with no body, choose to go to the implementation
// that has a method body.
if (symbol is IMethodSymbol method)
symbol = method.PartialImplementationPart ?? symbol;

return symbol;
}

public static async Task<bool> TryNavigateToLocationAsync(
ISymbol symbol,
Solution solution,
Expand All @@ -131,7 +40,8 @@ public static async Task<bool> TryNavigateToLocationAsync(
var title = string.Format(EditorFeaturesResources._0_declarations,
FindUsagesHelpers.GetDisplayName(symbol));

var definitions = await GetDefinitionsAsync(symbol, solution, thirdPartyNavigationAllowed, cancellationToken).ConfigureAwait(false);
var definitions = await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
symbol, solution, thirdPartyNavigationAllowed, cancellationToken).ConfigureAwait(false);

return await streamingPresenter.GetStreamingLocationAsync(
threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ public async Task<ImmutableArray<INavigableItem>> FindDefinitionsAsync(
var symbolService = document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();
var (symbol, project, _) = await symbolService.GetSymbolProjectAndBoundSpanAsync(document, position, includeType: true, cancellationToken).ConfigureAwait(false);

symbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, project.Solution, cancellationToken).ConfigureAwait(false) ?? symbol;
var solution = project.Solution;
symbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, solution, cancellationToken).ConfigureAwait(false) ?? symbol;
symbol = await GoToDefinitionFeatureHelpers.TryGetPreferredSymbolAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
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 continues the long pain of having something like 3 different goto-def impls in roslyn, which do/don't use the same set of underlying helpers to get consistent results. This includes IGoToDefinitionService, IAsyncGoToDefinitionService, and IFindDefinitionService. Ideally we can get to a place where this all gets burned down. But it's woven so deeply into different systems that that's going to be very hard for a while.


// Try to compute source definitions from symbol.
return symbol != null
? NavigableItemFactory.GetItemsFromPreferredSourceLocations(project.Solution, symbol, displayTaggedParts: FindUsagesHelpers.GetDisplayParts(symbol), cancellationToken: cancellationToken)
? NavigableItemFactory.GetItemsFromPreferredSourceLocations(solution, symbol, displayTaggedParts: FindUsagesHelpers.GetDisplayParts(symbol), cancellationToken: cancellationToken)
: ImmutableArray<INavigableItem>.Empty;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis.GoToDefinition
{
internal static class GoToDefinitionFeatureHelpers
{
public static async Task<ISymbol?> TryGetPreferredSymbolAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

pure move.

Solution solution, ISymbol? symbol, CancellationToken cancellationToken)
{
// VB global import aliases have a synthesized SyntaxTree.
// We can't go to the definition of the alias, so use the target type.

var alias = symbol as IAliasSymbol;
if (alias != null)
{
if (alias.Target is INamespaceSymbol ns && ns.IsGlobalNamespace)
return null;
}

// VB global import aliases have a synthesized SyntaxTree.
// We can't go to the definition of the alias, so use the target type.

if (alias != null)
{
var sourceLocations = NavigableItemFactory.GetPreferredSourceLocations(
solution, symbol, cancellationToken);

if (sourceLocations.All(l => solution.GetDocument(l.SourceTree) == null))
symbol = alias.Target;
}

var definition = await SymbolFinder.FindSourceDefinitionAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

symbol = definition ?? symbol;

// If it is a partial method declaration with no body, choose to go to the implementation
// that has a method body.
if (symbol is IMethodSymbol method)
symbol = method.PartialImplementationPart ?? symbol;

return symbol;
}

public static async Task<ImmutableArray<DefinitionItem>> GetDefinitionsAsync(
ISymbol? symbol,
Solution solution,
bool thirdPartyNavigationAllowed,
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.

pure move.

{
symbol = await TryGetPreferredSymbolAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
if (symbol is null)
return ImmutableArray.Create<DefinitionItem>();

using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var definitions);

// Going to a symbol may end up actually showing the symbol in the Find-Usages window.
// This happens when there is more than one location for the symbol (i.e. for partial
// symbols) and we don't know the best place to take you to.
//
// The FindUsages window supports showing the classified text for an item. It does this
// in two ways. Either the item can pass along its classified text (and the window will
// defer to that), or the item will have no classified text, and the window will compute
// it in the BG.
//
// Passing along the classified information is valuable for OOP scenarios where we want
// all that expensive computation done on the OOP side and not in the VS side.
//
// However, Go To Definition is all in-process, and is also synchronous. So we do not
// want to fetch the classifications here. It slows down the command and leads to a
// measurable delay in our perf tests.
//
// 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);

if (thirdPartyNavigationAllowed)
{
var factory = solution.Services.GetService<IDefinitionsAndReferencesFactory>();
if (factory != null)
{
var thirdPartyItem = await factory.GetThirdPartyDefinitionItemAsync(solution, definitionItem, cancellationToken).ConfigureAwait(false);
definitions.AddIfNotNull(thirdPartyItem);
}
}

definitions.Add(definitionItem);
return definitions.ToImmutable();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,33 @@ End Class
AssertLocationsEqual(testLspServer.GetLocations("definition"), results);
}

[Theory, CombinatorialData]
[WorkItem("https://github.com/dotnet/vscode-csharp/issues/5740")]
public async Task TestGotoDefinitionPartialMethods(bool mutatingLspWorkspace)
{
var markup =
"""
using System;

public partial class C
{
partial void {|caret:|}P();
}

public partial class C
{
partial void {|definition:P|}()
{
Console.WriteLine(");
}
}
""";
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);

var results = await RunGotoDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single());
AssertLocationsEqual(testLspServer.GetLocations("definition"), results);
}

private static async Task<LSP.Location[]> RunGotoDefinitionAsync(TestLspServer testLspServer, LSP.Location caret)
{
return await testLspServer.ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(LSP.Methods.TextDocumentDefinitionName,
Expand Down