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);
+ });
+ }
}
}