Fix setting breakpoints with self versioned documents (#10811)

Fixes an issue found in app building. Thanks @phil-allen-msft!

Also fixes https://github.com/dotnet/razor/issues/9161

To make reviewing easier:
* First commit is entirely mechanical cleanup, renames, etc. and can be
skipped.
* Second commit is the fix.
* Third commit is tests.
* Fourth commit is updating more tests because these days when you ask
VS to build things it doesn't build all of it and I need to get into the
habit of doing a command line build before pushing

Because of the type and file renames, looking at the PR as a whole is
inadvised.
This commit is contained in:
David Wengier 2024-08-30 06:33:07 +10:00 коммит произвёл GitHub
Родитель 12f5194184 ebe3878544
Коммит e1f6fbaad7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
27 изменённых файлов: 424 добавлений и 480 удалений

Просмотреть файл

@ -19,30 +19,17 @@ using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Debugging;
[RazorLanguageServerEndpoint(LanguageServerConstants.RazorBreakpointSpanEndpoint)]
internal class RazorBreakpointSpanEndpoint : IRazorDocumentlessRequestHandler<RazorBreakpointSpanParams, RazorBreakpointSpanResponse?>, ITextDocumentIdentifierHandler<RazorBreakpointSpanParams, Uri>
internal class RazorBreakpointSpanEndpoint(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory) : IRazorDocumentlessRequestHandler<RazorBreakpointSpanParams, RazorBreakpointSpanResponse?>, ITextDocumentIdentifierHandler<RazorBreakpointSpanParams, Uri>
{
private readonly IDocumentMappingService _documentMappingService;
private readonly ILogger _logger;
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RazorBreakpointSpanEndpoint>();
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<RazorBreakpointSpanEndpoint>();
}
public Uri GetTextDocumentIdentifier(RazorBreakpointSpanParams request)
{
return request.Uri;
}
=> request.Uri;
public async Task<RazorBreakpointSpanResponse?> HandleRequestAsync(RazorBreakpointSpanParams request, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
@ -52,6 +39,12 @@ internal class RazorBreakpointSpanEndpoint : IRazorDocumentlessRequestHandler<Ra
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);

Просмотреть файл

@ -20,35 +20,17 @@ using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Debugging;
[RazorLanguageServerEndpoint(LanguageServerConstants.RazorProximityExpressionsEndpoint)]
internal class RazorProximityExpressionsEndpoint : IRazorDocumentlessRequestHandler<RazorProximityExpressionsParams, RazorProximityExpressionsResponse?>, ITextDocumentIdentifierHandler<RazorProximityExpressionsParams, Uri>
internal class RazorProximityExpressionsEndpoint(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory) : IRazorDocumentlessRequestHandler<RazorProximityExpressionsParams, RazorProximityExpressionsResponse?>, ITextDocumentIdentifierHandler<RazorProximityExpressionsParams, Uri>
{
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<RazorBreakpointSpanEndpoint>();
}
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RazorBreakpointSpanEndpoint>();
public bool MutatesSolutionState => false;
public Uri GetTextDocumentIdentifier(RazorProximityExpressionsParams request)
{
return request.Uri;
}
=> request.Uri;
public async Task<RazorProximityExpressionsResponse?> 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);

Просмотреть файл

@ -14,4 +14,7 @@ internal class RazorBreakpointSpanParams
[JsonPropertyName("position")]
public required Position Position { get; init; }
[JsonPropertyName("hostDocumentSyncVersion")]
public required long HostDocumentSyncVersion { get; init; }
}

Просмотреть файл

@ -14,4 +14,7 @@ internal class RazorProximityExpressionsParams
[JsonPropertyName("position")]
public required Position Position { get; init; }
[JsonPropertyName("hostDocumentSyncVersion")]
public required long HostDocumentSyncVersion { get; init; }
}

Просмотреть файл

@ -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<Range?> TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken);
Task<Range?> TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken);
}

Просмотреть файл

@ -8,7 +8,7 @@ using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.Razor.Debugging;
internal abstract class RazorProximityExpressionResolver
internal interface IRazorProximityExpressionResolver
{
public abstract Task<IReadOnlyList<string>?> TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken);
Task<IReadOnlyList<string>?> TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken);
}

Просмотреть файл

@ -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<DefaultLSPBreakpointSpanProvider>();
}
public async override Task<Range?> 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<RazorBreakpointSpanParams, RazorBreakpointSpanResponse>(
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;
}
}

