-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Switch go-to-impl to be entirely async/non-blocking. #57861
Conversation
@@ -2,132 +2,263 @@ | |||
// The .NET Foundation licenses this file to you under the MIT license. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this type is substantially rewritten. i'll try to comment what is going on for clarity.
await context.SetSearchTitleAsync( | ||
string.Format(EditorFeaturesResources._0_implementations, | ||
FindUsagesHelpers.GetDisplayName(symbol)), | ||
cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved this to be first so that the title of the FAR window properly says "IFoo" implementations
even if we end up finding nothing.
.../Core/Def/Implementation/FindReferences/Contexts/AbstractTableDataSourceFindUsagesContext.cs
Outdated
Show resolved
Hide resolved
…ts/AbstractTableDataSourceFindUsagesContext.cs
/// <summary> | ||
/// Message we show if we find no definitions. Consumers of the streaming presenter can set their own title. | ||
/// </summary> | ||
protected string NoDefinitionsFoundMessage = ServicesVSResources.Search_found_no_results; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we used to hardcode this. now we just support the contract we always had before (but this type ignored).
@@ -414,7 +419,14 @@ protected RoslynDefinitionBucket GetOrCreateDefinitionBucket(DefinitionItem defi | |||
} | |||
|
|||
public sealed override ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken) | |||
=> throw new InvalidOperationException("This should never be called in the streaming case."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can now be called in the streaming case. in streaming-go-to-impl, we may end up finding no results. so we want to show that message to the user in the FAR window. We could just use the default string, but since hte API allows the caller to specify the message to show hte user, we should respect it.
@CyrusNajmabadi As far as the experience goes, do we have a way to give the user some indication we're doing something before the 1.5 second cutoff? My worry is if I hit the key and then still see my cursor flashing, I'm going to think that I didn't do anything in the first place. Wondering if there's a potential alternate approach which is something more like:
That way there's always visual indication we're doing something, and the I'm not paying a behavior change penalty if my search happened to take 1.6 seconds. (I won't pretend that condition of the second step will be easy to implement....) |
@jasonmalinowski I don't think we need the indicator. There's already this amount of time until the twd would normally appear. So both before and after there's a small delay with nothing happening before you get the UI element which let's you know that long running work is happening |
Also, 8 really don't like that. It feels like a unfortunate ui for a case that will happen a lot. |
The Background work indicator would be a good fit here. |
Note: to be clear, there is an indication that background work is being done. You get the same indication that you get with find-refs. Namely a progress indicator in the FAR window :-) We coudl still decide to show a BackgroundWorkIndicator immediately if we thought that would help. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would +1 @davidwengier suggestion to use that UI once available (for the pre-1.5s time)
/// if the user kicks off another actual go-to-impl command. In that case, we just attempt to cancel the prior | ||
/// command (if it is still running), then wait for it to complete, then run our command. The second is if we have | ||
/// switched over to the streaming presenter and then the user starts some other command (like FAR) that takes over | ||
/// the presenter. In that case, the presenter will notify us that it has be re-purposed and we will also cancel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
once we've computed the items and put them in the presenter, why is there anything that needs to be cancelled here if another presenter takes over? That seems a bit strange - it seems like the other presenter should just take over, and the existing one is 'disposed' of
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, there's only one FAR window (unless you select 'keep results' in the window). So once another feature wants to push results into that window we want to stop what we're doing as there's no point anymore.
var findContext = new BufferedFindUsagesContext(); | ||
|
||
var cancellationToken = cancellationTokenSource.Token; | ||
var delayTask = Task.Delay(TaggerDelay.OnIdle.ComputeTimeDelay(), cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm I think I'd prefer to not use the tagger delay since this doesnt really have anything to do with tagging. I don't think we'd want this changed if we changed the idle tagger delay for tagging reasons
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so, 'TaggerDelay' is poorly named. It should really be called something like 'PerceptionConstants'. These are values provided form research from teh platform/UX teams about how people perceive delays. i.e. what a timespan needs to be to be considered with the 'instantaneous' threshold for people, etc.
This timespan (which we use in many places outside of tagging) is the: enough time has passed for me to start realizing it's going to take a while to get results, so transition the UI to let hte user know.
{ | ||
var cancellationToken = cancellationTokenSource.Token; | ||
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); | ||
var (presenterContext, presenterCancellationToken) = _streamingPresenter.StartSearch(this.DisplayName, supportsReferences: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this could be made cleaner by allowing the IStreamingFindReferencesPresenter to take in an optional find usages context that it uses. Then it seems like you wouldn't need all the complexity in BufferedFindUsagesContext to switch between the presenters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't work. The FindContext is what features push into. The window itself (which is the context actually), needs to pull the already computed values.
|
||
public async Task<string?> GetMessageAsync(CancellationToken cancellationToken) | ||
{ | ||
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like all these could be simple using statements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I can change that
src/EditorFeatures/Core/CommandHandlers/BufferedFindUsagesContext.cs
Outdated
Show resolved
Hide resolved
src/VisualStudio/Core/Def/Implementation/FindReferences/StreamingFindUsagesPresenter.cs
Show resolved
Hide resolved
e492eb2
to
703bb7e
Compare
src/VisualStudio/Core/Def/Implementation/FindReferences/StreamingFindUsagesPresenter.cs
Outdated
Show resolved
Hide resolved
…ingFindUsagesPresenter.cs Co-authored-by: Marius Ungureanu <[email protected]>
Tagging @davidwengier |
|
||
var cancellationToken = cancellationTokenSource.Token; | ||
var delayTask = Task.Delay(TaggerDelay.OnIdle.ComputeTimeDelay(), cancellationToken); | ||
var findTask = Task.Run(() => FindResultsAsync(document, position, findContext, cancellationToken), cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my curiosity: Is the Task.Run
needed here, if we're already on a background thread due to the TaskScheduler.Default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It likely isn't. i can remove in a followup :)
var guid = new Guid("a80febb4-e7e0-4147-b476-21aaf2453969"); | ||
|
||
if (_serviceProvider.GetService(typeof(SVsUIShell)) is not IVsUIShell uiShell || | ||
uiShell.FindToolWindow((uint)__VSFINDTOOLWIN.FTW_fFindFirst, ref guid, out var windowFrame) != VSConstants.S_OK || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't look like there is an FTW_fFindLast
, but does this do the right thing when Keep Results is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good question. I tried combinations of tings to see if i could break this, but it always seemed to work. So, afaict, this is ok... but i'm not sure why TBH :)
Fixes AB#1419331
Fixes #41374
The prior approach to go-to-impl was:
This approach is unfortunate as it blocks all interaction with VS while the search is going on. As finding impls can be expensive (they can be anywhere in the solution) this can mean waiting a very long time.
The new approach is as follows.
This now allows the user to continue doing whatever they want while the operations are in progress. Similar to FInd-Refs, they are not blocked in any fashion.
Another way to think about this is that we have almost the same experience as Find-Refs, except:
--
Note: this PR does not move us to a point where go-to-impl results are streamed in. That woudl be very nice, but is currently problematic as the current go-to-impl algorithm needs to find everything in order to filter out parts it may not want to show based on the full set of symbols found. We can consider revisiting that approach if it we think it would be better to just present results as we find them so that hte user can interact with them (like we do with FAR).
--
This PR also updates the feature to not block on things while the solution is loading, but instead do tings best effort. In the event that the solution is loading while hte search is performed, teh user will see:
This follows a similar message that comes up in nav-to.