Skip to content

Commit f372de7

Browse files
Merge pull request #73992 from CyrusNajmabadi/interceptsLocationWork
Support goto-def taking you from an interceptor method to the location being intercepted.
2 parents 0447e0c + 3936d56 commit f372de7

26 files changed

+1035
-76
lines changed

src/EditorFeatures/Core/EditorFeaturesResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,9 @@ Do you want to proceed?</value>
613613
<data name="_0_declarations" xml:space="preserve">
614614
<value>'{0}' declarations</value>
615615
</data>
616+
<data name="_0_intercepted_locations" xml:space="preserve">
617+
<value>'{0}' intercepted locations</value>
618+
</data>
616619
<data name="An_inline_rename_session_is_active_for_identifier_0" xml:space="preserve">
617620
<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>
618621
<comment>For screenreaders. {0} is the identifier being renamed.</comment>

src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs

Lines changed: 161 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,27 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Generic;
56
using System.Threading;
67
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Classification;
79
using Microsoft.CodeAnalysis.Editor.Host;
810
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
911
using Microsoft.CodeAnalysis.FindUsages;
1012
using Microsoft.CodeAnalysis.GoToDefinition;
1113
using Microsoft.CodeAnalysis.LanguageService;
1214
using Microsoft.CodeAnalysis.PooledObjects;
1315
using Microsoft.CodeAnalysis.Shared.Extensions;
16+
using Microsoft.CodeAnalysis.Shared.Utilities;
1417

1518
namespace Microsoft.CodeAnalysis.Navigation;
1619

