diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 6908cc766..709f0d1df 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -112,6 +112,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs new file mode 100644 index 000000000..072917e06 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// A class for containing the commandName, the command's + /// possible signatures, and the script extent of the command + /// + public class ParameterSetSignatures + { + #region Properties + /// + /// Gets the name of the command + /// + public string CommandName { get; internal set; } + + /// + /// Gets the collection of signatures for the command + /// + public ParameterSetSignature[] Signatures { get; internal set; } + + /// + /// Gets the script extent of the command + /// + public ScriptRegion ScriptRegion { get; internal set; } + #endregion + + /// + /// Constructs an instance of a ParameterSetSignatures object + /// + /// Collection of parameter set info + /// The SymbolReference of the command + public ParameterSetSignatures(IEnumerable commandInfoSet, SymbolReference foundSymbol) + { + List paramSetSignatures = new List(); + foreach (CommandParameterSetInfo setInfo in commandInfoSet) + { + paramSetSignatures.Add(new ParameterSetSignature(setInfo)); + } + Signatures = paramSetSignatures.ToArray(); + CommandName = foundSymbol.ScriptRegion.Text; + ScriptRegion = foundSymbol.ScriptRegion; + } + } + + /// + /// A class for containing the signature text and the collection of parameters for a signature + /// + public class ParameterSetSignature + { + private static readonly ConcurrentDictionary commonParameterNames = + new ConcurrentDictionary(); + + static ParameterSetSignature() + { + commonParameterNames.TryAdd("Verbose", true); + commonParameterNames.TryAdd("Debug", true); + commonParameterNames.TryAdd("ErrorAction", true); + commonParameterNames.TryAdd("WarningAction", true); + commonParameterNames.TryAdd("InformationAction", true); + commonParameterNames.TryAdd("ErrorVariable", true); + commonParameterNames.TryAdd("WarningVariable", true); + commonParameterNames.TryAdd("InformationVariable", true); + commonParameterNames.TryAdd("OutVariable", true); + commonParameterNames.TryAdd("OutBuffer", true); + commonParameterNames.TryAdd("PipelineVariable", true); + } + + #region Properties + /// + /// Gets the signature text + /// + public string SignatureText { get; internal set; } + + /// + /// Gets the collection of parameters for the signature + /// + public IEnumerable Parameters { get; internal set; } + #endregion + + /// + /// Constructs an instance of a ParameterSetSignature + /// + /// Collection of parameter info + public ParameterSetSignature(CommandParameterSetInfo commandParamInfoSet) + { + List parameterInfo = new List(); + foreach (CommandParameterInfo commandParameterInfo in commandParamInfoSet.Parameters) + { + if (!commonParameterNames.ContainsKey(commandParameterInfo.Name)) + { + parameterInfo.Add(new ParameterInfo(commandParameterInfo)); + } + } + + SignatureText = commandParamInfoSet.ToString(); + Parameters = parameterInfo.ToArray(); + } + } + + /// + /// A class for containing the parameter info of a parameter + /// + public class ParameterInfo + { + #region Properties + /// + /// Gets the name of the parameter + /// + public string Name { get; internal set; } + + /// + /// Gets the type of the parameter + /// + public string ParameterType { get; internal set; } + + /// + /// Gets the position of the parameter + /// + public int Position { get; internal set; } + + /// + /// Gets a boolean for whetheer or not the parameter is required + /// + public bool IsMandatory { get; internal set; } + + /// + /// Gets the help message of the parameter + /// + public string HelpMessage { get; internal set; } + #endregion + + /// + /// Constructs an instance of a ParameterInfo object + /// + /// Parameter info of the parameter + public ParameterInfo(CommandParameterInfo parameterInfo) + { + this.Name = "-" + parameterInfo.Name; + this.ParameterType = parameterInfo.ParameterType.FullName; + this.Position = parameterInfo.Position; + this.IsMandatory = parameterInfo.IsMandatory; + this.HelpMessage = parameterInfo.HelpMessage; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index e0c1e57be..326bcc6d5 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Management.Automation; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -262,5 +263,62 @@ public async Task FindSymbolDetailsAtLocationAsync( return symbolDetails; } + + /// + /// Finds the parameter set hints of a specific command (determined by a given file location) + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// ParameterSetSignatures + public async Task FindParameterSetsInFileAsync( + ScriptFile file, + int lineNumber, + int columnNumber, + PowerShellContextService powerShellContext) + { + SymbolReference foundSymbol = + AstOperations.FindCommandAtPosition( + file.ScriptAst, + lineNumber, + columnNumber); + + if (foundSymbol == null) + { + return null; + } + + CommandInfo commandInfo = + await CommandHelpers.GetCommandInfoAsync( + foundSymbol.SymbolName, + powerShellContext); + + if (commandInfo == null) + { + return null; + } + + try + { + IEnumerable commandParamSets = commandInfo.ParameterSets; + return new ParameterSetSignatures(commandParamSets, foundSymbol); + } + catch (RuntimeException e) + { + // A RuntimeException will be thrown when an invalid attribute is + // on a parameter binding block and then that command/script has + // its signatures resolved by typing it into a script. + _logger.LogException("RuntimeException encountered while accessing command parameter sets", e); + + return null; + } + catch (InvalidOperationException) + { + // For some commands there are no paramsets (like applications). Until + // the valid command types are better understood, catch this exception + // which gets raised when there are no ParameterSets for the command type. + return null; + } + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs new file mode 100644 index 000000000..a28828464 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class SignatureHelpHandler : ISignatureHelpHandler + { + private static readonly SignatureInformation[] s_emptySignatureResult = new SignatureInformation[0]; + + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + private readonly PowerShellContextService _powerShellContextService; + + private SignatureHelpCapability _capability; + + public SignatureHelpHandler( + ILoggerFactory factory, + SymbolsService symbolsService, + WorkspaceService workspaceService, + PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + _powerShellContextService = powerShellContextService; + } + + public SignatureHelpRegistrationOptions GetRegistrationOptions() + { + return new SignatureHelpRegistrationOptions + { + DocumentSelector = _documentSelector, + // A sane default of " ". We may be able to include others like "-". + TriggerCharacters = new Container(" ") + }; + } + + public async Task Handle(SignatureHelpParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + ParameterSetSignatures parameterSets = + await _symbolsService.FindParameterSetsInFileAsync( + scriptFile, + (int) request.Position.Line + 1, + (int) request.Position.Character + 1, + _powerShellContextService); + + SignatureInformation[] signatures = s_emptySignatureResult; + + if (parameterSets != null) + { + signatures = new SignatureInformation[parameterSets.Signatures.Length]; + for (int i = 0; i < signatures.Length; i++) + { + var parameters = new ParameterInformation[parameterSets.Signatures[i].Parameters.Count()]; + int j = 0; + foreach (ParameterInfo param in parameterSets.Signatures[i].Parameters) + { + parameters[j] = CreateParameterInfo(param); + j++; + } + + signatures[i] = new SignatureInformation + { + Label = parameterSets.CommandName + " " + parameterSets.Signatures[i].SignatureText, + Documentation = null, + Parameters = parameters, + }; + } + } + + return new SignatureHelp + { + Signatures = signatures, + ActiveParameter = null, + ActiveSignature = 0 + }; + } + + public void SetCapability(SignatureHelpCapability capability) + { + _capability = capability; + } + + private static ParameterInformation CreateParameterInfo(ParameterInfo parameterInfo) + { + return new ParameterInformation + { + Label = parameterInfo.Name, + Documentation = string.Empty + }; + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index c7593dc4e..2f41e9cd7 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1,4 +1,9 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -664,5 +669,28 @@ public async Task CanSendHoverRequest() Assert.Equal("Writes customized output to a host.", str2.Value); }); } + + [Fact] + public async Task CanSendSignatureHelpRequest() + { + string filePath = NewTestFile("Get-Date "); + + SignatureHelp signatureHelp = await LanguageClient.SendRequest( + "textDocument/signatureHelp", + new SignatureHelpParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(filePath) + }, + Position = new Position + { + Line = 0, + Character = 9 + } + }); + + Assert.Contains("Get-Date", signatureHelp.Signatures.First().Label); + } } }