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

Switch go-to-impl to be entirely async/non-blocking. #57861

Merged
merged 46 commits into from
Nov 22, 2021

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Nov 18, 2021

Fixes AB#1419331
Fixes #41374

The prior approach to go-to-impl was:

  1. get the command from teh editor (already under a threaded wait dialog 'TWD').
  2. perform the search in a blocking (but cancellable) fashion while the TWD was up.
  3. once the results were found, either:
    1. pop up message if no results found
    2. navigate to the result if there was only one result found
    3. show find-refs-window if multiple results were found

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.

  1. Immediately return from the editor command, so no TWD is present
  2. Kick off the work to find results asynchronously.
  3. If the results come back in less than 1.5 seconds (delay at which TWD normally appears, and operations are considered to be 'long running') then do the same as the prior approach and either navigate, or present.
  4. If the results are taking more than 1.5 seconds, then unilaterally pop up the find-refs-window to indicate that the search is ongoing. From taht point on, no matter what hte search results (0, 1 or multiple), the results are put into the window. This way we do not interfere with typing/navigation by popping up dialogs or navigating the user.

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:

  1. if we find only a single result
  2. and we find that single result in <1.5 seconds
  3. then we navigate to that item immediately

--

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:

image

This follows a similar message that comes up in nav-to.

@@ -2,132 +2,263 @@
// The .NET Foundation licenses this file to you under the MIT license.
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 type is substantially rewritten. i'll try to comment what is going on for clarity.

@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner November 18, 2021 19:21
await context.SetSearchTitleAsync(
string.Format(EditorFeaturesResources._0_implementations,
FindUsagesHelpers.GetDisplayName(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.

moved this to be first so that the title of the FAR window properly says "IFoo" implementations even if we end up finding nothing.

…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;
Copy link
Member Author

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.");
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 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.

@jasonmalinowski
Copy link
Member

jasonmalinowski commented Nov 19, 2021

@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:

  1. Pop-up the tool window, always.
  2. If the result populates with a single result, and I hadn't clicked anywhere else or moved the caret or something, then close it and navigate if there's a single hit.

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....)

@CyrusNajmabadi
Copy link
Member Author

@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

@CyrusNajmabadi
Copy link
Member Author

Pop-up the tool window, always.

Also, 8 really don't like that. It feels like a unfortunate ui for a case that will happen a lot.

@davidwengier
Copy link
Contributor

The Background work indicator would be a good fit here.

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Nov 19, 2021

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.

Copy link
Member

@dibarbet dibarbet left a 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
Copy link
Member

@dibarbet dibarbet Nov 19, 2021

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

Copy link
Member Author

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);
Copy link
Member

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

Copy link
Member Author

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);
Copy link
Member

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

Copy link
Member Author

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))
Copy link
Member

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

Copy link
Member Author

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

…ingFindUsagesPresenter.cs

Co-authored-by: Marius Ungureanu <[email protected]>
@CyrusNajmabadi
Copy link
Member Author

Tagging @davidwengier


var cancellationToken = cancellationTokenSource.Token;
var delayTask = Task.Delay(TaggerDelay.OnIdle.ComputeTimeDelay(), cancellationToken);
var findTask = Task.Run(() => FindResultsAsync(document, position, findContext, cancellationToken), cancellationToken);
Copy link
Contributor

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?

Copy link
Member Author

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 ||
Copy link
Contributor

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?

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 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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"Find All References"/"Go to implementation" blocks editing for a significant amount of time
6 participants