17-
internal abstract class AbstractDefinitionLocationService : IDefinitionLocationService
20+
internal abstract partial class AbstractDefinitionLocationService(
21+
IThreadingContext threadingContext,
22+
IStreamingFindUsagesPresenter streamingPresenter) : IDefinitionLocationService
1823
{
19-
private readonly IThreadingContext _threadingContext;
20-
private readonly IStreamingFindUsagesPresenter _streamingPresenter;
21-
22-
protected AbstractDefinitionLocationService(
23-
IThreadingContext threadingContext,
24-
IStreamingFindUsagesPresenter streamingPresenter)
25-
{
26-
_threadingContext = threadingContext;
27-
_streamingPresenter = streamingPresenter;
28-
}
24+
private readonly IThreadingContext _threadingContext = threadingContext;
25+
private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter;
2926

3027
private static Task<INavigableLocation?> GetNavigableLocationAsync(
3128
Document document, int position, CancellationToken cancellationToken)
@@ -40,54 +37,62 @@ protected AbstractDefinitionLocationService(
4037

4138
public async Task<DefinitionLocation?> GetDefinitionLocationAsync(Document document, int position, CancellationToken cancellationToken)
4239
{
40+
var symbolService = document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();
41+
4342
// We want to compute this as quickly as possible so that the symbol be squiggled and navigated to. We
4443
// don't want to wait on expensive operations like computing source-generators or skeletons if we can avoid
4544
// it. So first try with a frozen document, then fallback to a normal document. This mirrors how go-to-def
4645
// works as well.
4746
return await GetDefinitionLocationWorkerAsync(document.WithFrozenPartialSemantics(cancellationToken)).ConfigureAwait(false) ??
4847
await GetDefinitionLocationWorkerAsync(document).ConfigureAwait(false);
4948

50-
async Task<DefinitionLocation?> GetDefinitionLocationWorkerAsync(Document document)
49+
async ValueTask<DefinitionLocation?> GetDefinitionLocationWorkerAsync(Document document)
50+
{
51+
return await GetControlFlowTargetLocationAsync(document).ConfigureAwait(false) ??
52+
await GetSymbolLocationAsync(document).ConfigureAwait(false);
53+
}
54+
55+
async ValueTask<DefinitionLocation?> GetControlFlowTargetLocationAsync(Document document)
5156
{
52-
var symbolService = document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();
5357
var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync(
5458
document, position, cancellationToken).ConfigureAwait(false);
55-
if (controlFlowTarget != null)
56-
{
57-
var location = await GetNavigableLocationAsync(
58-
document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false);
59-
return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan));
60-
}
61-
else
62-
{
63-
// Try to compute the referenced symbol and attempt to go to definition for the symbol.
64-
var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync(
65-
document, position, cancellationToken).ConfigureAwait(false);
66-
if (symbol is null)
67-
return null;
68-
69-
// if the symbol only has a single source location, and we're already on it,
70-
// try to see if there's a better symbol we could navigate to.
71-
var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync(
72-
project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false);
73-
if (remappedLocation != null)
74-
return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span));
75-
76-
var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync(
77-
symbol, position, document, cancellationToken).ConfigureAwait(false);
78-
79-
var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync(
80-
symbol,
81-
project.Solution,
82-
_threadingContext,
83-
_streamingPresenter,
84-
thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed,
85-
cancellationToken: cancellationToken).ConfigureAwait(false);
86-
if (location is null)
87-
return null;
88-
89-
return new DefinitionLocation(location, new DocumentSpan(document, span));
90-
}
59+
if (controlFlowTarget == null)
60+
return null;
61+
62+
var location = await GetNavigableLocationAsync(
63+
document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false);
64+
return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan));
65+
}
66+
67+
async ValueTask<DefinitionLocation?> GetSymbolLocationAsync(Document document)
68+
{
69+
// Try to compute the referenced symbol and attempt to go to definition for the symbol.
70+
var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync(
71+
document, position, cancellationToken).ConfigureAwait(false);
72+
if (symbol is null)
73+
return null;
74+
75+
// if the symbol only has a single source location, and we're already on it,
76+
// try to see if there's a better symbol we could navigate to.
77+
var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync(
78+
project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false);
79+
if (remappedLocation != null)
80+
return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span));
81+
82+
var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync(
83+
symbol, position, document, cancellationToken).ConfigureAwait(false);
84+
85+
var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync(
86+
symbol,
87+
project.Solution,
88+
_threadingContext,
89+
_streamingPresenter,
90+
thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed,
91+
cancellationToken: cancellationToken).ConfigureAwait(false);
92+
if (location is null)
93+
return null;
94+
95+
return new DefinitionLocation(location, new DocumentSpan(document, span));
9196
}
9297
}
9398

@@ -114,29 +119,118 @@ protected AbstractDefinitionLocationService(
114119
if (definitionDocument != originalDocument)
115120
return null;
116121

117-
// Ok, we were already on the definition. Look for better symbols we could show results
118-
// for instead. For now, just see if we're on an interface member impl. If so, we can
119-
// instead navigate to the actual interface member.
120-
//
121-
// In the future we can expand this with other mappings if appropriate.
122-
var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations();
123-
if (interfaceImpls.Length == 0)
124-
return null;
125-
126-
var title = string.Format(EditorFeaturesResources._0_implemented_members,
127-
FindUsagesHelpers.GetDisplayName(symbol));
122+
// Ok, we were already on the definition. Look for better symbols we could show results for instead. This can be
123+
// expanded with other mappings in the future if appropriate.
124+
return await TryGetExplicitInterfaceLocationAsync().ConfigureAwait(false) ??
125+
await TryGetInterceptedLocationAsync().ConfigureAwait(false);
128126

129-
using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var builder);
130-
foreach (var impl in interfaceImpls)
127+
async ValueTask<INavigableLocation?> TryGetExplicitInterfaceLocationAsync()
131128
{
132-
builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
133-
impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false));
129+
var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations();
130+
if (interfaceImpls.Length == 0)
131+
return null;
132+
133+
var title = string.Format(EditorFeaturesResources._0_implemented_members,
134+
FindUsagesHelpers.GetDisplayName(symbol));
135+
136+
using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var builder);
137+
foreach (var impl in interfaceImpls)
138+
{
139+
builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
140+
impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false));
141+
}
142+
143+
var definitions = builder.ToImmutable();
144+
145+
return await _streamingPresenter.GetStreamingLocationAsync(
146+
_threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false);
134147
}
135148