Просмотреть файл

@ -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<DefaultLSPProximityExpressionsProvider>();
}
public async override Task<IReadOnlyList<string>?> 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<RazorProximityExpressionsParams, RazorProximityExpressionsResponse>(
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;
}
}

Просмотреть файл

@ -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<CacheKey, Range> _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<CacheKey, Range>(sizeLimit: 4);
}
public override async Task<Range?> 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<CSharpVirtualDocumentSnapshot>(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);
}

Просмотреть файл

@ -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<CacheKey, IReadOnlyList<string>> _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<CacheKey, IReadOnlyList<string>>(sizeLimit: 10);
}
public override async Task<IReadOnlyList<string>?> 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<CSharpVirtualDocumentSnapshot>(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<string>());
return proximityExpressions;
}
private record CacheKey(Uri DocumentUri, int DocumentVersion, int Line, int Character);
}

Просмотреть файл

@ -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<Range?> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken);
}

Просмотреть файл

@ -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<IReadOnlyList<string>?> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken);
}

Просмотреть файл

@ -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<Range?> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken);
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<LSPBreakpointSpanProvider>();
public async Task<Range?> 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<RazorBreakpointSpanParams, RazorBreakpointSpanResponse>(
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;
}
}

Просмотреть файл

@ -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<IReadOnlyList<string>?> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken);
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<LSPProximityExpressionsProvider>();
public async Task<IReadOnlyList<string>?> 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<RazorProximityExpressionsParams, RazorProximityExpressionsResponse>(
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;
}
}

Просмотреть файл

@ -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<CacheKey, Range> _cache = new(sizeLimit: 4);
public async Task<Range?> 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<CSharpVirtualDocumentSnapshot>(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;
}
}

Просмотреть файл

@ -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<CacheKey, IReadOnlyList<string>> _cache = new(sizeLimit: 10);
public async Task<IReadOnlyList<string>?> 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<CSharpVirtualDocumentSnapshot>(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;
}
}

Просмотреть файл

@ -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,

Просмотреть файл

@ -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<RazorBreakpointResolver>();
var proximityExpressionResolver = componentModel.GetService<RazorProximityExpressionResolver>();
var breakpointResolver = componentModel.GetService<IRazorBreakpointResolver>();
var proximityExpressionResolver = componentModel.GetService<IRazorProximityExpressionResolver>();
var uiThreadOperationExecutor = componentModel.GetService<IUIThreadOperationExecutor>();
var editorAdaptersFactory = componentModel.GetService<IVsEditorAdaptersFactoryService>();
var lspServerActivationTracker = componentModel.GetService<ILspServerActivationTracker>();

Просмотреть файл

@ -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<TagHelperDescriptor>());
var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind ?? FileKinds.Legacy, importSources: default, tagHelpers: []);
return codeDocument;
}
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -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<FileUriProvider>(provider => provider.TryGet(_hostTextbuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextbuffer), out It.Ref<Uri>.IsAny) == false, MockBehavior.Strict);
uriProvider ??= Mock.Of<FileUriProvider>(provider => provider.TryGet(_hostTextBuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextBuffer), out It.Ref<Uri>.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<LSPDocumentManager>(manager => manager.TryGetDocument(_documentUri, out documentSnapshot) == true, MockBehavior.Strict);
if (projectionProvider is null)
{
projectionProvider = new Mock<LSPBreakpointSpanProvider>(MockBehavior.Strict).Object;
projectionProvider = new Mock<ILSPBreakpointSpanProvider>(MockBehavior.Strict).Object;
Mock.Get(projectionProvider)
.Setup(projectionProvider => projectionProvider.GetBreakpointSpanAsync(
It.IsAny<LSPDocumentSnapshot>(),
It.IsAny<long>(),
It.IsAny<Position>(),
DisposalToken))
.ReturnsAsync(value: null);
}
var razorBreakpointResolver = new DefaultRazorBreakpointResolver(uriProvider, documentManager, projectionProvider);
var razorBreakpointResolver = new RazorBreakpointResolver(uriProvider, documentManager, projectionProvider);
return razorBreakpointResolver;
}

Просмотреть файл

