Skip to content

Commit 241d91b

Browse files
Break code for handling a request to FindRefs/GoToBase/GoToImpl out of the editor command handlers for those features. (#78751)
2 parents 75feb2f + 2330fff commit 241d91b

File tree

12 files changed

+178
-83
lines changed

12 files changed

+178
-83
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
6+
using Microsoft.CodeAnalysis.Text;
7+
using Microsoft.VisualStudio.Commanding;
8+
using Microsoft.VisualStudio.Text.Editor.Commanding;
9+
10+
namespace Microsoft.CodeAnalysis.GoOrFind;
11+
12+
internal abstract class AbstractGoOrFindCommandHandler<TCommandArgs>(
13+
IGoOrFindNavigationService navigationService) : ICommandHandler<TCommandArgs>
14+
where TCommandArgs : EditorCommandArgs
15+
{
16+
private readonly IGoOrFindNavigationService _navigationService = navigationService;
17+
18+
public string DisplayName => _navigationService.DisplayName;
19+
20+
public CommandState GetCommandState(TCommandArgs args)
21+
{
22+
var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
23+
return _navigationService.IsAvailable(document)
24+
? CommandState.Available
25+
: CommandState.Unspecified;
26+
}
27+
28+
public bool ExecuteCommand(TCommandArgs args, CommandExecutionContext context)
29+
{
30+
var subjectBuffer = args.SubjectBuffer;
31+
var caret = args.TextView.GetCaretPoint(subjectBuffer);
32+
if (!caret.HasValue)
33+
return false;
34+
35+
var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
36+
if (!_navigationService.IsAvailable(document))
37+
return false;
38+
39+
return _navigationService.ExecuteCommand(document, caret.Value.Position);
40+
}
41+
}

src/EditorFeatures/Core/GoToDefinition/AbstractGoOrFindCommandHandler.cs renamed to src/EditorFeatures/Core/GoOrFind/AbstractGoOrFindNavigationService.cs

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.CodeAnalysis.Classification;
@@ -17,22 +18,22 @@
1718
using Microsoft.CodeAnalysis.Options;
1819
using Microsoft.CodeAnalysis.Shared.Extensions;
1920
using Microsoft.CodeAnalysis.Shared.TestHooks;
20-
using Microsoft.CodeAnalysis.Text;
2121
using Microsoft.CodeAnalysis.Threading;
22-
using Microsoft.VisualStudio.Commanding;
23-
using Microsoft.VisualStudio.Text;
24-
using Microsoft.VisualStudio.Text.Editor.Commanding;
2522
using Microsoft.VisualStudio.Threading;
2623

27-
namespace Microsoft.CodeAnalysis.GoToDefinition;
24+
namespace Microsoft.CodeAnalysis.GoOrFind;
2825

29-
internal abstract class AbstractGoOrFindCommandHandler<TLanguageService, TCommandArgs>(
26+
/// <summary>
27+
/// Core service responsible for handling an operation (like 'go to base, go to impl, find references')
28+
/// and trying to navigate quickly to them if possible, or show their results in the find-usages window.
29+
/// </summary>
30+
internal abstract class AbstractGoOrFindNavigationService<TLanguageService>(
3031
IThreadingContext threadingContext,
3132
IStreamingFindUsagesPresenter streamingPresenter,
3233
IAsynchronousOperationListener listener,
33-
IGlobalOptionService globalOptions) : ICommandHandler<TCommandArgs>
34+
IGlobalOptionService globalOptions)
35+
: IGoOrFindNavigationService
3436
where TLanguageService : class, ILanguageService
35-
where TCommandArgs : EditorCommandArgs
3637
{
3738
private readonly IThreadingContext _threadingContext = threadingContext;
3839
private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter;
@@ -59,7 +60,7 @@ internal abstract class AbstractGoOrFindCommandHandler<TLanguageService, TComman
5960
/// the presenter. In that case, the presenter will notify us that it has be re-purposed and we will also cancel
6061
/// this source.
6162
/// </remarks>
62-
private readonly CancellationSeries _cancellationSeries = new(threadingContext.DisposalToken);
63+
private CancellationTokenSource _cancellationTokenSource = new();
6364

6465
/// <summary>
6566
/// This hook allows for stabilizing the asynchronous nature of this command handler for integration testing.
@@ -80,49 +81,34 @@ protected virtual StreamingFindUsagesPresenterOptions GetStreamingPresenterOptio
8081

8182
protected abstract Task FindActionAsync(IFindUsagesContext context, Document document, TLanguageService service, int caretPosition, CancellationToken cancellationToken);
8283

83-
private static (Document?, TLanguageService?) GetDocumentAndService(ITextSnapshot snapshot)
84-
{
85-
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
86-
return (document, document?.GetLanguageService<TLanguageService>());
87-
}
84+
public bool IsAvailable([NotNullWhen(true)] Document? document)
85+
=> document?.GetLanguageService<TLanguageService>() != null;
8886

89-
public CommandState GetCommandState(TCommandArgs args)
90-
{
91-
var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot);
92-
return service != null
93-
? CommandState.Available
94-
: CommandState.Unspecified;
95-
}
96-
97-
public bool ExecuteCommand(TCommandArgs args, CommandExecutionContext context)
87+
public bool ExecuteCommand(Document document, int position)
9888
{
9989
_threadingContext.ThrowIfNotOnUIThread();
100-
101-
var subjectBuffer = args.SubjectBuffer;
102-
var caret = args.TextView.GetCaretPoint(subjectBuffer);
103-
if (!caret.HasValue)
90+
if (document is null)
10491
return false;
10592

106-
var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot);
93+
var service = document.GetLanguageService<TLanguageService>();
10794
if (service == null)
10895
return false;
10996

110-
Contract.ThrowIfNull(document);
111-
11297
// cancel any prior find-refs that might be in progress.
113-
var cancellationToken = _cancellationSeries.CreateNext();
98+
_cancellationTokenSource.Cancel();
99+
_cancellationTokenSource = new();
114100

115101
// we're going to return immediately from ExecuteCommand and kick off our own async work to invoke the
116102
// operation. Once this returns, the editor will close the threaded wait dialog it created.
117-
_inProgressCommand = ExecuteCommandAsync(document, service, caret.Value.Position, cancellationToken);
103+
_inProgressCommand = ExecuteCommandAsync(document, service, position, _cancellationTokenSource);
118104
return true;
119105
}
120106

