diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index f416d7615..868d3af76 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -40,15 +41,118 @@ public PsesDocumentSymbolHandler(ILoggerFactory factory, WorkspaceService worksp DocumentSelector = LspUtils.PowerShellDocumentSelector }; - // AKA the outline feature + // Modifies a flat list of symbols into a hierarchical list. + private static Task SortHierarchicalSymbols(List symbols, CancellationToken cancellationToken) + { + // Sort by the start of the symbol definition (they're probably sorted but we need to be + // certain otherwise this algorithm won't work). We only need to sort the list once, and + // since the implementation is recursive, it's easiest to use the stack to track that + // this is the first call. + symbols.Sort((x1, x2) => x1.Range.Start.CompareTo(x2.Range.Start)); + return SortHierarchicalSymbolsImpl(symbols, cancellationToken); + } + + private static async Task SortHierarchicalSymbolsImpl(List symbols, CancellationToken cancellationToken) + { + for (int i = 0; i < symbols.Count; i++) + { + // This async method is pretty dense with synchronous code + // so it's helpful to add some yields. + await Task.Yield(); + if (cancellationToken.IsCancellationRequested) + { + return; + } + + HierarchicalSymbol symbol = symbols[i]; + + // Base case where we haven't found any parents yet (the first symbol must be a + // parent by definition). + if (i == 0) + { + continue; + } + // If the symbol starts after end of last symbol parsed then it's a new parent. + else if (symbol.Range.Start > symbols[i - 1].Range.End) + { + continue; + } + // Otherwise it's a child, we just need to figure out whose child it is and move it there (which also means removing it from the current list). + else + { + for (int j = 0; j <= i; j++) + { + // While we should only check up to j < i, we iterate up to j <= i so that + // we can check this assertion that we didn't exhaust the parents. + Debug.Assert(j != i, "We didn't find the child's parent!"); + + HierarchicalSymbol parent = symbols[j]; + // If the symbol starts after the parent starts and ends before the parent + // ends then its a child. + if (symbol.Range.Start > parent.Range.Start && symbol.Range.End < parent.Range.End) + { + // Add it to the parent's list. + parent.Children.Add(symbol); + // Remove it from this "parents" list (because it's a child) and adjust + // our loop counter because it's been removed. + symbols.RemoveAt(i); + i--; + break; + } + } + } + } + + // Now recursively sort the children into nested buckets of children too. + foreach (HierarchicalSymbol parent in symbols) + { + // Since this modifies in place we just recurse, no re-assignment or clearing from + // parent.Children necessary. + await SortHierarchicalSymbols(parent.Children, cancellationToken).ConfigureAwait(false); + } + } + + // This struct and the mapping function below exist to allow us to skip a *bunch* of + // unnecessary allocations when sorting the symbols since DocumentSymbol (which this is + // pretty much a mirror of) is an immutable record...but we need to constantly modify the + // list of children when sorting. + private struct HierarchicalSymbol + { + public SymbolKind Kind; + public Range Range; + public Range SelectionRange; + public string Name; + public List Children; + } + + // Recursively turn our HierarchicalSymbol struct into OmniSharp's DocumentSymbol record. + private static List GetDocumentSymbolsFromHierarchicalSymbols(IEnumerable hierarchicalSymbols) + { + List documentSymbols = new(); + foreach (HierarchicalSymbol symbol in hierarchicalSymbols) + { + documentSymbols.Add(new DocumentSymbol + { + Kind = symbol.Kind, + Range = symbol.Range, + SelectionRange = symbol.SelectionRange, + Name = symbol.Name, + Children = GetDocumentSymbolsFromHierarchicalSymbols(symbol.Children) + }); + } + return documentSymbols; + } + + // AKA the outline feature! public override async Task Handle(DocumentSymbolParams request, CancellationToken cancellationToken) { _logger.LogDebug($"Handling document symbols for {request.TextDocument.Uri}"); ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); - List symbols = new(); - foreach (SymbolReference r in ProvideDocumentSymbols(scriptFile)) + List hierarchicalSymbols = new(); + + foreach (SymbolReference symbolReference in ProvideDocumentSymbols(scriptFile)) { // This async method is pretty dense with synchronous code // so it's helpful to add some yields. @@ -62,27 +166,37 @@ public override async Task Handle(Do // // TODO: We should also include function invocations that are part of DSLs (like // Invoke-Build etc.). - if (!r.IsDeclaration || r.Type is SymbolType.Parameter) + if (!symbolReference.IsDeclaration || symbolReference.Type is SymbolType.Parameter) { continue; } - // TODO: This now needs the Children property filled out to support hierarchical - // symbols, and we don't have the information nor algorithm to do that currently. - // OmniSharp was previously doing this for us based on the range, perhaps we can - // find that logic and reuse it. - symbols.Add(new SymbolInformationOrDocumentSymbol(new DocumentSymbol + hierarchicalSymbols.Add(new HierarchicalSymbol { - Kind = SymbolTypeUtils.GetSymbolKind(r.Type), - Range = r.ScriptRegion.ToRange(), - SelectionRange = r.NameRegion.ToRange(), - Name = r.Name - })); + Kind = SymbolTypeUtils.GetSymbolKind(symbolReference.Type), + Range = symbolReference.ScriptRegion.ToRange(), + SelectionRange = symbolReference.NameRegion.ToRange(), + Name = symbolReference.Name, + Children = new List() + }); } - return symbols.Count == 0 - ? s_emptySymbolInformationOrDocumentSymbolContainer - : new SymbolInformationOrDocumentSymbolContainer(symbols); + // Short-circuit if we have no symbols. + if (hierarchicalSymbols.Count == 0) + { + return s_emptySymbolInformationOrDocumentSymbolContainer; + } + + // Otherwise slowly sort them into a hierarchy (this modifies the list). + await SortHierarchicalSymbols(hierarchicalSymbols, cancellationToken).ConfigureAwait(false); + + // And finally convert them to the silly SymbolInformationOrDocumentSymbol wrapper. + List container = new(); + foreach (DocumentSymbol symbol in GetDocumentSymbolsFromHierarchicalSymbols(hierarchicalSymbols)) + { + container.Add(new SymbolInformationOrDocumentSymbol(symbol)); + } + return container; } private IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile)