Skip to content

Commit 40a85df

Browse files
Unify command handler code
1 parent 8410079 commit 40a85df

File tree

7 files changed

+92
-154
lines changed

7 files changed

+92
-154
lines changed

src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs

Lines changed: 29 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@
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-
#nullable disable
6-
7-
using System;
85
using System.ComponentModel.Composition;
96
using System.Diagnostics.CodeAnalysis;
7+
using System.Threading;
108
using System.Threading.Tasks;
11-
using Microsoft.CodeAnalysis.Classification;
129
using Microsoft.CodeAnalysis.Editor;
1310
using Microsoft.CodeAnalysis.Editor.Host;
14-
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
15-
using Microsoft.CodeAnalysis.ErrorReporting;
11+
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
1612
using Microsoft.CodeAnalysis.FindUsages;
1713
using Microsoft.CodeAnalysis.GoToDefinition;
1814
using Microsoft.CodeAnalysis.Internal.Log;
1915
using Microsoft.CodeAnalysis.Options;
20-
using Microsoft.CodeAnalysis.Shared.Extensions;
2116
using Microsoft.CodeAnalysis.Shared.TestHooks;
22-
using Microsoft.CodeAnalysis.Text;
2317
using Microsoft.VisualStudio.Commanding;
24-
using Microsoft.VisualStudio.Text;
2518
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
2619
using Microsoft.VisualStudio.Utilities;
2720

@@ -30,134 +23,37 @@ namespace Microsoft.CodeAnalysis.FindReferences;
3023
[Export(typeof(ICommandHandler))]
3124
[ContentType(ContentTypeNames.RoslynContentType)]
3225
[Name(PredefinedCommandHandlerNames.FindReferences)]
33-
internal sealed class FindReferencesCommandHandler : AbstractGoToCommandHandler<
34-
IFindUsagesService, FindReferencesCommandArgs>
26+
[method: ImportingConstructor]
27+
[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(
29+
IThreadingContext threadingContext,
30+
IStreamingFindUsagesPresenter streamingPresenter,
31+
IAsynchronousOperationListenerProvider listenerProvider,
32+
IGlobalOptionService globalOptions) : AbstractGoOrFindCommandHandler<IFindUsagesService, FindReferencesCommandArgs>(
33+
threadingContext,
34+
streamingPresenter,
35+
listenerProvider.GetListener(FeatureAttribute.FindReferences),
36+
globalOptions)
3537
{
36-
#if false
37-
private readonly IStreamingFindUsagesPresenter _streamingPresenter;
38-
private readonly IGlobalOptionService _globalOptions;
39-
private readonly IAsynchronousOperationListener _asyncListener;
40-
41-
public string DisplayName => EditorFeaturesResources.Find_References;
42-
43-
[ImportingConstructor]
44-
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
45-
public FindReferencesCommandHandler(
46-
IStreamingFindUsagesPresenter streamingPresenter,
47-
IGlobalOptionService globalOptions,
48-
IAsynchronousOperationListenerProvider listenerProvider)
49-
{
50-
Contract.ThrowIfNull(listenerProvider);
51-
52-
_streamingPresenter = streamingPresenter;
53-
_globalOptions = globalOptions;
54-
_asyncListener = listenerProvider.GetListener(FeatureAttribute.FindReferences);
55-
}
56-
57-
public CommandState GetCommandState(FindReferencesCommandArgs args)
58-
{
59-
var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot);
60-
return service != null
61-
? CommandState.Available
62-
: CommandState.Unspecified;
63-
}
64-
65-
public bool ExecuteCommand(FindReferencesCommandArgs args, CommandExecutionContext context)
66-
{
67-
var subjectBuffer = args.SubjectBuffer;
68-
69-
// Get the selection that user has in our buffer (this also works if there
70-
// is no selection and the caret is just at a single position). If we
71-
// can't get the selection, or there are multiple spans for it (i.e. a
72-
// box selection), then don't do anything.
73-
var snapshotSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer);
74-
if (snapshotSpans.Count == 1)
75-
{
76-
var selectedSpan = snapshotSpans[0];
77-
var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot);
78-
if (document != null)
79-
{
80-
// Do a find-refs at the *start* of the selection. That way if the
81-
// user has selected a symbol that has another symbol touching it
82-
// on the right (i.e. Goo++ ), then we'll do the find-refs on the
83-
// symbol selected, not the symbol following.
84-
if (TryExecuteCommand(selectedSpan.Start, document, service))
85-
{
86-
return true;
87-
}
88-
}
89-
}
90-
91-
return false;
92-
}
38+
public override string DisplayName => EditorFeaturesResources.Find_References;
9339

