File: DocumentOutline\DocumentOutlineViewModel_Utilities.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Client;
using Microsoft.VisualStudio.Text;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.DocumentOutline;
internal delegate Task<TResponse?> LanguageServiceBrokerCallback<TRequest, TResponse>(Request<TRequest, TResponse> request, CancellationToken cancellationToken);
 
internal sealed partial class DocumentOutlineViewModel
{
    /// <summary>
    /// Makes an LSP document symbol request and returns the response and the text snapshot used at 
    /// the time the LSP client sends the request to the server.
    /// </summary>
    public static async Task<(RoslynDocumentSymbol[] response, ITextSnapshot snapshot)?> DocumentSymbolsRequestAsync(
        ITextBuffer textBuffer,
        LanguageServiceBrokerCallback<RoslynDocumentSymbolParams, RoslynDocumentSymbol[]> callbackAsync,
        string textViewFilePath,
        CancellationToken cancellationToken)
    {
        ITextSnapshot? requestSnapshot = null;
 
        var request = new DocumentRequest<RoslynDocumentSymbolParams, RoslynDocumentSymbol[]>()
        {
            Method = Methods.TextDocumentDocumentSymbolName,
            LanguageServerName = WellKnownLspServerKinds.AlwaysActiveVSLspServer.ToUserVisibleString(),
            TextBuffer = textBuffer,
            ParameterFactory = (snapshot) =>
            {
                requestSnapshot = snapshot;
                return new RoslynDocumentSymbolParams
                {
                    TextDocument = new TextDocumentIdentifier
                    {
                        Uri = ProtocolConversions.CreateAbsoluteUri(textViewFilePath),
                    },
                    UseHierarchicalSymbols = true
                };
            }
        };
 
        var response = await callbackAsync(request, cancellationToken).ConfigureAwait(false);
 
        // The request snapshot or response can be null if there is no LSP server implementation for
        // the document symbol request for that language.
        return requestSnapshot is null || response is null ? null : (response, requestSnapshot);
    }
 