@ -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<FileUriProvider>(provider => provider.TryGet(_hostTextbuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextbuffer), out It.Ref<Uri>.IsAny) == false, MockBehavior.Strict);
uriProvider ??= Mock.Of<FileUriProvider>(provider => provider.TryGet(_hostTextBuffer, out documentUri) == true && provider.TryGet(It.IsNotIn(_hostTextBuffer), out It.Ref<Uri>.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<LSPDocumentManager>(
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<IReadOnlyList<string>> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken)
public Task<IReadOnlyList<string>> GetProximityExpressionsAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken)
{
return SpecializedTasks.Null<IReadOnlyList<string>>();
}

Просмотреть файл

@ -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<Position, Range> _mappings;
@ -36,7 +36,7 @@ internal class TestLSPBreakpointSpanProvider : LSPBreakpointSpanProvider
_mappings = mappings;
}
public override Task<Range> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken)
public Task<Range> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, long hostDocumentSyncVersion, Position position, CancellationToken cancellationToken)
{
if (documentSnapshot.Uri != _documentUri)
{

Просмотреть файл

@ -70,7 +70,7 @@ public class RazorLanguageService_IVsLanguageDebugInfoTest(ITestOutputHelper tes
{
// Arrange
var breakpointRange = VsLspFactory.CreateRange(2, 4, 3, 5);
var breakpointResolver = Mock.Of<RazorBreakpointResolver>(resolver => resolver.TryResolveBreakpointRangeAsync(It.IsAny<ITextBuffer>(), 0, 0, It.IsAny<CancellationToken>()) == System.Threading.Tasks.Task.FromResult(breakpointRange), MockBehavior.Strict);
var breakpointResolver = Mock.Of<IRazorBreakpointResolver>(resolver => resolver.TryResolveBreakpointRangeAsync(It.IsAny<ITextBuffer>(), 0, 0, It.IsAny<CancellationToken>()) == 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<string> expressions = new[] { "something" };
var resolver = Mock.Of<RazorProximityExpressionResolver>(resolver => resolver.TryResolveProximityExpressionsAsync(It.IsAny<ITextBuffer>(), 0, 0, It.IsAny<CancellationToken>()) == System.Threading.Tasks.Task.FromResult(expressions), MockBehavior.Strict);
var resolver = Mock.Of<IRazorProximityExpressionResolver>(resolver => resolver.TryResolveProximityExpressionsAsync(It.IsAny<ITextBuffer>(), 0, 0, It.IsAny<CancellationToken>()) == 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<RazorBreakpointResolver>(MockBehavior.Strict).Object;
breakpointResolver = new Mock<IRazorBreakpointResolver>(MockBehavior.Strict).Object;
Mock.Get(breakpointResolver)
.Setup(r => r.TryResolveBreakpointRangeAsync(
It.IsAny<ITextBuffer>(),
@ -193,7 +193,7 @@ public class RazorLanguageService_IVsLanguageDebugInfoTest(ITestOutputHelper tes
if (proximityExpressionResolver is null)
{
proximityExpressionResolver = new Mock<RazorProximityExpressionResolver>(MockBehavior.Strict).Object;
proximityExpressionResolver = new Mock<IRazorProximityExpressionResolver>(MockBehavior.Strict).Object;
Mock.Get(proximityExpressionResolver)
.Setup(r => r.TryResolveProximityExpressionsAsync(
It.IsAny<ITextBuffer>(),

Просмотреть файл

@ -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("<p>@{ var abc = 123; }</p>", 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("<p>@{ var abc = 123; }</p>", 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(@"<p>@{
var abc = 123;
}</p>", 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("""
<p>@{
var abc = 123;
}</p>
""", 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(@"<p>@{
var abc = 123;
}</p>", 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("""
<p>@{
var abc = 123;
}</p>
""", 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));
}
}

Просмотреть файл

@ -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)
{

Просмотреть файл

@ -127,4 +127,43 @@ internal partial class RazorProjectSystemInProcess
}, TimeSpan.FromMilliseconds(100), cancellationToken);
}
public async Task WaitForCSharpVirtualDocumentUpdateAsync(string projectName, string relativeFilePath, Func<Task> updater, CancellationToken cancellationToken)
{
var filePath = await TestServices.SolutionExplorer.GetAbsolutePathForProjectRelativeFilePathAsync(projectName, relativeFilePath, cancellationToken);
var documentManager = await TestServices.Shell.GetComponentModelServiceAsync<LSPDocumentManager>(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<CSharpVirtualDocumentSnapshot>(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);
}
}