diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index de2b092f4..4a08bec2d 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -117,6 +117,9 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs new file mode 100644 index 000000000..bab5d3ee1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs @@ -0,0 +1,82 @@ +// +// 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.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/expandAlias")] + public interface IExpandAliasHandler : IJsonRpcRequestHandler { } + + public class ExpandAliasParams : IRequest + { + public string Text { get; set; } + } + + public class ExpandAliasResult + { + public string Text { get; set; } + } + + public class ExpandAliasHandler : IExpandAliasHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public ExpandAliasHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task Handle(ExpandAliasParams request, CancellationToken cancellationToken) + { + const string script = @" +function __Expand-Alias { + + param($targetScript) + + [ref]$errors=$null + + $tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) | + Sort-Object Start -Descending + + foreach ($token in $tokens) { + $definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition + + if($definition) { + $lhs=$targetScript.Substring(0, $token.Start) + $rhs=$targetScript.Substring($token.Start + $token.Length) + + $targetScript=$lhs + $definition + $rhs + } + } + + $targetScript +}"; + + // TODO: Refactor to not rerun the function definition every time. + var psCommand = new PSCommand(); + psCommand + .AddScript(script) + .AddStatement() + .AddCommand("__Expand-Alias") + .AddArgument(request.Text); + var result = await _powerShellContextService.ExecuteCommandAsync(psCommand); + + return new ExpandAliasResult + { + Text = result.First() + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs new file mode 100644 index 000000000..aee02fe1b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs @@ -0,0 +1,81 @@ +// +// 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.Generic; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/getCommand")] + public interface IGetCommandHandler : IJsonRpcRequestHandler> { } + + public class GetCommandParams : IRequest> { } + + /// + /// Describes the message to get the details for a single PowerShell Command + /// from the current session + /// + public class PSCommandMessage + { + public string Name { get; set; } + public string ModuleName { get; set; } + public string DefaultParameterSet { get; set; } + public Dictionary Parameters { get; set; } + public System.Collections.ObjectModel.ReadOnlyCollection ParameterSets { get; set; } + } + + public class GetCommandHandler : IGetCommandHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public GetCommandHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task> Handle(GetCommandParams request, CancellationToken cancellationToken) + { + PSCommand psCommand = new PSCommand(); + + // Executes the following: + // Get-Command -CommandType Function,Cmdlet,ExternalScript | Select-Object -Property Name,ModuleName | Sort-Object -Property Name + psCommand + .AddCommand("Microsoft.PowerShell.Core\\Get-Command") + .AddParameter("CommandType", new[] { "Function", "Cmdlet", "ExternalScript" }) + .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") + .AddParameter("Property", new[] { "Name", "ModuleName" }) + .AddCommand("Microsoft.PowerShell.Utility\\Sort-Object") + .AddParameter("Property", "Name"); + + IEnumerable result = await _powerShellContextService.ExecuteCommandAsync(psCommand); + + var commandList = new List(); + if (result != null) + { + foreach (dynamic command in result) + { + commandList.Add(new PSCommandMessage + { + Name = command.Name, + ModuleName = command.ModuleName, + Parameters = command.Parameters, + ParameterSets = command.ParameterSets, + DefaultParameterSet = command.DefaultParameterSet + }); + } + } + + return commandList; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs index 724c921d4..4b57ecd39 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -15,7 +15,7 @@ public GetVersionHandler(ILoggerFactory factory) _logger = factory.CreateLogger(); } - public Task Handle(GetVersionParams request, CancellationToken cancellationToken) + public Task Handle(GetVersionParams request, CancellationToken cancellationToken) { var architecture = PowerShellProcessArchitecture.Unknown; // This should be changed to using a .NET call sometime in the future... but it's just for logging purposes. @@ -32,7 +32,8 @@ public Task Handle(GetVersionParams request, Cancellat } } - return Task.FromResult(new PowerShellVersionDetails { + return Task.FromResult(new PowerShellVersion + { Version = VersionUtils.PSVersion.ToString(), Edition = VersionUtils.PSEdition, DisplayVersion = VersionUtils.PSVersion.ToString(2), diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs index c4570d8b8..253e34daa 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs @@ -1,17 +1,43 @@ +using Microsoft.PowerShell.EditorServices.Session; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; namespace PowerShellEditorServices.Engine.Services.Handlers { [Serial, Method("powerShell/getVersion")] - public interface IGetVersionHandler : IJsonRpcRequestHandler { } + public interface IGetVersionHandler : IJsonRpcRequestHandler { } - public class GetVersionParams : IRequest { } + public class GetVersionParams : IRequest { } - public class PowerShellVersionDetails { + public class PowerShellVersion + { public string Version { get; set; } public string DisplayVersion { get; set; } public string Edition { get; set; } public string Architecture { get; set; } + + public PowerShellVersion() + { + } + + public PowerShellVersion(PowerShellVersionDetails versionDetails) + { + this.Version = versionDetails.VersionString; + this.DisplayVersion = $"{versionDetails.Version.Major}.{versionDetails.Version.Minor}"; + this.Edition = versionDetails.Edition; + + switch (versionDetails.Architecture) + { + case PowerShellProcessArchitecture.X64: + this.Architecture = "x64"; + break; + case PowerShellProcessArchitecture.X86: + this.Architecture = "x86"; + break; + default: + this.Architecture = "Architecture Unknown"; + break; + } + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs new file mode 100644 index 000000000..aecf58083 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/showHelp")] + public interface IShowHelpHandler : IJsonRpcNotificationHandler { } + + public class ShowHelpParams : IRequest + { + public string Text { get; set; } + } + + public class ShowHelpHandler : IShowHelpHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public ShowHelpHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task Handle(ShowHelpParams request, CancellationToken cancellationToken) + { + const string CheckHelpScript = @" + [CmdletBinding()] + param ( + [String]$CommandName + ) + try { + $command = Microsoft.PowerShell.Core\Get-Command $CommandName -ErrorAction Stop + } catch [System.Management.Automation.CommandNotFoundException] { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + try { + $helpUri = [Microsoft.PowerShell.Commands.GetHelpCodeMethods]::GetHelpUri($command) + + $oldSslVersion = [System.Net.ServicePointManager]::SecurityProtocol + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + + # HEAD means we don't need the content itself back, just the response header + $status = (Microsoft.PowerShell.Utility\Invoke-WebRequest -Method Head -Uri $helpUri -TimeoutSec 5 -ErrorAction Stop).StatusCode + if ($status -lt 400) { + $null = Microsoft.PowerShell.Core\Get-Help $CommandName -Online + return + } + } catch { + # Ignore - we want to drop out to Get-Help -Full + } finally { + [System.Net.ServicePointManager]::SecurityProtocol = $oldSslVersion + } + + return Microsoft.PowerShell.Core\Get-Help $CommandName -Full + "; + + string helpParams = request.Text; + if (string.IsNullOrEmpty(helpParams)) { helpParams = "Get-Help"; } + + PSCommand checkHelpPSCommand = new PSCommand() + .AddScript(CheckHelpScript, useLocalScope: true) + .AddArgument(helpParams); + + // TODO: Rather than print the help in the console, we should send the string back + // to VSCode to display in a help pop-up (or similar) + await _powerShellContextService.ExecuteCommandAsync(checkHelpPSCommand, sendOutputToHost: true); + return Unit.Value; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs index 6d7dc6462..b4dfeea3e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -20,6 +20,7 @@ using Microsoft.PowerShell.EditorServices.Engine; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; +using PowerShellEditorServices.Engine.Services.Handlers; namespace Microsoft.PowerShell.EditorServices { @@ -155,6 +156,7 @@ public PowerShellContextService( this.logger = logger; this.isPSReadLineEnabled = isPSReadLineEnabled; + RunspaceChanged += PowerShellContext_RunspaceChangedAsync; ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; } @@ -1726,6 +1728,39 @@ private void OnExecutionStatusChanged( hadErrors)); } + private void PowerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) + { + _languageServer.SendNotification( + "powerShell/runspaceChanged", + new MinifiedRunspaceDetails(e.NewRunspace)); + } + + + // TODO: Refactor this, RunspaceDetails, PowerShellVersion, and PowerShellVersionDetails + // It's crazy that this is 4 different types. + // P.S. MinifiedRunspaceDetails use to be called RunspaceDetails... as in, there were 2 DIFFERENT + // RunspaceDetails types in this codebase but I've changed it to be minified since the type is + // slightly simpler than the other RunspaceDetails. + public class MinifiedRunspaceDetails + { + public PowerShellVersion PowerShellVersion { get; set; } + + public RunspaceLocation RunspaceType { get; set; } + + public string ConnectionString { get; set; } + + public MinifiedRunspaceDetails() + { + } + + public MinifiedRunspaceDetails(RunspaceDetails eventArgs) + { + this.PowerShellVersion = new PowerShellVersion(eventArgs.PowerShellVersion); + this.RunspaceType = eventArgs.Location; + this.ConnectionString = eventArgs.ConnectionString; + } + } + /// /// Event hook on the PowerShell context to listen for changes in script execution status /// diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 2b71ba03f..6597f494e 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -102,8 +102,8 @@ private async Task WaitForDiagnostics() [Fact] public async Task CanSendPowerShellGetVersionRequest() { - PowerShellVersionDetails details - = await LanguageClient.SendRequest("powerShell/getVersion", new GetVersionParams()); + PowerShellVersion details + = await LanguageClient.SendRequest("powerShell/getVersion", new GetVersionParams()); if(PwshExe == "powershell") { @@ -797,5 +797,31 @@ await LanguageClient.SendRequest( Assert.Equal("", evaluateResponseBody.Result); Assert.Equal(0, evaluateResponseBody.VariablesReference); } + + [Fact] + public async Task CanSendGetCommandRequest() + { + List pSCommandMessages = + await LanguageClient.SendRequest>("powerShell/getCommand", new GetCommandParams()); + + Assert.NotEmpty(pSCommandMessages); + // There should be at least 20 commands or so. + Assert.True(pSCommandMessages.Count > 20); + } + + [Fact] + public async Task CanSendExpandAliasRequest() + { + ExpandAliasResult expandAliasResult = + await LanguageClient.SendRequest( + "powerShell/expandAlias", + new ExpandAliasParams + { + Text = "gci" + } + ); + + Assert.Equal("Get-ChildItem", expandAliasResult.Text); + } } }