Skip to content

Commit

Permalink
Merge pull request #2003 from PowerShell/andschwa/region-symbols
Browse files Browse the repository at this point in the history
Add document symbols for `#region`
  • Loading branch information
andyleejordan authored Feb 28, 2023
2 parents e5ca68d + e16ca88 commit 33f82b9
Show file tree
Hide file tree
Showing 24 changed files with 309 additions and 179 deletions.
2 changes: 2 additions & 0 deletions src/PowerShellEditorServices/Server/PsesLanguageServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ public PsesLanguageServer(
/// cref="PsesServiceCollectionExtensions.AddPsesLanguageServices"/>.
/// </remarks>
/// <returns>A task that completes when the server is ready and listening.</returns>
#pragma warning disable CA1506 // Coupling complexity we don't care about
public async Task StartAsync()
#pragma warning restore CA1506
{
LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ private PssaCmdletAnalysisEngine(

/// <summary>
/// Format a script given its contents.
/// TODO: This needs to be cancellable.
/// </summary>
/// <param name="scriptDefinition">The full text of a script.</param>
/// <param name="formatSettings">The formatter settings to use.</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

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

/// <summary>
/// Resolves a CodeLens that was created without a Command.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -97,21 +96,17 @@ private static CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, Scri
/// Get all Pester CodeLenses for a given script file.
/// </summary>
/// <param name="scriptFile">The script file to get Pester CodeLenses for.</param>
/// <param name="cancellationToken"></param>
/// <returns>All Pester CodeLenses for the given script file.</returns>
public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken)
public IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile)
{
// Don't return anything if codelens setting is disabled
if (!_configurationService.CurrentSettings.Pester.CodeLens)
{
return Array.Empty<CodeLens>();
yield break;
}

List<CodeLens> lenses = new();
foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile))
{
cancellationToken.ThrowIfCancellationRequested();

if (symbol is not PesterSymbolReference pesterSymbol)
{
continue;
Expand All @@ -129,10 +124,11 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken can
continue;
}

lenses.AddRange(GetPesterLens(pesterSymbol, scriptFile));
foreach (CodeLens codeLens in GetPesterLens(pesterSymbol, scriptFile))
{
yield return codeLens;
}
}

return lenses.ToArray();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,34 +52,29 @@ public ReferencesCodeLensProvider(WorkspaceService workspaceService, SymbolsServ
/// Get all reference code lenses for a given script file.
/// </summary>
/// <param name="scriptFile">The PowerShell script file to get code lenses for.</param>
/// <param name="cancellationToken"></param>
/// <returns>An array of CodeLenses describing all functions, classes and enums in the given script file.</returns>
public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken)
/// <returns>An IEnumerable of CodeLenses describing all functions, classes and enums in the given script file.</returns>
public IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile)
{
List<CodeLens> acc = new();
foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile))
{
cancellationToken.ThrowIfCancellationRequested();
// TODO: Can we support more here?
if (symbol.IsDeclaration &&
symbol.Type is
SymbolType.Function or
SymbolType.Class or
SymbolType.Enum)
{
acc.Add(new CodeLens
yield return new CodeLens
{
Data = JToken.FromObject(new
{
Uri = scriptFile.DocumentUri,
ProviderId = nameof(ReferencesCodeLensProvider)
}, LspSerializer.Instance.JsonSerializer),
Range = symbol.NameRegion.ToRange(),
});
};
}
}

