File: Handler\Breakpoints\ValidateBreakableRangeHandler.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
 
[ExportCSharpVisualBasicStatelessLspService(typeof(ValidateBreakableRangeHandler)), Shared]
[Method(LSP.VSInternalMethods.TextDocumentValidateBreakableRangeName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class ValidateBreakableRangeHandler() : ILspServiceDocumentRequestHandler<VSInternalValidateBreakableRangeParams, LSP.Range?>
{
    public bool MutatesSolutionState => false;
    public bool RequiresLSPSolution => true;
 
    public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.VSInternalValidateBreakableRangeParams request)
        => request.TextDocument;
 
    public Task<LSP.Range?> HandleRequestAsync(LSP.VSInternalValidateBreakableRangeParams request, RequestContext context, CancellationToken cancellationToken)
        => GetBreakableRangeAsync(context.GetRequiredDocument(), request.Range, cancellationToken);
 
    public static async Task<LSP.Range?> GetBreakableRangeAsync(Document document, LSP.Range range, CancellationToken cancellationToken)
    {
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var span = ProtocolConversions.RangeToTextSpan(range, text);
        var breakpointService = document.Project.Services.GetRequiredService<IBreakpointResolutionService>();
 
        if (span.Length > 0)
        {
            // If we have a non-empty span then it means that the debugger is asking us to adjust an
            // existing span.  In Everett we didn't do this so we had some good and some bad
            // behavior.  For example if you had a breakpoint on: "int i = 1;" and you changed it to "int
            // i = 1, j = 2;", then the breakpoint wouldn't adjust.  That was bad.  However, if you had the
            // breakpoint on an open or close curly brace then it would always "stick" to that brace
            // which was good.
            //
            // So we want to keep the best parts of both systems.  We want to appropriately "stick"
            // to tokens and we also want to adjust spans intelligently.
            //
            // However, it turns out the latter is hard to do when there are parse errors in the
            // code.  Things like missing name nodes cause a lot of havoc and make it difficult to
            // track a closing curly brace.
            //
            // So the way we do this is that we default to not intelligently adjusting the spans
            // while there are parse errors.  But when there are no parse errors then the span is
            // adjusted.
            if (document.SupportsSyntaxTree)
            {
                var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                Contract.ThrowIfNull(tree);
                if (tree.GetDiagnostics(cancellationToken).Any(d => d.Severity == DiagnosticSeverity.Error))
                {
                    // Keep the span as is.
                    return range;
                }
            }
        }
 
        var result = await breakpointService.ResolveBreakpointAsync(document, span, cancellationToken).ConfigureAwait(false);
        if (result == null)
        {
            return null;
        }
 
        // zero-width range means line breakpoint:
        var breakpointSpan = result.IsLineBreakpoint ? new TextSpan(span.Start, length: 0) : result.TextSpan;
 
        var breakpointRange = ProtocolConversions.TextSpanToRange(breakpointSpan, text);
 
        // if the breakpoint we get is smaller than what was requested, then we might be in a situation where
        // the breakpoint was expanded due to the user typing some code above the placement. For example:
        //
        //     $$
        // BP: Console.WriteLine(1);
        //
        // If the user types "int a =" we'll expand the breakpoint, as syntactically its an assigment expression, but then
        // when they continue to type "1;" we'll get a request for a breakpoint that spans two lines, and then the above
        // resolve call will shrink it to one. In that case, we prefer to stick to the end of the requested range.
        //
        // Similar exists for a single line, for example give:
        //
        // BP: int a = $$ GetData();
        //
        // If the user types "1;" we'd shrink the breakpoint, so stick to the end of the range.
        if (!result.IsLineBreakpoint && BreakpointRangeIsSmaller(breakpointRange, range))
        {
            var secondResult = await breakpointService.ResolveBreakpointAsync(document, new TextSpan(span.End, length: 0), cancellationToken).ConfigureAwait(false);
            if (secondResult is not null)
            {
                breakpointSpan = secondResult.IsLineBreakpoint ? new TextSpan(span.Start, length: 0) : secondResult.TextSpan;
                breakpointRange = ProtocolConversions.TextSpanToRange(breakpointSpan, text);
            }
        }
 
        return breakpointRange;
    }
 
    private static bool BreakpointRangeIsSmaller(LSP.Range breakpointRange, LSP.Range existingRange)
    {
        var breakpointLineDelta = breakpointRange.End.Line - breakpointRange.Start.Line;
        var existingLineDelta = existingRange.End.Line - existingRange.Start.Line;
        return breakpointLineDelta < existingLineDelta ||
            (breakpointLineDelta == existingLineDelta &&
            breakpointRange.End.Character - breakpointRange.Start.Character < existingRange.End.Character - existingRange.Start.Character);
    }
}