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

Make change signature more async and remove UI dialogs from the service #47768

Merged
merged 21 commits into from
Dec 18, 2020

Conversation

dibarbet
Copy link
Member

@dibarbet dibarbet commented Sep 16, 2020

When debugging roslyn master, I noticed that change signature was throwing in TryCreateUpdatedSolution as it expected to be called on the UI thread. However, this code path is triggered by GetOperationsAsync which is explicitly not running on the UI thread. The AbstractChangeSignatureService currently uses the UI thread as it pops up error messages and dialog boxes in certain cases.

There are two parts to the change:

  1. Make TryCreateUpdatedSolution (and bits above it) in AbstractChangeSignatureService async. This is mostly done in
    3c9b43c, but not quite all (I should have started with 2 first but oh well).
  2. Move the error message /dialog box handling outside of the AbstractChangeSignatureService. This is done in
    5144713

Note that most of the file changes are replacing WpfFact with Fact, the meat of the changes are in the following files
AbstractChangeSignatureService.cs
ChangeSignatureCodeAction.cs
ChangeSignatureCodeActionOperation.cs
AbstractChangeSignatureCommandHandler.cs

Resolves #44198
Resolves #41121
Resolves #41783

@dibarbet dibarbet requested a review from a team as a code owner September 16, 2020 23:22
@@ -97,7 +97,7 @@ internal ChangeSignatureResult ChangeSignature(Document document, int position,
switch (context)
{
case ChangeSignatureAnalysisSucceededContext changeSignatureAnalyzedSucceedContext:
return ChangeSignatureWithContext(changeSignatureAnalyzedSucceedContext, cancellationToken);
return ChangeSignatureWithContextAsync(changeSignatureAnalyzedSucceedContext, cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the next commit, I make this async as well and move the wait up to the command handler

@@ -387,7 +385,7 @@ internal ChangeSignatureResult ChangeSignatureWithContext(ChangeSignatureAnalysi
var notificationService = context.Solution.Workspace.Services.GetRequiredService<INotificationService>();
if (!notificationService.ConfirmMessageBox(FeaturesResources.This_symbol_has_related_definitions_or_references_in_metadata_Changing_its_signature_may_result_in_build_errors_Do_you_want_to_continue, severity: NotificationSeverity.Warning))
Copy link
Member Author

@dibarbet dibarbet Sep 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is moved in the next commit to the proper locations

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM. But def a few things i'd like tweaked before i sign off. but this is sooo much better :)

@jasonmalinowski
Copy link
Member

@dibarbet: I think this will close these bugs, right?

#44198
#41121

It looks like we may have changed the offending WaitAndGetResult to a _CanCallOnBackground but you've cleaned it up properly now.

{
return true;
}
context.OperationContext.UserCancellationToken).WaitAndGetResult(context.OperationContext.UserCancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So where in this process does the options dialog get displayed? Because now that this is async, how are we getting back to the UI thread to display the dialog? Does this need a JTF-friendly wait here as opposed to WaitAndGetResult?

Copy link
Member Author

@dibarbet dibarbet Dec 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonmalinowski see the HandleResult method that runs after this WaitAndGetResult
https://github.com/dotnet/roslyn/pull/47768/files#diff-becc7ba48505ea7db15e854f845d4702e0d44964ec9a05d20b65c26572049596R86
The service just returns the failure kind and doesn't do any UI thread operations, so WaitAndGetResult is fine. Then when it completes since we're already on the UI thread, HandleResult can just display the notification without any switching.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to add to that - the preview window is also part of HandleResult. If the result has preview changes then it will use the preview service to display them from the UI thread.

@dibarbet
Copy link
Member Author

dibarbet commented Dec 8, 2020

@dibarbet: I think this will close these bugs, right?

#44198
#41121

It looks like we may have changed the offending WaitAndGetResult to a _CanCallOnBackground but you've cleaned it up properly now.

Yes it will! Updated the PR description.

@allisonchou
Copy link
Contributor

@dibarbet I think this may also resolve #41783

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, or at least much better than what was there. I can't say I care for the pattern where the GetOptions() call runs even if the analysis fails and returns null, and then the actual refactoring operation runs even if options are null and then returns a different failure object. It seems like we're having to propogate failures when instead we could have stopped earlier and not had to have code dealing with failure modes. I'm happy for that to be addressed in a follow-up PR though (or not, if it makes a mess of something else) since this is a vast improvement for what's there.

@@ -54,7 +54,7 @@ class C
Using testState = ChangeSignatureTestState.Create(workspace)
Dim history = testState.Workspace.GetService(Of ITextUndoHistoryRegistry)().RegisterHistory(testState.Workspace.Documents.First().GetTextBuffer())
testState.TestChangeSignatureOptionsService.UpdatedSignature = permutation
Dim result = testState.ChangeSignature()
Dim result = Await testState.ChangeSignatureAsync().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.

The ConfigureAwait() shouldn't be required in tests -- did the analyzer run here?

@@ -248,10 +248,10 @@ void M()

using var testState = ChangeSignatureTestState.Create(XElement.Parse(workspaceXml));
testState.TestChangeSignatureOptionsService.UpdatedSignature = updatedSignature;
var result = testState.ChangeSignature();
var result = await testState.ChangeSignatureAsync().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.

You don't need the ConfigureAwait(false) in tests. If the analyzer was flagging this, that's a bug.

return true;
}
restrictToDeclarations: false,
cancellationToken).WaitAndGetResult(context.OperationContext.UserCancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cancellationToken).WaitAndGetResult(context.OperationContext.UserCancellationToken);
cancellationToken).WaitAndGetResult(cancellationToken);

Just to be consistent with which cancellation token is in use. Not that there aren't ways to use different ones, but those are tricky.


var finalSolution = result.UpdatedSolution;
// UI thread bound operation to show the change signature dialog.
var changeSignatureOptions = AbstractChangeSignatureService.GetChangeSignatureOptions(changeSignatureContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be null checked if the cancel button is clicked?

Comment on lines +198 to +201
if (context is not ChangeSignatureAnalysisSucceededContext succeededContext)
{
return null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit strange that the callers don't just check for the failure and immediately abort.

@@ -81,39 +79,14 @@ internal abstract class AbstractChangeSignatureService : ILanguageService

public async Task<ImmutableArray<ChangeSignatureCodeAction>> GetChangeSignatureCodeActionAsync(Document document, TextSpan span, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit I'm surprised this function is here versus just being inlined into the actual code fix provider. Not urgent to fix though.

@@ -248,23 +230,21 @@ internal ChangeSignatureResult ChangeSignatureWithContext(ChangeSignatureAnalysi

#nullable enable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything left in this file that isn't #nullable enabled or isn't easily updated?

@@ -26,19 +29,19 @@ public ChangeSignatureCodeAction(AbstractChangeSignatureService changeSignatureS
public override object? GetOptions(CancellationToken cancellationToken)
=> AbstractChangeSignatureService.GetChangeSignatureOptions(_context);

protected override Task<IEnumerable<CodeActionOperation>> ComputeOperationsAsync(object options, CancellationToken cancellationToken)
protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperationsAsync(object options, CancellationToken cancellationToken)
{
if (options is ChangeSignatureOptionsResult changeSignatureOptions && changeSignatureOptions != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (options is ChangeSignatureOptionsResult changeSignatureOptions && changeSignatureOptions != null)
if (options is ChangeSignatureOptionsResult changeSignatureOptions)

If the first test is true, it can't be null.

@dibarbet
Copy link
Member Author

merging, will address small feedback in followup

@dibarbet dibarbet merged commit e03e7a4 into dotnet:master Dec 18, 2020
@ghost ghost added this to the Next milestone Dec 18, 2020
@dibarbet dibarbet deleted the async_change_sig branch December 18, 2020 00:08
@dibarbet dibarbet modified the milestones: Next, 16.9.P3 Dec 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
5 participants