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

Change navigation helpers to operate in two steps. #59798

Merged

Conversation

CyrusNajmabadi
Copy link
Member

WIth the existing form, a feature simply asks teh service to navigate directly to a final document location. This PR changes things to make this two-step (with extensions to make things simple for most clients). The first step does any expensive work and returns a final object that can be then be used to actually perform the navigation.

This will be used as part of the BackgroundWorkIndicator (BWI) work. In that change a feature (like goto def) will open the BWI, do the work to figure out where to navigate to, then tell the BWI to ignore navigation, then do the final navigation. Telling the BWI to ignore navigation is important as otherwise the navigation the feature does will trigger the BWI to think that an arbitrary navigation happened, which will cause it to cancel. If it cancels then the very navigation that we're trying to attempt will be stopped.

/// method. This method is async only to allow final clients to call this from a non-UI thread while allowing
/// the navigation to jump to the UI thread.
/// </summary>
Task<bool> NavigateToAsync(CancellationToken cancelletionToken);
Copy link
Member Author

Choose a reason for hiding this comment

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

@jasonmalinowski i started with this being sync... but it still felt oogy. i'm not sure how i feel about any particular feature needing to have to have way to move itself to the UI thread before calling this.

that said, i don't feel super strongly about this. i think there's merit in your point that thsi must be fast/sync/UI bound.

Copy link
Member Author

Choose a reason for hiding this comment

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

note: for this PR/set-of-refactorings, i'd like to have this async for the moment. then, once all the work is done, we should have a good idea if it can be safely made sync.

Copy link
Member

Choose a reason for hiding this comment

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

I'm OK with this, but let's keep an eye out for places the "this should be async only..." rule gets violated. And maybe we will still have cases where this needs to be async; I could imagine in VS even opening a file this may still have some async bits since we at least have to check if the file exists, or something.

@@ -37,19 +37,19 @@ public Task<bool> CanNavigateToLineAndOffsetAsync(Workspace workspace, DocumentI
public Task<bool> CanNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken 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.

View with whitespace off.

if (!workspace.IsDocumentOpen(documentId))
return false;

if (IsSecondaryBuffer(workspace, documentId))
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's a shame we can't do secondary buffer mapping until docs are opened.

windowFrame.Show();
}
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var openDocumentService = _serviceProvider.GetService<SVsUIShellOpenDocument, IVsUIShellOpenDocument>();
Copy link
Member Author

Choose a reason for hiding this comment

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

could potentially do this on the BG as well. but i think ti's highly unlikely that will help anything. this is MAS. we've already loaded docs, so this service is already goign to be avialable.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, seems unlikely for that to be expensive. 😄

{
// Have to be on the UI thread to open a document.
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var document = OpenDocument(workspace, documentId);
Copy link
Member Author

Choose a reason for hiding this comment

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

inlined this helper method.

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.

Seems reasonable; if I had any concern with the navigation being async in addition to the computation, it's more that there's an implicit risk there to think about that whenever that code does an await and yields that the state could have changed out from underneath it if the caller doesn't have a threaded wait dialog up or something else that may prevent user action.

Comments are mostly mop-ups, OK for that go in a follow-up PR since you have a few stacked behind this.

/// method. This method is async only to allow final clients to call this from a non-UI thread while allowing
/// the navigation to jump to the UI thread.
/// </summary>
Task<bool> NavigateToAsync(CancellationToken cancelletionToken);
Copy link
Member

Choose a reason for hiding this comment

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

I'm OK with this, but let's keep an eye out for places the "this should be async only..." rule gets violated. And maybe we will still have cases where this needs to be async; I could imagine in VS even opening a file this may still have some async bits since we at least have to check if the file exists, or something.

@@ -37,19 +37,19 @@ public Task<bool> CanNavigateToLineAndOffsetAsync(Workspace workspace, DocumentI
public Task<bool> CanNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken)
=> SpecializedTasks.False;

public async Task<bool> TryNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken)
public async Task<INavigableLocation?> GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, 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.

Suggested change
public async Task<INavigableLocation?> GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken)
public async Task<INavigableLocation?> GetNavigableLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken 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.

