File: DocumentOutline\DocumentOutlineViewModel_Utilities.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices.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.VisualStudio.LanguageServer.Client;
using Microsoft.VisualStudio.Text;
using Roslyn.LanguageServer.Protocol;
 
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
                    {
                        DocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(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>
    public static ImmutableArray<DocumentSymbolData> CreateDocumentSymbolData(RoslynDocumentSymbol[] documentSymbols, ITextSnapshot textSnapshot)
    {
        return ConvertSymbols(documentSymbols);
 
        ImmutableArray<DocumentSymbolData> ConvertSymbols(RoslynDocumentSymbol[]? symbols)
        {
            if (symbols is null || symbols.Length == 0)
                return [];
 
            var result = new FixedSizeArrayBuilder<DocumentSymbolData>(symbols.Length);
            foreach (var symbol in symbols)
            {
                var converted = new DocumentSymbolData(
                    symbol.Detail ?? symbol.Name,
                    (Roslyn.LanguageServer.Protocol.SymbolKind)symbol.Kind,
                    (Glyph)symbol.Glyph,
                    GetSymbolRangeSpan(symbol.Range),
                    GetSymbolRangeSpan(symbol.SelectionRange),
                    ConvertSymbols(symbol.Children));
                result.Add(converted);
            }
 
            return result.MoveToImmutable();
        }
 
        // 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));
        }
    }
}