File: LanguageClient\Debugging\RazorBreakpointResolver.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.VisualStudio.LanguageServices.Razor\Microsoft.VisualStudio.LanguageServices.Razor.csproj (Microsoft.VisualStudio.LanguageServices.Razor)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Razor.Debugging;
using Microsoft.VisualStudio.Text;
 
namespace Microsoft.VisualStudio.Razor.LanguageClient.Debugging;
 
[Export(typeof(IRazorBreakpointResolver))]
[method: ImportingConstructor]
internal class RazorBreakpointResolver(
    IRemoteServiceInvoker remoteServiceInvoker) : IRazorBreakpointResolver
{
    private record CohostCacheKey(DocumentId DocumentId, VersionStamp Version, int Line, int Character) : CacheKey;
    private record LspCacheKey(Uri DocumentUri, long? HostDocumentSyncVersion, int Line, int Character) : CacheKey;
    private record CacheKey;
 
    private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
 
    // 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, LspRange> _cache = new(sizeLimit: 4);
 
    public async Task<LspRange?> TryResolveBreakpointRangeAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken)
    {
        if (!textBuffer.TryGetTextDocument(out var razorDocument))
        {
            // Razor document is not in the Roslyn workspace.
            return null;
        }
 
        if (razorDocument.TryGetTextVersion(out var version))
        {
            version = await razorDocument.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
        }
 
        var cacheKey = new CohostCacheKey(razorDocument.Id, version, lineIndex, characterIndex);
        if (_cache.TryGetValue(cacheKey, out var cachedRange))
        {
            // We've seen this request before. Hopefully the TryGetTextVersion call above was successful so this whole path
            // will have been sync, and the cache will have been useful.
            return cachedRange;
        }
 
        var response = await _remoteServiceInvoker
            .TryInvokeAsync<IRemoteDebugInfoService, LinePositionSpan?>(
                razorDocument.Project.Solution,
                (service, solutionInfo, cancellationToken) =>
                    service.ResolveBreakpointRangeAsync(solutionInfo, razorDocument.Id, new(lineIndex, characterIndex), cancellationToken),
                cancellationToken)
            .ConfigureAwait(false);
 
        if (response is not { } responseSpan)
        {
            // can't map the position, invalid breakpoint location.
            return null;
        }
 
        var hostDocumentRange = responseSpan.ToRange();
        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;
    }
}