diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 440e84a70..74319e828 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -70,7 +70,9 @@ public PsesLanguageServer( /// cref="PsesServiceCollectionExtensions.AddPsesLanguageServices"/>. /// /// A task that completes when the server is ready and listening. +#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 => { diff --git a/src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs b/src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs index 371950bc0..05f4de8b9 100644 --- a/src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs +++ b/src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs @@ -123,6 +123,7 @@ private PssaCmdletAnalysisEngine( /// /// Format a script given its contents. + /// TODO: This needs to be cancellable. /// /// The full text of a script. /// The formatter settings to use. diff --git a/src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs index a6255a41d..8ee6bc8b7 100644 --- a/src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs @@ -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; @@ -26,9 +27,8 @@ internal interface ICodeLensProvider /// /// The document for which CodeLenses should be provided. /// - /// - /// An array of CodeLenses. - CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken); + /// An IEnumerable of CodeLenses. + IEnumerable ProvideCodeLenses(ScriptFile scriptFile); /// /// Resolves a CodeLens that was created without a Command. diff --git a/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs index 50ddcfc22..22b563b1d 100644 --- a/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs @@ -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; @@ -97,21 +96,17 @@ private static CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, Scri /// Get all Pester CodeLenses for a given script file. /// /// The script file to get Pester CodeLenses for. - /// /// All Pester CodeLenses for the given script file. - public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken) + public IEnumerable ProvideCodeLenses(ScriptFile scriptFile) { // Don't return anything if codelens setting is disabled if (!_configurationService.CurrentSettings.Pester.CodeLens) { - return Array.Empty(); + yield break; } - List lenses = new(); foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) { - cancellationToken.ThrowIfCancellationRequested(); - if (symbol is not PesterSymbolReference pesterSymbol) { continue; @@ -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(); } /// diff --git a/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs index 6e1e4cc81..0307163cc 100644 --- a/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -52,14 +52,11 @@ public ReferencesCodeLensProvider(WorkspaceService workspaceService, SymbolsServ /// Get all reference code lenses for a given script file. /// /// The PowerShell script file to get code lenses for. - /// - /// An array of CodeLenses describing all functions, classes and enums in the given script file. - public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken) + /// An IEnumerable of CodeLenses describing all functions, classes and enums in the given script file. + public IEnumerable ProvideCodeLenses(ScriptFile scriptFile) { - List acc = new(); foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) { - cancellationToken.ThrowIfCancellationRequested(); // TODO: Can we support more here? if (symbol.IsDeclaration && symbol.Type is @@ -67,7 +64,7 @@ SymbolType.Function or SymbolType.Class or SymbolType.Enum) { - acc.Add(new CodeLens + yield return new CodeLens { Data = JToken.FromObject(new { @@ -75,11 +72,9 @@ SymbolType.Class or ProviderId = nameof(ReferencesCodeLensProvider) }, LspSerializer.Instance.JsonSerializer), Range = symbol.NameRegion.ToRange(), - }); + }; } } - - return acc.ToArray(); } /// diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs index 7750aa042..38a581bec 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs @@ -244,6 +244,11 @@ public static async Task GetAliasesAsync( foreach (AliasInfo aliasInfo in aliases.Cast()) { + 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( diff --git a/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs index dcf675975..ebc2e720f 100644 --- a/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs @@ -54,7 +54,7 @@ commandAst.InvocationOperator is not (TokenKind.Dot or TokenKind.Ampersand) && /// true if the CommandAst represents a Pester command, false otherwise private static bool IsPesterCommand(CommandAst commandAst) { - if (commandAst == null) + if (commandAst is null) { return false; } @@ -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; } @@ -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; } diff --git a/src/PowerShellEditorServices/Services/Symbols/RegionDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/RegionDocumentSymbolProvider.cs new file mode 100644 index 000000000..e3b33b076 --- /dev/null +++ b/src/PowerShellEditorServices/Services/Symbols/RegionDocumentSymbolProvider.cs @@ -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 +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating regions as symbols in script (.psd1, .psm1) files. + /// + internal class RegionDocumentSymbolProvider : IDocumentSymbolProvider + { + string IDocumentSymbolProvider.ProviderId => nameof(RegionDocumentSymbolProvider); + + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols(ScriptFile scriptFile) + { + Stack 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); + } + } + } + } + } +} diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs index ddca0f0a7..dc0a57fcd 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs @@ -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; @@ -37,7 +38,8 @@ internal class SymbolDetails internal static async Task CreateAsync( SymbolReference symbolReference, IRunspaceInfo currentRunspace, - IInternalPowerShellExecutionService executionService) + IInternalPowerShellExecutionService executionService, + CancellationToken cancellationToken) { SymbolDetails symbolDetails = new() { @@ -49,14 +51,16 @@ internal static async Task 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) { 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/SymbolsService.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs index cf4040642..457b1d651 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs @@ -77,13 +77,13 @@ public SymbolsService( PesterCodeLensProvider pesterProvider = new(configurationService); _ = _codeLensProviders.TryAdd(pesterProvider.ProviderId, pesterProvider); - // TODO: Is this complication so necessary? _documentSymbolProviders = new ConcurrentDictionary(); IDocumentSymbolProvider[] documentSymbolProviders = new IDocumentSymbolProvider[] { new ScriptDocumentSymbolProvider(), new PsdDocumentSymbolProvider(), - new PesterDocumentSymbolProvider(), + new PesterDocumentSymbolProvider() + // NOTE: This specifically does not include RegionDocumentSymbolProvider. }; foreach (IDocumentSymbolProvider documentSymbolProvider in documentSymbolProviders) @@ -187,7 +187,11 @@ public async Task> ScanForReferencesOfSymbolAsync( foreach (string targetIdentifier in allIdentifiers) { await Task.Yield(); - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + break; + } + symbols.AddRange(file.References.TryGetReferences(symbol with { Id = targetIdentifier })); } } @@ -218,12 +222,16 @@ public static IEnumerable FindOccurrencesInFile( /// Finds the details of the symbol at the given script file location. /// public Task 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(null) - : SymbolDetails.CreateAsync(symbol, _runspaceContext.CurrentRunspace, _executionService); + : SymbolDetails.CreateAsync( + symbol, + _runspaceContext.CurrentRunspace, + _executionService, + cancellationToken); } /// diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs index 2861fe27f..5606c80fa 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -19,6 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesCodeActionHandler : CodeActionHandlerBase { + private static readonly CommandOrCodeActionContainer s_emptyCommandOrCodeActionContainer = new(); private readonly ILogger _logger; private readonly AnalysisService _analysisService; @@ -42,16 +43,17 @@ public override async Task Handle(CodeActionParams if (cancellationToken.IsCancellationRequested) { _logger.LogDebug($"CodeAction request canceled at range: {request.Range}"); - return Array.Empty(); + return s_emptyCommandOrCodeActionContainer; } IReadOnlyDictionary> corrections = await _analysisService.GetMostRecentCodeActionsForFileAsync( request.TextDocument.Uri) .ConfigureAwait(false); - if (corrections == null) + // GetMostRecentCodeActionsForFileAsync actually returns null if there's no corrections. + if (corrections is null) { - return Array.Empty(); + return s_emptyCommandOrCodeActionContainer; } List codeActions = new(); @@ -59,6 +61,11 @@ public override async Task Handle(CodeActionParams // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI. foreach (Diagnostic diagnostic in request.Context.Diagnostics) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + if (string.IsNullOrEmpty(diagnostic.Code?.String)) { _logger.LogWarning( @@ -100,8 +107,7 @@ public override async Task Handle(CodeActionParams HashSet ruleNamesProcessed = new(); foreach (Diagnostic diagnostic in request.Context.Diagnostics) { - if ( - !diagnostic.Code.HasValue || + if (!diagnostic.Code.HasValue || !diagnostic.Code.Value.IsString || string.IsNullOrEmpty(diagnostic.Code?.String)) { @@ -134,7 +140,9 @@ public override async Task Handle(CodeActionParams } } - return codeActions; + return codeActions.Count == 0 + ? s_emptyCommandOrCodeActionContainer + : codeActions; } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs index 6ac18df8e..407b0d3a0 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -3,13 +3,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.CodeLenses; -using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; @@ -21,6 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesCodeLensHandlers : CodeLensHandlerBase { + private static readonly CodeLensContainer s_emptyCodeLensContainer = new(); private readonly ILogger _logger; private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; @@ -40,12 +39,17 @@ public PsesCodeLensHandlers(ILoggerFactory factory, SymbolsService symbolsServic public override Task Handle(CodeLensParams request, CancellationToken cancellationToken) { + _logger.LogDebug($"Handling code lens request for {request.TextDocument.Uri}"); + ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); - CodeLens[] codeLensResults = ProvideCodeLenses(scriptFile, cancellationToken); - return Task.FromResult(new CodeLensContainer(codeLensResults)); + IEnumerable codeLensResults = ProvideCodeLenses(scriptFile); + + return cancellationToken.IsCancellationRequested + ? Task.FromResult(s_emptyCodeLensContainer) + : Task.FromResult(new CodeLensContainer(codeLensResults)); } - public override async Task Handle(CodeLens request, CancellationToken cancellationToken) + public override Task Handle(CodeLens request, CancellationToken cancellationToken) { // TODO: Catch deserialization exception on bad object CodeLensData codeLensData = request.Data.ToObject(); @@ -55,53 +59,23 @@ public override async Task Handle(CodeLens request, CancellationToken .FirstOrDefault(provider => provider.ProviderId.Equals(codeLensData.ProviderId, StringComparison.Ordinal)); ScriptFile scriptFile = _workspaceService.GetFile(codeLensData.Uri); - return await originalProvider.ResolveCodeLens(request, scriptFile, cancellationToken) - .ConfigureAwait(false); + return originalProvider.ResolveCodeLens(request, scriptFile, cancellationToken); } /// /// Get all the CodeLenses for a given script file. /// /// The PowerShell script file to get CodeLenses for. - /// /// All generated CodeLenses for the given script file. - private CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken) + private IEnumerable ProvideCodeLenses(ScriptFile scriptFile) { - return InvokeProviders(provider => provider.ProvideCodeLenses(scriptFile, cancellationToken)) - .SelectMany(codeLens => codeLens) - .ToArray(); - } - - /// - /// Invokes the given function synchronously against all - /// registered providers. - /// - /// The function to be invoked. - /// - /// An IEnumerable containing the results of all providers - /// that were invoked successfully. - /// - private IEnumerable InvokeProviders(Func invokeFunc) - { - Stopwatch invokeTimer = new(); - List providerResults = new(); - foreach (ICodeLensProvider provider in _symbolsService.GetCodeLensProviders()) { - try + foreach (CodeLens codeLens in provider.ProvideCodeLenses(scriptFile)) { - invokeTimer.Restart(); - providerResults.Add(invokeFunc(provider)); - invokeTimer.Stop(); - _logger.LogTrace($"Invocation of provider '{provider.GetType().Name}' completed in {invokeTimer.ElapsedMilliseconds}ms."); - } - catch (Exception e) - { - _logger.LogException($"Exception caught while invoking provider {provider.GetType().Name}:", e); + yield return codeLens; } } - - return providerResults; } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs index f39ca9917..2519891f5 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs @@ -17,6 +17,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesDefinitionHandler : DefinitionHandlerBase { + private static readonly LocationOrLocationLinks s_emptyLocationOrLocationLinks = new(); private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; @@ -45,20 +46,19 @@ public override async Task Handle(DefinitionParams requ if (foundSymbol is null) { - return new LocationOrLocationLinks(); + return s_emptyLocationOrLocationLinks; } // Short-circuit if we're already on the definition. if (foundSymbol.IsDeclaration) { - return new LocationOrLocationLinks( - new LocationOrLocationLink[] { - new LocationOrLocationLink( - new Location - { - Uri = DocumentUri.From(foundSymbol.FilePath), - Range = foundSymbol.NameRegion.ToRange() - })}); + return new LocationOrLocationLink[] { + new LocationOrLocationLink( + new Location + { + Uri = DocumentUri.From(foundSymbol.FilePath), + Range = foundSymbol.NameRegion.ToRange() + })}; } List definitionLocations = new(); @@ -74,7 +74,9 @@ public override async Task Handle(DefinitionParams requ })); } - return new LocationOrLocationLinks(definitionLocations); + return definitionLocations.Count == 0 + ? s_emptyLocationOrLocationLinks + : definitionLocations; } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs index 5758e59a1..0e470d46f 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -45,11 +45,6 @@ public override Task Handle( request.Position.Line + 1, request.Position.Character + 1); - if (occurrences is null) - { - return Task.FromResult(s_emptyHighlightContainer); - } - List highlights = new(); foreach (SymbolReference occurrence in occurrences) { @@ -62,7 +57,9 @@ public override Task Handle( _logger.LogDebug("Highlights: " + highlights); - return Task.FromResult(new DocumentHighlightContainer(highlights)); + return cancellationToken.IsCancellationRequested || highlights.Count == 0 + ? Task.FromResult(s_emptyHighlightContainer) + : Task.FromResult(new DocumentHighlightContainer(highlights)); } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index 823d01f26..c6a24bc1b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -1,15 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Logging; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; @@ -23,6 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesDocumentSymbolHandler : DocumentSymbolHandlerBase { + private static readonly SymbolInformationOrDocumentSymbolContainer s_emptySymbolInformationOrDocumentSymbolContainer = new(); private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; private readonly IDocumentSymbolProvider[] _providers; @@ -35,7 +32,8 @@ public PsesDocumentSymbolHandler(ILoggerFactory factory, WorkspaceService worksp { new ScriptDocumentSymbolProvider(), new PsdDocumentSymbolProvider(), - new PesterDocumentSymbolProvider() + new PesterDocumentSymbolProvider(), + new RegionDocumentSymbolProvider() }; } @@ -47,23 +45,21 @@ public PsesDocumentSymbolHandler(ILoggerFactory factory, WorkspaceService worksp // AKA the outline feature public override async Task Handle(DocumentSymbolParams request, CancellationToken cancellationToken) { - ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); - - IEnumerable foundSymbols = ProvideDocumentSymbols(scriptFile); - if (foundSymbols is null) - { - return null; - } + _logger.LogDebug($"Handling document symbols for {request.TextDocument.Uri}"); + ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - List symbols = new(); - foreach (SymbolReference r in foundSymbols) + + foreach (SymbolReference r in ProvideDocumentSymbols(scriptFile)) { // This async method is pretty dense with synchronous code // so it's helpful to add some yields. await Task.Yield(); - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + break; + } // Outline view should only include declarations. // @@ -92,58 +88,27 @@ public override async Task Handle(Do Location = new Location { Uri = DocumentUri.From(r.FilePath), - Range = new Range(r.NameRegion.ToRange().Start, r.ScriptRegion.ToRange().End) // Jump to name start, but keep whole range to support symbol tree in outline + // Jump to name start, but keep whole range to support symbol tree in outline + Range = new Range(r.NameRegion.ToRange().Start, r.ScriptRegion.ToRange().End) }, Name = r.Name })); } - return new SymbolInformationOrDocumentSymbolContainer(symbols); + return symbols.Count == 0 + ? s_emptySymbolInformationOrDocumentSymbolContainer + : new SymbolInformationOrDocumentSymbolContainer(symbols); } private IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile) { - return InvokeProviders(p => p.ProvideDocumentSymbols(scriptFile)) - .SelectMany(r => r); - } - - /// - /// Invokes the given function synchronously against all - /// registered providers. - /// - /// The function to be invoked. - /// - /// An IEnumerable containing the results of all providers - /// that were invoked successfully. - /// - protected IEnumerable InvokeProviders( - Func invokeFunc) - { - Stopwatch invokeTimer = new(); - List providerResults = new(); - foreach (IDocumentSymbolProvider provider in _providers) { - try + foreach (SymbolReference symbol in provider.ProvideDocumentSymbols(scriptFile)) { - invokeTimer.Restart(); - - providerResults.Add(invokeFunc(provider)); - - invokeTimer.Stop(); - - _logger.LogTrace( - $"Invocation of provider '{provider.GetType().Name}' completed in {invokeTimer.ElapsedMilliseconds}ms."); - } - catch (Exception e) - { - _logger.LogException( - $"Exception caught while invoking provider {provider.GetType().Name}:", - e); + yield return symbol; } } - - return providerResults; } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs index fbf5ad9a5..85593447c 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs @@ -16,6 +16,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesFoldingRangeHandler : FoldingRangeHandlerBase { + private static readonly Container s_emptyFoldingRangeContainer = new(); private readonly ILogger _logger; private readonly ConfigurationService _configurationService; private readonly WorkspaceService _workspaceService; @@ -37,27 +38,36 @@ public override Task> Handle(FoldingRangeRequestParam re if (cancellationToken.IsCancellationRequested) { _logger.LogDebug("FoldingRange request canceled for file: {Uri}", request.TextDocument.Uri); - return Task.FromResult(new Container()); + return Task.FromResult(s_emptyFoldingRangeContainer); } - // TODO Should be using dynamic registrations - if (!_configurationService.CurrentSettings.CodeFolding.Enable) { return Task.FromResult(new Container()); } + // TODO: Should be using dynamic registrations + if (!_configurationService.CurrentSettings.CodeFolding.Enable) + { + return Task.FromResult(s_emptyFoldingRangeContainer); + } // Avoid crash when using untitled: scheme or any other scheme where the document doesn't // have a backing file. https://github.com/PowerShell/vscode-powershell/issues/1676 // Perhaps a better option would be to parse the contents of the document as a string // as opposed to reading a file but the scenario of "no backing file" probably doesn't // warrant the extra effort. - if (!_workspaceService.TryGetFile(request.TextDocument.Uri, out ScriptFile scriptFile)) { return Task.FromResult(new Container()); } - - List result = new(); + if (!_workspaceService.TryGetFile(request.TextDocument.Uri, out ScriptFile scriptFile)) + { + return Task.FromResult(s_emptyFoldingRangeContainer); + } // If we're showing the last line, decrement the Endline of all regions by one. int endLineOffset = _configurationService.CurrentSettings.CodeFolding.ShowLastLine ? -1 : 0; - + List folds = new(); foreach (FoldingReference fold in TokenOperations.FoldableReferences(scriptFile.ScriptTokens).References) { - result.Add(new FoldingRange + if (cancellationToken.IsCancellationRequested) + { + break; + } + + folds.Add(new FoldingRange { EndCharacter = fold.EndCharacter, EndLine = fold.EndLine + endLineOffset, @@ -67,7 +77,9 @@ public override Task> Handle(FoldingRangeRequestParam re }); } - return Task.FromResult(new Container(result)); + return folds.Count == 0 + ? Task.FromResult(s_emptyFoldingRangeContainer) + : Task.FromResult(new Container(folds)); } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs index 5b72f25cd..64ccb3156 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -15,6 +15,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers // TODO: Add IDocumentOnTypeFormatHandler to support on-type formatting. internal class PsesDocumentFormattingHandler : DocumentFormattingHandlerBase { + private static readonly TextEditContainer s_emptyTextEditContainer = new(); private readonly ILogger _logger; private readonly AnalysisService _analysisService; private readonly ConfigurationService _configurationService; @@ -39,6 +40,12 @@ public PsesDocumentFormattingHandler( public override async Task Handle(DocumentFormattingParams request, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug($"Formatting request canceled for: {request.TextDocument.Uri}"); + return s_emptyTextEditContainer; + } + Services.TextDocument.ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); System.Collections.Hashtable pssaSettings = _configurationService.CurrentSettings.CodeFormatting.GetPSSASettingsHashtable( request.Options.TabSize, @@ -72,14 +79,15 @@ public override async Task Handle(DocumentFormattingParams re if (formattedScript is null) { - _logger.LogWarning($"Formatting returned null. Not formatting: {scriptFile.DocumentUri}"); - return null; + _logger.LogDebug($"Formatting returned null. Not formatting: {scriptFile.DocumentUri}"); + return s_emptyTextEditContainer; } + // Just in case the user really requested a cancellation. if (cancellationToken.IsCancellationRequested) { - _logger.LogWarning($"Formatting request canceled for: {scriptFile.DocumentUri}"); - return null; + _logger.LogDebug($"Formatting request canceled for: {scriptFile.DocumentUri}"); + return s_emptyTextEditContainer; } return new TextEditContainer(new TextEdit @@ -92,6 +100,7 @@ public override async Task Handle(DocumentFormattingParams re internal class PsesDocumentRangeFormattingHandler : DocumentRangeFormattingHandlerBase { + private static readonly TextEditContainer s_emptyTextEditContainer = new(); private readonly ILogger _logger; private readonly AnalysisService _analysisService; private readonly ConfigurationService _configurationService; @@ -116,6 +125,12 @@ public PsesDocumentRangeFormattingHandler( public override async Task Handle(DocumentRangeFormattingParams request, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug($"Formatting request canceled for: {request.TextDocument.Uri}"); + return s_emptyTextEditContainer; + } + Services.TextDocument.ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); System.Collections.Hashtable pssaSettings = _configurationService.CurrentSettings.CodeFormatting.GetPSSASettingsHashtable( request.Options.TabSize, @@ -158,14 +173,15 @@ public override async Task Handle(DocumentRangeFormattingPara if (formattedScript is null) { - _logger.LogWarning($"Formatting returned null. Not formatting: {scriptFile.DocumentUri}"); - return null; + _logger.LogDebug($"Formatting returned null. Not formatting: {scriptFile.DocumentUri}"); + return s_emptyTextEditContainer; } + // Just in case the user really requested a cancellation. if (cancellationToken.IsCancellationRequested) { - _logger.LogWarning($"Formatting request canceled for: {scriptFile.DocumentUri}"); - return null; + _logger.LogDebug($"Formatting request canceled for: {scriptFile.DocumentUri}"); + return s_emptyTextEditContainer; } return new TextEditContainer(new TextEdit diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs index 567da0159..9da05490a 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs @@ -50,7 +50,8 @@ public override async Task Handle(HoverParams request, CancellationToken await _symbolsService.FindSymbolDetailsAtLocationAsync( scriptFile, request.Position.Line + 1, - request.Position.Character + 1).ConfigureAwait(false); + request.Position.Character + 1, + cancellationToken).ConfigureAwait(false); if (symbolDetails is null) { diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs index 1d41f3571..c7297e16f 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -17,6 +17,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesReferencesHandler : ReferencesHandlerBase { + private static readonly LocationContainer s_emptyLocationContainer = new(); private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; @@ -33,6 +34,11 @@ public PsesReferencesHandler(SymbolsService symbolsService, WorkspaceService wor public override async Task Handle(ReferenceParams request, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return s_emptyLocationContainer; + } + ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); SymbolReference foundSymbol = @@ -45,6 +51,11 @@ public override async Task Handle(ReferenceParams request, Ca foreach (SymbolReference foundReference in await _symbolsService.ScanForReferencesOfSymbolAsync( foundSymbol, cancellationToken).ConfigureAwait(false)) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + // Respect the request's setting to include declarations. if (!request.Context.IncludeDeclaration && foundReference.IsDeclaration) { @@ -58,7 +69,9 @@ public override async Task Handle(ReferenceParams request, Ca }); } - return new LocationContainer(locations); + return locations.Count == 0 + ? s_emptyLocationContainer + : locations; } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs index 4299993f7..6a6f789ff 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/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 3efe4edcf..fa7f1d49a 100644 --- a/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -19,6 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesWorkspaceSymbolsHandler : WorkspaceSymbolsHandlerBase { + private static readonly Container s_emptySymbolInformationContainer = new(); private readonly ILogger _logger; private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; @@ -34,23 +35,26 @@ public PsesWorkspaceSymbolsHandler(ILoggerFactory loggerFactory, SymbolsService public override async Task> Handle(WorkspaceSymbolParams request, CancellationToken cancellationToken) { + _logger.LogDebug($"Handling workspace symbols request for query {request.Query}"); + await _symbolsService.ScanWorkspacePSFiles(cancellationToken).ConfigureAwait(false); List symbols = new(); foreach (ScriptFile scriptFile in _workspaceService.GetOpenedFiles()) { _logger.LogDebug($"Handling workspace symbols request for: {request.Query}"); - IEnumerable foundSymbols = _symbolsService.FindSymbolsInFile(scriptFile); - // TODO: Need to compute a relative path that is based on common path for all workspace files string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - foreach (SymbolReference symbol in foundSymbols) + foreach (SymbolReference symbol in _symbolsService.FindSymbolsInFile(scriptFile)) { // This async method is pretty dense with synchronous code // so it's helpful to add some yields. await Task.Yield(); - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + break; + } if (!symbol.IsDeclaration) { @@ -91,13 +95,11 @@ public override async Task> Handle(WorkspaceSymbolP } } - return new Container(symbols); + return symbols.Count == 0 + ? s_emptySymbolInformationContainer + : symbols; } - #region private Methods - private static bool IsQueryMatch(string query, string symbolName) => symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; - - #endregion } } 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..109a3667f 100644 --- a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs @@ -755,7 +755,8 @@ public async Task FindsDetailsForBuiltInCommand() SymbolDetails symbolDetails = await symbolsService.FindSymbolDetailsAtLocationAsync( GetScriptFile(FindsDetailsForBuiltInCommandData.SourceDetails), FindsDetailsForBuiltInCommandData.SourceDetails.StartLineNumber, - FindsDetailsForBuiltInCommandData.SourceDetails.StartColumnNumber).ConfigureAwait(true); + FindsDetailsForBuiltInCommandData.SourceDetails.StartColumnNumber, + CancellationToken.None).ConfigureAwait(true); Assert.Equal("Gets the processes that are running on the local computer.", symbolDetails.Documentation); } @@ -821,6 +822,35 @@ public void FindsSymbolsInFile() Assert.Equal("prop AValue", symbol.Id); Assert.Equal("AValue", symbol.Name); Assert.True(symbol.IsDeclaration); + + // There should be no region symbols unless the provider has been registered. + Assert.Empty(symbols.Where(i => i.Type == SymbolType.Region)); + } + + [Fact] + public void FindsRegionsInFile() + { + symbolsService.TryRegisterDocumentSymbolProvider(new RegionDocumentSymbolProvider()); + IEnumerable symbols = FindSymbolsInFile(FindSymbolsInMultiSymbolFile.SourceDetails); + Assert.Collection(symbols.Where(i => i.Type == SymbolType.Region), + (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); + }); } [Fact]