I actually liked the new name, since it's the DocNavigationService and it returns a NavigableLocation, i found it just fine to say GetLocation. but if you feewl strongly, i can change :)

@@ -58,35 +58,40 @@ public async Task<bool> TryNavigateToSpanAsync(Workspace workspace, DocumentId d
var textSnapshot = document?.GetTextSynchronously(cancellationToken).FindCorrespondingEditorTextSnapshot();
if (textSnapshot == null)
{
return false;
return null;
}

var snapshotSpan = new SnapshotSpan(textSnapshot, textSpan.Start, textSpan.Length);
var virtualSnapshotSpan = new VirtualSnapshotSpan(snapshotSpan);

if (!textView.TryGetSurfaceBufferSpan(virtualSnapshotSpan, out var surfaceBufferSpan))
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 bit need to move into the deferred work? Since I think if there was async stuff streaming to the interactive window, the buffers could be updated and this would be stale?

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 this case, we're technically on the UI thread the entire time. It's teh first line of GetLocationForSpanAsync. So it's less clear how much value any split has here. The split i was at least going for was having the first half be the part that detemines "is this safe to nav to?" and the second half is the actual "ok, now actually go there".

Comment on lines -223 to +239
var text = document.GetTextSynchronously(cancellationToken);
var text = await document.GetTextAsync(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.

Is the assumption here this almost always completes synchronously? Because otherwise the ConfigureAwait(false) worries me a bit because if it doesn't complete, then we're going to be waiting on the thread pool while blocking the UI when we could have just used the UI thread originally.

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... that's a good point. bleag... this is a rough area.

windowFrame.Show();
}
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var openDocumentService = _serviceProvider.GetService<SVsUIShellOpenDocument, IVsUIShellOpenDocument>();
Copy link
Member

Choose a reason for hiding this comment

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

Agreed, seems unlikely for that to be expensive. 😄

}
// We should have the file now, so navigate to the right span
return _openFiles.TryGetValue(temporaryFilePath, out var openFile) &&
await openFile.NavigateToSpanAsync(sourceSpan, 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.

This seems like a reasonable argument for your navigate method being async: at some point I'd imagine that OpenDocumentViaProject could be come async, or at least we asynchronously populate the text buffer. At that point, we'd still need this navigation to happen after that.

{
Contract.ThrowIfNull(textBuffer);
Copy link
Member

Choose a reason for hiding this comment

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

Is this file and callers not annotated?

Copy link
Member Author

Choose a reason for hiding this comment

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

callers are not annotated. so i'm being hyper anal.

Comment on lines +338 to +341
// Now that we've opened the document reacquire the corresponding Document in the current solution.
var document = workspace.CurrentSolution.GetDocument(documentId);
if (document == null)
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, this is something else that would have also been busted by #59211 since we're assuming that's happening. Nothing impactful for this PR; I wanted to tag it so when we look to resurrect #59211 we can remember to look at this.

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 tlikely would be. We can talk more offline, however i think what we likely want is an async OpenFileAsync that can be non-UI-thread affinitized, and which returns back the actual opened document that later code can use.

Copy link
Member

Choose a reason for hiding this comment

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

Indeed, that would be useful here.

@CyrusNajmabadi CyrusNajmabadi enabled auto-merge March 1, 2022 01:12
@CyrusNajmabadi CyrusNajmabadi disabled auto-merge March 1, 2022 02:04
@CyrusNajmabadi
Copy link
Member Author

Will take this for 17.3 intead. @RikkiGibson where should 17.3 work go?

@CyrusNajmabadi CyrusNajmabadi enabled auto-merge March 1, 2022 05:16
@CyrusNajmabadi CyrusNajmabadi disabled auto-merge March 1, 2022 05:27
@CyrusNajmabadi CyrusNajmabadi changed the base branch from main to release/dev17.3 March 1, 2022 18:21
@CyrusNajmabadi CyrusNajmabadi enabled auto-merge March 1, 2022 18:21
@CyrusNajmabadi CyrusNajmabadi merged commit cd34435 into dotnet:release/dev17.3 Mar 1, 2022
@CyrusNajmabadi CyrusNajmabadi deleted the asyncNavigation2 branch March 1, 2022 20:06
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.

2 participants