    /// <summary>
    /// Given an array of Document Symbols in a document, returns a DocumentSymbolDataModel.
    /// </summary>
    /// 
    /// As of right now, the LSP document symbol response only has at most 2 levels of nesting, 
    /// so we nest the symbols first before converting the LSP DocumentSymbols to DocumentSymbolData.
    /// 
    /// Example file structure:
    /// Class A
    ///     ClassB
    ///         Method1
    ///         Method2
    ///         
    /// LSP document symbol response:
    /// [
    ///     {
    ///         Name: ClassA,
    ///         Children: []
    ///     },
    ///     {
    ///         Name: ClassB,
    ///         Children: 
    ///         [
    ///             {
    ///                 Name: Method1,
    ///                 Children: []
    ///             },
    ///             {
    ///                 Name: Method2,
    ///                 Children: []
    ///             }
    ///         ]
    ///     }
    /// ]
    public static ImmutableArray<DocumentSymbolData> CreateDocumentSymbolData(RoslynDocumentSymbol[] documentSymbols, ITextSnapshot textSnapshot)
    {
        // Obtain a flat list of all the document symbols sorted by location in the document.
        var allSymbols = documentSymbols
            .SelectMany(x => x.Children)
            .Concat(documentSymbols)
            .OrderBy(x => x.Range.Start.Line)
            .ThenBy(x => x.Range.Start.Character)
            .ToImmutableArray();
 
        // Iterate through the document symbols, nest them, and add the top level symbols to finalResult.
        using var _1 = ArrayBuilder<DocumentSymbolData>.GetInstance(out var finalResult);
        var currentStart = 0;
        while (currentStart < allSymbols.Length)
            finalResult.Add(NestDescendantSymbols(allSymbols, currentStart, out currentStart));
 
        return finalResult.ToImmutableAndClear();
 
        // Returns the symbol in the list at index start (the parent symbol) with the following symbols in the list
        // (descendants) appropriately nested into the parent.
        DocumentSymbolData NestDescendantSymbols(ImmutableArray<RoslynDocumentSymbol> allSymbols, int start, out int newStart)
        {
            var currentParent = allSymbols[start];
            start++;
            newStart = start;
 
            // Iterates through the following symbols and checks whether the next symbol is in range of the parent and needs
            // to be nested into the current parent symbol (along with following symbols that may be siblings/grandchildren/etc)
            // or if the next symbol is a new parent.
            using var _2 = ArrayBuilder<DocumentSymbolData>.GetInstance(out var currentSymbolChildren);
            while (newStart < allSymbols.Length)
            {
                var nextSymbol = allSymbols[newStart];
 
                // If the next symbol in the list is not in range of the current parent (i.e. is a new parent), break.
                if (!Contains(currentParent, nextSymbol))
                    break;
 
                // Otherwise, nest this child symbol and add it to currentSymbolChildren.
                currentSymbolChildren.Add(NestDescendantSymbols(allSymbols, start: newStart, out newStart));
            }
 
            // Return the nested parent symbol.
            return new DocumentSymbolData(
                currentParent.Detail ?? currentParent.Name,
                (Roslyn.LanguageServer.Protocol.SymbolKind)currentParent.Kind,
                (Glyph)currentParent.Glyph,
                GetSymbolRangeSpan(currentParent.Range),
                GetSymbolRangeSpan(currentParent.SelectionRange),
                currentSymbolChildren.ToImmutable());
        }
 
        // Returns whether the child symbol is in range of the parent symbol.
        static bool Contains(RoslynDocumentSymbol parent, RoslynDocumentSymbol child)
        {
            var parentRange = RangeToLinePositionSpan(parent.Range);
            var childRange = RangeToLinePositionSpan(child.Range);
            return childRange.Start > parentRange.Start && childRange.End <= parentRange.End;
 
            static LinePositionSpan RangeToLinePositionSpan(Range range)
            {
                return new(new LinePosition(range.Start.Line, range.Start.Character), new LinePosition(range.End.Line, range.End.Character));
            }
        }
 
        // Converts a Document Symbol Range to a SnapshotSpan within the text snapshot used for the LSP request.
        SnapshotSpan GetSymbolRangeSpan(Range symbolRange)
        {
            var originalStartPosition = textSnapshot.GetLineFromLineNumber(symbolRange.Start.Line).Start.Position + symbolRange.Start.Character;
            var originalEndPosition = textSnapshot.GetLineFromLineNumber(symbolRange.End.Line).Start.Position + symbolRange.End.Character;
 
            return new SnapshotSpan(textSnapshot, Span.FromBounds(originalStartPosition, originalEndPosition));
        }
    }
    /// <summary>
    /// Converts an immutable array of <see cref="DocumentSymbolData" /> to an immutable array of <see cref="DocumentSymbolDataViewModel"/>.
    /// </summary>
    public static ImmutableArray<DocumentSymbolDataViewModel> GetDocumentSymbolItemViewModels(
        SortOption sortOption,
        ImmutableArray<DocumentSymbolData> documentSymbolData)
    {
        var documentSymbolItems = new FixedSizeArrayBuilder<DocumentSymbolDataViewModel>(documentSymbolData.Length);
        foreach (var documentSymbol in documentSymbolData)
        {
            var children = GetDocumentSymbolItemViewModels(sortOption, documentSymbol.Children);
            var documentSymbolItem = new DocumentSymbolDataViewModel(documentSymbol, children);
            documentSymbolItems.Add(documentSymbolItem);
        }
 
        documentSymbolItems.Sort(DocumentSymbolDataViewModelSorter.GetComparer(sortOption));
        return documentSymbolItems.MoveToImmutable();
    }
 
    public static void SetExpansionOption(
        ImmutableArray<DocumentSymbolDataViewModel> currentDocumentSymbolItems,
        bool expand)
    {
        foreach (var item in currentDocumentSymbolItems)
        {
            item.IsExpanded = expand;
            SetExpansionOption(item.Children, expand);
        }
    }
 
    /// <summary>
    /// Returns an immutable array of DocumentSymbolData such that each node matches the given pattern.
    /// </summary>
    public static ImmutableArray<DocumentSymbolData> SearchDocumentSymbolData(
        ImmutableArray<DocumentSymbolData> documentSymbolData,
        string pattern,
        CancellationToken cancellationToken)
    {
        if (pattern == "")
            return documentSymbolData;
 
        cancellationToken.ThrowIfCancellationRequested();
 
        using var _ = ArrayBuilder<DocumentSymbolData>.GetInstance(out var filteredDocumentSymbols);
        var patternMatcher = PatternMatcher.CreatePatternMatcher(pattern, includeMatchedSpans: false, allowFuzzyMatching: true);
 
        foreach (var documentSymbol in documentSymbolData)
        {
            var filteredChildren = SearchDocumentSymbolData(documentSymbol.Children, pattern, cancellationToken);
            if (SearchNodeTree(documentSymbol, patternMatcher, cancellationToken))
                filteredDocumentSymbols.Add(documentSymbol with { Children = filteredChildren });
        }
 
        return filteredDocumentSymbols.ToImmutableAndClear();
 
        // Returns true if the name of one of the tree nodes results in a pattern match.
        static bool SearchNodeTree(DocumentSymbolData tree, PatternMatcher patternMatcher, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return patternMatcher.Matches(tree.Name) || tree.Children.Any(c => SearchNodeTree(c, patternMatcher, cancellationToken));
        }
    }
}