94-
private static (Document, IFindUsagesService) GetDocumentAndService(ITextSnapshot snapshot)
95-
{
96-
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
97-
return (document, document?.GetLanguageService<IFindUsagesService>());
98-
}
99-
100-
private bool TryExecuteCommand(int caretPosition, Document document, IFindUsagesService findUsagesService)
101-
{
102-
// See if we're running on a host that can provide streaming results.
103-
// We'll both need a FAR service that can stream results to us, and
104-
// a presenter that can accept streamed results.
105-
if (findUsagesService != null && _streamingPresenter != null)
106-
{
107-
// kick this work off in a fire and forget fashion. Importantly, this means we do
108-
// not pass in any ambient cancellation information as the execution of this command
109-
// will complete and will have no bearing on the computation of the references we compute.
110-
_ = StreamingFindReferencesAsync(document, caretPosition, findUsagesService, _streamingPresenter);
111-
return true;
112-
}
40+
protected override FunctionId FunctionId => FunctionId.CommandHandler_FindAllReference;
11341

114-
return false;
115-
}
42+
/// <summary>
43+
/// For find-refs, we *always* use the window. Even if there is only a single result. This is not a 'go' command
44+
/// which imperatively tries to navigate to the location if possible. The intent here is to keep the results in view
45+
/// so that the user can always refer to them, even as they do other work.
46+
/// </summary>
47+
protected override bool NavigateToSingleResultIfQuick => false;
11648

117-
private async Task StreamingFindReferencesAsync(
118-
Document document,
119-
int caretPosition,
120-
IFindUsagesService findUsagesService,
121-
IStreamingFindUsagesPresenter presenter)
122-
{
123-
try
49+
protected override StreamingFindUsagesPresenterOptions GetStreamingPresenterOptions(Document document)
50+
=> new()
12451
{
125-
using var token = _asyncListener.BeginAsyncOperation(nameof(StreamingFindReferencesAsync));
126-
var classificationOptions = _globalOptions.GetClassificationOptionsProvider();
52+
SupportsReferences = true,
53+
IncludeContainingTypeAndMemberColumns = document.Project.SupportsCompilation,
54+
IncludeKindColumn = document.Project.Language != LanguageNames.FSharp
55+
};
12756

128-
// Let the presented know we're starting a search. It will give us back the context object that the FAR
129-
// service will push results into. This operation is not externally cancellable. Instead, the find refs
130-
// window will cancel it if another request is made to use it.
131-
var (context, cancellationToken) = presenter.StartSearch(
132-
EditorFeaturesResources.Find_References,
133-
new StreamingFindUsagesPresenterOptions()
134-
{
135-
SupportsReferences = true,
136-
IncludeContainingTypeAndMemberColumns = document.Project.SupportsCompilation,
137-
IncludeKindColumn = document.Project.Language != LanguageNames.FSharp
138-
});
139-
140-
using (Logger.LogBlock(
141-
FunctionId.CommandHandler_FindAllReference,
142-
KeyValueLogMessage.Create(LogType.UserAction, static m => m["type"] = "streaming"),
143-
cancellationToken))
144-
{
145-
try
146-
{
147-
await findUsagesService.FindReferencesAsync(context, document, caretPosition, classificationOptions, cancellationToken).ConfigureAwait(false);
148-
}
149-
finally
150-
{
151-
await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false);
152-
}
153-
}
154-
}
155-
catch (OperationCanceledException)
156-
{
157-
}
158-
catch (Exception e) when (FatalError.ReportAndCatch(e))
159-
{
160-
}
161-
}
162-
#endif
57+
protected override Task FindActionAsync(IFindUsagesContext context, Document document, IFindUsagesService service, int caretPosition, CancellationToken cancellationToken)
58+
=> service.FindReferencesAsync(context, document, caretPosition, this.ClassificationOptionsProvider, cancellationToken);
16359
}