136-
var definitions = builder.ToImmutable();
149+
async ValueTask<INavigableLocation?> TryGetInterceptedLocationAsync()
150+
{
151+
if (symbol is not IMethodSymbol method)
152+
return null;
153+
154+
// Find attributes of the form: [InterceptsLocationAttribute(version: 1, data: "...")];
155+
156+
var attributes = method.GetAttributes();
157+
var interceptsLocationDatas = InterceptsLocationUtilities.GetInterceptsLocationData(attributes);
158+
if (interceptsLocationDatas.Length == 0)
159+
return null;
160+
161+
using var _ = ArrayBuilder<DocumentSpan>.GetInstance(out var documentSpans);
162+
163+
foreach (var (contentHash, position) in interceptsLocationDatas)
164+
{
165+
var document = await project.GetDocumentAsync(contentHash, cancellationToken).ConfigureAwait(false);
166+
167+
if (document != null)
168+
{
169+
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
137170

138-
return await _streamingPresenter.GetStreamingLocationAsync(
139-
_threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false);
171+
if (position >= 0 && position < root.FullSpan.Length)
172+
{
173+
var token = root.FindToken(position);
174+
documentSpans.Add(new DocumentSpan(document, token.Span));
175+
}
176+
}
177+
}
178+
179+
documentSpans.RemoveDuplicates();
180+
181+
if (documentSpans.Count == 0)
182+
{
183+
return null;
184+
}
185+
else if (documentSpans.Count == 1)
186+
{
187+
// Just one document span this mapped to. Navigate directly do that.
188+
return await documentSpans[0].GetNavigableLocationAsync(cancellationToken).ConfigureAwait(false);
189+
}
190+
else
191+
{
192+
var title = string.Format(EditorFeaturesResources._0_intercepted_locations,
193+
FindUsagesHelpers.GetDisplayName(method));
194+
195+
var definitionItem = method.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true);
196+
197+
var referenceItems = new List<SourceReferenceItem>(capacity: documentSpans.Count);
198+
var classificationOptions = ClassificationOptions.Default with { ClassifyObsoleteSymbols = false };
199+
foreach (var documentSpan in documentSpans)
200+
{
201+
var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync(
202+
documentSpan, classifiedSpans: null, classificationOptions, cancellationToken).ConfigureAwait(false);
203+
204+
referenceItems.Add(new SourceReferenceItem(
205+
definitionItem, documentSpan, classifiedSpans, SymbolUsageInfo.None, additionalProperties: []));
206+
}
207+
208+
// Multiple document spans this mapped to. Show them all.
209+
return new NavigableLocation(async (options, cancellationToken) =>
210+
{
211+
// Can only navigate or present items on UI thread.
212+
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
213+
214+
// We have multiple definitions, or we have definitions with multiple locations. Present this to the
215+
// user so they can decide where they want to go to.
216+
//
217+
// We ignore the cancellation token returned by StartSearch as we're in a context where
218+
// we've computed all the results and we're synchronously populating the UI with it.
219+
var (context, _) = _streamingPresenter.StartSearch(title, new StreamingFindUsagesPresenterOptions(SupportsReferences: true));
220+
try
221+
{
222+
await context.OnDefinitionFoundAsync(definitionItem, cancellationToken).ConfigureAwait(false);
223+
await context.OnReferencesFoundAsync(referenceItems.AsAsyncEnumerable(), cancellationToken).ConfigureAwait(false);
224+
}
225+
finally
226+
{
227+
await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false);
228+
}
229+
230+
return true;
231+
});
232+
}
233+
}
140234
}
141235

142236
private static async Task<bool> IsThirdPartyNavigationAllowedAsync(

src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)