-
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
Change navigation helpers to operate in two steps. #59798
Change navigation helpers to operate in two steps. #59798
Conversation
/// 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); |
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.
@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.
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.
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.
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'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) |
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.
View with whitespace off.
if (!workspace.IsDocumentOpen(documentId)) | ||
return false; | ||
|
||
if (IsSecondaryBuffer(workspace, documentId)) |
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'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>(); |
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.
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.
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.
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); |
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.
inlined this helper method.
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 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); |
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'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) |
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.
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) |
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 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 :)
src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationService.cs
Show resolved
Hide resolved
@@ -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)) |
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.
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?
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.
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".
var text = document.GetTextSynchronously(cancellationToken); | ||
var text = await document.GetTextAsync(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.
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.
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... that's a good point. bleag... this is a rough area.
windowFrame.Show(); | ||
} | ||
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); | ||
var openDocumentService = _serviceProvider.GetService<SVsUIShellOpenDocument, IVsUIShellOpenDocument>(); |
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.
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); |
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 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); |
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.
Is this file and callers not annotated?
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.
callers are not annotated. so i'm being hyper anal.
// 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; |
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.
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 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.
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.
Indeed, that would be useful here.
Will take this for 17.3 intead. @RikkiGibson where should 17.3 work go? |
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.