diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs index 6533e7726..02e34e6f8 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs @@ -79,6 +79,11 @@ internal enum SymbolType /// The symbol is a type reference /// Type, + + /// + /// The symbol is a region. Only used for navigation-features. + /// + Region } internal static class SymbolTypeUtils @@ -97,6 +102,7 @@ internal static SymbolKind GetSymbolKind(SymbolType symbolType) SymbolType.Variable or SymbolType.Parameter => SymbolKind.Variable, SymbolType.HashtableKey => SymbolKind.Key, SymbolType.Type => SymbolKind.TypeParameter, + SymbolType.Region => SymbolKind.String, SymbolType.Unknown or _ => SymbolKind.Object, }; } diff --git a/src/PowerShellEditorServices/Services/Symbols/Visitors/RegionVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Visitors/RegionVisitor.cs new file mode 100644 index 000000000..d85c06dd3 --- /dev/null +++ b/src/PowerShellEditorServices/Services/Symbols/Visitors/RegionVisitor.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Services.Symbols +{ + internal static class RegionVisitor + { + internal static IEnumerable GetRegionsInDocument(ScriptFile file) + { + Stack tokenCommentRegionStack = new(); + Token[] tokens = file.ScriptTokens; + + for (int i = 0; i < tokens.Length; i++) + { + Token token = tokens[i]; + + // Exclude everything but single-line comments + if (token.Kind != TokenKind.Comment || + token.Extent.StartLineNumber != token.Extent.EndLineNumber || + !TokenOperations.IsBlockComment(i, tokens)) + { + continue; + } + + // Processing for #region -> #endregion + if (TokenOperations.s_startRegionTextRegex.IsMatch(token.Text)) + { + tokenCommentRegionStack.Push(token); + continue; + } + + if (TokenOperations.s_endRegionTextRegex.IsMatch(token.Text)) + { + // Mismatched regions in the script can cause bad stacks. + if (tokenCommentRegionStack.Count > 0) + { + Token regionStart = tokenCommentRegionStack.Pop(); + Token regionEnd = token; + + BufferRange regionRange = new( + regionStart.Extent.StartLineNumber, + regionStart.Extent.StartColumnNumber, + regionEnd.Extent.EndLineNumber, + regionEnd.Extent.EndColumnNumber); + + yield return new SymbolReference( + SymbolType.Region, + regionStart.Extent.Text.Trim().TrimStart('#'), + regionStart.Extent.Text.Trim(), + regionStart.Extent, + new ScriptExtent() + { + Text = string.Join(Environment.NewLine, file.GetLinesInRange(regionRange)), + StartLineNumber = regionStart.Extent.StartLineNumber, + StartColumnNumber = regionStart.Extent.StartColumnNumber, + StartOffset = regionStart.Extent.StartOffset, + EndLineNumber = regionEnd.Extent.EndLineNumber, + EndColumnNumber = regionEnd.Extent.EndColumnNumber, + EndOffset = regionEnd.Extent.EndOffset, + File = regionStart.Extent.File + }, + file, + isDeclaration: true); + } + } + } + } + } +} diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index 823d01f26..435a4e585 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -50,10 +50,7 @@ public override async Task Handle(Do ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); IEnumerable foundSymbols = ProvideDocumentSymbols(scriptFile); - if (foundSymbols is null) - { - return null; - } + foundSymbols = foundSymbols.Concat(RegionVisitor.GetRegionsInDocument(scriptFile)); string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); diff --git a/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs index d1a3ca5e5..be6619ec1 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs @@ -17,9 +17,9 @@ internal static class TokenOperations // script. They are based on the defaults in the VS Code Language Configuration at; // https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31 // https://github.com/Microsoft/vscode/issues/49070 - private static readonly Regex s_startRegionTextRegex = new( + internal static readonly Regex s_startRegionTextRegex = new( @"^\s*#[rR]egion\b", RegexOptions.Compiled); - private static readonly Regex s_endRegionTextRegex = new( + internal static readonly Regex s_endRegionTextRegex = new( @"^\s*#[eE]nd[rR]egion\b", RegexOptions.Compiled); /// @@ -199,7 +199,7 @@ private static FoldingReference CreateFoldingReference( /// - Token text must start with a '#'.false This is because comment regions /// start with '<#' but have the same TokenKind /// - private static bool IsBlockComment(int index, Token[] tokens) + internal static bool IsBlockComment(int index, Token[] tokens) { Token thisToken = tokens[index]; if (thisToken.Kind != TokenKind.Comment) { return false; } diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 index b4f54c329..2831ea332 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 @@ -41,3 +41,16 @@ enum AEnum { AFunction 1..3 | AFilter AnAdvancedFunction + +<# +#region don't find me inside comment block +abc +#endregion +#> + +#region find me outer +#region find me inner + +#endregion +#endregion +#region ignore this unclosed region diff --git a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs index dbe0f8ca6..7ef2b67c1 100644 --- a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs @@ -145,6 +145,13 @@ private IEnumerable FindSymbolsInFile(ScriptRegion scriptRegion .OrderBy(symbol => symbol.ScriptRegion.ToRange().Start); } + private IEnumerable FindRegionsInFile(ScriptRegion scriptRegion) + { + return RegionVisitor + .GetRegionsInDocument(GetScriptFile(scriptRegion)) + .OrderBy(symbol => symbol.ScriptRegion.ToRange().Start); + } + [Fact] public async Task FindsParameterHintsOnCommand() { @@ -952,5 +959,30 @@ public void FindsSymbolsInNoSymbolsFile() IEnumerable symbolsResult = FindSymbolsInFile(FindSymbolsInNoSymbolsFile.SourceDetails); Assert.Empty(symbolsResult); } + + [Fact] + public void FindsRegionSymbolsInFile() + { + IEnumerable symbols = FindRegionsInFile(FindSymbolsInMultiSymbolFile.SourceDetails); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("region find me outer", i.Id); + Assert.Equal("#region find me outer", i.Name); + Assert.Equal(SymbolType.Region, i.Type); + Assert.True(i.IsDeclaration); + AssertIsRegion(i.NameRegion, 51, 1, 51, 22); + AssertIsRegion(i.ScriptRegion, 51, 1, 55, 11); + }, + (i) => + { + Assert.Equal("region find me inner", i.Id); + Assert.Equal("#region find me inner", i.Name); + Assert.Equal(SymbolType.Region, i.Type); + Assert.True(i.IsDeclaration); + AssertIsRegion(i.NameRegion, 52, 1, 52, 22); + AssertIsRegion(i.ScriptRegion, 52, 1, 54, 11); + }); + } } }