Skip to content

Commit 33f82b9

Browse files
Merge pull request #2003 from PowerShell/andschwa/region-symbols
Add document symbols for `#region`
2 parents e5ca68d + e16ca88 commit 33f82b9

24 files changed

+309
-179
lines changed

src/PowerShellEditorServices/Server/PsesLanguageServer.cs

+2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ public PsesLanguageServer(
7070
/// cref="PsesServiceCollectionExtensions.AddPsesLanguageServices"/>.
7171
/// </remarks>
7272
/// <returns>A task that completes when the server is ready and listening.</returns>
73+
#pragma warning disable CA1506 // Coupling complexity we don't care about
7374
public async Task StartAsync()
75+
#pragma warning restore CA1506
7476
{
7577
LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options =>
7678
{

src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ private PssaCmdletAnalysisEngine(
123123

124124
/// <summary>
125125
/// Format a script given its contents.
126+
/// TODO: This needs to be cancellable.
126127
/// </summary>
127128
/// <param name="scriptDefinition">The full text of a script.</param>
128129
/// <param name="formatSettings">The formatter settings to use.</param>

src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs

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

4+
using System.Collections.Generic;
45
using System.Threading;
56
using System.Threading.Tasks;
67
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
@@ -26,9 +27,8 @@ internal interface ICodeLensProvider
2627
/// <param name="scriptFile">
2728
/// The document for which CodeLenses should be provided.
2829
/// </param>
29-
/// <param name="cancellationToken"></param>
30-
/// <returns>An array of CodeLenses.</returns>
31-
CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken);
30+
/// <returns>An IEnumerable of CodeLenses.</returns>
31+
IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile);
3232

3333
/// <summary>
3434
/// Resolves a CodeLens that was created without a Command.

src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
3-
using System;
43
using System.Collections.Generic;
54
using System.Threading;
65
using System.Threading.Tasks;
@@ -97,21 +96,17 @@ private static CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, Scri
9796
/// Get all Pester CodeLenses for a given script file.
9897
/// </summary>
9998
/// <param name="scriptFile">The script file to get Pester CodeLenses for.</param>
100-
/// <param name="cancellationToken"></param>
10199
/// <returns>All Pester CodeLenses for the given script file.</returns>
102-
public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken)
100+
public IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile)
103101
{
104102
// Don't return anything if codelens setting is disabled
105103
if (!_configurationService.CurrentSettings.Pester.CodeLens)
106104
{
107-
return Array.Empty<CodeLens>();
105+
yield break;
108106
}
109107

110-
List<CodeLens> lenses = new();
111108
foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile))
112109
{
113-
cancellationToken.ThrowIfCancellationRequested();
114-
115110
if (symbol is not PesterSymbolReference pesterSymbol)
116111
{
117112
continue;
@@ -129,10 +124,11 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken can
129124
continue;
130125
}
131126

132-
lenses.AddRange(GetPesterLens(pesterSymbol, scriptFile));
127+
foreach (CodeLens codeLens in GetPesterLens(pesterSymbol, scriptFile))
128+
{
129+
yield return codeLens;
130+
}
133131
}
134-
135-
return lenses.ToArray();
136132
}
137133

138134
/// <summary>

src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs

+4-9
Original file line numberDiff line numberDiff line change
@@ -52,34 +52,29 @@ public ReferencesCodeLensProvider(WorkspaceService workspaceService, SymbolsServ
5252
/// Get all reference code lenses for a given script file.
5353
/// </summary>
5454
/// <param name="scriptFile">The PowerShell script file to get code lenses for.</param>
55-
/// <param name="cancellationToken"></param>
56-
/// <returns>An array of CodeLenses describing all functions, classes and enums in the given script file.</returns>
57-
public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken)
55+
/// <returns>An IEnumerable of CodeLenses describing all functions, classes and enums in the given script file.</returns>
56+
public IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile)
5857
{
59-
List<CodeLens> acc = new();
6058
foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile))
6159
{
62-
cancellationToken.ThrowIfCancellationRequested();
6360
// TODO: Can we support more here?
6461
if (symbol.IsDeclaration &&
6562
symbol.Type is
6663
SymbolType.Function or
6764
SymbolType.Class or
6865
SymbolType.Enum)
6966
{
70-
acc.Add(new CodeLens
67+
yield return new CodeLens
7168
{
7269
Data = JToken.FromObject(new
7370
{
7471
Uri = scriptFile.DocumentUri,
7572
ProviderId = nameof(ReferencesCodeLensProvider)
7673
}, LspSerializer.Instance.JsonSerializer),
7774
Range = symbol.NameRegion.ToRange(),
78-
});
75+
};
7976
}
8077
}
81-
82-
return acc.ToArray();
8378
}
8479

8580
/// <summary>

src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs

+5
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ public static async Task<AliasMap> GetAliasesAsync(
244244

245245
foreach (AliasInfo aliasInfo in aliases.Cast<AliasInfo>())
246246
{
247+
if (cancellationToken.IsCancellationRequested)
248+
{
249+
break;
250+
}
251+
247252
// TODO: When we move to netstandard2.1, we can use another overload which generates
248253
// static delegates and thus reduces allocations.
249254
s_cmdletToAliasCache.AddOrUpdate(

src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ commandAst.InvocationOperator is not (TokenKind.Dot or TokenKind.Ampersand) &&
5454
/// <returns>true if the CommandAst represents a Pester command, false otherwise</returns>
5555
private static bool IsPesterCommand(CommandAst commandAst)
5656
{
57-
if (commandAst == null)
57+
if (commandAst is null)
5858
{
5959
return false;
6060
}
@@ -94,7 +94,7 @@ private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFil
9494

9595
string commandName = CommandHelpers.StripModuleQualification(pesterCommandAst.GetCommandName(), out _);
9696
PesterCommandType? commandType = PesterSymbolReference.GetCommandType(commandName);
97-
if (commandType == null)
97+
if (commandType is null)
9898
{
9999
return null;
100100
}
@@ -247,10 +247,11 @@ internal PesterSymbolReference(
247247

248248
internal static PesterCommandType? GetCommandType(string commandName)
249249
{
250-
if (commandName == null || !PesterKeywords.TryGetValue(commandName, out PesterCommandType pesterCommandType))
250+
if (commandName is null || !PesterKeywords.TryGetValue(commandName, out PesterCommandType pesterCommandType))
251251
{
252252
return null;
253253
}
254+
254255
return pesterCommandType;
255256
}
256257

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/SymbolDetails.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Management.Automation;
6+
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
89
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
@@ -37,7 +38,8 @@ internal class SymbolDetails
3738
internal static async Task<SymbolDetails> CreateAsync(
3839
SymbolReference symbolReference,
3940
IRunspaceInfo currentRunspace,
40-
IInternalPowerShellExecutionService executionService)
41+
IInternalPowerShellExecutionService executionService,
42+
CancellationToken cancellationToken)
4143
{
4244
SymbolDetails symbolDetails = new()
4345
{
@@ -49,14 +51,16 @@ internal static async Task<SymbolDetails> CreateAsync(
4951
CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync(
5052
symbolReference.Id,
5153
currentRunspace,
52-
executionService).ConfigureAwait(false);
54+
executionService,
55+
cancellationToken).ConfigureAwait(false);
5356

5457
if (commandInfo is not null)
5558
{
5659
symbolDetails.Documentation =
5760
await CommandHelpers.GetCommandSynopsisAsync(
5861
commandInfo,
59-
executionService).ConfigureAwait(false);
62+
executionService,
63+
cancellationToken).ConfigureAwait(false);
6064

6165
if (commandInfo.CommandType == CommandTypes.Application)
6266
{

src/PowerShellEditorServices/Services/Symbols/SymbolType.cs

+6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ internal enum SymbolType
7979
/// The symbol is a type reference
8080
/// </summary>
8181
Type,
82+
83+
/// <summary>
84+
/// The symbol is a region. Only used for navigation-features.
85+
/// </summary>
86+
Region
8287
}
8388

8489
internal static class SymbolTypeUtils
@@ -97,6 +102,7 @@ internal static SymbolKind GetSymbolKind(SymbolType symbolType)
97102
SymbolType.Variable or SymbolType.Parameter => SymbolKind.Variable,
98103
SymbolType.HashtableKey => SymbolKind.Key,
99104
SymbolType.Type => SymbolKind.TypeParameter,
105+
SymbolType.Region => SymbolKind.String,
100106
SymbolType.Unknown or _ => SymbolKind.Object,
101107
};
102108
}

src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs

+13-5
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)
@@ -187,7 +187,11 @@ public async Task<IEnumerable<SymbolReference>> ScanForReferencesOfSymbolAsync(
187187
foreach (string targetIdentifier in allIdentifiers)
188188
{
189189
await Task.Yield();
190-
cancellationToken.ThrowIfCancellationRequested();
190+
if (cancellationToken.IsCancellationRequested)
191+
{
192+
break;
193+
}
194+
191195
symbols.AddRange(file.References.TryGetReferences(symbol with { Id = targetIdentifier }));
192196
}
193197
}
@@ -218,12 +222,16 @@ public static IEnumerable<SymbolReference> FindOccurrencesInFile(
218222
/// Finds the details of the symbol at the given script file location.
219223
/// </summary>
220224
public Task<SymbolDetails?> FindSymbolDetailsAtLocationAsync(
221-
ScriptFile scriptFile, int line, int column)
225+
ScriptFile scriptFile, int line, int column, CancellationToken cancellationToken)
222226
{
223227
SymbolReference? symbol = FindSymbolAtLocation(scriptFile, line, column);
224228
return symbol is null
225229
? Task.FromResult<SymbolDetails?>(null)
226-
: SymbolDetails.CreateAsync(symbol, _runspaceContext.CurrentRunspace, _executionService);
230+
: SymbolDetails.CreateAsync(
231+
symbol,
232+
_runspaceContext.CurrentRunspace,
233+
_executionService,
234+
cancellationToken);
227235
}
228236

229237
/// <summary>

0 commit comments

Comments
 (0)