src/EditorFeatures/Core/FindUsages/BufferedFindUsagesContext.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using Microsoft.CodeAnalysis.Notification;
11+
using Microsoft.CodeAnalysis.Shared.Extensions;
1112
using Microsoft.CodeAnalysis.Shared.Utilities;
1213
using Roslyn.Utilities;
1314

@@ -28,6 +29,7 @@ private sealed class State
2829
public string? InformationalMessage;
2930
public string? SearchTitle;
3031
public ImmutableArray<DefinitionItem>.Builder Definitions = ImmutableArray.CreateBuilder<DefinitionItem>();
32+
public ImmutableArray<SourceReferenceItem>.Builder References = ImmutableArray.CreateBuilder<SourceReferenceItem>();
3133
}
3234

3335
/// <summary>
@@ -108,6 +110,8 @@ public async Task AttachToStreamingPresenterAsync(IFindUsagesContext presenterCo
108110
foreach (var definition in _state.Definitions)
109111
await presenterContext.OnDefinitionFoundAsync(definition, cancellationToken).ConfigureAwait(false);
110112

113+
await presenterContext.OnReferencesFoundAsync(_state.References.AsAsyncEnumerable(), cancellationToken).ConfigureAwait(false);
114+
111115
// Now swap over to the presenter being the sink for all future callbacks, and clear any buffered data.
112116
_streamingPresenterContext = presenterContext;
113117
_state = null;
@@ -199,11 +203,18 @@ async ValueTask IFindUsagesContext.OnDefinitionFoundAsync(DefinitionItem definit
199203
}
200204
}
201205

202-
ValueTask IFindUsagesContext.OnReferencesFoundAsync(IAsyncEnumerable<SourceReferenceItem> references, CancellationToken cancellationToken)
206+
async ValueTask IFindUsagesContext.OnReferencesFoundAsync(IAsyncEnumerable<SourceReferenceItem> references, CancellationToken cancellationToken)
203207
{
204-
// Entirely ignored. These features do not show references.
205-
Contract.Fail("GoToImpl/Base should never report a reference.");
206-
return ValueTaskFactory.CompletedTask;
208+
using var _ = await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false);
209+
if (IsSwapped)
210+
{
211+
await _streamingPresenterContext.OnReferencesFoundAsync(references, cancellationToken).ConfigureAwait(false);
212+
}
213+
else
214+
{
215+
await foreach (var reference in references)
216+
_state.References.Add(reference);
217+
}
207218
}
208219

209220
#endregion

