Skip to content

Commit 085187f

Browse files
Add RegionDocumentSymbolProvider.cs
Co-authored-by: Andy Jordan <[email protected]>
1 parent 216727a commit 085187f

File tree

6 files changed

+129
-7
lines changed

6 files changed

+129
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using System.Management.Automation.Language;
6+
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
7+
8+
namespace Microsoft.PowerShell.EditorServices.Services.Symbols
9+
{
10+
/// <summary>
11+
/// Provides an IDocumentSymbolProvider implementation for
12+
/// enumerating regions as symbols in script (.psd1, .psm1) files.
13+
/// </summary>
14+
internal class RegionDocumentSymbolProvider : IDocumentSymbolProvider
15+
{
16+
string IDocumentSymbolProvider.ProviderId => nameof(RegionDocumentSymbolProvider);
17+
18+
IEnumerable<SymbolReference> IDocumentSymbolProvider.ProvideDocumentSymbols(ScriptFile scriptFile)
19+
{
20+
Stack<Token> tokenCommentRegionStack = new();
21+
Token[] tokens = scriptFile.ScriptTokens;
22+
23+
for (int i = 0; i < tokens.Length; i++)
24+
{
25+
Token token = tokens[i];
26+
27+
// Exclude everything but single-line comments
28+
if (token.Kind != TokenKind.Comment ||
29+
token.Extent.StartLineNumber != token.Extent.EndLineNumber ||
30+
!TokenOperations.IsBlockComment(i, tokens))
31+
{
32+
continue;
33+
}
34+
35+
// Processing for #region -> #endregion
36+
if (TokenOperations.s_startRegionTextRegex.IsMatch(token.Text))
37+
{
38+
tokenCommentRegionStack.Push(token);
39+
continue;
40+
}
41+
42+
if (TokenOperations.s_endRegionTextRegex.IsMatch(token.Text))
43+
{
44+
// Mismatched regions in the script can cause bad stacks.
45+
if (tokenCommentRegionStack.Count > 0)
46+
{
47+
Token regionStart = tokenCommentRegionStack.Pop();
48+
Token regionEnd = token;
49+
50+
BufferRange regionRange = new(
51+
regionStart.Extent.StartLineNumber,
52+
regionStart.Extent.StartColumnNumber,
53+
regionEnd.Extent.EndLineNumber,
54+
regionEnd.Extent.EndColumnNumber);
55+
56+
yield return new SymbolReference(
57+
SymbolType.Region,
58+
regionStart.Extent.Text.Trim().TrimStart('#'),
59+
regionStart.Extent.Text.Trim(),
60+
regionStart.Extent,
61+
new ScriptExtent()
62+
{
63+
Text = string.Join(System.Environment.NewLine, scriptFile.GetLinesInRange(regionRange)),
64+
StartLineNumber = regionStart.Extent.StartLineNumber,
65+
StartColumnNumber = regionStart.Extent.StartColumnNumber,
66+
StartOffset = regionStart.Extent.StartOffset,
67+
EndLineNumber = regionEnd.Extent.EndLineNumber,
68+
EndColumnNumber = regionEnd.Extent.EndColumnNumber,
69+
EndOffset = regionEnd.Extent.EndOffset,
70+
File = regionStart.Extent.File
71+
},
72+
scriptFile,
73+
isDeclaration: true);
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}

src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ public SymbolsService(
7777
PesterCodeLensProvider pesterProvider = new(configurationService);
7878
_ = _codeLensProviders.TryAdd(pesterProvider.ProviderId, pesterProvider);
7979

80-
// TODO: Is this complication so necessary?
8180
_documentSymbolProviders = new ConcurrentDictionary<string, IDocumentSymbolProvider>();
8281
IDocumentSymbolProvider[] documentSymbolProviders = new IDocumentSymbolProvider[]
8382
{
8483
new ScriptDocumentSymbolProvider(),
8584
new PsdDocumentSymbolProvider(),
86-
new PesterDocumentSymbolProvider(),
85+
new PesterDocumentSymbolProvider()
86+
// NOTE: This specifically does not include RegionDocumentSymbolProvider.
8787
};
8888

8989
foreach (IDocumentSymbolProvider documentSymbolProvider in documentSymbolProviders)

src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44
using System;
@@ -35,7 +35,8 @@ public PsesDocumentSymbolHandler(ILoggerFactory factory, WorkspaceService worksp
3535
{
3636
new ScriptDocumentSymbolProvider(),
3737
new PsdDocumentSymbolProvider(),
38-
new PesterDocumentSymbolProvider()
38+
new PesterDocumentSymbolProvider(),
39+
new RegionDocumentSymbolProvider()
3940
};
4041
}
4142

src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ internal static class TokenOperations
1717
// script. They are based on the defaults in the VS Code Language Configuration at;
1818
// https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31
1919
// https://github.com/Microsoft/vscode/issues/49070
20-
private static readonly Regex s_startRegionTextRegex = new(
20+
internal static readonly Regex s_startRegionTextRegex = new(
2121
@"^\s*#[rR]egion\b", RegexOptions.Compiled);
22-
private static readonly Regex s_endRegionTextRegex = new(
22+
internal static readonly Regex s_endRegionTextRegex = new(
2323
@"^\s*#[eE]nd[rR]egion\b", RegexOptions.Compiled);
2424

2525
/// <summary>
@@ -199,7 +199,7 @@ private static FoldingReference CreateFoldingReference(
199199
/// - Token text must start with a '#'.false This is because comment regions
200200
/// start with '&lt;#' but have the same TokenKind
201201
/// </summary>
202-
private static bool IsBlockComment(int index, Token[] tokens)
202+
internal static bool IsBlockComment(int index, Token[] tokens)
203203
{
204204
Token thisToken = tokens[index];
205205
if (thisToken.Kind != TokenKind.Comment) { return false; }

test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1

+13
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,16 @@ enum AEnum {
4141
AFunction
4242
1..3 | AFilter
4343
AnAdvancedFunction
44+
45+
<#
46+
#region don't find me inside comment block
47+
abc
48+
#endregion
49+
#>
50+
51+
#region find me outer
52+
#region find me inner
53+
54+
#endregion
55+
#endregion
56+
#region ignore this unclosed region

test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs

+29
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,35 @@ public void FindsSymbolsInFile()
821821
Assert.Equal("prop AValue", symbol.Id);
822822
Assert.Equal("AValue", symbol.Name);
823823
Assert.True(symbol.IsDeclaration);
824+
825+
// There should be no region symbols unless the provider has been registered.
826+
Assert.Empty(symbols.Where(i => i.Type == SymbolType.Region));
827+
}
828+
829+
[Fact]
830+
public void FindsRegionsInFile()
831+
{
832+
symbolsService.TryRegisterDocumentSymbolProvider(new RegionDocumentSymbolProvider());
833+
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInMultiSymbolFile.SourceDetails);
834+
Assert.Collection(symbols.Where(i => i.Type == SymbolType.Region),
835+
(i) =>
836+
{
837+
Assert.Equal("region find me outer", i.Id);
838+
Assert.Equal("#region find me outer", i.Name);
839+
Assert.Equal(SymbolType.Region, i.Type);
840+
Assert.True(i.IsDeclaration);
841+
AssertIsRegion(i.NameRegion, 51, 1, 51, 22);
842+
AssertIsRegion(i.ScriptRegion, 51, 1, 55, 11);
843+
},
844+
(i) =>
845+
{
846+
Assert.Equal("region find me inner", i.Id);
847+
Assert.Equal("#region find me inner", i.Name);
848+
Assert.Equal(SymbolType.Region, i.Type);
849+
Assert.True(i.IsDeclaration);
850+
AssertIsRegion(i.NameRegion, 52, 1, 52, 22);
851+
AssertIsRegion(i.ScriptRegion, 52, 1, 54, 11);
852+
});
824853
}
825854

826855
[Fact]

0 commit comments

Comments
 (0)