File: Handler\SelectionRanges\SelectionRangeHandler.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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
 
[ExportCSharpVisualBasicStatelessLspService(typeof(SelectionRangeHandler)), Shared]
[Method(Methods.TextDocumentSelectionRangeName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class SelectionRangeHandler() : ILspServiceDocumentRequestHandler<SelectionRangeParams, SelectionRange[]?>
{
    public bool MutatesSolutionState => false;
    public bool RequiresLSPSolution => true;
 
    public TextDocumentIdentifier GetTextDocumentIdentifier(SelectionRangeParams request) => request.TextDocument;
 
    public async Task<SelectionRange[]?> HandleRequestAsync(SelectionRangeParams request, RequestContext context, CancellationToken cancellationToken)
    {
        var document = context.Document;
        if (document is null)
            return null;
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        using var _ = ArrayBuilder<SelectionRange>.GetInstance(out var results);
        foreach (var position in request.Positions)
        {
            var linePosition = ProtocolConversions.PositionToLinePosition(position);
            var absolutePosition = text.Lines.GetPosition(linePosition);
 
            results.Add(GetSelectionRange(root, text, absolutePosition));
        }
 
        return results.ToArray();
    }
 
    private static SelectionRange GetSelectionRange(SyntaxNode root, SourceText text, int position)
    {
        // FindToken().Parent is null only for EOF tokens at the compilation unit level;
        // falling back to root ensures we still return a valid selection range in that case.
        var node = root.FindToken(position).Parent ?? root;
 
        // Collect spans from innermost to outermost, deduplicating consecutive equal spans.
        using var _ = ArrayBuilder<TextSpan>.GetInstance(out var spans);
        var previousSpan = (TextSpan?)null;
        foreach (var ancestor in node.AncestorsAndSelf())
        {
            var span = ancestor.Span;
 
            // Skip nodes with empty spans and deduplicate nodes that cover the same span.
            if (span.IsEmpty || span == previousSpan)
                continue;
 
            spans.Add(span);
            previousSpan = span;
        }
 
        // Build the SelectionRange linked list from outermost to innermost so that each
        // SelectionRange's Parent refers to a larger enclosing range, as the LSP spec requires.
        SelectionRange? current = null;
        for (var i = spans.Count - 1; i >= 0; i--)
        {
            current = new SelectionRange
            {
                Range = ProtocolConversions.TextSpanToRange(spans[i], text),
                Parent = current
            };
        }
 
        // If we somehow ended up with nothing (e.g. empty file), return an empty range at the position.
        return current ?? new SelectionRange { Range = ProtocolConversions.TextSpanToRange(new TextSpan(position, 0), text) };
    }
}