diff --git a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationService.cs b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationService.cs index 6b3e672a6e6ca..74ecc878e3c56 100644 --- a/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationService.cs +++ b/src/EditorFeatures/Core.Wpf/Interactive/InteractiveDocumentNavigationService.cs @@ -37,19 +37,19 @@ public Task CanNavigateToLineAndOffsetAsync(Workspace workspace, DocumentI public Task CanNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken) => SpecializedTasks.False; - public async Task TryNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) + public async Task GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) { await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); if (workspace is not InteractiveWindowWorkspace interactiveWorkspace) { Debug.Fail("InteractiveDocumentNavigationService called with incorrect workspace!"); - return false; + return null; } if (interactiveWorkspace.Window is null) { Debug.Fail("We are trying to navigate with a workspace that doesn't have a window!"); - return false; + return null; } var textView = interactiveWorkspace.Window.TextView; @@ -58,7 +58,7 @@ public async Task 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); @@ -66,27 +66,32 @@ public async Task TryNavigateToSpanAsync(Workspace workspace, DocumentId d if (!textView.TryGetSurfaceBufferSpan(virtualSnapshotSpan, out var surfaceBufferSpan)) { - return false; + return null; } - textView.Selection.Select(surfaceBufferSpan.Start, surfaceBufferSpan.End); - textView.ViewScroller.EnsureSpanVisible(surfaceBufferSpan.SnapshotSpan, EnsureSpanVisibleOptions.AlwaysCenter); + return new NavigableLocation(async cancellationToken => + { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + textView.Selection.Select(surfaceBufferSpan.Start, surfaceBufferSpan.End); + textView.ViewScroller.EnsureSpanVisible(surfaceBufferSpan.SnapshotSpan, EnsureSpanVisibleOptions.AlwaysCenter); - // Moving the caret must be the last operation involving surfaceBufferSpan because - // it might update the version number of textView.TextSnapshot (VB does line commit - // when the caret leaves a line which might cause pretty listing), which must be - // equal to surfaceBufferSpan.SnapshotSpan.Snapshot's version number. - textView.Caret.MoveTo(surfaceBufferSpan.Start); + // Moving the caret must be the last operation involving surfaceBufferSpan because + // it might update the version number of textView.TextSnapshot (VB does line commit + // when the caret leaves a line which might cause pretty listing), which must be + // equal to surfaceBufferSpan.SnapshotSpan.Snapshot's version number. + textView.Caret.MoveTo(surfaceBufferSpan.Start); - textView.VisualElement.Focus(); + textView.VisualElement.Focus(); - return true; + return true; + }); } - public Task TryNavigateToLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) + public Task GetLocationForLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) => throw new NotSupportedException(); - public Task TryNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) + public Task GetLocationForPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) => throw new NotSupportedException(); } } diff --git a/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockDocumentNavigationService.vb b/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockDocumentNavigationService.vb index 8e417ebf7183d..a403e05e79195 100644 --- a/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockDocumentNavigationService.vb +++ b/src/EditorFeatures/TestUtilities2/Utilities/GoToHelpers/MockDocumentNavigationService.vb @@ -40,33 +40,33 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Utilities.GoToHelpers Return If(_canNavigateToSpan, SpecializedTasks.True, SpecializedTasks.False) End Function - Public Function TryNavigateToLineAndOffsetAsync(workspace As Workspace, documentId As DocumentId, lineNumber As Integer, offset As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of Boolean) Implements IDocumentNavigationService.TryNavigateToLineAndOffsetAsync + Public Function GetLocationForLineAndOffsetAsync(workspace As Workspace, documentId As DocumentId, lineNumber As Integer, offset As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements IDocumentNavigationService.GetLocationForLineAndOffsetAsync _triedNavigationToLineAndOffset = True _documentId = documentId _options = options _line = lineNumber _offset = offset - Return If(_canNavigateToLineAndOffset, SpecializedTasks.True, SpecializedTasks.False) + Return NavigableLocation.TestAccessor.Create(_canNavigateToLineAndOffset) End Function - Public Function TryNavigateToPositionAsync(workspace As Workspace, documentId As DocumentId, position As Integer, virtualSpace As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of Boolean) Implements IDocumentNavigationService.TryNavigateToPositionAsync + Public Function GetLocationForPositionAsync(workspace As Workspace, documentId As DocumentId, position As Integer, virtualSpace As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements IDocumentNavigationService.GetLocationForPositionAsync _triedNavigationToPosition = True _documentId = documentId _options = options _position = position _positionVirtualSpace = virtualSpace - Return If(_canNavigateToPosition, SpecializedTasks.True, SpecializedTasks.False) + Return NavigableLocation.TestAccessor.Create(_canNavigateToPosition) End Function - Public Function TryNavigateToSpanAsync(workspace As Workspace, documentId As DocumentId, textSpan As TextSpan, options As NavigationOptions, allowInvalidSpan As Boolean, cancellationToken As CancellationToken) As Task(Of Boolean) Implements IDocumentNavigationService.TryNavigateToSpanAsync + Public Function GetLocationForSpanAsync(workspace As Workspace, documentId As DocumentId, textSpan As TextSpan, options As NavigationOptions, allowInvalidSpan As Boolean, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements IDocumentNavigationService.GetLocationForSpanAsync _triedNavigationToSpan = True _documentId = documentId _options = options _span = textSpan - Return If(_canNavigateToSpan, SpecializedTasks.True, SpecializedTasks.False) + Return NavigableLocation.TestAccessor.Create(_canNavigateToSpan) End Function End Class End Namespace diff --git a/src/EditorFeatures/TestUtilities2/Utilities/MockDocumentNavigationServiceProvider.vb b/src/EditorFeatures/TestUtilities2/Utilities/MockDocumentNavigationServiceProvider.vb index 188a9732f3f60..1856a4c676c16 100644 --- a/src/EditorFeatures/TestUtilities2/Utilities/MockDocumentNavigationServiceProvider.vb +++ b/src/EditorFeatures/TestUtilities2/Utilities/MockDocumentNavigationServiceProvider.vb @@ -70,30 +70,30 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Utilities Return If(CanNavigateToSpanReturnValue, SpecializedTasks.True, SpecializedTasks.False) End Function - Public Function TryNavigateToLineAndOffsetAsync(workspace As Workspace, documentId As DocumentId, lineNumber As Integer, offset As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of Boolean) Implements IDocumentNavigationService.TryNavigateToLineAndOffsetAsync + Public Function GetLocationForLineAndOffsetAsync(workspace As Workspace, documentId As DocumentId, lineNumber As Integer, offset As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements IDocumentNavigationService.GetLocationForLineAndOffsetAsync Me.ProvidedDocumentId = documentId Me.ProvidedLineNumber = lineNumber Me.ProvidedOffset = offset Me.ProvidedOptions = options - Return If(TryNavigateToLineAndOffsetReturnValue, SpecializedTasks.True, SpecializedTasks.False) + Return NavigableLocation.TestAccessor.Create(TryNavigateToLineAndOffsetReturnValue) End Function - Public Function TryNavigateToPositionAsync(workspace As Workspace, documentId As DocumentId, position As Integer, virtualSpace As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of Boolean) Implements IDocumentNavigationService.TryNavigateToPositionAsync + Public Function GetLocationForPositionAsync(workspace As Workspace, documentId As DocumentId, position As Integer, virtualSpace As Integer, options As NavigationOptions, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements IDocumentNavigationService.GetLocationForPositionAsync Me.ProvidedDocumentId = documentId Me.ProvidedPosition = position Me.ProvidedVirtualSpace = virtualSpace Me.ProvidedOptions = options - Return If(TryNavigateToPositionReturnValue, SpecializedTasks.True, SpecializedTasks.False) + Return NavigableLocation.TestAccessor.Create(TryNavigateToPositionReturnValue) End Function - Public Function TryNavigateToSpanAsync(workspace As Workspace, documentId As DocumentId, textSpan As TextSpan, options As NavigationOptions, allowInvalidSpans As Boolean, cancellationToken As CancellationToken) As Task(Of Boolean) Implements IDocumentNavigationService.TryNavigateToSpanAsync + Public Function GetLocationForSpanAsync(workspace As Workspace, documentId As DocumentId, textSpan As TextSpan, options As NavigationOptions, allowInvalidSpans As Boolean, cancellationToken As CancellationToken) As Task(Of INavigableLocation) Implements IDocumentNavigationService.GetLocationForSpanAsync Me.ProvidedDocumentId = documentId Me.ProvidedTextSpan = textSpan Me.ProvidedOptions = options - Return If(TryNavigateToSpanReturnValue, SpecializedTasks.True, SpecializedTasks.False) + Return NavigableLocation.TestAccessor.Create(TryNavigateToSpanReturnValue) End Function End Class End Class diff --git a/src/Features/Core/Portable/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentNavigationServiceWrapper.cs b/src/Features/Core/Portable/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentNavigationServiceWrapper.cs index 85a93ae77860a..717c08db06e27 100644 --- a/src/Features/Core/Portable/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentNavigationServiceWrapper.cs +++ b/src/Features/Core/Portable/ExternalAccess/VSTypeScript/Api/VSTypeScriptDocumentNavigationServiceWrapper.cs @@ -38,7 +38,6 @@ public bool TryNavigateToPosition(Workspace workspace, DocumentId documentId, in return _threadingProvider.Service.Run(() => obj.TryNavigateToPositionAsync(workspace, documentId, position, virtualSpace, NavigationOptions.Default, cancellationToken)); } - /// public bool TryNavigateToPosition(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken) { var obj = _underlyingObject; diff --git a/src/Features/Core/Portable/Navigation/DefaultDocumentNavigationService.cs b/src/Features/Core/Portable/Navigation/DefaultDocumentNavigationService.cs index 52b619fe058fe..c55565a677a01 100644 --- a/src/Features/Core/Portable/Navigation/DefaultDocumentNavigationService.cs +++ b/src/Features/Core/Portable/Navigation/DefaultDocumentNavigationService.cs @@ -20,13 +20,13 @@ public Task CanNavigateToLineAndOffsetAsync(Workspace workspace, DocumentI public Task CanNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken) => SpecializedTasks.False; - public Task TryNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) - => SpecializedTasks.False; + public Task GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) + => SpecializedTasks.Null(); - public Task TryNavigateToLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) - => SpecializedTasks.False; + public Task GetLocationForLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) + => SpecializedTasks.Null(); - public Task TryNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) - => SpecializedTasks.False; + public Task GetLocationForPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) + => SpecializedTasks.Null(); } } diff --git a/src/Features/Core/Portable/Navigation/IDocumentNavigationService.cs b/src/Features/Core/Portable/Navigation/IDocumentNavigationService.cs index 5f94a6ee784e4..2b95e9b088eff 100644 --- a/src/Features/Core/Portable/Navigation/IDocumentNavigationService.cs +++ b/src/Features/Core/Portable/Navigation/IDocumentNavigationService.cs @@ -27,20 +27,9 @@ internal interface IDocumentNavigationService : IWorkspaceService /// Task CanNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken); - /// - /// Navigates to the given position in the specified document, opening it if necessary. - /// - Task TryNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken); - - /// - /// Navigates to the given line/offset in the specified document, opening it if necessary. - /// - Task TryNavigateToLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken); - - /// - /// Navigates to the given virtual position in the specified document, opening it if necessary. - /// - Task TryNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken); + Task GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken); + Task GetLocationForLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken); + Task GetLocationForPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken); } internal static class IDocumentNavigationServiceExtensions @@ -59,5 +48,38 @@ public static Task TryNavigateToLineAndOffsetAsync(this IDocumentNavigatio public static Task TryNavigateToPositionAsync(this IDocumentNavigationService service, Workspace workspace, DocumentId documentId, int position, CancellationToken cancellationToken) => service.TryNavigateToPositionAsync(workspace, documentId, position, virtualSpace: 0, NavigationOptions.Default, cancellationToken); + + /// + /// Navigates to the given position in the specified document, opening it if necessary. + /// + public static async Task TryNavigateToSpanAsync(this IDocumentNavigationService service, Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) + { + var location = await service.GetLocationForSpanAsync( + workspace, documentId, textSpan, options, allowInvalidSpan, cancellationToken).ConfigureAwait(false); + return location != null && + await location.NavigateToAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Navigates to the given line/offset in the specified document, opening it if necessary. + /// + public static async Task TryNavigateToLineAndOffsetAsync(this IDocumentNavigationService service, Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) + { + var location = await service.GetLocationForLineAndOffsetAsync( + workspace, documentId, lineNumber, offset, options, cancellationToken).ConfigureAwait(false); + return location != null && + await location.NavigateToAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Navigates to the given virtual position in the specified document, opening it if necessary. + /// + public static async Task TryNavigateToPositionAsync(this IDocumentNavigationService service, Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) + { + var location = await service.GetLocationForPositionAsync( + workspace, documentId, position, virtualSpace, options, cancellationToken).ConfigureAwait(false); + return location != null && + await location.NavigateToAsync(cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Features/Core/Portable/Navigation/INavigableDocumentLocation.cs b/src/Features/Core/Portable/Navigation/INavigableDocumentLocation.cs new file mode 100644 index 0000000000000..011603afce558 --- /dev/null +++ b/src/Features/Core/Portable/Navigation/INavigableDocumentLocation.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Navigation +{ + internal interface INavigableLocation + { + /// + /// Navigates to a location opening or presenting it in a UI if necessary. This work must happen quickly. Any + /// expensive async work must be done by whatever component creates this value. 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. + /// + Task NavigateToAsync(CancellationToken cancelletionToken); + } + + internal class NavigableLocation : INavigableLocation + { + private readonly Func> _callback; + + public NavigableLocation(Func> callback) + => _callback = callback; + + public Task NavigateToAsync(CancellationToken cancellationToken) + => _callback(cancellationToken); + + public static class TestAccessor + { +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods + public static Task Create(bool value) +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods + { + return Task.FromResult( + new NavigableLocation(c => value ? SpecializedTasks.True : SpecializedTasks.False)); + } + } + } +} diff --git a/src/Tools/ExternalAccess/FSharp/Navigation/IFSharpDocumentNavigationService.cs b/src/Tools/ExternalAccess/FSharp/Navigation/IFSharpDocumentNavigationService.cs index 3542d4370a7f5..fa11477a1131c 100644 --- a/src/Tools/ExternalAccess/FSharp/Navigation/IFSharpDocumentNavigationService.cs +++ b/src/Tools/ExternalAccess/FSharp/Navigation/IFSharpDocumentNavigationService.cs @@ -35,11 +35,11 @@ internal interface IFSharpDocumentNavigationService : IWorkspaceService /// bool CanNavigateToPosition(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken); - /// + /// bool TryNavigateToSpan(Workspace workspace, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken); - /// + /// bool TryNavigateToLineAndOffset(Workspace workspace, DocumentId documentId, int lineNumber, int offset, CancellationToken cancellationToken); - /// + /// bool TryNavigateToPosition(Workspace workspace, DocumentId documentId, int position, int virtualSpace, CancellationToken cancellationToken); } } diff --git a/src/VisualStudio/Core/Def/Implementation/Extensions/VsTextSpanExtensions.cs b/src/VisualStudio/Core/Def/Implementation/Extensions/VsTextSpanExtensions.cs index ce7bc62ada346..1f12c105b57d9 100644 --- a/src/VisualStudio/Core/Def/Implementation/Extensions/VsTextSpanExtensions.cs +++ b/src/VisualStudio/Core/Def/Implementation/Extensions/VsTextSpanExtensions.cs @@ -4,38 +4,40 @@ #nullable disable +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; -using Microsoft.VisualStudio.LanguageServices.Implementation.Venus; using VsTextSpan = Microsoft.VisualStudio.TextManager.Interop.TextSpan; namespace Microsoft.VisualStudio.LanguageServices.Implementation.Extensions { internal static class VsTextSpanExtensions { - public static bool TryMapSpanFromSecondaryBufferToPrimaryBuffer(this VsTextSpan spanInSecondaryBuffer, Workspace workspace, DocumentId documentId, out VsTextSpan spanInPrimaryBuffer) + public static async Task MapSpanFromSecondaryBufferToPrimaryBufferAsync( + this VsTextSpan spanInSecondaryBuffer, + IThreadingContext threadingContext, + Workspace workspace, + DocumentId documentId, + CancellationToken cancellationToken) { - spanInPrimaryBuffer = default; - if (workspace is not VisualStudioWorkspaceImpl visualStudioWorkspace) - { - return false; - } + return null; var containedDocument = visualStudioWorkspace.TryGetContainedDocument(documentId); if (containedDocument == null) - { - return false; - } + return null; + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); var bufferCoordinator = containedDocument.BufferCoordinator; var primary = new VsTextSpan[1]; var hresult = bufferCoordinator.MapSecondaryToPrimarySpan(spanInSecondaryBuffer, primary); - spanInPrimaryBuffer = primary[0]; + var result = primary[0]; - return ErrorHandler.Succeeded(hresult); + return ErrorHandler.Succeeded(hresult) ? result : null; } } } diff --git a/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs b/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs index 2ef98bcb14226..f299d3a547aa1 100644 --- a/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs +++ b/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.VsLanguageDebugInfo.cs @@ -5,11 +5,14 @@ using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Debugging; using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.LanguageServices.Implementation.Extensions; @@ -30,6 +33,7 @@ internal sealed class VsLanguageDebugInfo : IVsLanguageDebugInfo { private readonly Guid _languageId; private readonly TLanguageService _languageService; + private readonly IThreadingContext _threadingContext; private readonly ILanguageDebugInfoService? _languageDebugInfo; private readonly IBreakpointResolutionService? _breakpointService; private readonly IProximityExpressionsService? _proximityExpressionsService; @@ -39,6 +43,7 @@ public VsLanguageDebugInfo( Guid languageId, TLanguageService languageService, HostLanguageServices languageServiceProvider, + IThreadingContext threadingContext, IUIThreadOperationExecutor uiThreadOperationExecutor) { Contract.ThrowIfNull(languageService); @@ -46,6 +51,7 @@ public VsLanguageDebugInfo( _languageId = languageId; _languageService = languageService; + _threadingContext = threadingContext; _languageDebugInfo = languageServiceProvider.GetService(); _breakpointService = languageServiceProvider.GetService(); _proximityExpressionsService = languageServiceProvider.GetService(); @@ -96,13 +102,16 @@ public int GetNameOfLocation(IVsTextBuffer pBuffer, int iLine, int iCol, out str // NOTE(cyrusn): We have to wait here because the debuggers' // GetNameOfLocation is a blocking call. In the future, it // would be nice if they could make it async. - var debugLocationInfo = _languageDebugInfo.GetLocationInfoAsync(document, point, cancellationToken).WaitAndGetResult(cancellationToken); - - if (!debugLocationInfo.IsDefault) + _threadingContext.JoinableTaskFactory.Run(async () => { - name = debugLocationInfo.Name; - lineOffset = debugLocationInfo.LineOffset; - } + var debugLocationInfo = await _languageDebugInfo.GetLocationInfoAsync(document, point, cancellationToken).ConfigureAwait(false); + + if (!debugLocationInfo.IsDefault) + { + name = debugLocationInfo.Name; + lineOffset = debugLocationInfo.LineOffset; + } + }); } } } @@ -194,21 +203,27 @@ public int ResolveName(string pszName, uint dwFlags, out IVsEnumDebugName? ppNam showProgress: false, action: waitContext => { - var cancellationToken = waitContext.UserCancellationToken; - if (dwFlags == (uint)RESOLVENAMEFLAGS.RNF_BREAKPOINT) + _threadingContext.JoinableTaskFactory.Run(async () => { - 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 cancellationToken = waitContext.UserCancellationToken; + if (dwFlags == (uint)RESOLVENAMEFLAGS.RNF_BREAKPOINT) { - var breakpoints = _breakpointService.ResolveBreakpointsAsync(solution, pszName, cancellationToken).WaitAndGetResult(cancellationToken); - var debugNames = breakpoints.Select(bp => CreateDebugName(bp, solution, cancellationToken)).WhereNotNull().ToList(); + var solution = _languageService.Workspace.CurrentSolution; - enumName = new VsEnumDebugName(debugNames); + // 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( + async bp => await CreateDebugNameAsync( + bp, solution, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + + enumName = new VsEnumDebugName(debugNames); + } } - } + }); }); ppNames = enumName; @@ -216,20 +231,21 @@ public int ResolveName(string pszName, uint dwFlags, out IVsEnumDebugName? ppNam } } - private IVsDebugName CreateDebugName(BreakpointResolutionResult breakpoint, Solution solution, CancellationToken cancellationToken) + private async Task CreateDebugNameAsync( + BreakpointResolutionResult breakpoint, Solution solution, CancellationToken cancellationToken) { var document = breakpoint.Document; var filePath = _languageService.Workspace.GetFilePath(document.Id); - var text = document.GetTextSynchronously(cancellationToken); + var text = await document.GetTextAsync(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. - if (!span.TryMapSpanFromSecondaryBufferToPrimaryBuffer(solution.Workspace, document.Id, out var mappedSpan)) - { - mappedSpan = span; - } + var mappedSpan = await span.MapSpanFromSecondaryBufferToPrimaryBufferAsync( + _threadingContext, solution.Workspace, document.Id, cancellationToken).ConfigureAwait(false); + if (mappedSpan != null) + span = mappedSpan.Value; - return new VsDebugName(breakpoint.LocationNameOpt, filePath, mappedSpan); + return new VsDebugName(breakpoint.LocationNameOpt, filePath, span); } public int ValidateBreakpointLocation(IVsTextBuffer pBuffer, int iLine, int iCol, VsTextSpan[] pCodeSpan) diff --git a/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.cs b/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.cs index 87da60aa58d4a..38012fbf48bbc 100644 --- a/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.cs +++ b/src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractLanguageService`2.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Options; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; @@ -263,6 +264,7 @@ private VsLanguageDebugInfo CreateLanguageDebugInfo() this.DebuggerLanguageId, (TLanguageService)this, languageServices, + this.Package.ComponentModel.GetService(), this.Package.ComponentModel.GetService()); } diff --git a/src/VisualStudio/Core/Def/Implementation/Workspace/SourceGeneratedFileManager.cs b/src/VisualStudio/Core/Def/Implementation/Workspace/SourceGeneratedFileManager.cs index 9b42945de8529..f29c5356c9d8f 100644 --- a/src/VisualStudio/Core/Def/Implementation/Workspace/SourceGeneratedFileManager.cs +++ b/src/VisualStudio/Core/Def/Implementation/Workspace/SourceGeneratedFileManager.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; @@ -101,10 +102,8 @@ public SourceGeneratedFileManager( this); } - public async Task NavigateToSourceGeneratedFileAsync(SourceGeneratedDocument document, TextSpan sourceSpan, CancellationToken cancellationToken) + public Func> GetNavigationCallback(SourceGeneratedDocument document, TextSpan sourceSpan) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - // We will create an file name to represent this generated file; the Visual Studio shell APIs imply you can use a URI, // but most URIs are blocked other than file:// and http://; they also get extra handling to attempt to download the file so // those aren't really usable anyways. @@ -130,25 +129,27 @@ public async Task NavigateToSourceGeneratedFileAsync(SourceGeneratedDocument doc File.WriteAllText(temporaryFilePath, ""); } - var openDocumentService = _serviceProvider.GetService(); - var hr = openDocumentService.OpenDocumentViaProject( - temporaryFilePath, - VSConstants.LOGVIEWID.TextView_guid, - out _, - out _, - out _, - out var windowFrame); - - if (ErrorHandler.Succeeded(hr) && windowFrame != null) + return async cancellationToken => { - windowFrame.Show(); - } + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var openDocumentService = _serviceProvider.GetService(); + var hr = openDocumentService.OpenDocumentViaProject( + temporaryFilePath, + VSConstants.LOGVIEWID.TextView_guid, + out _, + out _, + out _, + out var windowFrame); + + if (ErrorHandler.Succeeded(hr) && windowFrame != null) + { + windowFrame.Show(); + } - // We should have the file now, so navigate to the right span - if (_openFiles.TryGetValue(temporaryFilePath, out var openFile)) - { - openFile.NavigateToSpan(sourceSpan, cancellationToken); - } + // 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); + }; } public bool TryGetGeneratedFileInformation( @@ -497,10 +498,11 @@ private void EnsureWindowFrameInfoBarUpdated() _currentWindowFrameInfoBarElement = infoBarUI; } - public void NavigateToSpan(TextSpan sourceSpan, CancellationToken cancellationToken) + public Task NavigateToSpanAsync(TextSpan sourceSpan, CancellationToken cancellationToken) { var sourceText = _textBuffer.CurrentSnapshot.AsText(); - _fileManager._visualStudioDocumentNavigationService.NavigateTo(_textBuffer, sourceText.GetVsTextSpanForSpan(sourceSpan), cancellationToken); + return _fileManager._visualStudioDocumentNavigationService.NavigateToTextBufferAsync( + _textBuffer, sourceText.GetVsTextSpanForSpan(sourceSpan), cancellationToken); } } } diff --git a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs index 8fc18231df722..89907f79bcdb7 100644 --- a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs +++ b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentNavigationService.cs @@ -142,9 +142,10 @@ public async Task CanNavigateToPositionAsync(Workspace workspace, Document workspace, documentId, vsTextSpan, cancellationToken).ConfigureAwait(false); } - public Task TryNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) + public Task GetLocationForSpanAsync( + Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) { - return TryNavigateToLocationAsync(workspace, + return GetNavigableLocationAsync(workspace, documentId, _ => Task.FromResult(textSpan), text => GetVsTextSpan(text, textSpan, allowInvalidSpan), @@ -169,10 +170,10 @@ static VsTextSpan GetVsTextSpan(SourceText text, TextSpan textSpan, bool allowIn } } - public Task TryNavigateToLineAndOffsetAsync( + public Task GetLocationForLineAndOffsetAsync( Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) { - return TryNavigateToLocationAsync(workspace, + return GetNavigableLocationAsync(workspace, documentId, document => GetTextSpanFromLineAndOffsetAsync(document, lineNumber, offset, cancellationToken), text => GetVsTextSpan(text, lineNumber, offset), @@ -193,10 +194,10 @@ static VsTextSpan GetVsTextSpan(SourceText text, int lineNumber, int offset) } } - public Task TryNavigateToPositionAsync( + public Task GetLocationForPositionAsync( Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) { - return TryNavigateToLocationAsync(workspace, + return GetNavigableLocationAsync(workspace, documentId, document => GetTextSpanFromPositionAsync(document, position, virtualSpace, cancellationToken), text => GetVsTextSpan(text, position, virtualSpace), @@ -232,7 +233,7 @@ static VsTextSpan GetVsTextSpan(SourceText text, int position, int virtualSpace) } } - private async Task TryNavigateToLocationAsync( + private async Task GetNavigableLocationAsync( Workspace workspace, DocumentId documentId, Func> getTextSpanForMappingAsync, @@ -240,26 +241,32 @@ private async Task TryNavigateToLocationAsync( NavigationOptions options, CancellationToken cancellationToken) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - // Navigation should not change the context of linked files and Shared Projects. - documentId = workspace.GetDocumentIdInCurrentContext(documentId); + var callback = await GetNavigationCallbackAsync( + workspace, documentId, getTextSpanForMappingAsync, getVsTextSpan, cancellationToken).ConfigureAwait(true); + if (callback == null) + return null; - using (OpenNewDocumentStateScope(options)) + return new NavigableLocation(async cancellationToken => { - // Keep this on the UI thread so that the scope disposal stays there. - return await TryNavigateToLocationUnderScopeAsync( - workspace, documentId, getTextSpanForMappingAsync, getVsTextSpan, cancellationToken).ConfigureAwait(true); - } + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + using (OpenNewDocumentStateScope(options)) + { + // Ensure we come back to the UI Thread after navigating so we close the state scope. + return await callback(cancellationToken).ConfigureAwait(true); + } + }); } - private async Task TryNavigateToLocationUnderScopeAsync( + private async Task>?> GetNavigationCallbackAsync( Workspace workspace, DocumentId documentId, Func> getTextSpanForMappingAsync, Func getVsTextSpan, CancellationToken cancellationToken) { + // Navigation should not change the context of linked files and Shared Projects. + documentId = workspace.GetDocumentIdInCurrentContext(documentId); + var solution = workspace.CurrentSolution; var document = solution.GetDocument(documentId); if (document == null) @@ -269,20 +276,16 @@ private async Task TryNavigateToLocationUnderScopeAsync( { // This is a source generated document shown in Solution Explorer, but is no longer valid since // the configuration and/or platform changed since the last generation completed. - return false; + return null; } var generatedDocument = await project.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - if (generatedDocument != null) - { - await _sourceGeneratedFileManager.Value.NavigateToSourceGeneratedFileAsync( - generatedDocument, - await getTextSpanForMappingAsync(generatedDocument).ConfigureAwait(false), - cancellationToken).ConfigureAwait(false); - return true; - } + if (generatedDocument == null) + return null; - return false; + return _sourceGeneratedFileManager.Value.GetNavigationCallback( + generatedDocument, + await getTextSpanForMappingAsync(generatedDocument).ConfigureAwait(false)); } // Before attempting to open the document, check if the location maps to a different file that should be opened instead. @@ -304,48 +307,70 @@ await getTextSpanForMappingAsync(document).ConfigureAwait(false), // If the mapped file maps to the same document that was passed in, then re-use the documentId to preserve context. // Otherwise, just pick one of the ids to use for navigation. var documentIdToNavigate = documentIdsForFilePath.Contains(documentId) ? documentId : documentIdsForFilePath.First(); - return await NavigateToFileInWorkspaceAsync( - documentIdToNavigate, workspace, getVsTextSpan, cancellationToken).ConfigureAwait(false); + return GetNavigationCallback(documentIdToNavigate, workspace, getVsTextSpan); } - return await TryNavigateToMappedFileAsync( + return await GetNavigableLocationForMappedFileAsync( workspace, document, mappedSpan.Value, cancellationToken).ConfigureAwait(false); } } - return await NavigateToFileInWorkspaceAsync( - documentId, workspace, getVsTextSpan, cancellationToken).ConfigureAwait(false); + return GetNavigationCallback(documentId, workspace, getVsTextSpan); } - private async Task NavigateToFileInWorkspaceAsync( + private Func>? GetNavigationCallback( DocumentId documentId, Workspace workspace, - Func getVsTextSpan, - CancellationToken cancellationToken) + Func getVsTextSpan) { - // Have to be on the UI thread to open a document. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - var document = OpenDocument(workspace, documentId); - if (document == null) + return async cancellationToken => { - return false; - } + // Always open the document again, even if the document is already open in the + // workspace. If a document is already open in a preview tab and it is opened again + // in a permanent tab, this allows the document to transition to the new state. + + if (workspace.CanOpenDocuments) + await OpenDocumentAsync(_threadingContext, workspace, documentId, cancellationToken).ConfigureAwait(false); - // Keep this on the UI thread since we're about to do span mapping. - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(true); - var textBuffer = text.Container.GetTextBuffer(); + if (!workspace.IsDocumentOpen(documentId)) + return false; + + // 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; + + // Reacquire the SourceText for it as well. This will be a practically free as this just wraps + // the open text buffer. So it's ok to do this in the navigation step. + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + + // Map the given span to the right location in the buffer. If we're in a projection scenario, ensure + // the span reflects that. + var vsTextSpan = getVsTextSpan(text); + if (IsSecondaryBuffer(workspace, documentId)) + { + var mapped = await vsTextSpan.MapSpanFromSecondaryBufferToPrimaryBufferAsync( + _threadingContext, workspace, documentId, cancellationToken).ConfigureAwait(false); + if (mapped == null) + return false; - var vsTextSpan = getVsTextSpan(text); - if (IsSecondaryBuffer(workspace, documentId) && - !vsTextSpan.TryMapSpanFromSecondaryBufferToPrimaryBuffer(workspace, documentId, out vsTextSpan)) + vsTextSpan = mapped.Value; + } + + return await NavigateToTextBufferAsync( + text.Container.GetTextBuffer(), vsTextSpan, cancellationToken).ConfigureAwait(false); + }; + + async static Task OpenDocumentAsync( + IThreadingContext threadingContext, Workspace workspace, DocumentId documentId, CancellationToken cancellationToken) { - return false; + // OpenDocument must be called on the UI thread. + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + workspace.OpenDocument(documentId); } - - return NavigateTo(textBuffer, vsTextSpan, cancellationToken); } - private async Task TryNavigateToMappedFileAsync( + private async Task>?> GetNavigableLocationForMappedFileAsync( Workspace workspace, Document generatedDocument, MappedSpanResult mappedSpanResult, CancellationToken cancellationToken) { await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); @@ -354,20 +379,18 @@ private async Task TryNavigateToMappedFileAsync( // Pass the original result's project context so that if the mapped file has the same context available, we navigate // to the mapped file with a consistent project context. vsWorkspace.OpenDocumentFromPath(mappedSpanResult.FilePath, generatedDocument.Project.Id); - if (_runningDocumentTable.TryGetBufferFromMoniker(_editorAdaptersFactoryService, mappedSpanResult.FilePath, out var textBuffer)) - { - var vsTextSpan = new VsTextSpan - { - iStartIndex = mappedSpanResult.LinePositionSpan.Start.Character, - iStartLine = mappedSpanResult.LinePositionSpan.Start.Line, - iEndIndex = mappedSpanResult.LinePositionSpan.End.Character, - iEndLine = mappedSpanResult.LinePositionSpan.End.Line - }; + if (!_runningDocumentTable.TryGetBufferFromMoniker(_editorAdaptersFactoryService, mappedSpanResult.FilePath, out var textBuffer)) + return null; - return NavigateTo(textBuffer, vsTextSpan, cancellationToken); - } + var vsTextSpan = new VsTextSpan + { + iStartIndex = mappedSpanResult.LinePositionSpan.Start.Character, + iStartLine = mappedSpanResult.LinePositionSpan.Start.Line, + iEndIndex = mappedSpanResult.LinePositionSpan.End.Character, + iEndLine = mappedSpanResult.LinePositionSpan.End.Line + }; - return false; + return cancellationToken => NavigateToTextBufferAsync(textBuffer, vsTextSpan, cancellationToken); } private static async Task GetMappedSpanAsync( @@ -410,26 +433,11 @@ private static int GetPositionWithinDocumentBounds(int position, int documentLen private static TextSpan GetSpanWithinDocumentBounds(TextSpan span, int documentLength) => TextSpan.FromBounds(GetPositionWithinDocumentBounds(span.Start, documentLength), GetPositionWithinDocumentBounds(span.End, documentLength)); - private static Document? OpenDocument(Workspace workspace, DocumentId documentId) - { - // Always open the document again, even if the document is already open in the - // workspace. If a document is already open in a preview tab and it is opened again - // in a permanent tab, this allows the document to transition to the new state. - if (workspace.CanOpenDocuments) - { - workspace.OpenDocument(documentId); - } - - if (!workspace.IsDocumentOpen(documentId)) - { - return null; - } - - return workspace.CurrentSolution.GetDocument(documentId); - } - - public bool NavigateTo(ITextBuffer textBuffer, VsTextSpan vsTextSpan, CancellationToken cancellationToken) + public async Task NavigateToTextBufferAsync( + ITextBuffer textBuffer, VsTextSpan vsTextSpan, CancellationToken cancellationToken) { + Contract.ThrowIfNull(textBuffer); + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); using (Logger.LogBlock(FunctionId.NavigationService_VSDocumentNavigationService_NavigateTo, cancellationToken)) { var vsTextBuffer = _editorAdaptersFactoryService.GetBufferAdapter(textBuffer); @@ -466,19 +474,15 @@ private static bool IsSecondaryBuffer(Workspace workspace, DocumentId documentId } var containedDocument = visualStudioWorkspace.TryGetContainedDocument(documentId); - if (containedDocument == null) - { - return false; - } - - return true; + return containedDocument != null; } private async Task CanMapFromSecondaryBufferToPrimaryBufferAsync( Workspace workspace, DocumentId documentId, VsTextSpan spanInSecondaryBuffer, CancellationToken cancellationToken) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - return spanInSecondaryBuffer.TryMapSpanFromSecondaryBufferToPrimaryBuffer(workspace, documentId, out _); + var mapped = await spanInSecondaryBuffer.MapSpanFromSecondaryBufferToPrimaryBufferAsync( + _threadingContext, workspace, documentId, cancellationToken).ConfigureAwait(false); + return mapped != null; } private static IDisposable OpenNewDocumentStateScope(NavigationOptions options) diff --git a/src/VisualStudio/LiveShare/Test/MockDocumentNavigationServiceFactory.cs b/src/VisualStudio/LiveShare/Test/MockDocumentNavigationServiceFactory.cs index de1873087a660..449d8d9e5cb08 100644 --- a/src/VisualStudio/LiveShare/Test/MockDocumentNavigationServiceFactory.cs +++ b/src/VisualStudio/LiveShare/Test/MockDocumentNavigationServiceFactory.cs @@ -10,7 +10,6 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Navigation; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -40,11 +39,14 @@ private class MockDocumentNavigationService : IDocumentNavigationService public Task CanNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, CancellationToken cancellationToken) => SpecializedTasks.True; - public Task TryNavigateToLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) => SpecializedTasks.True; + public Task GetLocationForLineAndOffsetAsync(Workspace workspace, DocumentId documentId, int lineNumber, int offset, NavigationOptions options, CancellationToken cancellationToken) + => NavigableLocation.TestAccessor.Create(true); - public Task TryNavigateToPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) => SpecializedTasks.True; + public Task GetLocationForPositionAsync(Workspace workspace, DocumentId documentId, int position, int virtualSpace, NavigationOptions options, CancellationToken cancellationToken) + => NavigableLocation.TestAccessor.Create(true); - public Task TryNavigateToSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) => SpecializedTasks.True; + public Task GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, NavigationOptions options, bool allowInvalidSpan, CancellationToken cancellationToken) + => NavigableLocation.TestAccessor.Create(true); } } }