diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Debugging/RazorBreakpointSpanEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Debugging/RazorBreakpointSpanEndpoint.cs index 1bcb7c82ec..e2bdd2dc6d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Debugging/RazorBreakpointSpanEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Debugging/RazorBreakpointSpanEndpoint.cs @@ -19,30 +19,17 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Debugging; [RazorLanguageServerEndpoint(LanguageServerConstants.RazorBreakpointSpanEndpoint)] -internal class RazorBreakpointSpanEndpoint : IRazorDocumentlessRequestHandler, ITextDocumentIdentifierHandler +internal class RazorBreakpointSpanEndpoint( + IDocumentMappingService documentMappingService, + ILoggerFactory loggerFactory) : IRazorDocumentlessRequestHandler, ITextDocumentIdentifierHandler { - private readonly IDocumentMappingService _documentMappingService; - private readonly ILogger _logger; + private readonly IDocumentMappingService _documentMappingService = documentMappingService; + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); public bool MutatesSolutionState => false; - public RazorBreakpointSpanEndpoint( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - { - if (loggerFactory is null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - _logger = loggerFactory.GetOrCreateLogger(); - } - public Uri GetTextDocumentIdentifier(RazorBreakpointSpanParams request) - { - return request.Uri; - } + => request.Uri; public async Task HandleRequestAsync(RazorBreakpointSpanParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { @@ -52,6 +39,12 @@ internal class RazorBreakpointSpanEndpoint : IRazorDocumentlessRequestHandler, ITextDocumentIdentifierHandler +internal class RazorProximityExpressionsEndpoint( + IDocumentMappingService documentMappingService, + ILoggerFactory loggerFactory) : IRazorDocumentlessRequestHandler, ITextDocumentIdentifierHandler { - private readonly IDocumentMappingService _documentMappingService; - private readonly ILogger _logger; - - public RazorProximityExpressionsEndpoint( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - { - if (documentMappingService is null) - { - throw new ArgumentNullException(nameof(documentMappingService)); - } - - if (loggerFactory is null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - _documentMappingService = documentMappingService; - _logger = loggerFactory.GetOrCreateLogger(); - } + private readonly IDocumentMappingService _documentMappingService = documentMappingService; + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); public bool MutatesSolutionState => false; public Uri GetTextDocumentIdentifier(RazorProximityExpressionsParams request) - { - return request.Uri; - } + => request.Uri; public async Task HandleRequestAsync(RazorProximityExpressionsParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { @@ -58,6 +40,12 @@ internal class RazorProximityExpressionsEndpoint : IRazorDocumentlessRequestHand return null; } + if (documentContext.Snapshot.Version != request.HostDocumentSyncVersion) + { + // Whether we are being asked about an old version of the C# document, or somehow a future one, we can't rely on the result. + return null; + } + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); var hostDocumentIndex = sourceText.GetPosition(request.Position); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorBreakpointSpanParams.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorBreakpointSpanParams.cs index f486bc6230..fdf956fc7f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorBreakpointSpanParams.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorBreakpointSpanParams.cs @@ -14,4 +14,7 @@ internal class RazorBreakpointSpanParams [JsonPropertyName("position")] public required Position Position { get; init; } + + [JsonPropertyName("hostDocumentSyncVersion")] + public required long HostDocumentSyncVersion { get; init; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorProximityExpressionsParams.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorProximityExpressionsParams.cs index d56edee55b..8f20b3ff8a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorProximityExpressionsParams.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Debugging/RazorProximityExpressionsParams.cs @@ -14,4 +14,7 @@ internal class RazorProximityExpressionsParams [JsonPropertyName("position")] public required Position Position { get; init; } + + [JsonPropertyName("hostDocumentSyncVersion")] + public required long HostDocumentSyncVersion { get; init; } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/RazorBreakpointResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/IRazorBreakpointResolver.cs similarity index 63% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/RazorBreakpointResolver.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/IRazorBreakpointResolver.cs index a9dfe15d5b..62d9ec2296 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/RazorBreakpointResolver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/IRazorBreakpointResolver.cs @@ -8,7 +8,7 @@ using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.VisualStudio.Razor.Debugging; -internal abstract class RazorBreakpointResolver +internal interface IRazorBreakpointResolver { - public abstract Task TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken); + Task TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/RazorProximityExpressionResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/IRazorProximityExpressionResolver.cs similarity index 58% rename from src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/RazorProximityExpressionResolver.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/IRazorProximityExpressionResolver.cs index 20ea51fa8c..29ab08313b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/RazorProximityExpressionResolver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Debugging/IRazorProximityExpressionResolver.cs @@ -8,7 +8,7 @@ using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.Razor.Debugging; -internal abstract class RazorProximityExpressionResolver +internal interface IRazorProximityExpressionResolver { - public abstract Task?> TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken); + Task?> TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultLSPBreakpointSpanProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultLSPBreakpointSpanProvider.cs deleted file mode 100644 index ee9438b506..0000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultLSPBreakpointSpanProvider.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Protocol.Debugging; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; - -[Export(typeof(LSPBreakpointSpanProvider))] -internal class DefaultLSPBreakpointSpanProvider : LSPBreakpointSpanProvider -{ - private readonly LSPRequestInvoker _requestInvoker; - private readonly ILogger _logger; - - [ImportingConstructor] - public DefaultLSPBreakpointSpanProvider( - LSPRequestInvoker requestInvoker, - ILoggerFactory loggerFactory) - { - _requestInvoker = requestInvoker; - _logger = loggerFactory.GetOrCreateLogger(); - } - - public async override Task GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) - { - if (documentSnapshot is null) - { - throw new ArgumentNullException(nameof(documentSnapshot)); - } - - if (position is null) - { - throw new ArgumentNullException(nameof(position)); - } - - var languageQueryParams = new RazorBreakpointSpanParams() - { - Position = position, - Uri = documentSnapshot.Uri - }; - - var response = await _requestInvoker.ReinvokeRequestOnServerAsync( - documentSnapshot.Snapshot.TextBuffer, - LanguageServerConstants.RazorBreakpointSpanEndpoint, - RazorLSPConstants.RazorLanguageServerName, - languageQueryParams, - cancellationToken).ConfigureAwait(false); - - var languageResponse = response?.Response; - if (languageResponse is null) - { - _logger.LogInformation($"The breakpoint position could not be mapped to a valid range."); - return null; - } - - return languageResponse.Range; - } -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultLSPProximityExpressionsProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultLSPProximityExpressionsProvider.cs deleted file mode 100644 index 1452ca9f16..0000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultLSPProximityExpressionsProvider.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Protocol.Debugging; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; - -[Export(typeof(LSPProximityExpressionsProvider))] -internal class DefaultLSPProximityExpressionsProvider : LSPProximityExpressionsProvider -{ - private readonly LSPRequestInvoker _requestInvoker; - - private readonly ILogger _logger; - - [ImportingConstructor] - public DefaultLSPProximityExpressionsProvider( - LSPRequestInvoker requestInvoker, - ILoggerFactory loggerFactory) - { - _requestInvoker = requestInvoker; - _logger = loggerFactory.GetOrCreateLogger(); - } - - public async override Task?> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) - { - if (documentSnapshot is null) - { - throw new ArgumentNullException(nameof(documentSnapshot)); - } - - if (position is null) - { - throw new ArgumentNullException(nameof(position)); - } - - var proximityExpressionsParams = new RazorProximityExpressionsParams() - { - Position = position, - Uri = documentSnapshot.Uri - }; - - var response = await _requestInvoker.ReinvokeRequestOnServerAsync( - documentSnapshot.Snapshot.TextBuffer, - LanguageServerConstants.RazorProximityExpressionsEndpoint, - RazorLSPConstants.RazorLanguageServerName, - proximityExpressionsParams, - cancellationToken).ConfigureAwait(false); - - var languageResponse = response?.Response; - if (languageResponse is null) - { - _logger.LogInformation($"The proximity expressions could not be resolved."); - return null; - } - - return languageResponse.Expressions; - } -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultRazorBreakpointResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultRazorBreakpointResolver.cs deleted file mode 100644 index 84dd82b1de..0000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultRazorBreakpointResolver.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.Razor.Debugging; -using Microsoft.VisualStudio.Text; -using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; - -[Export(typeof(RazorBreakpointResolver))] -internal class DefaultRazorBreakpointResolver : RazorBreakpointResolver -{ - private readonly FileUriProvider _fileUriProvider; - private readonly LSPDocumentManager _documentManager; - private readonly LSPBreakpointSpanProvider _breakpointSpanProvider; - private readonly MemoryCache _cache; - - [ImportingConstructor] - public DefaultRazorBreakpointResolver( - FileUriProvider fileUriProvider, - LSPDocumentManager documentManager, - LSPBreakpointSpanProvider breakpointSpanProvider) - { - if (fileUriProvider is null) - { - throw new ArgumentNullException(nameof(fileUriProvider)); - } - - if (documentManager is null) - { - throw new ArgumentNullException(nameof(documentManager)); - } - - if (breakpointSpanProvider is null) - { - throw new ArgumentNullException(nameof(breakpointSpanProvider)); - } - - _fileUriProvider = fileUriProvider; - _documentManager = documentManager; - _breakpointSpanProvider = breakpointSpanProvider; - - // 4 is a magic number that was determined based on the functionality of VisualStudio. Currently when you set or edit a breakpoint - // we get called with two different locations for the same breakpoint. Because of this 2 time call our size must be at least 2, - // we grow it to 4 just to be safe for lesser known scenarios. - _cache = new MemoryCache(sizeLimit: 4); - } - - public override async Task TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken) - { - if (textBuffer is null) - { - throw new ArgumentNullException(nameof(textBuffer)); - } - - if (!_fileUriProvider.TryGet(textBuffer, out var documentUri)) - { - // Not an addressable Razor document. Do not allow a breakpoint here. In practice this shouldn't happen, just being defensive. - return null; - } - - if (!_documentManager.TryGetDocument(documentUri, out var documentSnapshot)) - { - // No associated Razor document. Do not allow a breakpoint here. In practice this shouldn't happen, just being defensive. - return null; - } - - // TODO: Support multiple C# documents per Razor document. - if (!documentSnapshot.TryGetVirtualDocument(out var virtualDocument)) - { - Debug.Fail($"Some how there's no C# document associated with the host Razor document {documentUri.OriginalString} when validating breakpoint locations."); - return null; - } - - if (virtualDocument.HostDocumentSyncVersion != documentSnapshot.Version) - { - // C# document isn't up-to-date with the Razor document. Because VS' debugging tech is synchronous on the UI thread we have to bail. Ideally we'd wait - // for the C# document to become "updated"; however, that'd require the UI thread to see that the C# buffer is updated. Because this call path blocks - // the UI thread the C# document will never update until this path has exited. This means as a user types around the point of interest data may get stale - // but will re-adjust later. - return null; - } - - var cacheKey = new CacheKey(documentSnapshot.Uri, documentSnapshot.Version, lineIndex, characterIndex); - if (_cache.TryGetValue(cacheKey, out var cachedRange)) - { - // We've seen this request before, no need to go async. - return cachedRange; - } - - var position = VsLspFactory.CreatePosition(lineIndex, characterIndex); - var hostDocumentRange = await _breakpointSpanProvider.GetBreakpointSpanAsync(documentSnapshot, position, cancellationToken).ConfigureAwait(false); - if (hostDocumentRange is null) - { - // can't map the position, invalid breakpoint location. - return null; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // Cache range so if we're asked again for this document/line/character we don't have to go async. - _cache.Set(cacheKey, hostDocumentRange); - - return hostDocumentRange; - } - - private record CacheKey(Uri DocumentUri, int DocumentVersion, int Line, int Character); -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultRazorProximityExpressionResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultRazorProximityExpressionResolver.cs deleted file mode 100644 index 1b129fef49..0000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/DefaultRazorProximityExpressionResolver.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.Razor.Debugging; -using Microsoft.VisualStudio.Text; - -namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; - -[Export(typeof(RazorProximityExpressionResolver))] -internal class DefaultRazorProximityExpressionResolver : RazorProximityExpressionResolver -{ - private readonly FileUriProvider _fileUriProvider; - private readonly LSPDocumentManager _documentManager; - private readonly LSPProximityExpressionsProvider _proximityExpressionsProvider; - private readonly MemoryCache> _cache; - - [ImportingConstructor] - public DefaultRazorProximityExpressionResolver( - FileUriProvider fileUriProvider, - LSPDocumentManager documentManager, - LSPProximityExpressionsProvider proximityExpressionsProvider) - { - if (fileUriProvider is null) - { - throw new ArgumentNullException(nameof(fileUriProvider)); - } - - if (documentManager is null) - { - throw new ArgumentNullException(nameof(documentManager)); - } - - if (proximityExpressionsProvider is null) - { - throw new ArgumentNullException(nameof(proximityExpressionsProvider)); - } - - _fileUriProvider = fileUriProvider; - _documentManager = documentManager; - _proximityExpressionsProvider = proximityExpressionsProvider; - - // 10 is a magic number where this effectively represents our ability to cache the last 10 "hit" breakpoint locations - // corresponding proximity expressions which enables us not to go "async" in those re-hit scenarios. - _cache = new MemoryCache>(sizeLimit: 10); - } - - public override async Task?> TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken) - { - if (textBuffer is null) - { - throw new ArgumentNullException(nameof(textBuffer)); - } - - if (!_fileUriProvider.TryGet(textBuffer, out var documentUri)) - { - // Not an addressable Razor document. Do not allow expression resolution here. In practice this shouldn't happen, just being defensive. - return null; - } - - if (!_documentManager.TryGetDocument(documentUri, out var documentSnapshot)) - { - // No associated Razor document. Do not resolve expressions here. In practice this shouldn't happen, just being defensive. - return null; - } - - // TODO: Support multiple C# documents per Razor document. - if (!documentSnapshot.TryGetVirtualDocument(out var virtualDocument)) - { - Debug.Fail($"Some how there's no C# document associated with the host Razor document {documentUri.OriginalString} when resolving proximity expressions."); - return null; - } - - if (virtualDocument.HostDocumentSyncVersion != documentSnapshot.Version) - { - // C# document isn't up-to-date with the Razor document. Because VS' debugging tech is synchronous on the UI thread we have to bail. Ideally we'd wait - // for the C# document to become "updated"; however, that'd require the UI thread to see that the C# buffer is updated. Because this call path blocks - // the UI thread the C# document will never update until this path has exited. This means as a user types around the point of interest data may get stale - // but will re-adjust later. - return null; - } - - var cacheKey = new CacheKey(documentSnapshot.Uri, documentSnapshot.Version, lineIndex, characterIndex); - if (_cache.TryGetValue(cacheKey, out var cachedExpressions)) - { - // We've seen this request before, no need to go async. - return cachedExpressions; - } - - var position = VsLspFactory.CreatePosition(lineIndex, characterIndex); - var proximityExpressions = await _proximityExpressionsProvider.GetProximityExpressionsAsync(documentSnapshot, position, cancellationToken).ConfigureAwait(false); - - // Cache range so if we're asked again for this document/line/character we don't have to go async. - // Note: If we didn't get any proximity expressions back--likely due to an error--we cache an empty array. - _cache.Set(cacheKey, proximityExpressions ?? Array.Empty()); - - return proximityExpressions; - } - - private record CacheKey(Uri DocumentUri, int DocumentVersion, int Line, int Character); -} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/ILSPBreakpointSpanProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/ILSPBreakpointSpanProvider.cs new file mode 100644 index 0000000000..cdd2d28822 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/ILSPBreakpointSpanProvider.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; + +internal interface ILSPBreakpointSpanProvider +{ + Task GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/ILSPProximityExpressionsProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/ILSPProximityExpressionsProvider.cs new file mode 100644 index 0000000000..990d58f6db --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/ILSPProximityExpressionsProvider.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; + +internal interface ILSPProximityExpressionsProvider +{ + Task?> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPBreakpointSpanProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPBreakpointSpanProvider.cs index 0c1a05b4b7..c8adfc7df4 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPBreakpointSpanProvider.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPBreakpointSpanProvider.cs @@ -1,14 +1,50 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.ComponentModel.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.Debugging; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; -internal abstract class LSPBreakpointSpanProvider +[Export(typeof(ILSPBreakpointSpanProvider))] +[method: ImportingConstructor] +internal class LSPBreakpointSpanProvider( + LSPRequestInvoker requestInvoker, + ILoggerFactory loggerFactory) : ILSPBreakpointSpanProvider { - public abstract Task GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken); + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); + + public async Task GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken) + { + var languageQueryParams = new RazorBreakpointSpanParams() + { + Position = position, + Uri = documentSnapshot.Uri, + HostDocumentSyncVersion = hostDocumentSyncVersion + }; + + var response = await _requestInvoker.ReinvokeRequestOnServerAsync( + documentSnapshot.Snapshot.TextBuffer, + LanguageServerConstants.RazorBreakpointSpanEndpoint, + RazorLSPConstants.RazorLanguageServerName, + languageQueryParams, + cancellationToken).ConfigureAwait(false); + + var languageResponse = response?.Response; + if (languageResponse is null) + { + _logger.LogInformation($"The breakpoint position could not be mapped to a valid range."); + return null; + } + + return languageResponse.Range; + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPProximityExpressionsProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPProximityExpressionsProvider.cs index 20747767e8..955dc90112 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPProximityExpressionsProvider.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/LSPProximityExpressionsProvider.cs @@ -2,14 +2,49 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Generic; +using System.ComponentModel.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.Debugging; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; -internal abstract class LSPProximityExpressionsProvider +[Export(typeof(ILSPProximityExpressionsProvider))] +[method: ImportingConstructor] +internal class LSPProximityExpressionsProvider( + LSPRequestInvoker requestInvoker, + ILoggerFactory loggerFactory) : ILSPProximityExpressionsProvider { - public abstract Task?> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken); + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); + + public async Task?> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken) + { + var proximityExpressionsParams = new RazorProximityExpressionsParams() + { + Position = position, + Uri = documentSnapshot.Uri, + HostDocumentSyncVersion = hostDocumentSyncVersion + }; + + var response = await _requestInvoker.ReinvokeRequestOnServerAsync( + documentSnapshot.Snapshot.TextBuffer, + LanguageServerConstants.RazorProximityExpressionsEndpoint, + RazorLSPConstants.RazorLanguageServerName, + proximityExpressionsParams, + cancellationToken).ConfigureAwait(false); + + var languageResponse = response?.Response; + if (languageResponse is null) + { + _logger.LogInformation($"The proximity expressions could not be resolved."); + return null; + } + + return languageResponse.Expressions; + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/RazorBreakpointResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/RazorBreakpointResolver.cs new file mode 100644 index 0000000000..edc30f795f --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/RazorBreakpointResolver.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Debugging; +using Microsoft.VisualStudio.Text; +using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; + +[Export(typeof(IRazorBreakpointResolver))] +[method: ImportingConstructor] +internal class RazorBreakpointResolver( + FileUriProvider fileUriProvider, + LSPDocumentManager documentManager, + ILSPBreakpointSpanProvider breakpointSpanProvider) : IRazorBreakpointResolver +{ + private record CacheKey(Uri DocumentUri, long? HostDocumentSyncVersion, int Line, int Character); + + private readonly FileUriProvider _fileUriProvider = fileUriProvider; + private readonly LSPDocumentManager _documentManager = documentManager; + private readonly ILSPBreakpointSpanProvider _breakpointSpanProvider = breakpointSpanProvider; + + // 4 is a magic number that was determined based on the functionality of VisualStudio. Currently when you set or edit a breakpoint + // we get called with two different locations for the same breakpoint. Because of this 2 time call our size must be at least 2, + // we grow it to 4 just to be safe for lesser known scenarios. + private readonly MemoryCache _cache = new(sizeLimit: 4); + + public async Task TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken) + { + if (!_fileUriProvider.TryGet(textBuffer, out var documentUri)) + { + // Not an addressable Razor document. Do not allow a breakpoint here. In practice this shouldn't happen, just being defensive. + return null; + } + + if (!_documentManager.TryGetDocument(documentUri, out var documentSnapshot)) + { + // No associated Razor document. Do not allow a breakpoint here. In practice this shouldn't happen, just being defensive. + return null; + } + + // TODO: Support multiple C# documents per Razor document. + if (!documentSnapshot.TryGetVirtualDocument(out var virtualDocument) || + virtualDocument.HostDocumentSyncVersion is not { } hostDocumentSyncVersion) + { + Debug.Fail($"Some how there's no C# document associated with the host Razor document {documentUri.OriginalString} when validating breakpoint locations."); + return null; + } + + var cacheKey = new CacheKey(documentSnapshot.Uri, virtualDocument.HostDocumentSyncVersion, lineIndex, characterIndex); + if (_cache.TryGetValue(cacheKey, out var cachedRange)) + { + // We've seen this request before, no need to go async. + return cachedRange; + } + + var position = VsLspFactory.CreatePosition(lineIndex, characterIndex); + var hostDocumentRange = await _breakpointSpanProvider.GetBreakpointSpanAsync(documentSnapshot, hostDocumentSyncVersion, position, cancellationToken).ConfigureAwait(false); + if (hostDocumentRange is null) + { + // can't map the position, invalid breakpoint location. + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Cache range so if we're asked again for this document/line/character we don't have to go async. + _cache.Set(cacheKey, hostDocumentRange); + + return hostDocumentRange; + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/RazorProximityExpressionResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/RazorProximityExpressionResolver.cs new file mode 100644 index 0000000000..143fac97e9 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Debugging/RazorProximityExpressionResolver.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Debugging; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; + +[Export(typeof(IRazorProximityExpressionResolver))] +[method: ImportingConstructor] +internal class RazorProximityExpressionResolver( + FileUriProvider fileUriProvider, + LSPDocumentManager documentManager, + ILSPProximityExpressionsProvider proximityExpressionsProvider) : IRazorProximityExpressionResolver +{ + private record CacheKey(Uri DocumentUri, long? HostDocumentSyncVersion, int Line, int Character); + + private readonly FileUriProvider _fileUriProvider = fileUriProvider; + private readonly LSPDocumentManager _documentManager = documentManager; + private readonly ILSPProximityExpressionsProvider _proximityExpressionsProvider = proximityExpressionsProvider; + + // 10 is a magic number where this effectively represents our ability to cache the last 10 "hit" breakpoint locations + // corresponding proximity expressions which enables us not to go "async" in those re-hit scenarios. + private readonly MemoryCache> _cache = new(sizeLimit: 10); + + public async Task?> TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken) + { + if (!_fileUriProvider.TryGet(textBuffer, out var documentUri)) + { + // Not an addressable Razor document. Do not allow expression resolution here. In practice this shouldn't happen, just being defensive. + return null; + } + + if (!_documentManager.TryGetDocument(documentUri, out var documentSnapshot)) + { + // No associated Razor document. Do not resolve expressions here. In practice this shouldn't happen, just being defensive. + return null; + } + + // TODO: Support multiple C# documents per Razor document. + if (!documentSnapshot.TryGetVirtualDocument(out var virtualDocument) || + virtualDocument.HostDocumentSyncVersion is not { } hostDocumentSyncVersion) + { + Debug.Fail($"Some how there's no C# document associated with the host Razor document {documentUri.OriginalString} when resolving proximity expressions."); + return null; + } + + var cacheKey = new CacheKey(documentSnapshot.Uri, virtualDocument.HostDocumentSyncVersion, lineIndex, characterIndex); + if (_cache.TryGetValue(cacheKey, out var cachedExpressions)) + { + // We've seen this request before, no need to go async. + return cachedExpressions; + } + + var position = VsLspFactory.CreatePosition(lineIndex, characterIndex); + var proximityExpressions = await _proximityExpressionsProvider.GetProximityExpressionsAsync(documentSnapshot, hostDocumentSyncVersion, position, cancellationToken).ConfigureAwait(false); + + // Cache range so if we're asked again for this document/line/character we don't have to go async. + // Note: If we didn't get any proximity expressions back--likely due to an error--we cache an empty array. + _cache.Set(cacheKey, proximityExpressions ?? []); + + return proximityExpressions; + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorLanguageService_IVsLanguageDebugInfo.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorLanguageService_IVsLanguageDebugInfo.cs index 233def0b2a..4386df4d33 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorLanguageService_IVsLanguageDebugInfo.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorLanguageService_IVsLanguageDebugInfo.cs @@ -16,16 +16,16 @@ namespace Microsoft.VisualStudio.Razor; internal partial class RazorLanguageService : IVsLanguageDebugInfo { - private readonly RazorBreakpointResolver _breakpointResolver; - private readonly RazorProximityExpressionResolver _proximityExpressionResolver; + private readonly IRazorBreakpointResolver _breakpointResolver; + private readonly IRazorProximityExpressionResolver _proximityExpressionResolver; private readonly ILspServerActivationTracker _lspServerActivationTracker; private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactory; private readonly JoinableTaskFactory _joinableTaskFactory; public RazorLanguageService( - RazorBreakpointResolver breakpointResolver, - RazorProximityExpressionResolver proximityExpressionResolver, + IRazorBreakpointResolver breakpointResolver, + IRazorProximityExpressionResolver proximityExpressionResolver, ILspServerActivationTracker lspServerActivationTracker, IUIThreadOperationExecutor uiThreadOperationExecutor, IVsEditorAdaptersFactoryService editorAdaptersFactory, diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/RazorPackage.cs b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/RazorPackage.cs index acb9099758..6c9708a4f6 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/RazorPackage.cs +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/RazorPackage.cs @@ -64,8 +64,8 @@ internal sealed class RazorPackage : AsyncPackage container.AddService(typeof(RazorLanguageService), (container, type) => { var componentModel = (IComponentModel)GetGlobalService(typeof(SComponentModel)); - var breakpointResolver = componentModel.GetService(); - var proximityExpressionResolver = componentModel.GetService(); + var breakpointResolver = componentModel.GetService(); + var proximityExpressionResolver = componentModel.GetService(); var uiThreadOperationExecutor = componentModel.GetService(); var editorAdaptersFactory = componentModel.GetService(); var lspServerActivationTracker = componentModel.GetService(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorBreakpointSpanEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorBreakpointSpanEndpointTest.cs index 718cbcfc0d..7fa3f0ae3e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorBreakpointSpanEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorBreakpointSpanEndpointTest.cs @@ -39,7 +39,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; codeDocument.SetUnsupported(); var requestContext = CreateRazorRequestContext(documentContext); @@ -64,7 +65,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 5, length: 14); var requestContext = CreateRazorRequestContext(documentContext); @@ -89,7 +91,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 4, length: 12); var requestContext = CreateRazorRequestContext(documentContext); @@ -114,7 +117,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 5, length: 14); var requestContext = CreateRazorRequestContext(documentContext); @@ -139,7 +143,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var expectedRange = VsLspFactory.CreateSingleLineRange(line: 1, character: 4, length: 12); var requestContext = CreateRazorRequestContext(documentContext); @@ -165,7 +170,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -189,7 +195,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -216,7 +223,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(1, 0) + Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -243,7 +251,8 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase var request = new RazorBreakpointSpanParams() { Uri = documentPath, - Position = VsLspFactory.CreatePosition(2, 0) + Position = VsLspFactory.CreatePosition(2, 0), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -258,7 +267,7 @@ public class RazorBreakpointSpanEndpointTest : LanguageServerTestBase { var sourceDocument = TestRazorSourceDocument.Create(text); var projectEngine = RazorProjectEngine.Create(builder => { }); - var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind ?? FileKinds.Legacy, importSources: default, Array.Empty()); + var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind ?? FileKinds.Legacy, importSources: default, tagHelpers: []); return codeDocument; } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorProximityExpressionsEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorProximityExpressionsEndpointTest.cs index 66f45a89fc..092d8731e6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorProximityExpressionsEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Debugging/RazorProximityExpressionsEndpointTest.cs @@ -40,6 +40,7 @@ public class RazorProximityExpressionsEndpointTest : LanguageServerTestBase { Uri = documentPath, Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; codeDocument.SetUnsupported(); var requestContext = CreateRazorRequestContext(documentContext); @@ -65,6 +66,7 @@ public class RazorProximityExpressionsEndpointTest : LanguageServerTestBase { Uri = documentPath, Position = VsLspFactory.CreatePosition(1, 8), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -90,6 +92,7 @@ public class RazorProximityExpressionsEndpointTest : LanguageServerTestBase { Uri = documentPath, Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -115,6 +118,7 @@ public class RazorProximityExpressionsEndpointTest : LanguageServerTestBase { Uri = documentPath, Position = VsLspFactory.CreatePosition(1, 0), + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -142,6 +146,7 @@ public class RazorProximityExpressionsEndpointTest : LanguageServerTestBase { Uri = documentPath, Position = VsLspFactory.DefaultPosition, + HostDocumentSyncVersion = 0, }; var requestContext = CreateRazorRequestContext(documentContext); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/DefaultRazorBreakpointResolverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/RazorBreakpointResolverTest.cs similarity index 85% rename from src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/DefaultRazorBreakpointResolverTest.cs rename to src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/RazorBreakpointResolverTest.cs index f52c195fd0..5116954c42 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/DefaultRazorBreakpointResolverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/RazorBreakpointResolverTest.cs @@ -19,7 +19,7 @@ using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; -public class DefaultRazorBreakpointResolverTest : ToolingTestBase +public class RazorBreakpointResolverTest : ToolingTestBase { private const string ValidBreakpointCSharp = "private int foo = 123;"; private const string InvalidBreakpointCSharp = "private int bar;"; @@ -27,9 +27,9 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase private readonly ITextBuffer _csharpTextBuffer; private readonly Uri _documentUri; private readonly Uri _csharpDocumentUri; - private readonly ITextBuffer _hostTextbuffer; + private readonly ITextBuffer _hostTextBuffer; - public DefaultRazorBreakpointResolverTest(ITestOutputHelper testOutput) + public RazorBreakpointResolverTest(ITestOutputHelper testOutput) : base(testOutput) { _documentUri = new Uri("file://C:/path/to/file.razor", UriKind.Absolute); @@ -51,7 +51,7 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase {{InvalidBreakpointCSharp}} } """); - _hostTextbuffer = new TestTextBuffer(textBufferSnapshot); + _hostTextBuffer = new TestTextBuffer(textBufferSnapshot); } [Fact] @@ -77,7 +77,7 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase var resolver = CreateResolverWith(documentManager: documentManager); // Act - var result = await resolver.TryResolveBreakpointRangeAsync(_hostTextbuffer, lineIndex: 0, characterIndex: 1, DisposalToken); + var result = await resolver.TryResolveBreakpointRangeAsync(_hostTextBuffer, lineIndex: 0, characterIndex: 1, DisposalToken); // Assert Assert.Null(result); @@ -94,7 +94,7 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase var resolver = CreateResolverWith(documentManager: documentManager); // Act - var expressions = await resolver.TryResolveBreakpointRangeAsync(_hostTextbuffer, lineIndex: 0, characterIndex: 1, DisposalToken); + var expressions = await resolver.TryResolveBreakpointRangeAsync(_hostTextBuffer, lineIndex: 0, characterIndex: 1, DisposalToken); // Assert Assert.Null(expressions); @@ -107,7 +107,7 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase var resolver = CreateResolverWith(); // Act - var breakpointRange = await resolver.TryResolveBreakpointRangeAsync(_hostTextbuffer, lineIndex: 0, characterIndex: 1, DisposalToken); + var breakpointRange = await resolver.TryResolveBreakpointRangeAsync(_hostTextBuffer, lineIndex: 0, characterIndex: 1, DisposalToken); // Assert Assert.Null(breakpointRange); @@ -117,11 +117,11 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase public async Task TryResolveBreakpointRangeAsync_NotValidBreakpointLocation_ReturnsNull() { // Arrange - var hostDocumentPosition = GetPosition(InvalidBreakpointCSharp, _hostTextbuffer); + var hostDocumentPosition = GetPosition(InvalidBreakpointCSharp, _hostTextBuffer); var resolver = CreateResolverWith(); // Act - var breakpointRange = await resolver.TryResolveBreakpointRangeAsync(_hostTextbuffer, hostDocumentPosition.Line, hostDocumentPosition.Character, DisposalToken); + var breakpointRange = await resolver.TryResolveBreakpointRangeAsync(_hostTextBuffer, hostDocumentPosition.Line, hostDocumentPosition.Character, DisposalToken); // Assert Assert.Null(breakpointRange); @@ -131,7 +131,7 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase public async Task TryResolveBreakpointRangeAsync_MappableCSharpBreakpointLocation_ReturnsHostBreakpointLocation() { // Arrange - var hostDocumentPosition = GetPosition(ValidBreakpointCSharp, _hostTextbuffer); + var hostDocumentPosition = GetPosition(ValidBreakpointCSharp, _hostTextBuffer); var hostBreakpointRange = VsLspFactory.CreateSingleLineRange(start: hostDocumentPosition, length: ValidBreakpointCSharp.Length); var projectionProvider = new TestLSPBreakpointSpanProvider( _documentUri, @@ -142,34 +142,35 @@ public class DefaultRazorBreakpointResolverTest : ToolingTestBase var resolver = CreateResolverWith(projectionProvider: projectionProvider); // Act - var breakpointRange = await resolver.TryResolveBreakpointRangeAsync(_hostTextbuffer, hostDocumentPosition.Line, hostDocumentPosition.Character, DisposalToken); + var breakpointRange = await resolver.TryResolveBreakpointRangeAsync(_hostTextBuffer, hostDocumentPosition.Line, hostDocumentPosition.Character, DisposalToken); // Assert Assert.Equal(hostBreakpointRange, breakpointRange); } - private RazorBreakpointResolver CreateResolverWith( + private IRazorBreakpointResolver CreateResolverWith( FileUriProvider uriProvider = null, LSPDocumentManager documentManager = null, - LSPBreakpointSpanProvider projectionProvider = null) + ILSPBreakpointSpanProvider projectionProvider = null) { var documentUri = _documentUri; - uriProvider ??= Mock.Of(provider => provider.TryGet(_hostTextbuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextbuffer), out It.Ref.IsAny) == false, MockBehavior.Strict); + uriProvider ??= Mock.Of(provider => provider.TryGet(_hostTextBuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextBuffer), out It.Ref.IsAny) == false, MockBehavior.Strict); var csharpVirtualDocumentSnapshot = new CSharpVirtualDocumentSnapshot(projectKey: default, _csharpDocumentUri, _csharpTextBuffer.CurrentSnapshot, hostDocumentSyncVersion: 0); LSPDocumentSnapshot documentSnapshot = new TestLSPDocumentSnapshot(_documentUri, 0, csharpVirtualDocumentSnapshot); documentManager ??= Mock.Of(manager => manager.TryGetDocument(_documentUri, out documentSnapshot) == true, MockBehavior.Strict); if (projectionProvider is null) { - projectionProvider = new Mock(MockBehavior.Strict).Object; + projectionProvider = new Mock(MockBehavior.Strict).Object; Mock.Get(projectionProvider) .Setup(projectionProvider => projectionProvider.GetBreakpointSpanAsync( It.IsAny(), + It.IsAny(), It.IsAny(), DisposalToken)) .ReturnsAsync(value: null); } - var razorBreakpointResolver = new DefaultRazorBreakpointResolver(uriProvider, documentManager, projectionProvider); + var razorBreakpointResolver = new RazorBreakpointResolver(uriProvider, documentManager, projectionProvider); return razorBreakpointResolver; } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/DefaultRazorProximityExpressionResolverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/RazorProximityExpressionResolverTest.cs similarity index 83% rename from src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/DefaultRazorProximityExpressionResolverTest.cs rename to src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/RazorProximityExpressionResolverTest.cs index b107a2fb3f..23bc07fd62 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/DefaultRazorProximityExpressionResolverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/RazorProximityExpressionResolverTest.cs @@ -20,16 +20,16 @@ using Xunit.Abstractions; namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; -public class DefaultRazorProximityExpressionResolverTest : ToolingTestBase +public class RazorProximityExpressionResolverTest : ToolingTestBase { private readonly string _validProximityExpressionRoot; private readonly string _invalidProximityExpressionRoot; private readonly ITextBuffer _csharpTextBuffer; private readonly Uri _documentUri; private readonly Uri _csharpDocumentUri; - private readonly ITextBuffer _hostTextbuffer; + private readonly ITextBuffer _hostTextBuffer; - public DefaultRazorProximityExpressionResolverTest(ITestOutputHelper testOutput) + public RazorProximityExpressionResolverTest(ITestOutputHelper testOutput) : base(testOutput) { _documentUri = new Uri("file://C:/path/to/file.razor", UriKind.Absolute); @@ -51,7 +51,7 @@ public class DefaultRazorProximityExpressionResolverTest : ToolingTestBase _csharpTextBuffer = new TestTextBuffer(csharpTextSnapshot); var textBufferSnapshot = new StringTextSnapshot($$"""@{{{_invalidProximityExpressionRoot}}} @code {{{_validProximityExpressionRoot}}}"""); - _hostTextbuffer = new TestTextBuffer(textBufferSnapshot); + _hostTextBuffer = new TestTextBuffer(textBufferSnapshot); } [Fact] @@ -79,7 +79,7 @@ public class DefaultRazorProximityExpressionResolverTest : ToolingTestBase var resolver = CreateResolverWith(documentManager: documentManager); // Act - var result = await resolver.TryResolveProximityExpressionsAsync(_hostTextbuffer, lineIndex: 0, characterIndex: 1, DisposalToken); + var result = await resolver.TryResolveProximityExpressionsAsync(_hostTextBuffer, lineIndex: 0, characterIndex: 1, DisposalToken); // Assert Assert.Null(result); @@ -96,25 +96,25 @@ public class DefaultRazorProximityExpressionResolverTest : ToolingTestBase var resolver = CreateResolverWith(documentManager: documentManager); // Act - var expressions = await resolver.TryResolveProximityExpressionsAsync(_hostTextbuffer, lineIndex: 0, characterIndex: 1, DisposalToken); + var expressions = await resolver.TryResolveProximityExpressionsAsync(_hostTextBuffer, lineIndex: 0, characterIndex: 1, DisposalToken); // Assert Assert.Null(expressions); } - private RazorProximityExpressionResolver CreateResolverWith( + private IRazorProximityExpressionResolver CreateResolverWith( FileUriProvider uriProvider = null, LSPDocumentManager documentManager = null) { var documentUri = _documentUri; - uriProvider ??= Mock.Of(provider => provider.TryGet(_hostTextbuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextbuffer), out It.Ref.IsAny) == false, MockBehavior.Strict); + uriProvider ??= Mock.Of(provider => provider.TryGet(_hostTextBuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextBuffer), out It.Ref.IsAny) == false, MockBehavior.Strict); var csharpVirtualDocumentSnapshot = new CSharpVirtualDocumentSnapshot(projectKey: default, _csharpDocumentUri, _csharpTextBuffer.CurrentSnapshot, hostDocumentSyncVersion: 0); LSPDocumentSnapshot documentSnapshot = new TestLSPDocumentSnapshot(_documentUri, 0, csharpVirtualDocumentSnapshot); documentManager ??= Mock.Of( manager => manager.TryGetDocument(_documentUri, out documentSnapshot) == true, MockBehavior.Strict); - var razorProximityExpressionResolver = new DefaultRazorProximityExpressionResolver( + var razorProximityExpressionResolver = new RazorProximityExpressionResolver( uriProvider, documentManager, TestLSPProximityExpressionProvider.Instance); @@ -122,7 +122,7 @@ public class DefaultRazorProximityExpressionResolverTest : ToolingTestBase return razorProximityExpressionResolver; } - private class TestLSPProximityExpressionProvider : LSPProximityExpressionsProvider + private class TestLSPProximityExpressionProvider : ILSPProximityExpressionsProvider { public static readonly TestLSPProximityExpressionProvider Instance = new(); @@ -130,7 +130,7 @@ public class DefaultRazorProximityExpressionResolverTest : ToolingTestBase { } - public override Task> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) + public Task> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken) { return SpecializedTasks.Null>(); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/TestLSPBreakpointSpanProvider.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/TestLSPBreakpointSpanProvider.cs index b594365bdd..dc7e352755 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/TestLSPBreakpointSpanProvider.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/Debugging/TestLSPBreakpointSpanProvider.cs @@ -15,7 +15,7 @@ using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging; -internal class TestLSPBreakpointSpanProvider : LSPBreakpointSpanProvider +internal class TestLSPBreakpointSpanProvider : ILSPBreakpointSpanProvider { private readonly Uri _documentUri; private readonly IReadOnlyDictionary _mappings; @@ -36,7 +36,7 @@ internal class TestLSPBreakpointSpanProvider : LSPBreakpointSpanProvider _mappings = mappings; } - public override Task GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) + public Task GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken) { if (documentSnapshot.Uri != _documentUri) { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorLanguageService_IVsLanguageDebugInfoTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorLanguageService_IVsLanguageDebugInfoTest.cs index 4dd8aa8e59..b4509b4df5 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorLanguageService_IVsLanguageDebugInfoTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorLanguageService_IVsLanguageDebugInfoTest.cs @@ -70,7 +70,7 @@ public class RazorLanguageService_IVsLanguageDebugInfoTest(ITestOutputHelper tes { // Arrange var breakpointRange = VsLspFactory.CreateRange(2, 4, 3, 5); - var breakpointResolver = Mock.Of(resolver => resolver.TryResolveBreakpointRangeAsync(It.IsAny(), 0, 0, It.IsAny()) == System.Threading.Tasks.Task.FromResult(breakpointRange), MockBehavior.Strict); + var breakpointResolver = Mock.Of(resolver => resolver.TryResolveBreakpointRangeAsync(It.IsAny(), 0, 0, It.IsAny()) == System.Threading.Tasks.Task.FromResult(breakpointRange), MockBehavior.Strict); var languageService = CreateLanguageServiceWith(breakpointResolver); // Act @@ -146,7 +146,7 @@ public class RazorLanguageService_IVsLanguageDebugInfoTest(ITestOutputHelper tes { // Arrange IReadOnlyList expressions = new[] { "something" }; - var resolver = Mock.Of(resolver => resolver.TryResolveProximityExpressionsAsync(It.IsAny(), 0, 0, It.IsAny()) == System.Threading.Tasks.Task.FromResult(expressions), MockBehavior.Strict); + var resolver = Mock.Of(resolver => resolver.TryResolveProximityExpressionsAsync(It.IsAny(), 0, 0, It.IsAny()) == System.Threading.Tasks.Task.FromResult(expressions), MockBehavior.Strict); var languageService = CreateLanguageServiceWith(proximityExpressionResolver: resolver); // Act @@ -174,14 +174,14 @@ public class RazorLanguageService_IVsLanguageDebugInfoTest(ITestOutputHelper tes } private RazorLanguageService CreateLanguageServiceWith( - RazorBreakpointResolver breakpointResolver = null, - RazorProximityExpressionResolver proximityExpressionResolver = null, + IRazorBreakpointResolver breakpointResolver = null, + IRazorProximityExpressionResolver proximityExpressionResolver = null, IUIThreadOperationExecutor uiThreadOperationExecutor = null, IVsEditorAdaptersFactoryService editorAdaptersFactory = null) { if (breakpointResolver is null) { - breakpointResolver = new Mock(MockBehavior.Strict).Object; + breakpointResolver = new Mock(MockBehavior.Strict).Object; Mock.Get(breakpointResolver) .Setup(r => r.TryResolveBreakpointRangeAsync( It.IsAny(), @@ -193,7 +193,7 @@ public class RazorLanguageService_IVsLanguageDebugInfoTest(ITestOutputHelper tes if (proximityExpressionResolver is null) { - proximityExpressionResolver = new Mock(MockBehavior.Strict).Object; + proximityExpressionResolver = new Mock(MockBehavior.Strict).Object; Mock.Get(proximityExpressionResolver) .Setup(r => r.TryResolveProximityExpressionsAsync( It.IsAny(), diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/BreakpointSpanTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/BreakpointSpanTests.cs index 81eb2aa660..7cacf5c1d9 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/BreakpointSpanTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/BreakpointSpanTests.cs @@ -17,13 +17,15 @@ public class BreakpointSpanTests(ITestOutputHelper testOutputHelper) : AbstractR // Wait for classifications to indicate Razor LSP is up and running await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken); - await TestServices.Editor.SetTextAsync("

@{ var abc = 123; }

", ControlledHangMitigatingCancellationToken); - // Act - await TestServices.Debugger.SetBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 1, character: 1, ControlledHangMitigatingCancellationToken); + await TestServices.RazorProjectSystem.WaitForCSharpVirtualDocumentUpdateAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.CounterRazorFile, async () => + { + await TestServices.Editor.SetTextAsync("

@{ var abc = 123; }

", ControlledHangMitigatingCancellationToken); + }, ControlledHangMitigatingCancellationToken); - // Assert - await TestServices.Debugger.VerifyBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 1, character: 7, ControlledHangMitigatingCancellationToken); + Assert.True(await TestServices.Debugger.SetBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 1, character: 1, ControlledHangMitigatingCancellationToken)); + + Assert.True(await TestServices.Debugger.VerifyBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 1, character: 7, ControlledHangMitigatingCancellationToken)); } [IdeFact] @@ -34,15 +36,17 @@ public class BreakpointSpanTests(ITestOutputHelper testOutputHelper) : AbstractR // Wait for classifications to indicate Razor LSP is up and running await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken); - await TestServices.Editor.SetTextAsync(@"

@{ - var abc = 123; -}

", ControlledHangMitigatingCancellationToken); - // Act - var result = await TestServices.Debugger.SetBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 1, character: 1, ControlledHangMitigatingCancellationToken); + await TestServices.RazorProjectSystem.WaitForCSharpVirtualDocumentUpdateAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.CounterRazorFile, async () => + { + await TestServices.Editor.SetTextAsync(""" +

@{ + var abc = 123; + }

+ """, ControlledHangMitigatingCancellationToken); + }, ControlledHangMitigatingCancellationToken); - // Assert - Assert.False(result); + Assert.False(await TestServices.Debugger.SetBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 1, character: 1, ControlledHangMitigatingCancellationToken)); } [IdeFact] @@ -53,14 +57,18 @@ public class BreakpointSpanTests(ITestOutputHelper testOutputHelper) : AbstractR // Wait for classifications to indicate Razor LSP is up and running await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken); - await TestServices.Editor.SetTextAsync(@"

@{ - var abc = 123; -}

", ControlledHangMitigatingCancellationToken); - // Act - await TestServices.Debugger.SetBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 2, character: 1, ControlledHangMitigatingCancellationToken); + await TestServices.RazorProjectSystem.WaitForCSharpVirtualDocumentUpdateAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.CounterRazorFile, async () => + { + await TestServices.Editor.SetTextAsync(""" +

@{ + var abc = 123; + }

+ """, ControlledHangMitigatingCancellationToken); + }, ControlledHangMitigatingCancellationToken); - // Assert - await TestServices.Debugger.VerifyBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 2, character: 4, ControlledHangMitigatingCancellationToken); + Assert.True(await TestServices.Debugger.SetBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 2, character: 1, ControlledHangMitigatingCancellationToken)); + + Assert.True(await TestServices.Debugger.VerifyBreakpointAsync(RazorProjectConstants.CounterRazorFile, line: 2, character: 5, ControlledHangMitigatingCancellationToken)); } } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/DebuggerInProcess.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/DebuggerInProcess.cs index d8c08224a5..4d8c7f4529 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/DebuggerInProcess.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/DebuggerInProcess.cs @@ -39,7 +39,7 @@ internal partial class DebuggerInProcess foreach (EnvDTE.Breakpoint breakpoint in debugger.Breakpoints) { - if (breakpoint.File == fileName && + if (breakpoint.File.EndsWith(fileName) && breakpoint.FileLine == line && breakpoint.FileColumn == character) { diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs index 04f8659b0f..98b4f5c753 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/RazorProjectSystemInProcess.cs @@ -127,4 +127,43 @@ internal partial class RazorProjectSystemInProcess }, TimeSpan.FromMilliseconds(100), cancellationToken); } + + public async Task WaitForCSharpVirtualDocumentUpdateAsync(string projectName, string relativeFilePath, Func updater, CancellationToken cancellationToken) + { + var filePath = await TestServices.SolutionExplorer.GetAbsolutePathForProjectRelativeFilePathAsync(projectName, relativeFilePath, cancellationToken); + + var documentManager = await TestServices.Shell.GetComponentModelServiceAsync(cancellationToken); + + var uri = new Uri(filePath, UriKind.Absolute); + + long? desiredVersion = null; + + await Helper.RetryAsync(async ct => + { + if (documentManager.TryGetDocument(uri, out var snapshot)) + { + if (snapshot.TryGetVirtualDocument(out var virtualDocument)) + { + if (!virtualDocument.ProjectKey.IsUnknown && + virtualDocument.Snapshot.Length > 0) + { + if (desiredVersion is null) + { + desiredVersion = virtualDocument.HostDocumentSyncVersion + 1; + await updater(); + } + else if (virtualDocument.HostDocumentSyncVersion == desiredVersion) + { + return true; + } + } + + return false; + } + } + + return false; + + }, TimeSpan.FromMilliseconds(100), cancellationToken); + } }