src/EditorFeatures/Core/GoToBase/GoToBaseCommandHandler.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal sealed class GoToBaseCommandHandler(
3030
IThreadingContext threadingContext,
3131
IStreamingFindUsagesPresenter streamingPresenter,
3232
IAsynchronousOperationListenerProvider listenerProvider,
33-
IGlobalOptionService globalOptions) : AbstractGoToCommandHandler<IGoToBaseService, GoToBaseCommandArgs>(
33+
IGlobalOptionService globalOptions) : AbstractGoOrFindCommandHandler<IGoToBaseService, GoToBaseCommandArgs>(
3434
threadingContext,
3535
streamingPresenter,
3636
listenerProvider.GetListener(FeatureAttribute.GoToBase),
@@ -40,6 +40,12 @@ internal sealed class GoToBaseCommandHandler(
4040

4141
protected override FunctionId FunctionId => FunctionId.CommandHandler_GoToBase;
4242

43+
/// <summary>
44+
/// If we find a single results quickly enough, we do want to take the user directly to it,
45+
/// instead of popping up the FAR window to show it.
46+
/// </summary>
47+
protected override bool NavigateToSingleResultIfQuick => true;
48+
4349
protected override Task FindActionAsync(IFindUsagesContext context, Document document, IGoToBaseService service, int caretPosition, CancellationToken cancellationToken)
4450
=> service.FindBasesAsync(context, document, caretPosition, ClassificationOptionsProvider, cancellationToken);
4551
}

src/EditorFeatures/Core/GoToDefinition/AbstractGoToCommandHandler`2.cs renamed to src/EditorFeatures/Core/GoToDefinition/AbstractGoOrFindCommandHandler.cs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
using Microsoft.CodeAnalysis.Classification;
99
using Microsoft.CodeAnalysis.Editor.Host;
1010
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
11-
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
1211
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
13-
using Microsoft.CodeAnalysis.Editor.Tagging;
1412
using Microsoft.CodeAnalysis.ErrorReporting;
1513
using Microsoft.CodeAnalysis.FindUsages;
1614
using Microsoft.CodeAnalysis.Host;
@@ -28,7 +26,7 @@
2826

2927
namespace Microsoft.CodeAnalysis.GoToDefinition;
3028

31-
internal abstract class AbstractGoToCommandHandler<TLanguageService, TCommandArgs>(
29+
internal abstract class AbstractGoOrFindCommandHandler<TLanguageService, TCommandArgs>(
3230
IThreadingContext threadingContext,
3331
IStreamingFindUsagesPresenter streamingPresenter,
3432
IAsynchronousOperationListener listener,
@@ -69,8 +67,17 @@ internal abstract class AbstractGoToCommandHandler<TLanguageService, TCommandArg
6967
private Func<CancellationToken, Task>? _delayHook;
7068

7169
public abstract string DisplayName { get; }
70+
7271
protected abstract FunctionId FunctionId { get; }
7372

73+
/// <summary>
74+
/// If we should try to navigate to the sole item found, if that item was found within 1.5seconds.
75+
/// </summary>
76+
protected abstract bool NavigateToSingleResultIfQuick { get; }
77+
78+
protected virtual StreamingFindUsagesPresenterOptions GetStreamingPresenterOptions(Document document)
79+
=> StreamingFindUsagesPresenterOptions.Default;
80+
7481
protected abstract Task FindActionAsync(IFindUsagesContext context, Document document, TLanguageService service, int caretPosition, CancellationToken cancellationToken);
7582

7683
private static (Document?, TLanguageService?) GetDocumentAndService(ITextSnapshot snapshot)
@@ -166,16 +173,16 @@ private async Task ExecuteCommandWorkerAsync(
166173
// IStreamingFindUsagesPresenter.
167174
var findContext = new BufferedFindUsagesContext();
168175

169-
var delayTask = DelayAsync(cancellationToken);
176+
var delayBeforeShowingResultsWindowTask = DelayAsync(cancellationToken);
170177
var findTask = FindResultsAsync(findContext, document, service, position, cancellationToken);
171178

172-
var firstFinishedTask = await Task.WhenAny(delayTask, findTask).ConfigureAwait(false);
179+
var firstFinishedTask = await Task.WhenAny(delayBeforeShowingResultsWindowTask, findTask).ConfigureAwait(false);
173180
if (cancellationToken.IsCancellationRequested)
174181
// we bailed out because another command was issued. Immediately stop everything we're doing and return
175182
// back so the next operation can run.
176183
return;
177184

178-
if (firstFinishedTask == findTask)
185+
if (this.NavigateToSingleResultIfQuick && firstFinishedTask == findTask)
179186
{
180187
// We completed the search within 1.5 seconds. If we had at least one result then Navigate to it directly
181188
// (if there is just one) or present them all if there are many.
@@ -196,7 +203,7 @@ await _streamingPresenter.TryPresentLocationOrNavigateIfOneAsync(
196203
// We either got no results, or 1.5 has passed and we didn't figure out the symbols to navigate to or
197204
// present. So pop up the presenter to show the user that we're involved in a longer search, without
198205
// blocking them.
199-
await PresentResultsInStreamingPresenterAsync(findContext, findTask, cancellationToken).ConfigureAwait(false);
206+
await PresentResultsInStreamingPresenterAsync(document, findContext, findTask, cancellationToken).ConfigureAwait(false);
200207
}
201208

202209
private Task DelayAsync(CancellationToken cancellationToken)
@@ -206,16 +213,24 @@ private Task DelayAsync(CancellationToken cancellationToken)
206213
return delayHook(cancellationToken);
207214
}
208215

209-
return Task.Delay(TaggerDelay.OnIdle.ComputeTimeDelay(), cancellationToken);
216+
// If we want to navigate to a single result if it is found quickly, then delay showing the find-refs winfor
217+
// for 1.5 seconds to see if a result comes in by then. If we're not navigating and are always showing the
218+
// far window, then don't have any delay showing the window.
219+
var delay = this.NavigateToSingleResultIfQuick
220+
? DelayTimeSpan.Idle
221+
: TimeSpan.Zero;
222+
223+
return Task.Delay(delay, cancellationToken);
210224
}
211225

212226
private async Task PresentResultsInStreamingPresenterAsync(
227+
Document document,
213228
BufferedFindUsagesContext findContext,
214229
Task findTask,
215230
CancellationToken cancellationToken)
216231
{
217232
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
218-
var (presenterContext, presenterCancellationToken) = _streamingPresenter.StartSearch(DisplayName, StreamingFindUsagesPresenterOptions.Default);
233+
var (presenterContext, presenterCancellationToken) = _streamingPresenter.StartSearch(DisplayName, GetStreamingPresenterOptions(document));
219234

220235
try
221236
{
@@ -276,9 +291,9 @@ internal TestAccessor GetTestAccessor()
276291

277292
internal readonly struct TestAccessor
278293
{
279-
private readonly AbstractGoToCommandHandler<TLanguageService, TCommandArgs> _instance;
294+
private readonly AbstractGoOrFindCommandHandler<TLanguageService, TCommandArgs> _instance;
280295

281-
internal TestAccessor(AbstractGoToCommandHandler<TLanguageService, TCommandArgs> instance)
296+
internal TestAccessor(AbstractGoOrFindCommandHandler<TLanguageService, TCommandArgs> instance)
282297
=> _instance = instance;
283298

284299
internal ref Func<CancellationToken, Task>? DelayHook

src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal sealed class GoToImplementationCommandHandler(
3030
IThreadingContext threadingContext,
3131
IStreamingFindUsagesPresenter streamingPresenter,
3232
IAsynchronousOperationListenerProvider listenerProvider,
33-
IGlobalOptionService globalOptions) : AbstractGoToCommandHandler<IFindUsagesService, GoToImplementationCommandArgs>(
33+
IGlobalOptionService globalOptions) : AbstractGoOrFindCommandHandler<IFindUsagesService, GoToImplementationCommandArgs>(
3434
threadingContext,
3535
streamingPresenter,
3636
listenerProvider.GetListener(FeatureAttribute.GoToImplementation),
@@ -40,6 +40,12 @@ internal sealed class GoToImplementationCommandHandler(
4040

4141
protected override FunctionId FunctionId => FunctionId.CommandHandler_GoToImplementation;
4242

43+
/// <summary>
44+
/// If we find a single results quickly enough, we do want to take the user directly to it,
45+
/// instead of popping up the FAR window to show it.
46+
/// </summary>
47+
protected override bool NavigateToSingleResultIfQuick => true;
48+
4349
protected override Task FindActionAsync(IFindUsagesContext context, Document document, IFindUsagesService service, int caretPosition, CancellationToken cancellationToken)
4450
=> service.FindImplementationsAsync(context, document, caretPosition, ClassificationOptionsProvider, cancellationToken);
4551
}

0 commit comments

Comments
 (0)