diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 9629dab9b..36ade043e 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -22,7 +22,6 @@ $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "C $script:TargetPlatform = "netstandard2.0" $script:TargetFrameworksParam = "/p:TargetFrameworks=`"$script:TargetPlatform`"" $script:RequiredSdkVersion = (Get-Content (Join-Path $PSScriptRoot 'global.json') | ConvertFrom-Json).sdk.version -$script:MinimumPesterVersion = '4.7' $script:NugetApiUriBase = 'https://www.nuget.org/api/v2/package' $script:ModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices/bin/" $script:VSCodeModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices.VSCode/bin/" @@ -173,7 +172,7 @@ function Invoke-WithCreateDefaultHook { } } -task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, PackageNuGet { +task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestE2E, PackageNuGet { $dotnetPath = "$PSScriptRoot/.dotnet" $dotnetExePath = if ($script:IsUnix) { "$dotnetPath/dotnet" } else { "$dotnetPath/dotnet.exe" } @@ -324,18 +323,13 @@ task Build { exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } } -task BuildPsesClientModule SetupDotNet,{ - Write-Verbose 'Building PsesPsClient testing module' - & $PSScriptRoot/tools/PsesPsClient/build.ps1 -DotnetExe $script:dotnetExe -} - function DotNetTestFilter { # Reference https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests if ($TestFilter) { @("--filter",$TestFilter) } else { "" } } -# task Test TestServer,TestProtocol,TestPester -task Test TestPester +# task Test TestServer,TestProtocol,TestE2E +task Test TestE2E task TestServer { Set-Location .\test\PowerShellEditorServices.Test\ @@ -373,26 +367,11 @@ task TestHost { exec { & $script:dotnetExe test -f $script:TestRuntime.Core (DotNetTestFilter) } } -task TestPester Build,BuildPsesClientModule,EnsurePesterInstalled,{ - $testParams = @{} - if ($env:TF_BUILD) - { - $testParams += @{ - OutputFormat = 'NUnitXml' - OutputFile = 'TestResults.xml' - } - } - $result = Invoke-Pester "$PSScriptRoot/test/Pester/" @testParams -PassThru - - if ($result.FailedCount -gt 0) - { - throw "$($result.FailedCount) tests failed." - } -} +task TestE2E { + Set-Location .\test\PowerShellEditorServices.Test.E2E\ -task EnsurePesterInstalled -If (-not (Get-Module Pester -ListAvailable | Where-Object Version -ge $script:MinimumPesterVersion)) { - Write-Warning "Required Pester version not found, installing Pester to current user scope" - Install-Module -Scope CurrentUser Pester -Force -SkipPublisherCheck + $env:PWSH_EXE_NAME = if ($IsCoreCLR) { "pwsh" } else { "powershell" } + exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } } task LayoutModule -After Build { diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index fce19ffef..71e28ed0d 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.VS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Engine", "src\PowerShellEditorServices.Engine\PowerShellEditorServices.Engine.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.E2E", "test\PowerShellEditorServices.Test.E2E\PowerShellEditorServices.Test.E2E.csproj", "{2561F253-8F72-436A-BCC3-AA63AB82EDC0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -148,6 +150,18 @@ Global {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x64.Build.0 = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x86.ActiveCfg = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x86.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|x64.Build.0 = Debug|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|x86.Build.0 = Debug|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|Any CPU.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x64.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x64.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x86.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -162,5 +176,6 @@ Global {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} {3B38E8DA-8BFF-4264-AF16-47929E6398A3} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {29EEDF03-0990-45F4-846E-2616970D1FA2} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} + {2561F253-8F72-436A-BCC3-AA63AB82EDC0} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} EndGlobalSection EndGlobal diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index f67f28c08..55bf2ae2a 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Host; using Serilog; namespace Microsoft.PowerShell.EditorServices.Engine @@ -234,13 +235,15 @@ public void StartLanguageService( _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - var powerShellContext = GetFullyInitializedPowerShellContext(profilePaths); - _serviceCollection .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(powerShellContext) + .AddSingleton( + (provider) => + GetFullyInitializedPowerShellContext( + provider.GetService(), + profilePaths)) .AddSingleton() .AddSingleton( (provider) => @@ -263,14 +266,29 @@ public void StartLanguageService( _factory.CreateLogger()); }); - _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) + switch (config.TransportType) { - NamedPipeName = config.InOutPipeName ?? config.InPipeName, - OutNamedPipeName = config.OutPipeName, - LoggerFactory = _factory, - MinimumLogLevel = LogLevel.Trace, + case EditorServiceTransportType.NamedPipe: + _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) + { + NamedPipeName = config.InOutPipeName ?? config.InPipeName, + OutNamedPipeName = config.OutPipeName, + LoggerFactory = _factory, + MinimumLogLevel = LogLevel.Trace, + } + .BuildLanguageServer(); + break; + + case EditorServiceTransportType.Stdio: + _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) + { + Stdio = true, + LoggerFactory = _factory, + MinimumLogLevel = LogLevel.Trace, + } + .BuildLanguageServer(); + break; } - .BuildLanguageServer(); _logger.LogInformation("Starting language server"); @@ -282,21 +300,22 @@ public void StartLanguageService( config.TransportType, config.Endpoint)); } - private PowerShellContextService GetFullyInitializedPowerShellContext(ProfilePaths profilePaths) + private PowerShellContextService GetFullyInitializedPowerShellContext( + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + ProfilePaths profilePaths) { var logger = _factory.CreateLogger(); + + // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise + // issues arise when redirecting stdio. var powerShellContext = new PowerShellContextService( logger, - _featureFlags.Contains("PSReadLine")); + _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); - // TODO: Bring this back - //EditorServicesPSHostUserInterface hostUserInterface = - // _enableConsoleRepl - // ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) - // : new ProtocolPSHostUserInterface(powerShellContext, messageSender, logger); EditorServicesPSHostUserInterface hostUserInterface = - new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost); - + _enableConsoleRepl + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) + : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); EditorServicesPSHost psHost = new EditorServicesPSHost( diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 3bbf26dff..e40b1e60d 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -3,18 +3,19 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System.IO; using System.IO.Pipes; using System.Reflection; -using System.Threading.Tasks; +using System.Security.AccessControl; using System.Security.Principal; +using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using OS = OmniSharp.Extensions.LanguageServer.Server; -using System.Security.AccessControl; +using Microsoft.PowerShell.EditorServices.TextDocument; using OmniSharp.Extensions.LanguageServer.Server; +using OS = OmniSharp.Extensions.LanguageServer.Server; using PowerShellEditorServices.Engine.Services.Handlers; -using Microsoft.PowerShell.EditorServices.TextDocument; -using System.IO; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -22,6 +23,8 @@ public class OmnisharpLanguageServer : ILanguageServer { public class Configuration { + public bool Stdio { get; set; } + public string NamedPipeName { get; set; } public string OutNamedPipeName { get; set; } @@ -58,24 +61,33 @@ public OmnisharpLanguageServer( public async Task StartAsync() { _languageServer = await OS.LanguageServer.From(options => { - NamedPipeServerStream namedPipe = CreateNamedPipe( - _configuration.NamedPipeName, - _configuration.OutNamedPipeName, - out NamedPipeServerStream outNamedPipe); ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); - logger.LogInformation("Waiting for connection"); - namedPipe.WaitForConnection(); - if (outNamedPipe != null) + if (_configuration.Stdio) { - outNamedPipe.WaitForConnection(); + options.WithInput(System.Console.OpenStandardInput()); + options.WithOutput(System.Console.OpenStandardOutput()); } + else + { + NamedPipeServerStream namedPipe = CreateNamedPipe( + _configuration.NamedPipeName, + _configuration.OutNamedPipeName, + out NamedPipeServerStream outNamedPipe); - logger.LogInformation("Connected"); + logger.LogInformation("Waiting for connection"); + namedPipe.WaitForConnection(); + if (outNamedPipe != null) + { + outNamedPipe.WaitForConnection(); + } - options.Input = namedPipe; - options.Output = outNamedPipe ?? namedPipe; + logger.LogInformation("Connected"); + + options.Input = namedPipe; + options.Output = outNamedPipe ?? namedPipe; + } options.LoggerFactory = _configuration.LoggerFactory; options.MinimumLogLevel = _configuration.MinimumLogLevel; diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs index 801f74037..d283b99a8 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs @@ -10,6 +10,8 @@ public OmnisharpLanguageServerBuilder(IServiceCollection serviceCollection) Services = serviceCollection; } + public bool Stdio { get; set; } + public string NamedPipeName { get; set; } public string OutNamedPipeName { get; set; } @@ -28,7 +30,8 @@ public ILanguageServer BuildLanguageServer() MinimumLogLevel = MinimumLogLevel, NamedPipeName = NamedPipeName, OutNamedPipeName = OutNamedPipeName, - Services = Services + Services = Services, + Stdio = Stdio }; return new OmnisharpLanguageServer(config); diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs new file mode 100644 index 000000000..72de43587 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Protocol.Messages +{ + public class ShowChoicePromptRequest + { + public bool IsMultiChoice { get; set; } + + public string Caption { get; set; } + + public string Message { get; set; } + + public ChoiceDetails[] Choices { get; set; } + + public int[] DefaultChoices { get; set; } + } + + public class ShowChoicePromptResponse + { + public bool PromptCancelled { get; set; } + + public string ResponseText { get; set; } + } + + public class ShowInputPromptRequest + { + /// + /// Gets or sets the name of the field. + /// + public string Name { get; set; } + + /// + /// Gets or sets the descriptive label for the field. + /// + public string Label { get; set; } + } + + public class ShowInputPromptResponse + { + public bool PromptCancelled { get; set; } + + public string ResponseText { get; set; } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs new file mode 100644 index 000000000..38a619741 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs @@ -0,0 +1,180 @@ +// +// 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 Microsoft.PowerShell.EditorServices.Console; +using System.Threading.Tasks; +using System.Threading; +using System.Security; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Protocol.Messages; + +namespace Microsoft.PowerShell.EditorServices.Host +{ + internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler + { + private readonly ILanguageServer _languageServer; + private readonly IHostInput _hostInput; + private TaskCompletionSource _readLineTask; + + public ProtocolChoicePromptHandler( + ILanguageServer languageServer, + IHostInput hostInput, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) + { + _languageServer = languageServer; + this._hostInput = hostInput; + this.hostOutput = hostOutput; + } + + protected override void ShowPrompt(PromptStyle promptStyle) + { + base.ShowPrompt(promptStyle); + + _languageServer.SendRequest( + "powerShell/showChoicePrompt", + new ShowChoicePromptRequest + { + IsMultiChoice = this.IsMultiChoice, + Caption = this.Caption, + Message = this.Message, + Choices = this.Choices, + DefaultChoices = this.DefaultChoices + }) + .ContinueWith(HandlePromptResponse) + .ConfigureAwait(false); + } + + protected override Task ReadInputStringAsync(CancellationToken cancellationToken) + { + this._readLineTask = new TaskCompletionSource(); + return this._readLineTask.Task; + } + + private void HandlePromptResponse( + Task responseTask) + { + if (responseTask.IsCompleted) + { + ShowChoicePromptResponse response = responseTask.Result; + + if (!response.PromptCancelled) + { + this.hostOutput.WriteOutput( + response.ResponseText, + OutputType.Normal); + + this._readLineTask.TrySetResult(response.ResponseText); + } + else + { + // Cancel the current prompt + this._hostInput.SendControlC(); + } + } + else + { + if (responseTask.IsFaulted) + { + // Log the error + Logger.LogError( + "ShowChoicePrompt request failed with error:\r\n{0}", + responseTask.Exception.ToString()); + } + + // Cancel the current prompt + this._hostInput.SendControlC(); + } + + this._readLineTask = null; + } + } + + internal class ProtocolInputPromptHandler : ConsoleInputPromptHandler + { + private readonly ILanguageServer _languageServer; + private readonly IHostInput hostInput; + private TaskCompletionSource readLineTask; + + public ProtocolInputPromptHandler( + ILanguageServer languageServer, + IHostInput hostInput, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) + { + _languageServer = languageServer; + this.hostInput = hostInput; + this.hostOutput = hostOutput; + } + + protected override void ShowFieldPrompt(FieldDetails fieldDetails) + { + base.ShowFieldPrompt(fieldDetails); + + _languageServer.SendRequest( + "powerShell/showInputPrompt", + new ShowInputPromptRequest + { + Name = fieldDetails.Name, + Label = fieldDetails.Label + }).ContinueWith(HandlePromptResponse) + .ConfigureAwait(false); + } + + protected override Task ReadInputStringAsync(CancellationToken cancellationToken) + { + this.readLineTask = new TaskCompletionSource(); + return this.readLineTask.Task; + } + + private void HandlePromptResponse( + Task responseTask) + { + if (responseTask.IsCompleted) + { + ShowInputPromptResponse response = responseTask.Result; + + if (!response.PromptCancelled) + { + this.hostOutput.WriteOutput( + response.ResponseText, + OutputType.Normal); + + this.readLineTask.TrySetResult(response.ResponseText); + } + else + { + // Cancel the current prompt + this.hostInput.SendControlC(); + } + } + else + { + if (responseTask.IsFaulted) + { + // Log the error + Logger.LogError( + "ShowInputPrompt request failed with error:\r\n{0}", + responseTask.Exception.ToString()); + } + + // Cancel the current prompt + this.hostInput.SendControlC(); + } + + this.readLineTask = null; + } + + protected override Task ReadSecureStringAsync(CancellationToken cancellationToken) + { + // TODO: Write a message to the console + throw new NotImplementedException(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs new file mode 100644 index 000000000..3bbec39ae --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Console; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Host +{ + internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface + { + #region Private Fields + + private readonly ILanguageServer _languageServer; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// + public ProtocolPSHostUserInterface( + ILanguageServer languageServer, + PowerShellContextService powerShellContext, + ILogger logger) + : base(powerShellContext, new SimplePSHostRawUserInterface(logger), logger) + { + _languageServer = languageServer; + } + + #endregion + + /// + /// Writes output of the given type to the user interface with + /// the given foreground and background colors. Also includes + /// a newline if requested. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + /// + /// Specifies the type of output to be written. + /// + /// + /// Specifies the foreground color of the output to be written. + /// + /// + /// Specifies the background color of the output to be written. + /// + public override void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + // TODO: Invoke the "output" notification! + } + + /// + /// Sends a progress update event to the user. + /// + /// The source ID of the progress event. + /// The details of the activity's current progress. + protected override void UpdateProgress( + long sourceId, + ProgressDetails progressDetails) + { + // TODO: Send a new message. + } + + protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) + { + // This currently does nothing because the "evaluate" request + // will cancel the current prompt and execute the user's + // script selection. + return new TaskCompletionSource().Task; + } + + protected override InputPromptHandler OnCreateInputPromptHandler() + { + return new ProtocolInputPromptHandler(_languageServer, this, this, this.Logger); + } + + protected override ChoicePromptHandler OnCreateChoicePromptHandler() + { + return new ProtocolChoicePromptHandler(_languageServer, this, this, this.Logger); + } + } +} diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 deleted file mode 100644 index 1054c8268..000000000 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ /dev/null @@ -1,532 +0,0 @@ - -$script:ExceptionRegex = [regex]::new('\s*Exception: (.*)$', 'Compiled,Multiline,IgnoreCase') -function ReportLogErrors -{ - param( - [Parameter()][string]$LogPath, - - [Parameter()][ref]<#[int]#>$FromIndex = 0, - - [Parameter()][string[]]$IgnoreException = @() - ) - - $logEntries = Parse-PsesLog $LogPath | - Where-Object Index -ge $FromIndex.Value - - # Update the index to the latest in the log - $FromIndex.Value = ($FromIndex.Value,$errorLogs.Index | Measure-Object -Maximum).Maximum - - $errorLogs = $logEntries | - Where-Object LogLevel -eq Error | - Where-Object { - $match = $script:ExceptionRegex.Match($_.Message.Data) - - (-not $match) -or ($match.Groups[1].Value.Trim() -notin $IgnoreException) - } - - if ($errorLogs) - { - $errorLogs | ForEach-Object { Write-Error "ERROR from PSES log: $($_.Message.Data)" } - } -} - -function CheckErrorResponse -{ - [CmdletBinding()] - param( - $Response - ) - - if (-not ($Response -is [PsesPsClient.LspErrorResponse])) - { - return - } - - $msg = @" -Error Response Received -Code: $($Response.Code) -Message: - $($Response.Message) - -Data: - $($Response.Data) -"@ - - throw $msg -} - -function New-TestFile -{ - param( - [Parameter(Mandatory)] - [string] - $Script, - - [Parameter()] - [string] - $FileName = "$([System.IO.Path]::GetRandomFileName()).ps1" - ) - - $file = Set-Content -Path (Join-Path $TestDrive $FileName) -Value $Script -PassThru -Force - - $request = Send-LspDidOpenTextDocumentRequest -Client $client ` - -Uri ([Uri]::new($file.PSPath).AbsoluteUri) ` - -Text ($file[0].ToString()) - - # To give PSScriptAnalyzer a chance to run. - Start-Sleep 1 - - # There's no response for this message, but we need to call Get-LspResponse - # to increment the counter. - Get-LspResponse -Client $client -Id $request.Id | Out-Null - - # Throw out any notifications from the first PSScriptAnalyzer run. - Get-LspNotification -Client $client | Out-Null - - $file.PSPath -} - -Describe "Loading and running PowerShellEditorServices" { - BeforeAll { - Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" - Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" - - $logIdx = 0 - $psesServer = Start-PsesServer - $client = Connect-PsesServer -InPipeName $psesServer.SessionDetails.languageServiceWritePipeName -OutPipeName $psesServer.SessionDetails.languageServiceReadPipeName - } - - # This test MUST be first - It "Starts and responds to an initialization request" { - $startDir = New-Item -ItemType Directory TestDrive:\start - $request = Send-LspInitializeRequest -Client $client -RootPath ($startDir.FullName) - $response = Get-LspResponse -Client $client -Id $request.Id #-WaitMillis 99999 - $response.Id | Should -BeExactly $request.Id - - CheckErrorResponse -Response $response - - #ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) - } - - It "Can handle powerShell/getVersion request" { - $request = Send-LspRequest -Client $client -Method "powerShell/getVersion" - $response = Get-LspResponse -Client $client -Id $request.Id - if ($IsCoreCLR) { - $response.Result.edition | Should -Be "Core" - } else { - $response.Result.edition | Should -Be "Desktop" - } - } - - It "Can handle WorkspaceSymbol request" { - New-TestFile -Script " -function Get-Foo { - Write-Host 'hello' -} -" - - $request = Send-LspRequest -Client $client -Method "workspace/symbol" -Parameters @{ - query = "" - } - $response = Get-LspResponse -Client $client -Id $request.Id -WaitMillis 99999 - $response.Id | Should -BeExactly $request.Id - - $response.Result.Count | Should -Be 1 - $response.Result.name | Should -BeLike "Get-Foo*" - CheckErrorResponse -Response $response - - # ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) - } - - It "Can get Diagnostics after opening a text document" { - $script = '$a = 4' - $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $script -PassThru -Force - - $request = Send-LspDidOpenTextDocumentRequest -Client $client ` - -Uri ([Uri]::new($file.PSPath).AbsoluteUri) ` - -Text ($file[0].ToString()) - - # There's no response for this message, but we need to call Get-LspResponse - # to increment the counter. - Get-LspResponse -Client $client -Id $request.Id | Out-Null - - # Grab notifications for just the file opened in this test. - $notifications = Get-LspNotification -Client $client | Where-Object { - $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) - } - - $notifications | Should -Not -BeNullOrEmpty - $notifications.Params.diagnostics | Should -Not -BeNullOrEmpty - $notifications.Params.diagnostics.Count | Should -Be 1 - $notifications.Params.diagnostics.code | Should -Be "PSUseDeclaredVarsMoreThanAssignments" - } - - It "Can get Diagnostics after changing settings" { - $file = New-TestFile -Script 'gci | % { $_ }' - - try - { - $request = Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ - PowerShell = @{ - ScriptAnalysis = @{ - Enable = $false - } - } - } - - # Grab notifications for just the file opened in this test. - $notifications = Get-LspNotification -Client $client | Where-Object { - $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) - } - $notifications | Should -Not -BeNullOrEmpty - $notifications.Params.diagnostics | Should -BeNullOrEmpty - } - finally - { - # Restore PSSA state - Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ - PowerShell = @{ - ScriptAnalysis = @{ - Enable = $true - } - } - } - } - } - - It "Can handle folding request" { - $filePath = New-TestFile -Script 'gci | % { -$_ - -@" - $_ -"@ -}' - - $request = Send-LspRequest -Client $client -Method "textDocument/foldingRange" -Parameters ([Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.FoldingRangeParams] @{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier] @{ - Uri = ([Uri]::new($filePath).AbsoluteUri) - } - }) - - $response = Get-LspResponse -Client $client -Id $request.Id - - $sortedResults = $response.Result | Sort-Object -Property startLine - $sortedResults[0].startLine | Should -Be 0 - $sortedResults[0].startCharacter | Should -Be 8 - $sortedResults[0].endLine | Should -Be 5 - $sortedResults[0].endCharacter | Should -Be 1 - - $sortedResults[1].startLine | Should -Be 3 - $sortedResults[1].startCharacter | Should -Be 0 - $sortedResults[1].endLine | Should -Be 4 - $sortedResults[1].endCharacter | Should -Be 2 - } - - It "Can handle a normal formatting request" { - $filePath = New-TestFile -Script ' -gci | % { -Get-Process -} - -' - - $request = Send-LspFormattingRequest -Client $client ` - -Uri ([Uri]::new($filePath).AbsoluteUri) - - $response = Get-LspResponse -Client $client -Id $request.Id - - # If we have a tab, formatting ran. - $response.Result.newText.Contains("`t") | Should -BeTrue -Because "We expect a tab." - } - - It "Can handle a range formatting request" { - $filePath = New-TestFile -Script ' -gci | % { -Get-Process -} - -' - - $range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ - Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = 2 - Character = 0 - } - End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = 3 - Character = 0 - } - } - - $request = Send-LspRangeFormattingRequest -Client $client ` - -Uri ([Uri]::new($filePath).AbsoluteUri) ` - -Range $range - - $response = Get-LspResponse -Client $client -Id $request.Id - - # If we have a tab, formatting ran. - $response.Result.newText.Contains("`t") | Should -BeTrue -Because "We expect a tab." - } - - It "Can handle a textDocument/documentSymbol request" { - $filePath = New-TestFile -Script ' -function Get-Foo { - -} - -Get-Foo -' - - $request = Send-LspDocumentSymbolRequest -Client $client ` - -Uri ([Uri]::new($filePath).AbsoluteUri) - - $response = Get-LspResponse -Client $client -Id $request.Id - - $response.Result.location.range.start.line | Should -BeExactly 1 - $response.Result.location.range.start.character | Should -BeExactly 0 - $response.Result.location.range.end.line | Should -BeExactly 3 - $response.Result.location.range.end.character | Should -BeExactly 1 - } - - It "Can handle a textDocument/references request" { - $filePath = New-TestFile -Script ' -function Get-Bar { - -} - -Get-Bar -' - - $request = Send-LspReferencesRequest -Client $client ` - -Uri ([Uri]::new($filePath).AbsoluteUri) ` - -LineNumber 5 ` - -CharacterNumber 0 - $response = Get-LspResponse -Client $client -Id $request.Id - - $response.Result.Count | Should -BeExactly 2 - $response.Result[0].range.start.line | Should -BeExactly 1 - $response.Result[0].range.start.character | Should -BeExactly 9 - $response.Result[0].range.end.line | Should -BeExactly 1 - $response.Result[0].range.end.character | Should -BeExactly 16 - $response.Result[1].range.start.line | Should -BeExactly 5 - $response.Result[1].range.start.character | Should -BeExactly 0 - $response.Result[1].range.end.line | Should -BeExactly 5 - $response.Result[1].range.end.character | Should -BeExactly 7 - } - - It "Can handle a textDocument/documentHighlight request" { - $filePath = New-TestFile -Script @' -Write-Host 'Hello!' - -Write-Host 'Goodbye' -'@ - - $documentHighlightParams = @{ - Client = $client - Uri = ([uri]::new($filePath).AbsoluteUri) - LineNumber = 3 - CharacterNumber = 1 - } - $request = Send-LspDocumentHighlightRequest @documentHighlightParams - - $response = Get-LspResponse -Client $client -Id $request.Id - - $response.Result.Count | Should -BeExactly 2 - $response.Result[0].Range.Start.Line | Should -BeExactly 0 - $response.Result[0].Range.Start.Character | Should -BeExactly 0 - $response.Result[0].Range.End.Line | Should -BeExactly 0 - $response.Result[0].Range.End.Character | Should -BeExactly 10 - $response.Result[1].Range.Start.Line | Should -BeExactly 2 - $response.Result[1].Range.Start.Character | Should -BeExactly 0 - $response.Result[1].Range.End.Line | Should -BeExactly 2 - $response.Result[1].Range.End.Character | Should -BeExactly 10 - } - - It "Can handle a powerShell/getPSHostProcesses request" { - $request = Send-LspRequest -Client $client -Method "powerShell/getPSHostProcesses" - $response = Get-LspResponse -Client $client -Id $request.Id - $response.Result | Should -Not -BeNullOrEmpty - - $processInfos = @(Get-PSHostProcessInfo) - - # We need to subtract one because this message fiilters out the "current" process. - $processInfos.Count - 1 | Should -BeExactly $response.Result.Count - - $response.Result[0].processName | - Should -MatchExactly -RegularExpression "((pwsh)|(powershell))(.exe)*" - } - - It "Can handle a powerShell/getRunspace request" { - $processInfos = Get-PSHostProcessInfo - - $request = Send-LspGetRunspaceRequest -Client $client -ProcessId $processInfos[0].ProcessId - $response = Get-LspResponse -Client $client -Id $request.Id - - $response.Result | Should -Not -BeNullOrEmpty - $response.Result.Count | Should -BeGreaterThan 0 - } - - It "Can handle a textDocument/codeLens Pester request" { - $filePath = New-TestFile -FileName ("$([System.IO.Path]::GetRandomFileName()).Tests.ps1") -Script ' -Describe "DescribeName" { - Context "ContextName" { - It "ItName" { - 1 | Should -Be 1 - } - } -} -' - - $request = Send-LspCodeLensRequest -Client $client ` - -Uri ([Uri]::new($filePath).AbsoluteUri) - - $response = Get-LspResponse -Client $client -Id $request.Id - $response.Result.Count | Should -BeExactly 2 - - # Both commands will have the same values for these so we can check them like so. - $response.Result.range.start.line | Should -Be @(1, 1) - $response.Result.range.start.character | Should -Be @(0, 0) - $response.Result.range.end.line | Should -Be @(7, 7) - $response.Result.range.end.character | Should -Be @(1, 1) - - $response.Result.command.title[0] | Should -Be "Run tests" - $response.Result.command.title[1] | Should -Be "Debug tests" - } - - It "Can handle a textDocument/codeLens and codeLens/resolve References request" { - $filePath = New-TestFile -Script ' -function Get-Foo { - -} - -Get-Foo -' - - $request = Send-LspCodeLensRequest -Client $client ` - -Uri ([Uri]::new($filePath).AbsoluteUri) - - $response = Get-LspResponse -Client $client -Id $request.Id - $response.Result.Count | Should -BeExactly 1 - $response.Result.data.data.ProviderId | Should -Be ReferencesCodeLensProvider - $response.Result.range.start.line | Should -BeExactly 1 - $response.Result.range.start.character | Should -BeExactly 0 - $response.Result.range.end.line | Should -BeExactly 3 - $response.Result.range.end.character | Should -BeExactly 1 - - $request = Send-LspCodeLensResolveRequest -Client $client -CodeLens $response.Result[0] - $response = Get-LspResponse -Client $client -Id $request.Id - - $response.Result.command.title | Should -Be '1 reference' - $response.Result.command.command | Should -Be 'editor.action.showReferences' - } - - It "Can handle a textDocument/codeAction request" { - $script = 'gci' - $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $script -PassThru -Force - - $request = Send-LspDidOpenTextDocumentRequest -Client $client ` - -Uri ([Uri]::new($file.PSPath).AbsoluteUri) ` - -Text ($file[0].ToString()) - - # There's no response for this message, but we need to call Get-LspResponse - # to increment the counter. - Get-LspResponse -Client $client -Id $request.Id | Out-Null - - Start-Sleep 1 - - # Grab notifications for just the file opened in this test. - $notifications = Get-LspNotification -Client $client | Where-Object { - $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) - } - - $notifications | Should -Not -BeNullOrEmpty - - $codeActionParams = @{ - Client = $client - Uri = $notifications.Params.uri - StartLine = 1 - StartCharacter = 1 - EndLine = 1 - EndCharacter = 4 - Diagnostics = $notifications.Params.diagnostics - } - $request = Send-LspCodeActionRequest @codeActionParams - - $response = Get-LspResponse -Client $client -Id $request.Id - - $edit = $response.Result | Where-Object command -eq 'PowerShell.ApplyCodeActionEdits' | Select-Object -First 1 - $edit | Should -Not -BeNullOrEmpty - $edit.Arguments.Text | Should -BeExactly 'Get-ChildItem' - $edit.Arguments.StartLineNumber | Should -Be 1 - $edit.Arguments.StartColumnNumber | Should -Be 1 - $edit.Arguments.EndLineNumber | Should -Be 1 - $edit.Arguments.EndColumnNumber | Should -Be 4 - } - - # This test MUST be last - It "Shuts down the process properly" { - $request = Send-LspShutdownRequest -Client $client - $response = Get-LspResponse -Client $client -Id $request.Id #-WaitMillis 99999 - $response.Id | Should -BeExactly $request.Id - $response.Result | Should -BeNull - - CheckErrorResponse -Response $response - - # TODO: The server seems to stay up waiting for the debug connection - # $psesServer.PsesProcess.HasExited | Should -BeTrue - - # We close the process here rather than in an AfterAll - # since errors can occur and we want to test for them. - # Naturally this depends on Pester executing tests in order. - - # We also have to dispose of everything properly, - # which means we have to use these cascading try/finally statements - try - { - $psesServer.PsesProcess.Kill() - } - finally - { - try - { - $psesServer.PsesProcess.Dispose() - } - finally - { - $client.Dispose() - $client = $null - } - } - - #ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) - } - - AfterEach { - if($client) { - # Drain notifications - Get-LspNotification -Client $client | Out-Null - } - } - - AfterAll { - if ($psesServer.PsesProcess.HasExited -eq $false) - { - try - { - $psesServer.PsesProcess.Kill() - } - finally - { - try - { - $psesServer.PsesProcess.Dispose() - } - finally - { - $client.Dispose() - } - } - } - } -} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs new file mode 100644 index 000000000..6c169c1bf --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -0,0 +1,611 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using PowerShellEditorServices.Engine.Services.Handlers; +using Xunit; +using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; + +namespace PowerShellEditorServices.Test.E2E +{ + public class LanguageServerProtocolMessageTests : IClassFixture, IDisposable + { + private readonly static string s_binDir = + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + private readonly LanguageClient LanguageClient; + private readonly List Diagnostics; + private readonly string PwshExe; + + public LanguageServerProtocolMessageTests(TestsFixture data) + { + Diagnostics = new List(); + LanguageClient = data.LanguageClient; + Diagnostics = data.Diagnostics; + PwshExe = TestsFixture.PwshExe; + Diagnostics.Clear(); + } + + public void Dispose() + { + Diagnostics.Clear(); + } + + private string NewTestFile(string script, bool isPester = false) + { + string fileExt = isPester ? ".Tests.ps1" : ".ps1"; + string filePath = Path.Combine(s_binDir, Path.GetRandomFileName() + fileExt); + File.WriteAllText(filePath, script); + + LanguageClient.SendNotification("textDocument/didOpen", new DidOpenTextDocumentParams + { + TextDocument = new TextDocumentItem + { + LanguageId = "powershell", + Version = 0, + Text = script, + Uri = new Uri(filePath) + } + }); + + return filePath; + } + + private async Task WaitForDiagnostics() + { + // Wait for PSSA to finish. + int i = 0; + while(Diagnostics.Count == 0) + { + if(i >= 10) + { + throw new InvalidDataException("No diagnostics showed up after 20s."); + } + + await Task.Delay(2000); + i++; + } + } + + [Fact] + public async Task CanSendPowerShellGetVersionRequest() + { + PowerShellVersionDetails details + = await LanguageClient.SendRequest("powerShell/getVersion", new GetVersionParams()); + + if(PwshExe == "powershell") + { + Assert.Equal("Desktop", details.Edition); + } + else + { + Assert.Equal("Core", details.Edition); + } + } + + [Fact] + public async Task CanSendWorkspaceSymbolRequest() + { + + NewTestFile(@" +function CanSendWorkspaceSymbolRequest { + Write-Host 'hello' +} +"); + + SymbolInformationContainer symbols = await LanguageClient.SendRequest( + "workspace/symbol", + new WorkspaceSymbolParams + { + Query = "CanSendWorkspaceSymbolRequest" + }); + + SymbolInformation symbol = Assert.Single(symbols); + Assert.Equal("CanSendWorkspaceSymbolRequest { }", symbol.Name); + } + + [Fact] + public async Task CanReceiveDiagnosticsFromFileOpen() + { + NewTestFile("$a = 4"); + await WaitForDiagnostics(); + + Diagnostic diagnostic = Assert.Single(Diagnostics); + Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code); + } + + [Fact] + public async Task CanReceiveDiagnosticsFromConfigurationChange() + { + NewTestFile("gci | % { $_ }"); + await WaitForDiagnostics(); + + // NewTestFile doesn't clear diagnostic notifications so we need to do that for this test. + Diagnostics.Clear(); + + try + { + LanguageClient.SendNotification("workspace/didChangeConfiguration", + new DidChangeConfigurationParams + { + Settings = JToken.Parse(@" +{ + ""PowerShell"": { + ""ScriptAnalysis"": { + ""Enable"": false + } + } +} +") + }); + + Assert.Empty(Diagnostics); + } + finally + { + LanguageClient.SendNotification("workspace/didChangeConfiguration", + new DidChangeConfigurationParams + { + Settings = JToken.Parse(@" +{ + ""PowerShell"": { + ""ScriptAnalysis"": { + ""Enable"": true + } + } +} +") + }); + } + } + + [Fact] + public async Task CanSendFoldingRangeRequest() + { + string scriptPath = NewTestFile(@"gci | % { +$_ + +@"" + $_ +""@ +}"); + + Container foldingRanges = + await LanguageClient.SendRequest>( + "textDocument/foldingRange", + new FoldingRangeRequestParam + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + } + }); + + Assert.Collection(foldingRanges.OrderBy(f => f.StartLine), + range1 => + { + Assert.Equal(0, range1.StartLine); + Assert.Equal(8, range1.StartCharacter); + Assert.Equal(5, range1.EndLine); + Assert.Equal(1, range1.EndCharacter); + }, + range2 => + { + Assert.Equal(3, range2.StartLine); + Assert.Equal(0, range2.StartCharacter); + Assert.Equal(4, range2.EndLine); + Assert.Equal(2, range2.EndCharacter); + }); + } + + [Fact] + public async Task CanSendFormattingRequest() + { + string scriptPath = NewTestFile(@" +gci | % { +Get-Process +} + +"); + + TextEditContainer textEdits = await LanguageClient.SendRequest( + "textDocument/formatting", + new DocumentFormattingParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + }, + Options = new FormattingOptions + { + TabSize = 4, + InsertSpaces = false + } + }); + + TextEdit textEdit = Assert.Single(textEdits); + + // If we have a tab, formatting ran. + Assert.Contains("\t", textEdit.NewText); + } + + [Fact] + public async Task CanSendRangeFormattingRequest() + { + string scriptPath = NewTestFile(@" +gci | % { +Get-Process +} + +"); + + TextEditContainer textEdits = await LanguageClient.SendRequest( + "textDocument/formatting", + new DocumentRangeFormattingParams + { + Range = new Range + { + Start = new Position + { + Line = 2, + Character = 0 + }, + End = new Position + { + Line = 3, + Character = 0 + } + }, + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + }, + Options = new FormattingOptions + { + TabSize = 4, + InsertSpaces = false + } + }); + + TextEdit textEdit = Assert.Single(textEdits); + + // If we have a tab, formatting ran. + Assert.Contains("\t", textEdit.NewText); + } + + [Fact] + public async Task CanSendDocumentSymbolRequest() + { + string scriptPath = NewTestFile(@" +function CanSendDocumentSymbolRequest { + +} + +CanSendDocumentSymbolRequest +"); + + SymbolInformationOrDocumentSymbolContainer symbolInformationOrDocumentSymbols = + await LanguageClient.SendRequest( + "textDocument/documentSymbol", + new DocumentSymbolParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + } + }); + + Assert.Collection(symbolInformationOrDocumentSymbols, + symInfoOrDocSym => { + Range range = symInfoOrDocSym.SymbolInformation.Location.Range; + + Assert.Equal(1, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(3, range.End.Line); + Assert.Equal(1, range.End.Character); + }); + } + + [Fact] + public async Task CanSendReferencesRequest() + { + string scriptPath = NewTestFile(@" +function CanSendReferencesRequest { + +} + +CanSendReferencesRequest +"); + + LocationContainer locations = await LanguageClient.SendRequest( + "textDocument/references", + new ReferenceParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + }, + Position = new Position + { + Line = 5, + Character = 0 + }, + Context = new ReferenceContext + { + IncludeDeclaration = false + } + }); + + Assert.Collection(locations, + location1 => + { + Range range = location1.Range; + Assert.Equal(1, range.Start.Line); + Assert.Equal(9, range.Start.Character); + Assert.Equal(1, range.End.Line); + Assert.Equal(33, range.End.Character); + + }, + location2 => + { + Range range = location2.Range; + Assert.Equal(5, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(5, range.End.Line); + Assert.Equal(24, range.End.Character); + }); + } + + [Fact] + public async Task CanSendDocumentHighlightRequest() + { + string scriptPath = NewTestFile(@" +Write-Host 'Hello!' + +Write-Host 'Goodbye' +"); + + DocumentHighlightContainer documentHighlights = + await LanguageClient.SendRequest( + "textDocument/documentHighlight", + new DocumentHighlightParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + }, + Position = new Position + { + Line = 4, + Character = 1 + } + }); + + Assert.Collection(documentHighlights, + documentHighlight1 => + { + Range range = documentHighlight1.Range; + Assert.Equal(1, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(1, range.End.Line); + Assert.Equal(10, range.End.Character); + + }, + documentHighlight2 => + { + Range range = documentHighlight2.Range; + Assert.Equal(3, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(3, range.End.Line); + Assert.Equal(10, range.End.Character); + }); + } + + [Fact] + public async Task CanSendPowerShellGetPSHostProcessesRequest() + { + var process = new Process(); + process.StartInfo.FileName = PwshExe; + process.StartInfo.ArgumentList.Add("-NoProfile"); + process.StartInfo.ArgumentList.Add("-NoLogo"); + process.StartInfo.ArgumentList.Add("-NoExit"); + + process.StartInfo.CreateNoWindow = true; + process.StartInfo.UseShellExecute = false; + + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + + process.Start(); + + // Wait for the process to start. + Thread.Sleep(1000); + + PSHostProcessResponse[] pSHostProcessResponses = null; + + try + { + pSHostProcessResponses = + await LanguageClient.SendRequest( + "powerShell/getPSHostProcesses", + new GetPSHostProcesssesParams { }); + } + finally + { + process.Kill(); + process.Dispose(); + } + + Assert.NotEmpty(pSHostProcessResponses); + } + + [Fact] + public async Task CanSendPowerShellGetRunspaceRequest() + { + var process = new Process(); + process.StartInfo.FileName = PwshExe; + process.StartInfo.ArgumentList.Add("-NoProfile"); + process.StartInfo.ArgumentList.Add("-NoLogo"); + process.StartInfo.ArgumentList.Add("-NoExit"); + + process.StartInfo.CreateNoWindow = true; + process.StartInfo.UseShellExecute = false; + + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + + process.Start(); + + // Wait for the process to start. + Thread.Sleep(1000); + + RunspaceResponse[] runspaceResponses = null; + try + { + runspaceResponses = + await LanguageClient.SendRequest( + "powerShell/getRunspace", + new GetRunspaceParams + { + ProcessId = $"{process.Id}" + }); + } + finally + { + process.Kill(); + process.Dispose(); + } + + Assert.NotEmpty(runspaceResponses); + } + + [Fact] + public async Task CanSendPesterCodeLensRequest() + { + string filePath = NewTestFile(@" +Describe 'DescribeName' { + Context 'ContextName' { + It 'ItName' { + 1 | Should - Be 1 + } + } +} +", isPester: true); + + CodeLensContainer codeLenses = await LanguageClient.SendRequest( + "textDocument/codeLens", + new CodeLensParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(filePath) + } + }); + + Assert.Collection(codeLenses, + codeLens1 => + { + Range range = codeLens1.Range; + + Assert.Equal(1, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(7, range.End.Line); + Assert.Equal(1, range.End.Character); + + Assert.Equal("Run tests", codeLens1.Command.Title); + }, + codeLens2 => + { + Range range = codeLens2.Range; + + Assert.Equal(1, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(7, range.End.Line); + Assert.Equal(1, range.End.Character); + + Assert.Equal("Debug tests", codeLens2.Command.Title); + }); + } + + [Fact] + public async Task CanSendReferencesCodeLensRequest() + { + string filePath = NewTestFile(@" +function CanSendReferencesCodeLensRequest { + +} + +CanSendReferencesCodeLensRequest +"); + + CodeLensContainer codeLenses = await LanguageClient.SendRequest( + "textDocument/codeLens", + new CodeLensParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(filePath) + } + }); + + CodeLens codeLens = Assert.Single(codeLenses); + + Range range = codeLens.Range; + Assert.Equal(1, range.Start.Line); + Assert.Equal(0, range.Start.Character); + Assert.Equal(3, range.End.Line); + Assert.Equal(1, range.End.Character); + + CodeLens codeLensResolveResult = await LanguageClient.SendRequest( + "codeLens/resolve", + codeLens); + + Assert.Equal("1 reference", codeLensResolveResult.Command.Title); + } + + [Fact] + public async Task CanSendCodeActionRequest() + { + string filePath = NewTestFile("gci"); + await WaitForDiagnostics(); + + CommandOrCodeActionContainer commandOrCodeActions = + await LanguageClient.SendRequest( + "textDocument/codeAction", + new CodeActionParams + { + TextDocument = new TextDocumentIdentifier( + new Uri(filePath, UriKind.Absolute)), + Range = new Range + { + Start = new Position + { + Line = 0, + Character = 0 + }, + End = new Position + { + Line = 0, + Character = 3 + } + }, + Context = new CodeActionContext + { + Diagnostics = new Container(Diagnostics) + } + }); + + Assert.Single(commandOrCodeActions, + command => command.Command.Name == "PowerShell.ApplyCodeActionEdits"); + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj new file mode 100644 index 000000000..8b7c085b4 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs new file mode 100644 index 000000000..a9bd12195 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Client; +using OmniSharp.Extensions.LanguageServer.Client.Processes; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using Xunit; + +namespace PowerShellEditorServices.Test.E2E +{ + public class TestsFixture : IAsyncLifetime + { + private readonly static string s_binDir = + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + private readonly static string s_bundledModulePath = new FileInfo(Path.Combine( + s_binDir, + "..", "..", "..", "..", "..", + "module")).FullName; + + private readonly static string s_sessionDetailsPath = Path.Combine( + s_binDir, + $"pses_test_sessiondetails_{Path.GetRandomFileName()}"); + + private readonly static string s_logPath = Path.Combine( + s_binDir, + $"pses_test_logs_{Path.GetRandomFileName()}"); + + const string s_logLevel = "Diagnostic"; + readonly static string[] s_featureFlags = { "PSReadLine" }; + const string s_hostName = "TestHost"; + const string s_hostProfileId = "TestHost"; + const string s_hostVersion = "1.0.0"; + readonly static string[] s_additionalModules = { "PowerShellEditorServices.VSCode" }; + + private StdioServerProcess _psesProcess; + + public static string PwshExe { get; } = Environment.GetEnvironmentVariable("PWSH_EXE_NAME") ?? "pwsh"; + public LanguageClient LanguageClient { get; private set; } + public List Diagnostics { get; set; } + + public async Task InitializeAsync() + { + var factory = new LoggerFactory(); + + ProcessStartInfo processStartInfo = new ProcessStartInfo + { + FileName = PwshExe + }; + processStartInfo.ArgumentList.Add("-NoLogo"); + processStartInfo.ArgumentList.Add("-NoProfile"); + processStartInfo.ArgumentList.Add("-EncodedCommand"); + + string[] args = { + Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1"), + "-LogPath", s_logPath, + "-LogLevel", s_logLevel, + "-SessionDetailsPath", s_sessionDetailsPath, + "-FeatureFlags", string.Join(',', s_featureFlags), + "-HostName", s_hostName, + "-HostProfileId", s_hostProfileId, + "-HostVersion", s_hostVersion, + "-AdditionalModules", string.Join(',', s_additionalModules), + "-BundledModulesPath", s_bundledModulePath, + "-Stdio" + }; + + string base64Str = Convert.ToBase64String( + System.Text.Encoding.Unicode.GetBytes(string.Join(' ', args))); + + processStartInfo.ArgumentList.Add(base64Str); + + _psesProcess = new StdioServerProcess(factory, processStartInfo); + await _psesProcess.Start(); + + LanguageClient = new LanguageClient(factory, _psesProcess); + + DirectoryInfo testdir = + Directory.CreateDirectory(Path.Combine(s_binDir, Path.GetRandomFileName())); + await LanguageClient.Initialize(testdir.FullName); + + Diagnostics = new List(); + LanguageClient.TextDocument.OnPublishDiagnostics((uri, diagnostics) => + { + Diagnostics.AddRange(diagnostics); + }); + } + + public async Task DisposeAsync() + { + await LanguageClient.Shutdown(); + await _psesProcess.Stop(); + LanguageClient?.Dispose(); + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json new file mode 100644 index 000000000..79d1ad980 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "parallelizeTestCollections": false +} + diff --git a/tools/PsesPsClient/Client.cs b/tools/PsesPsClient/Client.cs deleted file mode 100644 index 35d86b278..000000000 --- a/tools/PsesPsClient/Client.cs +++ /dev/null @@ -1,594 +0,0 @@ -// -// 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.IO.Pipes; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; -using System.Text; -using System.IO; -using Newtonsoft.Json.Linq; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System.Collections.Generic; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using System.Threading; -using System.Linq; - -namespace PsesPsClient -{ - /// - /// A Language Server Protocol named pipe connection. - /// - public class PsesLspClient : IDisposable - { - /// - /// Create a new LSP pipe around a given named pipe. - /// - /// The name of the named pipe to use. - /// A new LspPipe instance around the given named pipe. - public static PsesLspClient Create(string inPipeName, string outPipeName) - { - var inPipeStream = new NamedPipeClientStream( - pipeName: inPipeName, - serverName: ".", - direction: PipeDirection.In, - options: PipeOptions.Asynchronous); - - var outPipeStream = new NamedPipeClientStream( - pipeName: outPipeName, - serverName: ".", - direction: PipeDirection.Out, - options: PipeOptions.Asynchronous); - - return new PsesLspClient(inPipeStream, outPipeStream); - } - - private readonly NamedPipeClientStream _inPipe; - - private readonly NamedPipeClientStream _outPipe; - - private readonly JsonSerializerSettings _jsonSettings; - - private readonly JsonSerializer _jsonSerializer; - - private readonly JsonRpcMessageSerializer _jsonRpcSerializer; - - private readonly Encoding _pipeEncoding; - - private int _msgId; - - private StreamWriter _writer; - - private MessageStreamListener _listener; - - /// - /// Create a new LSP pipe around a named pipe client stream. - /// - /// The named pipe client stream to use for the LSP pipe. - public PsesLspClient(NamedPipeClientStream inPipe, NamedPipeClientStream outPipe) - { - _inPipe = inPipe; - _outPipe = outPipe; - - _jsonSettings = new JsonSerializerSettings() - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - Formatting = Formatting.Indented - }; - - _jsonSerializer = JsonSerializer.Create(_jsonSettings); - - // Reuse the PSES JSON RPC serializer - _jsonRpcSerializer = new JsonRpcMessageSerializer(); - - _pipeEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - } - - /// - /// Connect to the named pipe server. - /// - public void Connect() - { - _inPipe.Connect(timeout: 1000); - _outPipe.Connect(timeout: 1000); - _listener = new MessageStreamListener(new StreamReader(_inPipe, _pipeEncoding)); - _writer = new StreamWriter(_outPipe, _pipeEncoding) - { - AutoFlush = true - }; - - _listener.Start(); - } - - /// - /// Write a request to the LSP pipe. - /// - /// The method of the request. - /// The parameters of the request. May be null. - /// A representation of the request sent. - public LspRequest WriteRequest( - string method, - object parameters) - { - _msgId++; - - Message msg = Message.Request( - _msgId.ToString(), - method, - parameters != null ? JToken.FromObject(parameters, _jsonSerializer) : JValue.CreateNull()); - - JObject msgJson = _jsonRpcSerializer.SerializeMessage(msg); - string msgString = JsonConvert.SerializeObject(msgJson, _jsonSettings); - byte[] msgBytes = _pipeEncoding.GetBytes(msgString); - - string header = "Content-Length: " + msgBytes.Length + "\r\n\r\n"; - - _writer.Write(header + msgString); - _writer.Flush(); - - return new LspRequest(msg.Id, method, msgJson["params"]); - } - - /// - /// Get all the pending notifications from the server. - /// - /// Any pending notifications from the server. - public IEnumerable GetNotifications() - { - return _listener.DrainNotifications(); - } - - /// - /// Get all the pending requests from the server. - /// - /// Any pending requests from the server. - public IEnumerable GetRequests() - { - return _listener.DrainRequests(); - } - - /// - /// Get the next response from the server, if one is available within the given time. - /// - /// The next response from the server. - /// How long to wait for a response. - /// True if there is a next response, false if it timed out. - public bool TryGetResponse(string id, out LspResponse response, int millisTimeout) - { - return _listener.TryGetResponse(id, out response, millisTimeout); - } - - /// - /// Dispose of the pipe. This will also close the pipe. - /// - public void Dispose() - { - _writer.Dispose(); - _listener.Dispose(); - _inPipe.Close(); - _outPipe.Close(); - _inPipe.Dispose(); - _outPipe.Dispose(); - } - } - - /// - /// A dedicated listener to run a thread for receiving pipe messages, - /// so the the pipe is not blocked. - /// - public class MessageStreamListener : IDisposable - { - private readonly StreamReader _stream; - - private readonly StringBuilder _headerBuffer; - - private readonly ConcurrentQueue _requestQueue; - - private readonly ConcurrentQueue _notificationQueue; - - private readonly ConcurrentDictionary _responses; - - private readonly CancellationTokenSource _cancellationSource; - - private readonly BlockingCollection _responseReceivedChannel; - - private char[] _readerBuffer; - - /// - /// Create a listener around a stream. - /// - /// The stream to listen for messages on. - public MessageStreamListener(StreamReader stream) - { - _stream = stream; - _readerBuffer = new char[1024]; - _headerBuffer = new StringBuilder(128); - _notificationQueue = new ConcurrentQueue(); - _requestQueue = new ConcurrentQueue(); - _responses = new ConcurrentDictionary(); - _cancellationSource = new CancellationTokenSource(); - _responseReceivedChannel = new BlockingCollection(); - } - - /// - /// Get all pending notifications. - /// - public IEnumerable DrainNotifications() - { - return DrainQueue(_notificationQueue); - } - - /// - /// Get all pending requests. - /// - public IEnumerable DrainRequests() - { - return DrainQueue(_requestQueue); - } - - /// - /// Get the next response if there is one, otherwise instantly return false. - /// - /// The first response in the response queue if any, otherwise null. - /// True if there was a response to get, false otherwise. - public bool TryGetResponse(string id, out LspResponse response) - { - _responseReceivedChannel.TryTake(out bool _, millisecondsTimeout: 0); - return _responses.TryRemove(id, out response); - } - - /// - /// Get the next response within the given timeout. - /// - /// The first response in the queue, if any. - /// The maximum number of milliseconds to wait for a response. - /// True if there was a response to get, false otherwise. - public bool TryGetResponse(string id, out LspResponse response, int millisTimeout) - { - if (_responses.TryRemove(id, out response)) - { - return true; - } - - if (_responseReceivedChannel.TryTake(out bool _, millisTimeout)) - { - return _responses.TryRemove(id, out response); - } - - response = null; - return false; - } - - /// - /// Start the pipe listener on its own thread. - /// - public void Start() - { - Task.Run(() => RunListenLoop()); - } - - /// - /// End the pipe listener loop. - /// - public void Stop() - { - _cancellationSource.Cancel(); - } - - /// - /// Stops and disposes the pipe listener. - /// - public void Dispose() - { - Stop(); - _stream.Dispose(); - } - - private async Task RunListenLoop() - { - CancellationToken cancellationToken = _cancellationSource.Token; - while (!cancellationToken.IsCancellationRequested) - { - LspMessage msg; - msg = await ReadMessage().ConfigureAwait(false); - switch (msg) - { - case LspNotification notification: - _notificationQueue.Enqueue(notification); - continue; - - case LspResponse response: - _responses[response.Id] = response; - _responseReceivedChannel.Add(true); - continue; - - case LspRequest request: - _requestQueue.Enqueue(request); - continue; - } - } - } - - private async Task ReadMessage() - { - int contentLength = GetContentLength(); - string msgString = await ReadString(contentLength).ConfigureAwait(false); - JObject msgJson = JObject.Parse(msgString); - - if (msgJson.TryGetValue("method", out JToken methodToken)) - { - string method = ((JValue)methodToken).Value.ToString(); - if (msgJson.TryGetValue("id", out JToken idToken)) - { - string requestId = ((JValue)idToken).Value.ToString(); - return new LspRequest(requestId, method, msgJson["params"]); - } - - return new LspNotification(method, msgJson["params"]); - } - - string id = ((JValue)msgJson["id"])?.Value?.ToString(); - - if (msgJson.TryGetValue("result", out JToken resultToken)) - { - return new LspSuccessfulResponse(id, resultToken); - } - - JObject errorBody = (JObject)msgJson["error"]; - JsonRpcErrorCode errorCode = (JsonRpcErrorCode)((JValue)errorBody["code"]).Value; - string message = (string)((JValue)errorBody["message"]).Value; - return new LspErrorResponse(id, errorCode, message, errorBody["data"]); - } - - private async Task ReadString(int bytesToRead) - { - if (bytesToRead > _readerBuffer.Length) - { - Array.Resize(ref _readerBuffer, _readerBuffer.Length * 2); - } - - int readLen = await _stream.ReadAsync(_readerBuffer, 0, bytesToRead).ConfigureAwait(false); - - return new string(_readerBuffer, 0, readLen); - } - - private int GetContentLength() - { - _headerBuffer.Clear(); - int endHeaderState = 0; - int currChar; - while ((currChar = _stream.Read()) >= 0) - { - char c = (char)currChar; - _headerBuffer.Append(c); - switch (c) - { - case '\r': - if (endHeaderState == 2) - { - endHeaderState = 3; - continue; - } - - if (endHeaderState == 0) - { - endHeaderState = 1; - continue; - } - - endHeaderState = 0; - continue; - - case '\n': - if (endHeaderState == 1) - { - endHeaderState = 2; - continue; - } - - if (endHeaderState == 3) - { - return ParseContentLength(_headerBuffer.ToString()); - } - - endHeaderState = 0; - continue; - - default: - endHeaderState = 0; - continue; - } - } - - throw new InvalidDataException("Buffer emptied before end of headers"); - } - - private static int ParseContentLength(string headers) - { - const string clHeaderPrefix = "Content-Length: "; - - int clIdx = headers.IndexOf(clHeaderPrefix, StringComparison.Ordinal); - if (clIdx < 0) - { - throw new InvalidDataException("No Content-Length header found"); - } - - int endIdx = headers.IndexOf("\r\n", clIdx, StringComparison.Ordinal); - if (endIdx < 0) - { - throw new InvalidDataException("Header CRLF terminator not found"); - } - - int numStartIdx = clIdx + clHeaderPrefix.Length; - int numLength = endIdx - numStartIdx; - - return int.Parse(headers.Substring(numStartIdx, numLength)); - } - - private static IEnumerable DrainQueue(ConcurrentQueue queue) - { - if (queue.IsEmpty) - { - return Enumerable.Empty(); - } - - var list = new List(); - while (queue.TryDequeue(out TElement element)) - { - list.Add(element); - } - return list; - } - - } - - /// - /// Represents a Language Server Protocol message. - /// - public abstract class LspMessage - { - protected LspMessage() - { - } - } - - /// - /// A Language Server Protocol notifcation or event. - /// - public class LspNotification : LspMessage - { - public LspNotification(string method, JToken parameters) - { - Method = method; - Params = parameters; - } - - /// - /// The notification method. - /// - public string Method { get; } - - /// - /// Any parameters for the notification. - /// - public JToken Params { get; } - } - - /// - /// A Language Server Protocol request. - /// May be a client -> server or a server -> client request. - /// - public class LspRequest : LspMessage - { - public LspRequest(string id, string method, JToken parameters) - { - Id = id; - Method = method; - Params = parameters; - } - - /// - /// The ID of the request. Usually an integer. - /// - public string Id { get; } - - /// - /// The method of the request. - /// - public string Method { get; } - - /// - /// Any parameters of the request. - /// - public JToken Params { get; } - } - - /// - /// A Language Server Protocol response message. - /// - public abstract class LspResponse : LspMessage - { - protected LspResponse(string id) - { - Id = id; - } - - /// - /// The ID of the response. Will match the ID of the request triggering it. - /// - public string Id { get; } - } - - /// - /// A successful Language Server Protocol response message. - /// - public class LspSuccessfulResponse : LspResponse - { - public LspSuccessfulResponse(string id, JToken result) - : base(id) - { - Result = result; - } - - /// - /// The result field of the response. - /// - public JToken Result { get; } - } - - /// - /// A Language Server Protocol error response message. - /// - public class LspErrorResponse : LspResponse - { - public LspErrorResponse( - string id, - JsonRpcErrorCode code, - string message, - JToken data) - : base(id) - { - Code = code; - Message = message; - Data = data; - } - - /// - /// The error code sent by the server, may not correspond to a known enum type. - /// - public JsonRpcErrorCode Code { get; } - - /// - /// The error message. - /// - public string Message { get; } - - /// - /// Extra error data. - /// - public JToken Data { get; } - } - - /// - /// Error codes used by the Language Server Protocol. - /// - public enum JsonRpcErrorCode : long - { - ParseError = -32700, - InvalidRequest = -32600, - MethodNotFound = -32601, - InvalidParams = -32602, - InternalError = -32603, - ServerErrorStart = -32099, - ServerErrorEnd = -32000, - ServerNotInitialized = -32002, - UnknownErrorCode = -32001, - RequestCancelled = -32800, - ContentModified = -32801, - } -} diff --git a/tools/PsesPsClient/PsesPsClient.csproj b/tools/PsesPsClient/PsesPsClient.csproj deleted file mode 100644 index 6e80e81fa..000000000 --- a/tools/PsesPsClient/PsesPsClient.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - - - - diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 deleted file mode 100644 index 2d1f65aaa..000000000 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ /dev/null @@ -1,145 +0,0 @@ -# -# Module manifest for module 'PsesPsClient' -# -# Generated by: Microsoft Corporation -# -# Generated on: 26/4/19 -# - -@{ - -# Script module or binary module file associated with this manifest. -RootModule = 'PsesPsClient.psm1' - -# Version number of this module. -ModuleVersion = '0.0.1' - -# Supported PSEditions -CompatiblePSEditions = 'Core', 'Desktop' - -# ID used to uniquely identify this module -GUID = 'ce491ff9-3eab-443c-b3a2-cc412ddeef65' - -# Author of this module -Author = 'Microsoft Corporation' - -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' - -# Copyright statement for this module -Copyright = '(c) Microsoft Corporation' - -# Description of the functionality provided by this module -# Description = '' - -# Minimum version of the PowerShell engine required by this module -PowerShellVersion = '5.1' - -# Name of the PowerShell host required by this module -# PowerShellHostName = '' - -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' - -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' - -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# CLRVersion = '' - -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' - -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() - -# Assemblies that must be loaded prior to importing this module -RequiredAssemblies = @( - 'Microsoft.PowerShell.EditorServices.dll' - 'Microsoft.PowerShell.EditorServices.Protocol.dll' -) - -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() - -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() - -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() - -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -NestedModules = @('PsesPsClient.dll') - -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @( - 'Start-PsesServer', - 'Connect-PsesServer', - 'Send-LspRequest', - 'Send-LspInitializeRequest', - 'Send-LspCodeActionRequest', - 'Send-LspDidOpenTextDocumentRequest', - 'Send-LspDidChangeConfigurationRequest', - 'Send-LspFormattingRequest', - 'Send-LspRangeFormattingRequest', - 'Send-LspDocumentSymbolRequest', - 'Send-LspDocumentHighlightRequest', - 'Send-LspReferencesRequest', - 'Send-LspGetRunspaceRequest', - 'Send-LspCodeLensRequest', - 'Send-LspCodeLensResolveRequest', - 'Send-LspShutdownRequest', - 'Get-LspNotification', - 'Get-LspResponse' -) - -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '*' - -# Variables to export from this module -VariablesToExport = '*' - -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' - -# DSC resources to export from this module -# DscResourcesToExport = @() - -# List of all modules packaged with this module -# ModuleList = @() - -# List of all files packaged with this module -# FileList = @() - -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() - - # A URL to the license for this module. - # LicenseUri = '' - - # A URL to the main website for this project. - # ProjectUri = '' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - # ReleaseNotes = '' - - } # End of PSData hashtable - -} # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' - -} - diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 deleted file mode 100644 index 132e912a5..000000000 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ /dev/null @@ -1,865 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -$script:PsesBundledModulesDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - "$PSScriptRoot/../../../../module") - -Import-Module -Force "$PSScriptRoot/../../../../module/PowerShellEditorServices" -Import-Module -Force (Resolve-Path "$PSScriptRoot/../../../../src/PowerShellEditorServices.Engine/bin/*/netstandard2.0/publish/Omnisharp.Extensions.LanguageProtocol.dll") - -class PsesStartupOptions -{ - [string] $LogPath - [string] $LogLevel - [string] $SessionDetailsPath - [string[]] $FeatureFlags - [string] $HostName - [string] $HostProfileId - [version] $HostVersion - [string[]] $AdditionalModules - [string] $BundledModulesPath - [bool] $EnableConsoleRepl - [switch] $SplitInOutPipes -} - -class PsesServerInfo -{ - [pscustomobject]$SessionDetails - [System.Diagnostics.Process]$PsesProcess - [PsesStartupOptions]$StartupOptions - [string]$LogPath -} - -function Start-PsesServer -{ - [CmdletBinding(SupportsShouldProcess)] - [OutputType([PsesServerInfo])] - param( - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $EditorServicesPath = "$script:PsesBundledModulesDir/PowerShellEditorServices/Start-EditorServices.ps1", - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $LogPath, - - [Parameter()] - [ValidateSet("Diagnostic", "Normal", "Verbose", "Error")] - [string] - $LogLevel = 'Diagnostic', - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $SessionDetailsPath, - - [Parameter()] - [ValidateNotNull()] - [string[]] - $FeatureFlags = @('PSReadLine'), - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $HostName = 'PSES Test Host', - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $HostProfileId = 'TestHost', - - [Parameter()] - [ValidateNotNull()] - [version] - $HostVersion = '1.99', - - [Parameter()] - [ValidateNotNull()] - [string[]] - $AdditionalModules = @('PowerShellEditorServices.VSCode'), - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $BundledModulesPath, - - [Parameter()] - [switch] - $EnableConsoleRepl, - - [Parameter()] - [string] - $ErrorFile - ) - - $EditorServicesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($EditorServicesPath) - - $instanceId = Get-RandomHexString - - $tempDir = [System.IO.Path]::GetTempPath() - - if (-not $LogPath) - { - $LogPath = Join-Path $tempDir "pseslogs_$instanceId.log" - } - - if (-not $SessionDetailsPath) - { - $SessionDetailsPath = Join-Path $tempDir "psessession_$instanceId.log" - } - - if (-not $BundledModulesPath) - { - $BundledModulesPath = $script:PsesBundledModulesDir - } - - $editorServicesOptions = @{ - LogPath = $LogPath - LogLevel = $LogLevel - SessionDetailsPath = $SessionDetailsPath - FeatureFlags = $FeatureFlags - HostName = $HostName - HostProfileId = $HostProfileId - HostVersion = $HostVersion - AdditionalModules = $AdditionalModules - BundledModulesPath = $BundledModulesPath - EnableConsoleRepl = $EnableConsoleRepl - SplitInOutPipes = [switch]::Present - } - - $startPsesCommand = Unsplat -Prefix "& '$EditorServicesPath'" -SplatParams $editorServicesOptions - - $pwshPath = (Get-Process -Id $PID).Path - - if (-not $PSCmdlet.ShouldProcess("& '$pwshPath' -Command '$startPsesCommand'")) - { - return - } - - $startArgs = @( - '-NoLogo', - '-NoProfile', - '-NoExit', - '-Command', - $startPsesCommand - ) - - $startProcParams = @{ - PassThru = $true - FilePath = $pwshPath - ArgumentList = $startArgs - } - - if ($ErrorFile) - { - $startProcParams.RedirectStandardError = $ErrorFile - } - - $serverProcess = Start-Process @startProcParams - - $sessionPath = $editorServicesOptions.SessionDetailsPath - - $i = 0 - while (-not (Test-Path $sessionPath)) - { - if ($i -ge 10) - { - throw "No session file found - server failed to start" - } - - Start-Sleep 1 - $null = $i++ - } - - return [PsesServerInfo]@{ - PsesProcess = $serverProcess - SessionDetails = Get-Content -Raw $editorServicesOptions.SessionDetailsPath | ConvertFrom-Json - StartupOptions = $editorServicesOptions - LogPath = $LogPath - } -} - -function Connect-PsesServer -{ - [OutputType([PsesPsClient.PsesLspClient])] - param( - [Parameter(Mandatory)] - [string] - $InPipeName, - - [Parameter(Mandatory)] - [string] - $OutPipeName - ) - - $psesIdx = $InPipeName.IndexOf('PSES') - if ($psesIdx -gt 0) - { - $InPipeName = $InPipeName.Substring($psesIdx) - $OutPipeName = $OutPipeName.Substring($psesIdx) - } - - $client = [PsesPsClient.PsesLspClient]::Create($InPipeName, $OutPipeName) - $client.Connect() - return $client -} - -function Send-LspInitializeRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter()] - [int] - $ProcessId = $PID, - - [Parameter()] - [string] - $RootPath = (Get-Location), - - [Parameter()] - [string] - $RootUri, - - [Parameter()] - [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ClientCapabilities] - $ClientCapabilities = (Get-ClientCapabilities), - - [Parameter()] - [object] - $IntializationOptions = ([object]::new()) - ) - - $parameters = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.InitializeParams]@{ - ProcessId = $ProcessId - Capabilities = $ClientCapabilities - InitializationOptions = $IntializationOptions - } - - if ($RootUri) - { - $parameters.RootUri = $RootUri - } - else - { - $parameters.RootUri = [uri]::new($RootPath) - } - - return Send-LspRequest -Client $Client -Method 'initialize' -Parameters $parameters -} - -function Send-LspDidOpenTextDocumentRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [string] - $Uri, - - [Parameter()] - [int] - $Version = 0, - - [Parameter()] - [string] - $LanguageId = "powershell", - - [Parameter()] - [string] - $Text - ) - - $parameters = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DidOpenTextDocumentParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentItem]@{ - Uri = $Uri - LanguageId = $LanguageId - Text = $Text - Version = $Version - } - } - - $result = Send-LspRequest -Client $Client -Method 'textDocument/didOpen' -Parameters $parameters - - # Give PSScriptAnalyzer enough time to run - Start-Sleep -Seconds 1 - - $result -} - -function Send-LspDidChangeConfigurationRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [Microsoft.PowerShell.EditorServices.Protocol.Server.LanguageServerSettingsWrapper] - $Settings - ) - - $parameters = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DidChangeConfigurationParams[Microsoft.PowerShell.EditorServices.Protocol.Server.LanguageServerSettingsWrapper]]@{ - Settings = $Settings - } - - $result = Send-LspRequest -Client $Client -Method 'workspace/didChangeConfiguration' -Parameters $parameters - - # Give PSScriptAnalyzer enough time to run - Start-Sleep -Seconds 1 - - $result -} - -function Send-LspFormattingRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [string] - $Uri, - - [Parameter()] - [int] - $TabSize = 4, - - [Parameter()] - [switch] - $InsertSpaces - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DocumentFormattingParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - options = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.FormattingOptions]@{ - TabSize = $TabSize - InsertSpaces = $InsertSpaces.IsPresent - } - } - - return Send-LspRequest -Client $Client -Method 'textDocument/formatting' -Parameters $params -} - -function Send-LspRangeFormattingRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [string] - $Uri, - - [Parameter(Mandatory)] - [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range] - $Range, - - [Parameter()] - [int] - $TabSize = 4, - - [Parameter()] - [switch] - $InsertSpaces - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DocumentRangeFormattingParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - Range = $Range - options = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.FormattingOptions]@{ - TabSize = $TabSize - InsertSpaces = $InsertSpaces.IsPresent - } - } - - return Send-LspRequest -Client $Client -Method 'textDocument/rangeFormatting' -Parameters $params -} - -function Send-LspDocumentSymbolRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [string] - $Uri - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DocumentSymbolParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - } - return Send-LspRequest -Client $Client -Method 'textDocument/documentSymbol' -Parameters $params -} - -function Send-LspReferencesRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [string] - $Uri, - - [Parameter(Mandatory)] - [int] - $LineNumber, - - [Parameter(Mandatory)] - [int] - $CharacterNumber, - - [Parameter()] - [switch] - $IncludeDeclaration - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ReferencesParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - Position = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $LineNumber - Character = $CharacterNumber - } - Context = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ReferencesContext]@{ - IncludeDeclaration = $IncludeDeclaration - } - } - return Send-LspRequest -Client $Client -Method 'textDocument/references' -Parameters $params -} - -function Send-LspDocumentHighlightRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Position = 1, Mandatory)] - [string] - $Uri, - - [Parameter(Mandatory)] - [int] - $LineNumber, - - [Parameter(Mandatory)] - [int] - $CharacterNumber - ) - - $documentHighlightParams = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentPositionParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - Position = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $LineNumber - Character = $CharacterNumber - } - } - - return Send-LspRequest -Client $Client -Method 'textDocument/documentHighlight' -Parameters $documentHighlightParams -} - -function Send-LspGetRunspaceRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - [int] - $ProcessId - ) - - $params = [PowerShellEditorServices.Engine.Services.Handlers.GetRunspaceParams]@{ - ProcessId = $ProcessId - } - return Send-LspRequest -Client $Client -Method 'powerShell/getRunspace' -Parameters $params -} - -function Send-LspCodeLensRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [string] - $Uri - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeLensRequest]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - } - return Send-LspRequest -Client $Client -Method 'textDocument/codeLens' -Parameters $params -} - -function Send-LspCodeLensResolveRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Mandatory)] - # Expects to be passed in a single item from the `Result` collection from - # Send-LspCodeLensRequest - $CodeLens - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeLens]@{ - Data = [Newtonsoft.Json.Linq.JToken]::Parse(($CodeLens.data.data | ConvertTo-Json)) - Range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ - Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $CodeLens.range.start.line - Character = $CodeLens.range.start.character - } - End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $CodeLens.range.end.line - Character = $CodeLens.range.end.character - } - } - } - - return Send-LspRequest -Client $Client -Method 'codeLens/resolve' -Parameters $params -} - -function Send-LspCodeActionRequest -{ - param( - [Parameter()] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter()] - [string] - $Uri, - - [Parameter()] - [int] - $StartLine, - - [Parameter()] - [int] - $StartCharacter, - - [Parameter()] - [int] - $EndLine, - - [Parameter()] - [int] - $EndCharacter, - - [Parameter()] - $Diagnostics - ) - - $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeActionParams]@{ - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ - Uri = $Uri - } - Range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ - Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $StartLine - Character = $StartCharacter - } - End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $EndLine - Character = $EndCharacter - } - } - Context = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeActionContext]@{ - Diagnostics = $Diagnostics | ForEach-Object { - [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Diagnostic]@{ - Code = $_.code - Severity = $_.severity - Source = $_.source - Message = $_.message - Range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ - Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $_.range.start.line - Character = $_.range.start.character - } - End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ - Line = $_.range.end.line - Character = $_.range.end.character - } - } - } - } - } - } - - return Send-LspRequest -Client $Client -Method 'textDocument/codeAction' -Parameters $params -} - -function Send-LspShutdownRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client - ) - - return Send-LspRequest -Client $Client -Method 'shutdown' -} - -function Send-LspRequest -{ - [OutputType([PsesPsClient.LspRequest])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Position = 1, Mandatory)] - [string] - $Method, - - [Parameter(Position = 2)] - $Parameters = $null - ) - - - $result = $Client.WriteRequest($Method, $Parameters) - - # To allow for result/notification queue to fill up - Start-Sleep 1 - - $result -} - -function Get-LspResponse -{ - [OutputType([PsesPsClient.LspResponse])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client, - - [Parameter(Position = 1, Mandatory)] - [string] - $Id, - - [Parameter()] - [int] - $WaitMillis = 10000 - ) - - $lspResponse = $null - - if ($Client.TryGetResponse($Id, [ref]$lspResponse, $WaitMillis)) - { - $result = if ($lspResponse.Result) { $lspResponse.Result.ToString() | ConvertFrom-Json } - return [PSCustomObject]@{ - Id = $lspResponse.Id - Result = $result - } - } -} - -function Get-LspNotification -{ - [OutputType([PsesPsClient.LspResponse])] - param( - [Parameter(Position = 0, Mandatory)] - [PsesPsClient.PsesLspClient] - $Client - ) - - $Client.GetNotifications() | ForEach-Object { - $result = if ($_.Params) { $_.Params.ToString() | ConvertFrom-Json } - [PSCustomObject]@{ - Method = $_.Method - Params = $result - } - } -} - -function Unsplat -{ - param( - [string]$Prefix, - [hashtable]$SplatParams) - - $sb = New-Object 'System.Text.StringBuilder' ($Prefix) - - foreach ($key in $SplatParams.get_Keys()) - { - $val = $SplatParams[$key] - - if (-not $val) - { - continue - } - - $null = $sb.Append(" -$key") - - if ($val -is [switch]) - { - continue - } - - if ($val -is [array]) - { - $null = $sb.Append(' @(') - for ($i = 0; $i -lt $val.Count; $i++) - { - $null = $sb.Append("'").Append($val[$i]).Append("'") - if ($i -lt $val.Count - 1) - { - $null = $sb.Append(',') - } - } - $null = $sb.Append(')') - continue - } - - if ($val -is [version]) - { - $val = [string]$val - } - - if ($val -is [string]) - { - $null = $sb.Append(" '$val'") - continue - } - - throw "Bad value '$val' of type $($val.GetType())" - } - - return $sb.ToString() -} - -$script:Random = [System.Random]::new() -function Get-RandomHexString -{ - param([int]$Length = 10) - - $buffer = [byte[]]::new($Length / 2) - $script:Random.NextBytes($buffer) - $str = ($buffer | ForEach-Object { "{0:x02}" -f $_ }) -join '' - - if ($Length % 2 -ne 0) - { - $str += ($script:Random.Next() | ForEach-Object { "{0:02}" -f $_ }) - } - - return $str -} - -function Get-ClientCapabilities -{ - return [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ClientCapabilities]@{ - Workspace = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.WorkspaceClientCapabilities]@{ - ApplyEdit = $true - WorkspaceEdit = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.WorkspaceEditCapabilities]@{ - DocumentChanges = $false - } - DidChangeConfiguration = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - DidChangeWatchedFiles = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - Symbol = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - ExecuteCommand = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - } - TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentClientCapabilities]@{ - Synchronization = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.SynchronizationCapabilities]@{ - WillSave = $true - WillSaveWaitUntil = $true - DidSave = $true - } - Completion = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CompletionCapabilities]@{ - DynamicRegistration = $false - CompletionItem = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CompletionItemCapabilities]@{ - SnippetSupport = $true - } - } - Hover = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - SignatureHelp = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - References = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - DocumentHighlight = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - DocumentSymbol = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - Formatting = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - RangeFormatting = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - OnTypeFormatting = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - Definition = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - CodeLens = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - CodeAction = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - DocumentLink = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - Rename = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DynamicRegistrationCapability]@{ - DynamicRegistration = $false - } - } - Experimental = [System.Object]::new() - } -} diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 deleted file mode 100644 index 82874ab3e..000000000 --- a/tools/PsesPsClient/build.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -param( - [Parameter()] - [string] - $DotnetExe = 'dotnet' -) - -$ErrorActionPreference = 'Stop' - -$script:OutDir = "$PSScriptRoot/out" -$script:OutModDir = "$script:OutDir/PsesPsClient" - -$script:ModuleComponents = @{ - "bin/Debug/netstandard2.0/publish/PsesPsClient.dll" = "PsesPsClient.dll" - "bin/Debug/netstandard2.0/publish/Newtonsoft.Json.dll" = "Newtonsoft.Json.dll" - "PsesPsClient.psm1" = "PsesPsClient.psm1" - "PsesPsClient.psd1" = "PsesPsClient.psd1" - "bin/Debug/netstandard2.0/Microsoft.PowerShell.EditorServices.Protocol.dll" = "Microsoft.PowerShell.EditorServices.Protocol.dll" - "bin/Debug/netstandard2.0/Microsoft.PowerShell.EditorServices.dll" = "Microsoft.PowerShell.EditorServices.dll" -} - -$binDir = "$PSScriptRoot/bin" -$objDir = "$PSScriptRoot/obj" -foreach ($dir in $binDir,$objDir,$script:OutDir) -{ - if (Test-Path $dir) - { - Remove-Item -Force -Recurse $dir - } -} - -Push-Location $PSScriptRoot -try -{ - & $DotnetExe publish --framework 'netstandard2.0' - - New-Item -Path $script:OutModDir -ItemType Directory - foreach ($key in $script:ModuleComponents.get_Keys()) - { - $val = $script:ModuleComponents[$key] - Copy-Item -Path "$PSScriptRoot/$key" -Destination "$script:OutModDir/$val" - } -} -finally -{ - Pop-Location -}