121107
private async Task ExecuteCommandAsync(
122108
Document document,
123109
TLanguageService service,
124110
int position,
125-
CancellationToken cancellationToken)
111+
CancellationTokenSource cancellationTokenSource)
126112
{
127113
// This is a fire-and-forget method (nothing guarantees observing it). As such, we have to handle cancellation
128114
// and failure ourselves.
@@ -141,7 +127,7 @@ private async Task ExecuteCommandAsync(
141127
// any failures from it. Technically this should not be possible as it should be inside this same
142128
// try/catch. however this code wants to be very resilient to any prior mistakes infecting later operations.
143129
await _inProgressCommand.NoThrowAwaitable(captureContext: false);
144-
await ExecuteCommandWorkerAsync(document, service, position, cancellationToken).ConfigureAwait(false);
130+
await ExecuteCommandWorkerAsync(document, service, position, cancellationTokenSource).ConfigureAwait(false);
145131
}
146132
catch (OperationCanceledException)
147133
{
@@ -155,7 +141,7 @@ private async Task ExecuteCommandWorkerAsync(
155141
Document document,
156142
TLanguageService service,
157143
int position,
158-
CancellationToken cancellationToken)
144+
CancellationTokenSource cancellationTokenSource)
159145
{
160146
// Switch to the BG immediately so we can keep as much work off the UI thread.
161147
await TaskScheduler.Default;
@@ -173,6 +159,7 @@ private async Task ExecuteCommandWorkerAsync(
173159
// IStreamingFindUsagesPresenter.
174160
var findContext = new BufferedFindUsagesContext();
175161

162+
var cancellationToken = cancellationTokenSource.Token;
176163
var delayBeforeShowingResultsWindowTask = DelayAsync(cancellationToken);
177164
var findTask = FindResultsAsync(findContext, document, service, position, cancellationToken);
178165

@@ -203,7 +190,7 @@ await _streamingPresenter.TryPresentLocationOrNavigateIfOneAsync(
203190
// We either got no results, or 1.5 has passed and we didn't figure out the symbols to navigate to or
204191
// present. So pop up the presenter to show the user that we're involved in a longer search, without
205192
// blocking them.
206-
await PresentResultsInStreamingPresenterAsync(document, findContext, findTask, cancellationToken).ConfigureAwait(false);
193+
await PresentResultsInStreamingPresenterAsync(document, findContext, findTask, cancellationTokenSource).ConfigureAwait(false);
207194
}
208195

209196
private Task DelayAsync(CancellationToken cancellationToken)
@@ -213,7 +200,7 @@ private Task DelayAsync(CancellationToken cancellationToken)
213200
return delayHook(cancellationToken);
214201
}
215202

216-
// If we want to navigate to a single result if it is found quickly, then delay showing the find-refs winfor
203+
// If we want to navigate to a single result if it is found quickly, then delay showing the find-refs window
217204
// for 1.5 seconds to see if a result comes in by then. If we're not navigating and are always showing the
218205
// far window, then don't have any delay showing the window.
219206
var delay = this.NavigateToSingleResultIfQuick
@@ -227,8 +214,9 @@ private async Task PresentResultsInStreamingPresenterAsync(
227214
Document document,
228215
BufferedFindUsagesContext findContext,
229216
Task findTask,
230-
CancellationToken cancellationToken)
217+
CancellationTokenSource cancellationTokenSource)
231218
{
219+
var cancellationToken = cancellationTokenSource.Token;
232220
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
233221
var (presenterContext, presenterCancellationToken) = _streamingPresenter.StartSearch(DisplayName, GetStreamingPresenterOptions(document));
234222

@@ -244,7 +232,7 @@ private async Task PresentResultsInStreamingPresenterAsync(
244232
// Hook up the presenter's cancellation token to our overall governing cancellation token. In other
245233
// words, if something else decides to present in the presenter (like a find-refs call) we'll hear about
246234
// that and can cancel all our work.
247-
presenterCancellationToken.Register(() => _cancellationSeries.CreateNext());
235+
presenterCancellationToken.Register(() => cancellationTokenSource.Cancel());
248236

249237
// now actually wait for the find work to be done.
250238
await findTask.ConfigureAwait(false);
@@ -291,9 +279,9 @@ internal TestAccessor GetTestAccessor()
291279

292280
internal readonly struct TestAccessor
293281
{
294-
private readonly AbstractGoOrFindCommandHandler<TLanguageService, TCommandArgs> _instance;
282+
private readonly AbstractGoOrFindNavigationService<TLanguageService> _instance;
295283

296-
internal TestAccessor(AbstractGoOrFindCommandHandler<TLanguageService, TCommandArgs> instance)
284+
internal TestAccessor(AbstractGoOrFindNavigationService<TLanguageService> instance)
297285
=> _instance = instance;
298286

299287
internal ref Func<CancellationToken, Task>? DelayHook
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.ComponentModel.Composition;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Editor;
8+
using Microsoft.CodeAnalysis.GoOrFind;
9+
using Microsoft.VisualStudio.Commanding;
10+
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
11+
using Microsoft.VisualStudio.Utilities;
12+
13+
namespace Microsoft.CodeAnalysis.FindReferences;
14+
15+
[Export(typeof(ICommandHandler))]
16+
[ContentType(ContentTypeNames.RoslynContentType)]
17+
[Name(PredefinedCommandHandlerNames.FindReferences)]
18+
[method: ImportingConstructor]
19+
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
20+
internal sealed class FindReferencesCommandHandler(FindReferencesNavigationService navigationService)
21+
: AbstractGoOrFindCommandHandler<FindReferencesCommandArgs>(navigationService);

src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs renamed to src/EditorFeatures/Core/GoOrFind/FindReferences/FindReferencesNavigationService.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,24 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Threading;
88
using System.Threading.Tasks;
9-
using Microsoft.CodeAnalysis.Editor;
109
using Microsoft.CodeAnalysis.Editor.Host;
1110
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
1211
using Microsoft.CodeAnalysis.FindUsages;
13-
using Microsoft.CodeAnalysis.GoToDefinition;
12+
using Microsoft.CodeAnalysis.GoOrFind;
1413
using Microsoft.CodeAnalysis.Internal.Log;
1514
using Microsoft.CodeAnalysis.Options;
1615
using Microsoft.CodeAnalysis.Shared.TestHooks;
17-
using Microsoft.VisualStudio.Commanding;
18-
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
19-
using Microsoft.VisualStudio.Utilities;
2016

2117
namespace Microsoft.CodeAnalysis.FindReferences;
2218

23-
[Export(typeof(ICommandHandler))]
24-
[ContentType(ContentTypeNames.RoslynContentType)]
25-
[Name(PredefinedCommandHandlerNames.FindReferences)]
19+
[Export(typeof(FindReferencesNavigationService))]
2620
[method: ImportingConstructor]
2721
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
28-
internal sealed class FindReferencesCommandHandler(
22+
internal sealed class FindReferencesNavigationService(
2923
IThreadingContext threadingContext,
3024
IStreamingFindUsagesPresenter streamingPresenter,
3125
IAsynchronousOperationListenerProvider listenerProvider,
32-
IGlobalOptionService globalOptions) : AbstractGoOrFindCommandHandler<IFindUsagesService, FindReferencesCommandArgs>(
26+
IGlobalOptionService globalOptions) : AbstractGoOrFindNavigationService<IFindUsagesService>(
3327
threadingContext,
3428
streamingPresenter,
3529
listenerProvider.GetListener(FeatureAttribute.FindReferences),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.ComponentModel.Composition;
7+
using Microsoft.CodeAnalysis.Editor;
8+
using Microsoft.CodeAnalysis.GoOrFind;
9+
using Microsoft.CodeAnalysis.Host.Mef;
10+
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
11+
using Microsoft.VisualStudio.Utilities;
12+
using VSCommanding = Microsoft.VisualStudio.Commanding;
13+
14+
namespace Microsoft.CodeAnalysis.GoToBase;
15+
16+
[Export(typeof(VSCommanding.ICommandHandler))]
17+
[ContentType(ContentTypeNames.RoslynContentType)]
18+
[Name(PredefinedCommandHandlerNames.GoToBase)]
19+
[method: ImportingConstructor]
20+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
21+
internal sealed class GoToBaseCommandHandler(GoToBaseNavigationService navigationService)
22+
: AbstractGoOrFindCommandHandler<GoToBaseCommandArgs>(navigationService);

src/EditorFeatures/Core/GoToBase/GoToBaseCommandHandler.cs renamed to src/EditorFeatures/Core/GoOrFind/GoToBase/GoToBaseNavigationService.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,26 @@
66
using System.ComponentModel.Composition;
77
using System.Threading;
88
using System.Threading.Tasks;
9-
using Microsoft.CodeAnalysis.Editor;
109
using Microsoft.CodeAnalysis.Editor.Host;
1110
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
1211
using Microsoft.CodeAnalysis.FindUsages;
13-
using Microsoft.CodeAnalysis.GoToDefinition;
12+
using Microsoft.CodeAnalysis.GoOrFind;
1413
using Microsoft.CodeAnalysis.Host.Mef;
1514
using Microsoft.CodeAnalysis.Internal.Log;
1615
using Microsoft.CodeAnalysis.Options;
1716
using Microsoft.CodeAnalysis.Shared.TestHooks;
18-
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
19-
using Microsoft.VisualStudio.Utilities;
20-
using VSCommanding = Microsoft.VisualStudio.Commanding;
2117

2218
namespace Microsoft.CodeAnalysis.GoToBase;
2319

24-
[Export(typeof(VSCommanding.ICommandHandler))]
25-
[ContentType(ContentTypeNames.RoslynContentType)]
26-
[Name(PredefinedCommandHandlerNames.GoToBase)]
20+
[Export(typeof(GoToBaseNavigationService))]
2721
[method: ImportingConstructor]
2822
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
29-
internal sealed class GoToBaseCommandHandler(
23+
internal sealed class GoToBaseNavigationService(
3024
IThreadingContext threadingContext,
3125
IStreamingFindUsagesPresenter streamingPresenter,
3226
IAsynchronousOperationListenerProvider listenerProvider,
33-
IGlobalOptionService globalOptions) : AbstractGoOrFindCommandHandler<IGoToBaseService, GoToBaseCommandArgs>(
27+
IGlobalOptionService globalOptions)
28+
: AbstractGoOrFindNavigationService<IGoToBaseService>(
3429
threadingContext,
3530
streamingPresenter,
3631
listenerProvider.GetListener(FeatureAttribute.GoToBase),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.ComponentModel.Composition;
7+
using Microsoft.CodeAnalysis.Editor;
8+
using Microsoft.CodeAnalysis.Editor.Commanding.Commands;
9+
using Microsoft.CodeAnalysis.GoOrFind;
10+
using Microsoft.CodeAnalysis.GoToDefinition;
11+
using Microsoft.CodeAnalysis.Host.Mef;
12+
using Microsoft.VisualStudio.Commanding;
13+
using Microsoft.VisualStudio.Utilities;
14+
15+
namespace Microsoft.CodeAnalysis.GoToImplementation;
16+
17+
[Export(typeof(ICommandHandler))]
18+
[ContentType(ContentTypeNames.RoslynContentType)]
19+
[Name(PredefinedCommandHandlerNames.GoToImplementation)]
20+
[method: ImportingConstructor]
21+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
22+
internal sealed class GoToImplementationCommandHandler(GoToImplementationNavigationService navigationService)
23+
: AbstractGoOrFindCommandHandler<GoToImplementationCommandArgs>(navigationService);

0 commit comments

Comments
 (0)