return acc.ToArray();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ public static async Task<AliasMap> GetAliasesAsync(

foreach (AliasInfo aliasInfo in aliases.Cast<AliasInfo>())
{
if (cancellationToken.IsCancellationRequested)
{
break;
}

// TODO: When we move to netstandard2.1, we can use another overload which generates
// static delegates and thus reduces allocations.
s_cmdletToAliasCache.AddOrUpdate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ commandAst.InvocationOperator is not (TokenKind.Dot or TokenKind.Ampersand) &&
/// <returns>true if the CommandAst represents a Pester command, false otherwise</returns>
private static bool IsPesterCommand(CommandAst commandAst)
{
if (commandAst == null)
if (commandAst is null)
{
return false;
}
Expand Down Expand Up @@ -94,7 +94,7 @@ private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFil

string commandName = CommandHelpers.StripModuleQualification(pesterCommandAst.GetCommandName(), out _);
PesterCommandType? commandType = PesterSymbolReference.GetCommandType(commandName);
if (commandType == null)
if (commandType is null)
{
return null;
}
Expand Down Expand Up @@ -247,10 +247,11 @@ internal PesterSymbolReference(

internal static PesterCommandType? GetCommandType(string commandName)
{
if (commandName == null || !PesterKeywords.TryGetValue(commandName, out PesterCommandType pesterCommandType))
if (commandName is null || !PesterKeywords.TryGetValue(commandName, out PesterCommandType pesterCommandType))
{
return null;
}

return pesterCommandType;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Management.Automation.Language;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;

namespace Microsoft.PowerShell.EditorServices.Services.Symbols
{
/// <summary>
/// Provides an IDocumentSymbolProvider implementation for
/// enumerating regions as symbols in script (.psd1, .psm1) files.
/// </summary>
internal class RegionDocumentSymbolProvider : IDocumentSymbolProvider
{
string IDocumentSymbolProvider.ProviderId => nameof(RegionDocumentSymbolProvider);

IEnumerable<SymbolReference> IDocumentSymbolProvider.ProvideDocumentSymbols(ScriptFile scriptFile)
{
Stack<Token> tokenCommentRegionStack = new();
Token[] tokens = scriptFile.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(System.Environment.NewLine, scriptFile.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
},
scriptFile,
isDeclaration: true);
}
}
}
}
}
}
10 changes: 7 additions & 3 deletions src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Management.Automation;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
Expand Down Expand Up @@ -37,7 +38,8 @@ internal class SymbolDetails
internal static async Task<SymbolDetails> CreateAsync(
SymbolReference symbolReference,
IRunspaceInfo currentRunspace,
IInternalPowerShellExecutionService executionService)
IInternalPowerShellExecutionService executionService,
CancellationToken cancellationToken)
{
SymbolDetails symbolDetails = new()
{
Expand All @@ -49,14 +51,16 @@ internal static async Task<SymbolDetails> CreateAsync(
CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync(
symbolReference.Id,
currentRunspace,
executionService).ConfigureAwait(false);
executionService,
cancellationToken).ConfigureAwait(false);

if (commandInfo is not null)
{
symbolDetails.Documentation =
await CommandHelpers.GetCommandSynopsisAsync(
commandInfo,
executionService).ConfigureAwait(false);
executionService,
cancellationToken).ConfigureAwait(false);

if (commandInfo.CommandType == CommandTypes.Application)
{
Expand Down
6 changes: 6 additions & 0 deletions src/PowerShellEditorServices/Services/Symbols/SymbolType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ internal enum SymbolType
/// The symbol is a type reference
/// </summary>
Type,

/// <summary>
/// The symbol is a region. Only used for navigation-features.
/// </summary>
Region
}

internal static class SymbolTypeUtils
Expand All @@ -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,
};
}
Expand Down
18 changes: 13 additions & 5 deletions src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ public SymbolsService(
PesterCodeLensProvider pesterProvider = new(configurationService);
_ = _codeLensProviders.TryAdd(pesterProvider.ProviderId, pesterProvider);

// TODO: Is this complication so necessary?
_documentSymbolProviders = new ConcurrentDictionary<string, IDocumentSymbolProvider>();
IDocumentSymbolProvider[] documentSymbolProviders = new IDocumentSymbolProvider[]
{
new ScriptDocumentSymbolProvider(),
new PsdDocumentSymbolProvider(),
new PesterDocumentSymbolProvider(),
new PesterDocumentSymbolProvider()
// NOTE: This specifically does not include RegionDocumentSymbolProvider.
};

foreach (IDocumentSymbolProvider documentSymbolProvider in documentSymbolProviders)
Expand Down Expand Up @@ -187,7 +187,11 @@ public async Task<IEnumerable<SymbolReference>> ScanForReferencesOfSymbolAsync(
foreach (string targetIdentifier in allIdentifiers)
{
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
break;
}

symbols.AddRange(file.References.TryGetReferences(symbol with { Id = targetIdentifier }));
}
}
Expand Down Expand Up @@ -218,12 +222,16 @@ public static IEnumerable<SymbolReference> FindOccurrencesInFile(
/// Finds the details of the symbol at the given script file location.
/// </summary>
public Task<SymbolDetails?> FindSymbolDetailsAtLocationAsync(
ScriptFile scriptFile, int line, int column)
ScriptFile scriptFile, int line, int column, CancellationToken cancellationToken)
{
SymbolReference? symbol = FindSymbolAtLocation(scriptFile, line, column);
return symbol is null
? Task.FromResult<SymbolDetails?>(null)
: SymbolDetails.CreateAsync(symbol, _runspaceContext.CurrentRunspace, _executionService);
: SymbolDetails.CreateAsync(
symbol,
_runspaceContext.CurrentRunspace,
_executionService,
cancellationToken);
}

/// <summary>
Expand Down
Loading

0 comments on commit 33f82b9

Please sign in to comment.