diff --git a/src/VisualStudio/Core/Def/Extensions/VsTextSpanExtensions.cs b/src/VisualStudio/Core/Def/Extensions/VsTextSpanExtensions.cs index 6d8f9d9361fc7..c4a89df92f61a 100644 --- a/src/VisualStudio/Core/Def/Extensions/VsTextSpanExtensions.cs +++ b/src/VisualStudio/Core/Def/Extensions/VsTextSpanExtensions.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.VisualStudio.LanguageServices.Implementation.Venus; using VsTextSpan = Microsoft.VisualStudio.TextManager.Interop.TextSpan; @@ -24,6 +25,24 @@ internal static class VsTextSpanExtensions return null; await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + return MapSpanFromSecondaryBufferToPrimaryBuffer( + spanInSecondaryBuffer, threadingContext, documentId, containedDocument); + } + + /// + /// Only call this from the UI thread. See for the async version. + /// + public static VsTextSpan? MapSpanFromSecondaryBufferToPrimaryBuffer( + this VsTextSpan spanInSecondaryBuffer, + IThreadingContext threadingContext, + DocumentId documentId, + ContainedDocument? containedDocument = null) + { + threadingContext.ThrowIfNotOnUIThread(); + + containedDocument ??= ContainedDocument.TryGetContainedDocument(documentId); + if (containedDocument == null) + return null; var bufferCoordinator = containedDocument.BufferCoordinator; var primary = new VsTextSpan[1]; diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs index 790628ecd98ad..1e3e8b50c8b64 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs @@ -177,8 +177,13 @@ public int ResolveName(string? pszName, uint dwFlags, out IVsEnumDebugName? ppNa return VSConstants.S_FALSE; } + // NOTE(cyrusn): We have to wait here because the debuggers' ResolveName + // call is synchronous. In the future it would be nice to make it async. ppNames = this.ThreadingContext.JoinableTaskFactory.Run(async () => { + // We're in a blocking JTF run. So ConfigureAwait(true) all calls to ensure we're coming back + // and using the blocked thread whenever possible. + using (Logger.LogBlock(FunctionId.Debugging_VsLanguageDebugInfo_ResolveName, CancellationToken.None)) { using var waitContext = _uiThreadOperationExecutor.BeginExecute( @@ -192,14 +197,11 @@ public int ResolveName(string? pszName, uint dwFlags, out IVsEnumDebugName? ppNa { var solution = _languageService.Workspace.CurrentSolution; - // NOTE(cyrusn): We have to wait here because the debuggers' ResolveName - // call is synchronous. In the future it would be nice to make it async. if (_breakpointService != null) { var breakpoints = await _breakpointService.ResolveBreakpointsAsync( - solution, pszName, cancellationToken).ConfigureAwait(false); - var debugNames = await breakpoints.SelectAsArrayAsync( - bp => CreateDebugNameAsync(bp, cancellationToken)).ConfigureAwait(true); + solution, pszName, cancellationToken).ConfigureAwait(true); + var debugNames = breakpoints.SelectAsArray(bp => CreateDebugName(bp, cancellationToken)); return new VsEnumDebugName(debugNames); } @@ -210,23 +212,28 @@ public int ResolveName(string? pszName, uint dwFlags, out IVsEnumDebugName? ppNa }); return ppNames != null ? VSConstants.S_OK : VSConstants.E_NOTIMPL; - } - private async ValueTask CreateDebugNameAsync( - BreakpointResolutionResult breakpoint, CancellationToken cancellationToken) - { - var document = breakpoint.Document; - var filePath = _languageService.Workspace.GetFilePath(document.Id); - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var span = text.GetVsTextSpanForSpan(breakpoint.TextSpan); - // If we're inside an Venus code nugget, we need to map the span to the surface buffer. - // Otherwise, we'll just use the original span. - var mappedSpan = await span.MapSpanFromSecondaryBufferToPrimaryBufferAsync( - this.ThreadingContext, document.Id, cancellationToken).ConfigureAwait(true); - if (mappedSpan != null) - span = mappedSpan.Value; - - return new VsDebugName(breakpoint.LocationNameOpt, filePath!, span); + IVsDebugName CreateDebugName( + BreakpointResolutionResult breakpoint, CancellationToken cancellationToken) + { + // We're in a blocking jtf run. So CA(true) all calls to ensure we're coming bac + // and using the blocked thread whenever possible. + + var document = breakpoint.Document; + var filePath = _languageService.Workspace.GetFilePath(document.Id); + + // We're (unfortunately) blocking the UI thread here. So avoid async io as we actually + // awant the IO to complete as quickly as possible, on this thread if necessary. + var text = document.GetTextSynchronously(cancellationToken); + var span = text.GetVsTextSpanForSpan(breakpoint.TextSpan); + // If we're inside an Venus code nugget, we need to map the span to the surface buffer. + // Otherwise, we'll just use the original span. + var mappedSpan = span.MapSpanFromSecondaryBufferToPrimaryBuffer(this.ThreadingContext, document.Id); + if (mappedSpan != null) + span = mappedSpan.Value; + + return new VsDebugName(breakpoint.LocationNameOpt, filePath!, span); + } } public int ValidateBreakpointLocation(IVsTextBuffer pBuffer, int iLine, int iCol, VsTextSpan[] pCodeSpan)