From d5152c6a6784b8d870c4ba0d9586cf48593b7e6c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 30 May 2019 18:03:56 -0700 Subject: [PATCH 01/47] Add starting point --- .../Hosting/EditorServicesHost.cs | 306 ++++++++++++++++++ .../Hosting/HostDetails.cs | 92 ++++++ .../Interface/ILanguageServer.cs | 11 + .../Interface/ILanguageServerBuilder.cs | 21 ++ .../LanguageServer/OmnisharpLanguageServer.cs | 142 ++++++++ .../OmnisharpLanguageServerBuilder.cs | 46 +++ .../PowerShellEditorServices.Engine.csproj | 16 + .../Utility/Utility.cs | 16 + .../Utility/Validate.cs | 138 ++++++++ 9 files changed, 788 insertions(+) create mode 100644 src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs create mode 100644 src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs create mode 100644 src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs create mode 100644 src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs create mode 100644 src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs create mode 100644 src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj create mode 100644 src/PowerShellEditorServices.Engine/Utility/Utility.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/Validate.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs new file mode 100644 index 000000000..e98b11c7c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -0,0 +1,306 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation.Host; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public enum EditorServicesHostStatus + { + Started, + Failed, + Ended + } + + public enum EditorServiceTransportType + { + NamedPipe, + Stdio + } + + public class EditorServiceTransportConfig + { + public EditorServiceTransportType TransportType { get; set; } + /// + /// Configures the endpoint of the transport. + /// For Stdio it's ignored. + /// For NamedPipe it's the pipe name. + /// + public string InOutPipeName { get; set; } + + public string OutPipeName { get; set; } + + public string InPipeName { get; set; } + + internal string Endpoint => OutPipeName != null && InPipeName != null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}"; + } + + /// + /// Provides a simplified interface for hosting the language and debug services + /// over the named pipe server protocol. + /// + public class EditorServicesHost + { + #region Private Fields + + private readonly IServiceCollection _services; + + private ILanguageServer _languageServer; + + private ILogger _logger; + + #endregion + + #region Properties + + public EditorServicesHostStatus Status { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the EditorServicesHost class and waits for + /// the debugger to attach if waitForDebugger is true. + /// + /// The details of the host which is launching PowerShell Editor Services. + /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. + /// If true, causes the host to wait for the debugger to attach before proceeding. + /// Modules to be loaded when initializing the new runspace. + /// Features to enable for this instance. + public EditorServicesHost( + HostDetails hostDetails, + string bundledModulesPath, + bool enableConsoleRepl, + bool waitForDebugger, + string[] additionalModules, + string[] featureFlags) + : this( + hostDetails, + bundledModulesPath, + enableConsoleRepl, + waitForDebugger, + additionalModules, + featureFlags, + GetInternalHostFromDefaultRunspace()) + { + } + + /// + /// Initializes a new instance of the EditorServicesHost class and waits for + /// the debugger to attach if waitForDebugger is true. + /// + /// The details of the host which is launching PowerShell Editor Services. + /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. + /// If true, causes the host to wait for the debugger to attach before proceeding. + /// Modules to be loaded when initializing the new runspace. + /// Features to enable for this instance. + /// The value of the $Host variable in the original runspace. + public EditorServicesHost( + HostDetails hostDetails, + string bundledModulesPath, + bool enableConsoleRepl, + bool waitForDebugger, + string[] additionalModules, + string[] featureFlags, + PSHost internalHost) + { + Validate.IsNotNull(nameof(hostDetails), hostDetails); + Validate.IsNotNull(nameof(internalHost), internalHost); + + _services = new ServiceCollection(); + + _services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); + + this.hostDetails = hostDetails; + this.enableConsoleRepl = enableConsoleRepl; + this.bundledModulesPath = bundledModulesPath; + this.additionalModules = additionalModules ?? Array.Empty(); + this.featureFlags = new HashSet(featureFlags ?? Array.Empty(); + this.serverCompletedTask = new TaskCompletionSource(); + this.internalHost = internalHost; + +#if DEBUG + if (waitForDebugger) + { + if (System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Break(); + } + else + { + System.Diagnostics.Debugger.Launch(); + } + } +#endif + + // Catch unhandled exceptions for logging purposes + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + } + + #endregion + + #region Public Methods + + /// + /// Starts the Logger for the specified file path and log level. + /// + /// The path of the log file to be written. + /// The minimum level of log messages to be written. + public void StartLogging(string logFilePath, LogLevel logLevel) + { + FileVersionInfo fileVersionInfo = + FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); + + string osVersion = RuntimeInformation.OSDescription; + + string osArch = GetOSArchitecture(); + + string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? ""; + + string logHeader = $@" +PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id} + + Host application details: + + Name: {this.hostDetails.Name} + Version: {this.hostDetails.Version} + ProfileId: {this.hostDetails.ProfileId} + Arch: {osArch} + + Operating system details: + + Version: {osVersion} + Arch: {osArch} + + Build information: + + Version: {BuildInfo.BuildVersion} + Origin: {BuildInfo.BuildOrigin} + Date: {buildTime} +"; + + this.logger.Write(LogLevel.Normal, logHeader); + } + + /// + /// Starts the language service with the specified config. + /// + /// The config that contains information on the communication protocol that will be used. + /// The profiles that will be loaded in the session. + public void StartLanguageService( + EditorServiceTransportConfig config, + ProfilePaths profilePaths) + { + this.profilePaths = profilePaths; + + this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config); + + this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnectAsync; + this.languageServiceListener.Start(); + + this.logger.Write( + LogLevel.Normal, + string.Format( + "Language service started, type = {0}, endpoint = {1}", + config.TransportType, config.Endpoint)); + } + + /// + /// Starts the debug service with the specified config. + /// + /// The config that contains information on the communication protocol that will be used. + /// The profiles that will be loaded in the session. + /// Determines if we will reuse the session that we have. + public void StartDebugService( + EditorServiceTransportConfig config, + ProfilePaths profilePaths, + bool useExistingSession) + { + this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config); + this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; + this.debugServiceListener.Start(); + + this.logger.Write( + LogLevel.Normal, + string.Format( + "Debug service started, type = {0}, endpoint = {1}", + config.TransportType, config.Endpoint)); + } + + /// + /// Stops the language or debug services if either were started. + /// + public void StopServices() + { + // TODO: Need a new way to shut down the services + + this.languageServer = null; + this.debugAdapter = null; + } + + /// + /// Waits for either the language or debug service to shut down. + /// + public void WaitForCompletion() + { + // TODO: We need a way to know when to complete this task! + this.serverCompletedTask.Task.Wait(); + } + + #endregion + + #region Private Methods + + private static PSHost GetInternalHostFromDefaultRunspace() + { + using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + return pwsh.AddScript("$Host").Invoke().First(); + } + } + + /// + /// Gets the OSArchitecture for logging. Cannot use System.Runtime.InteropServices.RuntimeInformation.OSArchitecture + /// directly, since this tries to load API set DLLs in win7 and crashes. + /// + private string GetOSArchitecture() + { + // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation + if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version < new Version(6, 2)) + { + if (Environment.Is64BitProcess) + { + return "X64"; + } + + return "X86"; + } + + return RuntimeInformation.OSArchitecture.ToString(); + } + + private void CurrentDomain_UnhandledException( + object sender, + UnhandledExceptionEventArgs e) + { + // Log the exception + this.logger.Write(LogLevel.Error, $"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs b/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs new file mode 100644 index 000000000..febaaf7c8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + /// + /// Contains details about the current host application (most + /// likely the editor which is using the host process). + /// + public class HostDetails + { + #region Constants + + /// + /// The default host name for PowerShell Editor Services. Used + /// if no host name is specified by the host application. + /// + public const string DefaultHostName = "PowerShell Editor Services Host"; + + /// + /// The default host ID for PowerShell Editor Services. Used + /// for the host-specific profile path if no host ID is specified. + /// + public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; + + /// + /// The default host version for PowerShell Editor Services. If + /// no version is specified by the host application, we use 0.0.0 + /// to indicate a lack of version. + /// + public static readonly Version DefaultHostVersion = new Version("0.0.0"); + + /// + /// The default host details in a HostDetails object. + /// + public static readonly HostDetails Default = new HostDetails(null, null, null); + + #endregion + + #region Properties + + /// + /// Gets the name of the host. + /// + public string Name { get; private set; } + + /// + /// Gets the profile ID of the host, used to determine the + /// host-specific profile path. + /// + public string ProfileId { get; private set; } + + /// + /// Gets the version of the host. + /// + public Version Version { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the HostDetails class. + /// + /// + /// The display name for the host, typically in the form of + /// "[Application Name] Host". + /// + /// + /// The identifier of the PowerShell host to use for its profile path. + /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' + /// where 'X' represents the value of hostProfileId. If null, a default + /// will be used. + /// + /// The host application's version. + public HostDetails( + string name, + string profileId, + Version version) + { + this.Name = name ?? DefaultHostName; + this.ProfileId = profileId ?? DefaultHostProfileId; + this.Version = version ?? DefaultHostVersion; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs new file mode 100644 index 000000000..2ba0d7421 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public interface ILanguageServer + { + Task StartAsync(); + + Task ShutdownComplete { get; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs new file mode 100644 index 000000000..1074544ee --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public interface ILanguageServerBuilder + { + string NamedPipeName { get; set; } + + string OutNamedPipeName { get; set; } + + ILoggerFactory LoggerFactory { get; set; } + + LogLevel MinimumLogLevel { get; set; } + + ILanguageServerBuilder AddHandler(THandler handler); + + ILanguageServerBuilder AddService(TService service); + + ILanguageServer BuildLanguageServer(); + } +} diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs new file mode 100644 index 000000000..e7f879459 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -0,0 +1,142 @@ +using System.IO.Pipes; +using System.Reflection; +using System.Threading.Tasks; +using System.Security.Principal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OS = OmniSharp.Extensions.LanguageServer.Server; +using System.Security.AccessControl; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public class OmnisharpLanguageServer : ILanguageServer + { + public class Configuration + { + public string NamedPipeName { get; set; } + + public string OutNamedPipeName { get; set; } + + public ILoggerFactory LoggerFactory { get; set; } + + public LogLevel MinimumLogLevel { get; set; } + + public IServiceCollection Services { get; set; } + } + + // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. + private const int CurrentUserOnly = 0x20000000; + + // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, + // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. + // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ + private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = + typeof(NamedPipeServerStream).GetConstructor(new [] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); + + private OS.ILanguageServer _languageServer; + + private readonly Configuration _configuration; + + public Task ShutdownComplete => _languageServer.WaitForExit; + + public OmnisharpLanguageServer( + Configuration configuration) + { + _configuration = configuration; + } + + public async Task StartAsync() + { + _languageServer = await OS.LanguageServer.From(options => { + NamedPipeServerStream namedPipe = ConnectNamedPipe( + _configuration.NamedPipeName, + _configuration.OutNamedPipeName, + out NamedPipeServerStream outNamedPipe); + + options.Input = namedPipe; + options.Output = outNamedPipe != null + ? outNamedPipe + : namedPipe; + + options.LoggerFactory = _configuration.LoggerFactory; + options.MinimumLogLevel = _configuration.MinimumLogLevel; + options.Services = _configuration.Services; + }); + } + + private static NamedPipeServerStream ConnectNamedPipe( + string inOutPipeName, + string outPipeName, + out NamedPipeServerStream outPipe) + { + // .NET Core implementation is simplest so try that first + if (Utility.IsNetCore) + { + outPipe = outPipeName == null + ? null + : new NamedPipeServerStream( + pipeName: outPipeName, + direction: PipeDirection.Out, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: (PipeOptions)CurrentUserOnly); + + return new NamedPipeServerStream( + pipeName: inOutPipeName, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); + } + + // Now deal with Windows PowerShell + // We need to use reflection to get a nice constructor + + var pipeSecurity = new PipeSecurity(); + + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + // Allow the Administrators group full access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); + } + else + { + // Allow the current user read/write access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + WindowsIdentity.GetCurrent().User, + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + } + + outPipe = outPipeName == null + ? null + : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + outPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + + return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + inOutPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs new file mode 100644 index 000000000..e435f61c1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public class OmnisharpLanguageServerBuilder : ILanguageServerBuilder + { + public OmnisharpLanguageServerBuilder(IServiceCollection serviceCollection) + { + Services = serviceCollection; + } + + public string NamedPipeName { get; set; } + + public string OutNamedPipeName { get; set; } + + public ILoggerFactory LoggerFactory { get; set; } = new LoggerFactory(); + + public LogLevel MinimumLogLevel { get; set; } = LogLevel.Trace; + + public IServiceCollection Services { get; } + + public ILanguageServerBuilder AddHandler(THandler handler) + { + throw new System.NotImplementedException(); + } + + public ILanguageServerBuilder AddService(TService service) + { + throw new System.NotImplementedException(); + } + + public ILanguageServer BuildLanguageServer() + { + var config = new OmnisharpLanguageServer.Configuration() + { + LoggerFactory = LoggerFactory, + MinimumLogLevel = MinimumLogLevel, + NamedPipeName = NamedPipeName, + OutNamedPipeName = OutNamedPipeName, + Services = Services + }; + + return new OmnisharpLanguageServer(config); +} + } diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj new file mode 100644 index 000000000..0dfde364e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/src/PowerShellEditorServices.Engine/Utility/Utility.cs b/src/PowerShellEditorServices.Engine/Utility/Utility.cs new file mode 100644 index 000000000..14233c528 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/Utility.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + /// + /// General purpose common utilities to prevent reimplementation. + /// + internal static class Utility + { + /// + /// True if we are running on .NET Core, false otherwise. + /// + public static bool IsNetCore { get; } = RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.Ordinal); +} +} diff --git a/src/PowerShellEditorServices.Engine/Utility/Validate.cs b/src/PowerShellEditorServices.Engine/Utility/Validate.cs new file mode 100644 index 000000000..a8bb12e1c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/Validate.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + /// + /// Provides common validation methods to simplify method + /// parameter checks. + /// + public static class Validate + { + /// + /// Throws ArgumentNullException if value is null. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNull(string parameterName, object valueToCheck) + { + if (valueToCheck == null) + { + throw new ArgumentNullException(parameterName); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is outside + /// of the given lower and upper limits. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The lower limit which the value should not be less than. + /// The upper limit which the value should not be greater than. + public static void IsWithinRange( + string parameterName, + int valueToCheck, + int lowerLimit, + int upperLimit) + { + // TODO: Debug assert here if lowerLimit >= upperLimit + + if (valueToCheck < lowerLimit || valueToCheck > upperLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is not between {0} and {1}", + lowerLimit, + upperLimit)); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is greater than or equal + /// to the given upper limit. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The upper limit which the value should be less than. + public static void IsLessThan( + string parameterName, + int valueToCheck, + int upperLimit) + { + if (valueToCheck >= upperLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is greater than or equal to {0}", + upperLimit)); + } + } + + /// + /// Throws ArgumentOutOfRangeException if the value is less than or equal + /// to the given lower limit. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + /// The lower limit which the value should be greater than. + public static void IsGreaterThan( + string parameterName, + int valueToCheck, + int lowerLimit) + { + if (valueToCheck < lowerLimit) + { + throw new ArgumentOutOfRangeException( + parameterName, + valueToCheck, + string.Format( + "Value is less than or equal to {0}", + lowerLimit)); + } + } + + /// + /// Throws ArgumentException if the value is equal to the undesired value. + /// + /// The type of value to be validated. + /// The name of the parameter being validated. + /// The value that valueToCheck should not equal. + /// The value of the parameter being validated. + public static void IsNotEqual( + string parameterName, + TValue valueToCheck, + TValue undesiredValue) + { + if (EqualityComparer.Default.Equals(valueToCheck, undesiredValue)) + { + throw new ArgumentException( + string.Format( + "The given value '{0}' should not equal '{1}'", + valueToCheck, + undesiredValue), + parameterName); + } + } + + /// + /// Throws ArgumentException if the value is null, an empty string, + /// or a string containing only whitespace. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck) + { + if (string.IsNullOrWhiteSpace(valueToCheck)) + { + throw new ArgumentException( + "Parameter contains a null, empty, or whitespace string.", + parameterName); + } + } + } +} From 36a06c872f8f319cd86a66f46b27adfed7fed771 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 31 May 2019 08:48:35 -0700 Subject: [PATCH 02/47] x --- .../LanguageServer/OmnisharpLanguageServerBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs index e435f61c1..a26376883 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs @@ -42,5 +42,6 @@ public ILanguageServer BuildLanguageServer() }; return new OmnisharpLanguageServer(config); -} + } } +} From a363f07dee8558cc2d742876cba93c334f85d9f4 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 4 Jun 2019 14:55:51 -0700 Subject: [PATCH 03/47] More work --- PowerShellEditorServices.build.ps1 | 25 +++- .../PowerShellEditorServices.psm1 | 27 ++--- .../.vscode/launch.json | 42 +++++++ .../BuildInfo.cs | 9 ++ .../Hosting/EditorServicesHost.cs | 66 +++++++---- .../Hosting/LogLevel.cs | 40 +++++++ .../Hosting/ProfilePaths.cs | 110 ++++++++++++++++++ .../Interface/ILanguageServer.cs | 2 +- .../LanguageServer/OmnisharpLanguageServer.cs | 48 +++++--- .../PowerShellEditorServices.Engine.csproj | 9 +- tools/PsesPsClient/Client.cs | 38 +++--- tools/PsesPsClient/PsesPsClient.psm1 | 1 + 12 files changed, 345 insertions(+), 72 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/.vscode/launch.json create mode 100644 src/PowerShellEditorServices.Engine/BuildInfo.cs create mode 100644 src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs create mode 100644 src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 91046afb7..c9c8cd17f 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -74,6 +74,24 @@ $script:RequiredBuildAssets = @{ 'Microsoft.PowerShell.EditorServices.Protocol.dll', 'Microsoft.PowerShell.EditorServices.Protocol.pdb' ) + + 'PowerShellEditorServices.Engine' = @( + 'publish/Microsoft.PowerShell.EditorServices.Engine.dll', + 'publish/Microsoft.PowerShell.EditorServices.Engine.pdb', + 'publish/OmniSharp.Extensions.JsonRpc.dll', + 'publish/OmniSharp.Extensions.LanguageProtocol.dll', + 'publish/OmniSharp.Extensions.LanguageServer.dll', + 'publish/Serilog.dll', + 'publish/Serilog.Extensions.Logging.dll', + 'publish/Serilog.Sinks.Console.dll', + 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', + 'publish/Microsoft.Extensions.DependencyInjection.dll', + 'publish/Microsoft.Extensions.Logging.Abstractions.dll', + 'publish/Microsoft.Extensions.Logging.dll', + 'publish/Microsoft.Extensions.Options.dll', + 'publish/Microsoft.Extensions.Primitives.dll', + 'publish/System.Reactive.dll' + ) } $script:VSCodeModuleBinPath = @{ @@ -102,12 +120,6 @@ $script:RequiredNugetBinaries = @{ @{ PackageName = 'System.Security.AccessControl'; PackageVersion = '4.5.0'; TargetRuntime = 'net461' }, @{ PackageName = 'System.IO.Pipes.AccessControl'; PackageVersion = '4.5.1'; TargetRuntime = 'net461' } ) - - '6.0' = @( - @{ PackageName = 'System.Security.Principal.Windows'; PackageVersion = '4.5.0'; TargetRuntime = 'netcoreapp2.0' }, - @{ PackageName = 'System.Security.AccessControl'; PackageVersion = '4.5.0'; TargetRuntime = 'netcoreapp2.0' }, - @{ PackageName = 'System.IO.Pipes.AccessControl'; PackageVersion = '4.5.1'; TargetRuntime = 'netstandard2.0' } - ) } if (Get-Command git -ErrorAction SilentlyContinue) { @@ -326,6 +338,7 @@ namespace Microsoft.PowerShell.EditorServices.Host task Build { exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f $script:TargetPlatform } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Engine\PowerShellEditorServices.Engine.csproj -f $script:TargetPlatform } exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj -f $script:TargetPlatform } exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } } diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index 47efd591f..2d05b73ea 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -8,15 +8,12 @@ if ($PSEdition -eq 'Desktop') { Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.IO.Pipes.AccessControl.dll" Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.AccessControl.dll" Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.Principal.Windows.dll" -} elseif ($PSVersionTable.PSVersion -ge '6.0' -and $PSVersionTable.PSVersion -lt '6.1' -and $IsWindows) { - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/6.0/System.IO.Pipes.AccessControl.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/6.0/System.Security.AccessControl.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/6.0/System.Security.Principal.Windows.dll" } Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.dll" Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Host.dll" Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Protocol.dll" +Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Engine.dll" function Start-EditorServicesHost { [CmdletBinding()] @@ -97,13 +94,13 @@ function Start-EditorServicesHost { $editorServicesHost = $null $hostDetails = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Session.HostDetails @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.HostDetails @( $HostName, $HostProfileId, (Microsoft.PowerShell.Utility\New-Object System.Version @($HostVersion))) $editorServicesHost = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Host.EditorServicesHost @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.EditorServicesHost @( $hostDetails, $BundledModulesPath, $EnableConsoleRepl.IsPresent, @@ -114,7 +111,7 @@ function Start-EditorServicesHost { # Build the profile paths using the root paths of the current $profile variable $profilePaths = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Session.ProfilePaths @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.ProfilePaths @( $hostDetails.ProfileId, [System.IO.Path]::GetDirectoryName($profile.AllUsersAllHosts), [System.IO.Path]::GetDirectoryName($profile.CurrentUserAllHosts)) @@ -122,32 +119,32 @@ function Start-EditorServicesHost { $editorServicesHost.StartLogging($LogPath, $LogLevel); $languageServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportConfig + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportConfig $debugServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportConfig + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportConfig switch ($PSCmdlet.ParameterSetName) { "Stdio" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::Stdio + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::Stdio break } "NamedPipe" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe $languageServiceConfig.InOutPipeName = "$LanguageServiceNamedPipe" if ($DebugServiceNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe $debugServiceConfig.InOutPipeName = "$DebugServiceNamedPipe" } break } "NamedPipeSimplex" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe $languageServiceConfig.InPipeName = $LanguageServiceInNamedPipe $languageServiceConfig.OutPipeName = $LanguageServiceOutNamedPipe if ($DebugServiceInNamedPipe -and $DebugServiceOutNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe $debugServiceConfig.InPipeName = $DebugServiceInNamedPipe $debugServiceConfig.OutPipeName = $DebugServiceOutNamedPipe } diff --git a/src/PowerShellEditorServices.Engine/.vscode/launch.json b/src/PowerShellEditorServices.Engine/.vscode/launch.json new file mode 100644 index 000000000..8b60d4fab --- /dev/null +++ b/src/PowerShellEditorServices.Engine/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "WARNING01": "*********************************************************************************", + "WARNING02": "The C# extension was unable to automatically to decode projects in the current", + "WARNING03": "workspace to create a runnable lanch.json file. A template launch.json file has", + "WARNING04": "been created as a placeholder.", + "WARNING05": "", + "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", + "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", + "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", + "WARNING09": "If this allows OmniSharp to now load your project then --", + "WARNING10": " * Delete this file", + "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", + "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", + "WARNING13": "", + "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", + "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", + "WARNING16": "button at the bottom of this file.", + "WARNING17": "*********************************************************************************", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug//.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Engine/BuildInfo.cs b/src/PowerShellEditorServices.Engine/BuildInfo.cs new file mode 100644 index 000000000..f808390e5 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/BuildInfo.cs @@ -0,0 +1,9 @@ +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public static class BuildInfo + { + public const string BuildVersion = ""; + public const string BuildOrigin = ""; + public static readonly System.DateTime? BuildTime = null; + } +} diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index e98b11c7c..372a748a0 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Management.Automation; using System.Management.Automation.Host; using System.Reflection; using System.Runtime.InteropServices; @@ -15,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; +using Serilog.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -56,11 +58,13 @@ public class EditorServicesHost { #region Private Fields - private readonly IServiceCollection _services; + private readonly IServiceCollection _serviceCollection; + + private readonly HostDetails _hostDetails; private ILanguageServer _languageServer; - private ILogger _logger; + private Extensions.Logging.ILogger _logger; #endregion @@ -121,10 +125,19 @@ public EditorServicesHost( Validate.IsNotNull(nameof(hostDetails), hostDetails); Validate.IsNotNull(nameof(internalHost), internalHost); - _services = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); + + Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + _logger = new SerilogLoggerProvider().CreateLogger(nameof(EditorServicesHost)); + + _serviceCollection.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); - _services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); + _hostDetails = hostDetails; + /* this.hostDetails = hostDetails; this.enableConsoleRepl = enableConsoleRepl; this.bundledModulesPath = bundledModulesPath; @@ -132,6 +145,7 @@ public EditorServicesHost( this.featureFlags = new HashSet(featureFlags ?? Array.Empty(); this.serverCompletedTask = new TaskCompletionSource(); this.internalHost = internalHost; + */ #if DEBUG if (waitForDebugger) @@ -160,7 +174,7 @@ public EditorServicesHost( /// /// The path of the log file to be written. /// The minimum level of log messages to be written. - public void StartLogging(string logFilePath, LogLevel logLevel) + public void StartLogging(string logFilePath, PsesLogLevel logLevel) { FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); @@ -176,9 +190,9 @@ public void StartLogging(string logFilePath, LogLevel logLevel) Host application details: - Name: {this.hostDetails.Name} - Version: {this.hostDetails.Version} - ProfileId: {this.hostDetails.ProfileId} + Name: {_hostDetails.Name} + Version: {_hostDetails.Version} + ProfileId: {_hostDetails.ProfileId} Arch: {osArch} Operating system details: @@ -193,7 +207,7 @@ public void StartLogging(string logFilePath, LogLevel logLevel) Date: {buildTime} "; - this.logger.Write(LogLevel.Normal, logHeader); + _logger.LogInformation(logHeader); } /// @@ -205,15 +219,28 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - this.profilePaths = profilePaths; + if (!System.Diagnostics.Debugger.IsAttached) + { + Console.WriteLine($"{System.Diagnostics.Process.GetCurrentProcess().Id}"); + System.Threading.Thread.Sleep(2000); + } - this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config); + _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnectAsync; - this.languageServiceListener.Start(); + _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) + { + NamedPipeName = config.InOutPipeName ?? config.InPipeName, + OutNamedPipeName = config.OutPipeName, + } + .BuildLanguageServer(); - this.logger.Write( - LogLevel.Normal, + _logger.LogInformation("Starting language server"); + + Task serviceStart = _languageServer.StartAsync(); + serviceStart.ConfigureAwait(continueOnCapturedContext: false); + serviceStart.Wait(); + + _logger.LogInformation( string.Format( "Language service started, type = {0}, endpoint = {1}", config.TransportType, config.Endpoint)); @@ -230,6 +257,7 @@ public void StartDebugService( ProfilePaths profilePaths, bool useExistingSession) { + /* this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config); this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; this.debugServiceListener.Start(); @@ -239,6 +267,7 @@ public void StartDebugService( string.Format( "Debug service started, type = {0}, endpoint = {1}", config.TransportType, config.Endpoint)); + */ } /// @@ -247,9 +276,6 @@ public void StartDebugService( public void StopServices() { // TODO: Need a new way to shut down the services - - this.languageServer = null; - this.debugAdapter = null; } /// @@ -258,7 +284,7 @@ public void StopServices() public void WaitForCompletion() { // TODO: We need a way to know when to complete this task! - this.serverCompletedTask.Task.Wait(); + _languageServer.WaitForShutdown().Wait(); } #endregion @@ -298,7 +324,7 @@ private void CurrentDomain_UnhandledException( UnhandledExceptionEventArgs e) { // Log the exception - this.logger.Write(LogLevel.Error, $"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); + _logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); } #endregion diff --git a/src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs b/src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs new file mode 100644 index 000000000..dfd50ffaf --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public enum PsesLogLevel + { + Diagnostic, + Verbose, + Normal, + Warning, + Error, + } + + internal static class PsesLogLevelExtensions + { + public static LogLevel ToExtensionsLogLevel(this PsesLogLevel logLevel) + { + switch (logLevel) + { + case PsesLogLevel.Diagnostic: + return LogLevel.Trace; + + case PsesLogLevel.Verbose: + return LogLevel.Debug; + + case PsesLogLevel.Normal: + return LogLevel.Information; + + case PsesLogLevel.Warning: + return LogLevel.Warning; + + case PsesLogLevel.Error: + return LogLevel.Error; + + default: + return LogLevel.Information; + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs b/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs new file mode 100644 index 000000000..29bab2b56 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs @@ -0,0 +1,110 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + /// + /// Provides profile path resolution behavior relative to the name + /// of a particular PowerShell host. + /// + public class ProfilePaths + { + #region Constants + + /// + /// The file name for the "all hosts" profile. Also used as the + /// suffix for the host-specific profile filenames. + /// + public const string AllHostsProfileName = "profile.ps1"; + + #endregion + + #region Properties + + /// + /// Gets the profile path for all users, all hosts. + /// + public string AllUsersAllHosts { get; private set; } + + /// + /// Gets the profile path for all users, current host. + /// + public string AllUsersCurrentHost { get; private set; } + + /// + /// Gets the profile path for the current user, all hosts. + /// + public string CurrentUserAllHosts { get; private set; } + + /// + /// Gets the profile path for the current user and host. + /// + public string CurrentUserCurrentHost { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Creates a new instance of the ProfilePaths class. + /// + /// + /// The identifier of the host used in the host-specific X_profile.ps1 filename. + /// + /// The base path to use for constructing AllUsers profile paths. + /// The base path to use for constructing CurrentUser profile paths. + public ProfilePaths( + string hostProfileId, + string baseAllUsersPath, + string baseCurrentUserPath) + { + this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath); + } + + private void Initialize( + string hostProfileId, + string baseAllUsersPath, + string baseCurrentUserPath) + { + string currentHostProfileName = + string.Format( + "{0}_{1}", + hostProfileId, + AllHostsProfileName); + + this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName); + this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName); + this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName); + this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName); + } + + /// + /// Gets the list of profile paths that exist on the filesystem. + /// + /// An IEnumerable of profile path strings to be loaded. + public IEnumerable GetLoadableProfilePaths() + { + var profilePaths = + new string[] + { + this.AllUsersAllHosts, + this.AllUsersCurrentHost, + this.CurrentUserAllHosts, + this.CurrentUserCurrentHost + }; + + return profilePaths.Where(p => File.Exists(p)); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs index 2ba0d7421..07605468c 100644 --- a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs @@ -6,6 +6,6 @@ public interface ILanguageServer { Task StartAsync(); - Task ShutdownComplete { get; } + Task WaitForShutdown(); } } diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index e7f879459..3cf5442c8 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -35,9 +35,15 @@ public class Configuration private OS.ILanguageServer _languageServer; + private Task _serverStart; + private readonly Configuration _configuration; - public Task ShutdownComplete => _languageServer.WaitForExit; + public async Task WaitForShutdown() + { + await _serverStart; + await _languageServer.WaitForExit; + } public OmnisharpLanguageServer( Configuration configuration) @@ -47,24 +53,34 @@ public OmnisharpLanguageServer( public async Task StartAsync() { - _languageServer = await OS.LanguageServer.From(options => { - NamedPipeServerStream namedPipe = ConnectNamedPipe( - _configuration.NamedPipeName, - _configuration.OutNamedPipeName, - out NamedPipeServerStream outNamedPipe); - - options.Input = namedPipe; - options.Output = outNamedPipe != null - ? outNamedPipe - : namedPipe; - - options.LoggerFactory = _configuration.LoggerFactory; - options.MinimumLogLevel = _configuration.MinimumLogLevel; - options.Services = _configuration.Services; + _serverStart = OS.LanguageServer.From(options => { + Task.Run(async () => { + NamedPipeServerStream namedPipe = CreateNamedPipe( + _configuration.NamedPipeName, + _configuration.OutNamedPipeName, + out NamedPipeServerStream outNamedPipe); + + await namedPipe.WaitForConnectionAsync(); + if (outNamedPipe != null) + { + await outNamedPipe.WaitForConnectionAsync(); + } + + options.Input = namedPipe; + options.Output = outNamedPipe != null + ? outNamedPipe + : namedPipe; + + options.LoggerFactory = _configuration.LoggerFactory; + options.MinimumLogLevel = _configuration.MinimumLogLevel; + options.Services = _configuration.Services; + }); }); + + await _serverStart; } - private static NamedPipeServerStream ConnectNamedPipe( + private static NamedPipeServerStream CreateNamedPipe( string inOutPipeName, string outPipeName, out NamedPipeServerStream outPipe) diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index 0dfde364e..ba75ee307 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -1,13 +1,20 @@ + + - netstandard2.0 + PowerShell Editor Services Engine + Provides common PowerShell editor capabilities as a .NET library. + netstandard2.0 + Microsoft.PowerShell.EditorServices.Engine + Latest + diff --git a/tools/PsesPsClient/Client.cs b/tools/PsesPsClient/Client.cs index bae8c7247..05ebb3573 100644 --- a/tools/PsesPsClient/Client.cs +++ b/tools/PsesPsClient/Client.cs @@ -31,18 +31,26 @@ public class PsesLspClient : IDisposable /// /// The name of the named pipe to use. /// A new LspPipe instance around the given named pipe. - public static PsesLspClient Create(string pipeName) + public static PsesLspClient Create(string inPipeName, string outPipeName) { - var pipeClient = new NamedPipeClientStream( - pipeName: pipeName, + var inPipeStream = new NamedPipeClientStream( + pipeName: inPipeName, serverName: ".", - direction: PipeDirection.InOut, + direction: PipeDirection.In, options: PipeOptions.Asynchronous); - return new PsesLspClient(pipeClient); + var outPipeStream = new NamedPipeClientStream( + pipeName: outPipeName, + serverName: ".", + direction: PipeDirection.Out, + options: PipeOptions.Asynchronous); + + return new PsesLspClient(inPipeStream, outPipeStream); } - private readonly NamedPipeClientStream _namedPipeClient; + private readonly NamedPipeClientStream _inPipe; + + private readonly NamedPipeClientStream _outPipe; private readonly JsonSerializerSettings _jsonSettings; @@ -62,9 +70,10 @@ public static PsesLspClient Create(string pipeName) /// 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 namedPipeClient) + public PsesLspClient(NamedPipeClientStream inPipe, NamedPipeClientStream outPipe) { - _namedPipeClient = namedPipeClient; + _inPipe = inPipe; + _outPipe = outPipe; _jsonSettings = new JsonSerializerSettings() { @@ -84,9 +93,10 @@ public PsesLspClient(NamedPipeClientStream namedPipeClient) /// public void Connect() { - _namedPipeClient.Connect(timeout: 1000); - _listener = new MessageStreamListener(new StreamReader(_namedPipeClient, _pipeEncoding)); - _writer = new StreamWriter(_namedPipeClient, _pipeEncoding) + _inPipe.Connect(timeout: 1000); + _outPipe.Connect(timeout: 1000); + _listener = new MessageStreamListener(new StreamReader(_inPipe, _pipeEncoding)); + _writer = new StreamWriter(_outPipe, _pipeEncoding) { AutoFlush = true }; @@ -159,8 +169,10 @@ public void Dispose() { _writer.Dispose(); _listener.Dispose(); - _namedPipeClient.Close(); - _namedPipeClient.Dispose(); + _inPipe.Close(); + _outPipe.Close(); + _inPipe.Dispose(); + _outPipe.Dispose(); } } diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index a66e5c7d1..be932ee3f 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -124,6 +124,7 @@ function Start-PsesServer AdditionalModules = $AdditionalModules BundledModulesPath = $BundledModulesPath EnableConsoleRepl = $EnableConsoleRepl + SplitInOutPipes = [switch]::Present } $startPsesCommand = Unsplat -Prefix "& '$EditorServicesPath'" -SplatParams $editorServicesOptions From 9ef8fe5857de0daafb23f4797e204806545240ce Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 11 Jun 2019 16:10:40 -0700 Subject: [PATCH 04/47] Make integration tests pass for omnisharp --- .../Hosting/EditorServicesHost.cs | 6 +- .../Interface/ILanguageServerBuilder.cs | 4 - .../LanguageServer/OmnisharpLanguageServer.cs | 58 +++++----- .../OmnisharpLanguageServerBuilder.cs | 10 -- .../PowerShellEditorServices.Engine.csproj | 2 +- .../LanguageServer/Initialize.cs | 2 +- .../WorkspaceClientCapabilities.cs | 5 +- .../EditorServices.Integration.Tests.ps1 | 62 ++++++++++- tools/PsesPsClient/Client.cs | 9 +- tools/PsesPsClient/PsesPsClient.psm1 | 101 ++++++++++++++++-- 10 files changed, 189 insertions(+), 70 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 372a748a0..cf88fbcb6 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -219,7 +219,7 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - if (!System.Diagnostics.Debugger.IsAttached) + while (System.Diagnostics.Debugger.IsAttached) { Console.WriteLine($"{System.Diagnostics.Process.GetCurrentProcess().Id}"); System.Threading.Thread.Sleep(2000); @@ -236,9 +236,7 @@ public void StartLanguageService( _logger.LogInformation("Starting language server"); - Task serviceStart = _languageServer.StartAsync(); - serviceStart.ConfigureAwait(continueOnCapturedContext: false); - serviceStart.Wait(); + Task.Run(_languageServer.StartAsync); _logger.LogInformation( string.Format( diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs index 1074544ee..0b5801b36 100644 --- a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs +++ b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs @@ -12,10 +12,6 @@ public interface ILanguageServerBuilder LogLevel MinimumLogLevel { get; set; } - ILanguageServerBuilder AddHandler(THandler handler); - - ILanguageServerBuilder AddService(TService service); - ILanguageServer BuildLanguageServer(); } } diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 3cf5442c8..eb545ad6f 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -35,49 +35,47 @@ public class Configuration private OS.ILanguageServer _languageServer; - private Task _serverStart; + private TaskCompletionSource _serverStart; private readonly Configuration _configuration; - public async Task WaitForShutdown() - { - await _serverStart; - await _languageServer.WaitForExit; - } - public OmnisharpLanguageServer( Configuration configuration) { _configuration = configuration; + _serverStart = new TaskCompletionSource(); } public async Task StartAsync() { - _serverStart = OS.LanguageServer.From(options => { - Task.Run(async () => { - NamedPipeServerStream namedPipe = CreateNamedPipe( - _configuration.NamedPipeName, - _configuration.OutNamedPipeName, - out NamedPipeServerStream outNamedPipe); - - await namedPipe.WaitForConnectionAsync(); - if (outNamedPipe != null) - { - await outNamedPipe.WaitForConnectionAsync(); - } - - options.Input = namedPipe; - options.Output = outNamedPipe != null - ? outNamedPipe - : namedPipe; - - options.LoggerFactory = _configuration.LoggerFactory; - options.MinimumLogLevel = _configuration.MinimumLogLevel; - options.Services = _configuration.Services; - }); + _languageServer = await OS.LanguageServer.From(options => { + NamedPipeServerStream namedPipe = CreateNamedPipe( + _configuration.NamedPipeName, + _configuration.OutNamedPipeName, + out NamedPipeServerStream outNamedPipe); + + namedPipe.WaitForConnection(); + if (outNamedPipe != null) + { + outNamedPipe.WaitForConnection(); + } + + options.Input = namedPipe; + options.Output = outNamedPipe != null + ? outNamedPipe + : namedPipe; + + options.LoggerFactory = _configuration.LoggerFactory; + options.MinimumLogLevel = _configuration.MinimumLogLevel; + options.Services = _configuration.Services; }); + _serverStart.SetResult(true); + } - await _serverStart; + public async Task WaitForShutdown() + { + await _serverStart.Task; + await _languageServer.WaitForExit; } private static NamedPipeServerStream CreateNamedPipe( diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs index a26376883..801f74037 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs @@ -20,16 +20,6 @@ public OmnisharpLanguageServerBuilder(IServiceCollection serviceCollection) public IServiceCollection Services { get; } - public ILanguageServerBuilder AddHandler(THandler handler) - { - throw new System.NotImplementedException(); - } - - public ILanguageServerBuilder AddService(TService service) - { - throw new System.NotImplementedException(); - } - public ILanguageServer BuildLanguageServer() { var config = new OmnisharpLanguageServer.Configuration() diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index ba75ee307..5663015d7 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs index be73b74a0..234b02169 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs @@ -49,7 +49,7 @@ public class InitializeParams { /// /// This is defined as `any` type on the client side. /// - public object InitializeOptions { get; set; } + public object InitializationOptions { get; set; } // TODO We need to verify if the deserializer will map the type defined in the client // to an enum. diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs index 3e781a217..e83fdec5a 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs @@ -6,7 +6,8 @@ public class WorkspaceClientCapabilities /// The client supports applying batch edits to the workspace by /// by supporting the request `workspace/applyEdit' /// /// - bool? ApplyEdit { get; set; } + public bool? ApplyEdit { get; set; } + /// /// Capabilities specific to `WorkspaceEdit`. @@ -42,6 +43,6 @@ public class WorkspaceEditCapabilities /// /// The client supports versioned document changes in `WorkspaceEdit` /// - bool? DocumentChanges { get; set; } + public bool? DocumentChanges { get; set; } } } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index d5ea605c6..a025320b5 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -30,32 +30,63 @@ function ReportLogErrors } } +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 +} + Describe "Loading and running PowerShellEditorServices" { BeforeAll { Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" + Import-Module -Force "$PSScriptRoot/../../src/PowerShellEditorServices.Engine/bin/Debug/netstandard2.0/publish/Omnisharp.Extensions.LanguageProtocol.dll" Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" $logIdx = 0 $psesServer = Start-PsesServer - $client = Connect-PsesServer -PipeName $psesServer.SessionDetails.languageServicePipeName + $client = Connect-PsesServer -InPipeName $psesServer.SessionDetails.languageServiceWritePipeName -OutPipeName $psesServer.SessionDetails.languageServiceReadPipeName } # This test MUST be first It "Starts and responds to an initialization request" { $request = Send-LspInitializeRequest -Client $client - $response = Get-LspResponse -Client $client -Id $request.Id + $response = Get-LspResponse -Client $client -Id $request.Id #-WaitMillis 99999 $response.Id | Should -BeExactly $request.Id - ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) + CheckErrorResponse -Response $response + + #ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client - $response = Get-LspResponse -Client $client -Id $request.Id + $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 @@ -81,6 +112,27 @@ Describe "Loading and running PowerShellEditorServices" { } } - ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) + #ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) + } + + AfterAll { + if ($psesServer.PsesProcess.HasExited -eq $false) + { + try + { + $psesServer.PsesProcess.Kill() + } + finally + { + try + { + $psesServer.PsesProcess.Dispose() + } + finally + { + $client.Dispose() + } + } + } } } diff --git a/tools/PsesPsClient/Client.cs b/tools/PsesPsClient/Client.cs index 05ebb3573..35d86b278 100644 --- a/tools/PsesPsClient/Client.cs +++ b/tools/PsesPsClient/Client.cs @@ -77,7 +77,8 @@ public PsesLspClient(NamedPipeClientStream inPipe, NamedPipeClientStream outPipe _jsonSettings = new JsonSerializerSettings() { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Formatting = Formatting.Indented }; _jsonSerializer = JsonSerializer.Create(_jsonSettings); @@ -331,7 +332,7 @@ private async Task ReadMessage() return new LspNotification(method, msgJson["params"]); } - string id = ((JValue)msgJson["id"]).Value.ToString(); + string id = ((JValue)msgJson["id"])?.Value?.ToString(); if (msgJson.TryGetValue("result", out JToken resultToken)) { @@ -339,7 +340,7 @@ private async Task ReadMessage() } JObject errorBody = (JObject)msgJson["error"]; - JsonRpcErrorCode errorCode = (JsonRpcErrorCode)(int)((JValue)errorBody["code"]).Value; + JsonRpcErrorCode errorCode = (JsonRpcErrorCode)((JValue)errorBody["code"]).Value; string message = (string)((JValue)errorBody["message"]).Value; return new LspErrorResponse(id, errorCode, message, errorBody["data"]); } @@ -576,7 +577,7 @@ public LspErrorResponse( /// /// Error codes used by the Language Server Protocol. /// - public enum JsonRpcErrorCode : int + public enum JsonRpcErrorCode : long { ParseError = -32700, InvalidRequest = -32600, diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index be932ee3f..b4b0a247e 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -18,6 +18,7 @@ class PsesStartupOptions [string[]] $AdditionalModules [string] $BundledModulesPath [bool] $EnableConsoleRepl + [switch] $SplitInOutPipes } class PsesServerInfo @@ -185,16 +186,21 @@ function Connect-PsesServer param( [Parameter(Mandatory)] [string] - $PipeName + $InPipeName, + + [Parameter(Mandatory)] + [string] + $OutPipeName ) - $psesIdx = $PipeName.IndexOf('PSES') + $psesIdx = $InPipeName.IndexOf('PSES') if ($psesIdx -gt 0) { - $PipeName = $PipeName.Substring($psesIdx) + $InPipeName = $InPipeName.Substring($psesIdx) + $OutPipeName = $OutPipeName.Substring($psesIdx) } - $client = [PsesPsClient.PsesLspClient]::Create($PipeName) + $client = [PsesPsClient.PsesLspClient]::Create($InPipeName, $OutPipeName) $client.Connect() return $client } @@ -221,17 +227,17 @@ function Send-LspInitializeRequest [Parameter()] [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ClientCapabilities] - $ClientCapabilities = ([Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ClientCapabilities]::new()), + $ClientCapabilities = (Get-ClientCapabilities), [Parameter()] - [hashtable] - $InitializeOptions = $null + [object] + $IntializationOptions = ([object]::new()) ) $parameters = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.InitializeParams]@{ ProcessId = $ProcessId Capabilities = $ClientCapabilities - InitializeOptions = $InitializeOptions + InitializationOptions = $IntializationOptions } if ($RootUri) @@ -240,7 +246,7 @@ function Send-LspInitializeRequest } else { - $parameters.RootPath = $RootPath + $parameters.RootUri = [uri]::new($RootPath) } return Send-LspRequest -Client $Client -Method 'initialize' -Parameters $parameters @@ -374,3 +380,80 @@ function Get-RandomHexString 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() + } +} From 91e5166435b0eb01d59d4592c3baddcafd58c721 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 25 Jun 2019 15:22:46 -0700 Subject: [PATCH 05/47] Changes --- .../LanguageServer/OmnisharpLanguageServer.cs | 2 +- .../LanguageServerSettings.cs | 381 ++++++++++ .../Logging/LoggerExtensions.cs | 32 + .../PowerShellEditorServices.Engine.csproj | 1 + .../Services/Analysis/AnalysisService.cs | 702 ++++++++++++++++++ .../Services/TextDocument/BufferPosition.cs | 111 +++ .../Services/TextDocument/BufferRange.cs | 123 +++ .../Services/TextDocument/FileChange.cs | 45 ++ .../Services/TextDocument/FilePosition.cs | 109 +++ .../TextDocumentNotificationHandler.cs | 7 + .../Services/TextDocument/ScriptFile.cs | 678 +++++++++++++++++ .../Services/TextDocument/ScriptFileMarker.cs | 188 +++++ .../Services/TextDocument/ScriptRegion.cs | 112 +++ .../Services/TextDocument/Workspace.cs | 694 +++++++++++++++++ .../WorkspaceFileSystemWrapper.cs | 381 ++++++++++ .../Utility/{Utility.cs => DotNetFacade.cs} | 4 +- .../Utility/PathUtils.cs | 12 + .../Utility/Validate.cs | 2 +- .../Workspace/Workspace.cs | 14 + 19 files changed, 3594 insertions(+), 4 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/LanguageServerSettings.cs create mode 100644 src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs rename src/PowerShellEditorServices.Engine/Utility/{Utility.cs => DotNetFacade.cs} (82%) create mode 100644 src/PowerShellEditorServices.Engine/Utility/PathUtils.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index eb545ad6f..d0873fefa 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -84,7 +84,7 @@ private static NamedPipeServerStream CreateNamedPipe( out NamedPipeServerStream outPipe) { // .NET Core implementation is simplest so try that first - if (Utility.IsNetCore) + if (DotNetFacade.IsNetCore) { outPipe = outPipeName == null ? null diff --git a/src/PowerShellEditorServices.Engine/LanguageServerSettings.cs b/src/PowerShellEditorServices.Engine/LanguageServerSettings.cs new file mode 100644 index 000000000..26eb0e9a5 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/LanguageServerSettings.cs @@ -0,0 +1,381 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Security; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine +{ + public class LanguageServerSettings + { + public bool EnableProfileLoading { get; set; } + + public ScriptAnalysisSettings ScriptAnalysis { get; set; } + + public CodeFormattingSettings CodeFormatting { get; set; } + + public CodeFoldingSettings CodeFolding { get; set; } + + public LanguageServerSettings() + { + this.ScriptAnalysis = new ScriptAnalysisSettings(); + this.CodeFormatting = new CodeFormattingSettings(); + this.CodeFolding = new CodeFoldingSettings(); + } + + public void Update( + LanguageServerSettings settings, + string workspaceRootPath, + ILogger logger) + { + if (settings != null) + { + this.EnableProfileLoading = settings.EnableProfileLoading; + this.ScriptAnalysis.Update( + settings.ScriptAnalysis, + workspaceRootPath, + logger); + this.CodeFormatting = new CodeFormattingSettings(settings.CodeFormatting); + this.CodeFolding.Update(settings.CodeFolding, logger); + } + } + } + + public class ScriptAnalysisSettings + { + public bool? Enable { get; set; } + + public string SettingsPath { get; set; } + + public ScriptAnalysisSettings() + { + this.Enable = true; + } + + public void Update( + ScriptAnalysisSettings settings, + string workspaceRootPath, + ILogger logger) + { + if (settings != null) + { + this.Enable = settings.Enable; + + string settingsPath = settings.SettingsPath; + + try + { + if (string.IsNullOrWhiteSpace(settingsPath)) + { + settingsPath = null; + } + else if (!Path.IsPathRooted(settingsPath)) + { + if (string.IsNullOrEmpty(workspaceRootPath)) + { + // The workspace root path could be an empty string + // when the user has opened a PowerShell script file + // without opening an entire folder (workspace) first. + // In this case we should just log an error and let + // the specified settings path go through even though + // it will fail to load. + logger.LogError( + "Could not resolve Script Analyzer settings path due to null or empty workspaceRootPath."); + } + else + { + settingsPath = Path.GetFullPath(Path.Combine(workspaceRootPath, settingsPath)); + } + } + + this.SettingsPath = settingsPath; + logger.LogDebug($"Using Script Analyzer settings path - '{settingsPath ?? ""}'."); + } + catch (Exception ex) when ( + ex is NotSupportedException || + ex is PathTooLongException || + ex is SecurityException) + { + // Invalid chars in path like ${env:HOME} can cause Path.GetFullPath() to throw, catch such errors here + logger.LogException( + $"Invalid Script Analyzer settings path - '{settingsPath}'.", + ex); + + this.SettingsPath = null; + } + } + } + } + + /// + /// Code formatting presets. + /// See https://en.wikipedia.org/wiki/Indent_style for details on indent and brace styles. + /// + public enum CodeFormattingPreset + { + /// + /// Use the formatting settings as-is. + /// + Custom, + + /// + /// Configure the formatting settings to resemble the Allman indent/brace style. + /// + Allman, + + /// + /// Configure the formatting settings to resemble the one true brace style variant of K&R indent/brace style. + /// + OTBS, + + /// + /// Configure the formatting settings to resemble the Stroustrup brace style variant of K&R indent/brace style. + /// + Stroustrup + } + + /// + /// Multi-line pipeline style settings. + /// + public enum PipelineIndentationStyle + { + /// + /// After the indentation level only once after the first pipeline and keep this level for the following pipelines. + /// + IncreaseIndentationForFirstPipeline, + + /// + /// After every pipeline, keep increasing the indentation. + /// + IncreaseIndentationAfterEveryPipeline, + + /// + /// Do not increase indentation level at all after pipeline. + /// + NoIndentation + } + + public class CodeFormattingSettings + { + /// + /// Default constructor. + /// > + public CodeFormattingSettings() + { + + } + + /// + /// Copy constructor. + /// + /// An instance of type CodeFormattingSettings. + public CodeFormattingSettings(CodeFormattingSettings codeFormattingSettings) + { + if (codeFormattingSettings == null) + { + throw new ArgumentNullException(nameof(codeFormattingSettings)); + } + + foreach (var prop in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + prop.SetValue(this, prop.GetValue(codeFormattingSettings)); + } + } + + public CodeFormattingPreset Preset { get; set; } + public bool OpenBraceOnSameLine { get; set; } + public bool NewLineAfterOpenBrace { get; set; } + public bool NewLineAfterCloseBrace { get; set; } + public PipelineIndentationStyle PipelineIndentationStyle { get; set; } + public bool WhitespaceBeforeOpenBrace { get; set; } + public bool WhitespaceBeforeOpenParen { get; set; } + public bool WhitespaceAroundOperator { get; set; } + public bool WhitespaceAfterSeparator { get; set; } + public bool WhitespaceInsideBrace { get; set; } + public bool WhitespaceAroundPipe { get; set; } + public bool IgnoreOneLineBlock { get; set; } + public bool AlignPropertyValuePairs { get; set; } + public bool UseCorrectCasing { get; set; } + + + /// + /// Get the settings hashtable that will be consumed by PSScriptAnalyzer. + /// + /// The tab size in the number spaces. + /// If true, insert spaces otherwise insert tabs for indentation. + /// + public Hashtable GetPSSASettingsHashtable( + int tabSize, + bool insertSpaces) + { + var settings = GetCustomPSSASettingsHashtable(tabSize, insertSpaces); + var ruleSettings = (Hashtable)(settings["Rules"]); + var closeBraceSettings = (Hashtable)ruleSettings["PSPlaceCloseBrace"]; + var openBraceSettings = (Hashtable)ruleSettings["PSPlaceOpenBrace"]; + switch(Preset) + { + case CodeFormattingPreset.Allman: + openBraceSettings["OnSameLine"] = false; + openBraceSettings["NewLineAfter"] = true; + closeBraceSettings["NewLineAfter"] = true; + break; + + case CodeFormattingPreset.OTBS: + openBraceSettings["OnSameLine"] = true; + openBraceSettings["NewLineAfter"] = true; + closeBraceSettings["NewLineAfter"] = false; + break; + + case CodeFormattingPreset.Stroustrup: + openBraceSettings["OnSameLine"] = true; + openBraceSettings["NewLineAfter"] = true; + closeBraceSettings["NewLineAfter"] = true; + break; + + default: + break; + } + + return settings; + } + + private Hashtable GetCustomPSSASettingsHashtable(int tabSize, bool insertSpaces) + { + return new Hashtable + { + {"IncludeRules", new string[] { + "PSPlaceCloseBrace", + "PSPlaceOpenBrace", + "PSUseConsistentWhitespace", + "PSUseConsistentIndentation", + "PSAlignAssignmentStatement" + }}, + {"Rules", new Hashtable { + {"PSPlaceOpenBrace", new Hashtable { + {"Enable", true}, + {"OnSameLine", OpenBraceOnSameLine}, + {"NewLineAfter", NewLineAfterOpenBrace}, + {"IgnoreOneLineBlock", IgnoreOneLineBlock} + }}, + {"PSPlaceCloseBrace", new Hashtable { + {"Enable", true}, + {"NewLineAfter", NewLineAfterCloseBrace}, + {"IgnoreOneLineBlock", IgnoreOneLineBlock} + }}, + {"PSUseConsistentIndentation", new Hashtable { + {"Enable", true}, + {"IndentationSize", tabSize}, + {"PipelineIndentation", PipelineIndentationStyle }, + {"Kind", insertSpaces ? "space" : "tab"} + }}, + {"PSUseConsistentWhitespace", new Hashtable { + {"Enable", true}, + {"CheckOpenBrace", WhitespaceBeforeOpenBrace}, + {"CheckOpenParen", WhitespaceBeforeOpenParen}, + {"CheckOperator", WhitespaceAroundOperator}, + {"CheckSeparator", WhitespaceAfterSeparator}, + {"CheckInnerBrace", WhitespaceInsideBrace}, + {"CheckPipe", WhitespaceAroundPipe}, + }}, + {"PSAlignAssignmentStatement", new Hashtable { + {"Enable", true}, + {"CheckHashtable", AlignPropertyValuePairs} + }}, + {"PSUseCorrectCasing", new Hashtable { + {"Enable", UseCorrectCasing} + }}, + }} + }; + } + } + + /// + /// Code folding settings + /// + public class CodeFoldingSettings + { + /// + /// Whether the folding is enabled. Default is true as per VSCode + /// + public bool Enable { get; set; } = true; + + /// + /// Whether to show or hide the last line of a folding region. Default is true as per VSCode + /// + public bool ShowLastLine { get; set; } = true; + + /// + /// Update these settings from another settings object + /// + public void Update( + CodeFoldingSettings settings, + ILogger logger) + { + if (settings != null) { + if (this.Enable != settings.Enable) { + this.Enable = settings.Enable; + logger.LogDebug(string.Format("Using Code Folding Enabled - {0}", this.Enable)); + } + if (this.ShowLastLine != settings.ShowLastLine) { + this.ShowLastLine = settings.ShowLastLine; + logger.LogDebug(string.Format("Using Code Folding ShowLastLine - {0}", this.ShowLastLine)); + } + } + } + } + + /// + /// Additional settings from the Language Client that affect Language Server operations but + /// do not exist under the 'powershell' section + /// + public class EditorFileSettings + { + /// + /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the + /// the glob is in effect. + /// + public Dictionary Exclude { get; set; } + } + + /// + /// Additional settings from the Language Client that affect Language Server operations but + /// do not exist under the 'powershell' section + /// + public class EditorSearchSettings + { + /// + /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the + /// the glob is in effect. + /// + public Dictionary Exclude { get; set; } + /// + /// Whether to follow symlinks when searching + /// + public bool FollowSymlinks { get; set; } = true; + } + + public class LanguageServerSettingsWrapper + { + // NOTE: This property is capitalized as 'Powershell' because the + // mode name sent from the client is written as 'powershell' and + // JSON.net is using camelCasing. + public LanguageServerSettings Powershell { get; set; } + + // NOTE: This property is capitalized as 'Files' because the + // mode name sent from the client is written as 'files' and + // JSON.net is using camelCasing. + public EditorFileSettings Files { get; set; } + + // NOTE: This property is capitalized as 'Search' because the + // mode name sent from the client is written as 'search' and + // JSON.net is using camelCasing. + public EditorSearchSettings Search { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs new file mode 100644 index 000000000..4faf9629f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + internal static class LoggerExtensions + { + public static void LogException( + this ILogger logger, + string message, + Exception exception, + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerSourceFile = null, + [CallerLineNumber] int callerLineNumber = -1) + { + logger.LogError(message, exception); + } + + public static void LogHandledException( + this ILogger logger, + string message, + Exception exception, + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerSourceFile = null, + [CallerLineNumber] int callerLineNumber = -1) + { + logger.LogError(message, exception); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index 5663015d7..95fb48490 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -11,6 +11,7 @@ + diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs new file mode 100644 index 000000000..8d5e5a4b0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -0,0 +1,702 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using System.Management.Automation.Runspaces; +using System.Management.Automation; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Services +{ + /// + /// Provides a high-level service for performing semantic analysis + /// of PowerShell scripts. + /// + public class AnalysisService : IDisposable + { + #region Static fields + + /// + /// Defines the list of Script Analyzer rules to include by default if + /// no settings file is specified. + /// + private static readonly string[] s_includedRules = { + "PSUseToExportFieldsInManifest", + "PSMisleadingBacktick", + "PSAvoidUsingCmdletAliases", + "PSUseApprovedVerbs", + "PSAvoidUsingPlainTextForPassword", + "PSReservedCmdletChar", + "PSReservedParams", + "PSShouldProcess", + "PSMissingModuleManifestField", + "PSAvoidDefaultValueSwitchParameter", + "PSUseDeclaredVarsMoreThanAssignments", + "PSPossibleIncorrectComparisonWithNull", + "PSAvoidDefaultValueForMandatoryParameter", + "PSPossibleIncorrectUsageOfRedirectionOperator" + }; + + /// + /// An empty diagnostic result to return when a script fails analysis. + /// + private static readonly PSObject[] s_emptyDiagnosticResult = new PSObject[0]; + + private static readonly string[] s_emptyGetRuleResult = new string[0]; + + /// + /// The indentation to add when the logger lists errors. + /// + private static readonly string s_indentJoin = Environment.NewLine + " "; + + #endregion // Static fields + + #region Private Fields + + /// + /// Maximum number of runspaces we allow to be in use for script analysis. + /// + private const int NumRunspaces = 1; + + /// + /// Name of the PSScriptAnalyzer module, to be used for PowerShell module interactions. + /// + private const string PSSA_MODULE_NAME = "PSScriptAnalyzer"; + + /// + /// Provides logging. + /// + private ILogger _logger; + + /// + /// Runspace pool to generate runspaces for script analysis and handle + /// ansynchronous analysis requests. + /// + private RunspacePool _analysisRunspacePool; + + /// + /// Info object describing the PSScriptAnalyzer module that has been loaded in + /// to provide analysis services. + /// + private PSModuleInfo _pssaModuleInfo; + + #endregion // Private Fields + + #region Properties + + /// + /// Set of PSScriptAnalyzer rules used for analysis. + /// + public string[] ActiveRules { get; set; } + + /// + /// Gets or sets the path to a settings file (.psd1) + /// containing PSScriptAnalyzer settings. + /// + public string SettingsPath { get; set; } + + #endregion + + #region Constructors + + /// + /// Construct a new AnalysisService object. + /// + /// + /// The runspace pool with PSScriptAnalyzer module loaded that will handle + /// analysis tasks. + /// + /// + /// The path to the PSScriptAnalyzer settings file to handle analysis settings. + /// + /// An array of rules to be used for analysis. + /// Maintains logs for the analysis service. + /// + /// Optional module info of the loaded PSScriptAnalyzer module. If not provided, + /// the analysis service will populate it, but it can be given here to save time. + /// + private AnalysisService( + RunspacePool analysisRunspacePool, + string pssaSettingsPath, + IEnumerable activeRules, + ILogger logger, + PSModuleInfo pssaModuleInfo = null) + { + _analysisRunspacePool = analysisRunspacePool; + SettingsPath = pssaSettingsPath; + ActiveRules = activeRules.ToArray(); + _logger = logger; + _pssaModuleInfo = pssaModuleInfo; + } + + #endregion // constructors + + #region Public Methods + + /// + /// Factory method for producing AnalysisService instances. Handles loading of the PSScriptAnalyzer module + /// and runspace pool instantiation before creating the service instance. + /// + /// Path to the PSSA settings file to be used for this service instance. + /// EditorServices logger for logging information. + /// + /// A new analysis service instance with a freshly imported PSScriptAnalyzer module and runspace pool. + /// Returns null if problems occur. This method should never throw. + /// + public static AnalysisService Create(string settingsPath, ILogger logger) + { + try + { + RunspacePool analysisRunspacePool; + PSModuleInfo pssaModuleInfo; + try + { + // Try and load a PSScriptAnalyzer module with the required version + // by looking on the script path. Deep down, this internally runs Get-Module -ListAvailable, + // so we'll use this to check whether such a module exists + analysisRunspacePool = CreatePssaRunspacePool(out pssaModuleInfo); + + } + catch (Exception e) + { + throw new AnalysisServiceLoadException("PSScriptAnalyzer runspace pool could not be created", e); + } + + if (analysisRunspacePool == null) + { + throw new AnalysisServiceLoadException("PSScriptAnalyzer runspace pool failed to be created"); + } + + // Having more than one runspace doesn't block code formatting if one + // runspace is occupied for diagnostics + analysisRunspacePool.SetMaxRunspaces(NumRunspaces); + analysisRunspacePool.ThreadOptions = PSThreadOptions.ReuseThread; + analysisRunspacePool.Open(); + + var analysisService = new AnalysisService( + analysisRunspacePool, + settingsPath, + s_includedRules, + logger, + pssaModuleInfo); + + // Log what features are available in PSSA here + analysisService.LogAvailablePssaFeatures(); + + return analysisService; + } + catch (AnalysisServiceLoadException e) + { + logger.LogWarning("PSScriptAnalyzer cannot be imported, AnalysisService will be disabled", e); + return null; + } + catch (Exception e) + { + logger.LogWarning("AnalysisService could not be started due to an unexpected exception", e); + return null; + } + } + + /// + /// Get PSScriptAnalyzer settings hashtable for PSProvideCommentHelp rule. + /// + /// Enable the rule. + /// Analyze only exported functions/cmdlets. + /// Use block comment or line comment. + /// Return a vscode snipped correction should be returned. + /// Place comment help at the given location relative to the function definition. + /// A PSScriptAnalyzer settings hashtable. + public static Hashtable GetCommentHelpRuleSettings( + bool enable, + bool exportedOnly, + bool blockComment, + bool vscodeSnippetCorrection, + string placement) + { + var settings = new Dictionary(); + var ruleSettings = new Hashtable(); + ruleSettings.Add("Enable", enable); + ruleSettings.Add("ExportedOnly", exportedOnly); + ruleSettings.Add("BlockComment", blockComment); + ruleSettings.Add("VSCodeSnippetCorrection", vscodeSnippetCorrection); + ruleSettings.Add("Placement", placement); + settings.Add("PSProvideCommentHelp", ruleSettings); + return GetPSSASettingsHashtable(settings); + } + + /// + /// Construct a PSScriptAnalyzer settings hashtable + /// + /// A settings hashtable + /// + public static Hashtable GetPSSASettingsHashtable(IDictionary ruleSettingsMap) + { + var hashtable = new Hashtable(); + var ruleSettingsHashtable = new Hashtable(); + + hashtable["IncludeRules"] = ruleSettingsMap.Keys.ToArray(); + hashtable["Rules"] = ruleSettingsHashtable; + + foreach (var kvp in ruleSettingsMap) + { + ruleSettingsHashtable.Add(kvp.Key, kvp.Value); + } + + return hashtable; + } + + /// + /// Perform semantic analysis on the given ScriptFile and returns + /// an array of ScriptFileMarkers. + /// + /// The ScriptFile which will be analyzed for semantic markers. + /// An array of ScriptFileMarkers containing semantic analysis results. + public async Task> GetSemanticMarkersAsync(ScriptFile file) + { + return await GetSemanticMarkersAsync(file, ActiveRules, SettingsPath); + } + + /// + /// Perform semantic analysis on the given ScriptFile with the given settings. + /// + /// The ScriptFile to be analyzed. + /// ScriptAnalyzer settings + /// + public async Task> GetSemanticMarkersAsync(ScriptFile file, Hashtable settings) + { + return await GetSemanticMarkersAsync(file, null, settings); + } + + /// + /// Perform semantic analysis on the given script with the given settings. + /// + /// The script content to be analyzed. + /// ScriptAnalyzer settings + /// + public async Task> GetSemanticMarkersAsync( + string scriptContent, + Hashtable settings) + { + return await GetSemanticMarkersAsync(scriptContent, null, settings); + } + + /// + /// Returns a list of builtin-in PSScriptAnalyzer rules + /// + public IEnumerable GetPSScriptAnalyzerRules() + { + PowerShellResult getRuleResult = InvokePowerShell("Get-ScriptAnalyzerRule"); + if (getRuleResult == null) + { + _logger.LogWarning("Get-ScriptAnalyzerRule returned null result"); + return s_emptyGetRuleResult; + } + + var ruleNames = new List(); + foreach (var rule in getRuleResult.Output) + { + ruleNames.Add((string)rule.Members["RuleName"].Value); + } + + return ruleNames; + } + + /// + /// Format a given script text with default codeformatting settings. + /// + /// Script text to be formatted + /// ScriptAnalyzer settings + /// The range within which formatting should be applied. + /// The formatted script text. + public async Task FormatAsync( + string scriptDefinition, + Hashtable settings, + int[] rangeList) + { + // We cannot use Range type therefore this workaround of using -1 default value. + // Invoke-Formatter throws a ParameterBinderValidationException if the ScriptDefinition is an empty string. + if (string.IsNullOrEmpty(scriptDefinition)) + { + return null; + } + + var argsDict = new Dictionary { + {"ScriptDefinition", scriptDefinition}, + {"Settings", settings} + }; + if (rangeList != null) + { + argsDict.Add("Range", rangeList); + } + + PowerShellResult result = await InvokePowerShellAsync("Invoke-Formatter", argsDict); + + if (result == null) + { + _logger.LogError("Formatter returned null result"); + return null; + } + + if (result.HasErrors) + { + var errorBuilder = new StringBuilder().Append(s_indentJoin); + foreach (ErrorRecord err in result.Errors) + { + errorBuilder.Append(err).Append(s_indentJoin); + } + _logger.LogWarning($"Errors found while formatting file: {errorBuilder}"); + return null; + } + + foreach (PSObject resultObj in result.Output) + { + string formatResult = resultObj?.BaseObject as string; + if (formatResult != null) + { + return formatResult; + } + } + + return null; + } + + #endregion // public methods + + #region Private Methods + + private async Task> GetSemanticMarkersAsync( + ScriptFile file, + string[] rules, + TSettings settings) where TSettings : class + { + if (file.IsAnalysisEnabled) + { + return await GetSemanticMarkersAsync( + file.Contents, + rules, + settings); + } + else + { + // Return an empty marker list + return new List(); + } + } + + private async Task> GetSemanticMarkersAsync( + string scriptContent, + string[] rules, + TSettings settings) where TSettings : class + { + if ((typeof(TSettings) == typeof(string) || typeof(TSettings) == typeof(Hashtable)) + && (rules != null || settings != null)) + { + var scriptFileMarkers = await GetDiagnosticRecordsAsync(scriptContent, rules, settings); + return scriptFileMarkers.Select(ScriptFileMarker.FromDiagnosticRecord).ToList(); + } + else + { + // Return an empty marker list + return new List(); + } + } + + /// + /// Log the features available from the PSScriptAnalyzer module that has been imported + /// for use with the AnalysisService. + /// + private void LogAvailablePssaFeatures() + { + // Save ourselves some work here + if (!_logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + // If we already know the module that was imported, save some work + if (_pssaModuleInfo == null) + { + PowerShellResult getModuleResult = InvokePowerShell( + "Get-Module", + new Dictionary{ {"Name", PSSA_MODULE_NAME} }); + + if (getModuleResult == null) + { + throw new AnalysisServiceLoadException("Get-Module call to find PSScriptAnalyzer module failed"); + } + + _pssaModuleInfo = getModuleResult.Output + .Select(m => m.BaseObject) + .OfType() + .FirstOrDefault(); + } + + if (_pssaModuleInfo == null) + { + throw new AnalysisServiceLoadException("Unable to find loaded PSScriptAnalyzer module for logging"); + } + + var sb = new StringBuilder(); + sb.AppendLine("PSScriptAnalyzer successfully imported:"); + + // Log version + sb.Append(" Version: "); + sb.AppendLine(_pssaModuleInfo.Version.ToString()); + + // Log exported cmdlets + sb.AppendLine(" Exported Cmdlets:"); + foreach (string cmdletName in _pssaModuleInfo.ExportedCmdlets.Keys.OrderBy(name => name)) + { + sb.Append(" "); + sb.AppendLine(cmdletName); + } + + // Log available rules + sb.AppendLine(" Available Rules:"); + foreach (string ruleName in GetPSScriptAnalyzerRules()) + { + sb.Append(" "); + sb.AppendLine(ruleName); + } + + _logger.LogDebug(sb.ToString()); + } + + private async Task GetDiagnosticRecordsAsync( + string scriptContent, + string[] rules, + TSettings settings) where TSettings : class + { + var diagnosticRecords = s_emptyDiagnosticResult; + + // When a new, empty file is created there are by definition no issues. + // Furthermore, if you call Invoke-ScriptAnalyzer with an empty ScriptDefinition + // it will generate a ParameterBindingValidationException. + if (string.IsNullOrEmpty(scriptContent)) + { + return diagnosticRecords; + } + + if (typeof(TSettings) == typeof(string) || typeof(TSettings) == typeof(Hashtable)) + { + //Use a settings file if one is provided, otherwise use the default rule list. + string settingParameter; + object settingArgument; + if (settings != null) + { + settingParameter = "Settings"; + settingArgument = settings; + } + else + { + settingParameter = "IncludeRule"; + settingArgument = rules; + } + + PowerShellResult result = await InvokePowerShellAsync( + "Invoke-ScriptAnalyzer", + new Dictionary + { + { "ScriptDefinition", scriptContent }, + { settingParameter, settingArgument }, + // We ignore ParseErrors from PSSA because we already send them when we parse the file. + { "Severity", new [] { ScriptFileMarkerLevel.Error, ScriptFileMarkerLevel.Information, ScriptFileMarkerLevel.Warning }} + }); + + diagnosticRecords = result?.Output; + } + + _logger.LogDebug(String.Format("Found {0} violations", diagnosticRecords.Count())); + + return diagnosticRecords; + } + + private PowerShellResult InvokePowerShell(string command, IDictionary paramArgMap = null) + { + using (var powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.RunspacePool = _analysisRunspacePool; + powerShell.AddCommand(command); + if (paramArgMap != null) + { + foreach (KeyValuePair kvp in paramArgMap) + { + powerShell.AddParameter(kvp.Key, kvp.Value); + } + } + + PowerShellResult result = null; + try + { + PSObject[] output = powerShell.Invoke().ToArray(); + ErrorRecord[] errors = powerShell.Streams.Error.ToArray(); + result = new PowerShellResult(output, errors, powerShell.HadErrors); + } + catch (CommandNotFoundException ex) + { + // This exception is possible if the module path loaded + // is wrong even though PSScriptAnalyzer is available as a module + _logger.LogError(ex.Message); + } + catch (CmdletInvocationException ex) + { + // We do not want to crash EditorServices for exceptions caused by cmdlet invocation. + // Two main reasons that cause the exception are: + // * PSCmdlet.WriteOutput being called from another thread than Begin/Process + // * CompositionContainer.ComposeParts complaining that "...Only one batch can be composed at a time" + _logger.LogError(ex.Message); + } + + return result; + } + } + + private async Task InvokePowerShellAsync(string command, IDictionary paramArgMap = null) + { + var task = Task.Run(() => + { + return InvokePowerShell(command, paramArgMap); + }); + + return await task; + } + + /// + /// Create a new runspace pool around a PSScriptAnalyzer module for asynchronous script analysis tasks. + /// This looks for the latest version of PSScriptAnalyzer on the path and loads that. + /// + /// A runspace pool with PSScriptAnalyzer loaded for running script analysis tasks. + private static RunspacePool CreatePssaRunspacePool(out PSModuleInfo pssaModuleInfo) + { + using (var ps = System.Management.Automation.PowerShell.Create()) + { + // Run `Get-Module -ListAvailable -Name "PSScriptAnalyzer"` + ps.AddCommand("Get-Module") + .AddParameter("ListAvailable") + .AddParameter("Name", PSSA_MODULE_NAME); + + try + { + // Get the latest version of PSScriptAnalyzer we can find + pssaModuleInfo = ps.Invoke()? + .Select(psObj => psObj.BaseObject) + .OfType() + .OrderByDescending(moduleInfo => moduleInfo.Version) + .FirstOrDefault(); + } + catch (Exception e) + { + throw new AnalysisServiceLoadException("Unable to find PSScriptAnalyzer module on the module path", e); + } + + if (pssaModuleInfo == null) + { + throw new AnalysisServiceLoadException("Unable to find PSScriptAnalyzer module on the module path"); + } + + // Create a base session state with PSScriptAnalyzer loaded + InitialSessionState sessionState; + if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { + sessionState = InitialSessionState.CreateDefault(); + } else { + sessionState = InitialSessionState.CreateDefault2(); + } + sessionState.ImportPSModule(new [] { pssaModuleInfo.ModuleBase }); + + // RunspacePool takes care of queuing commands for us so we do not + // need to worry about executing concurrent commands + return RunspaceFactory.CreateRunspacePool(sessionState); + } + } + + #endregion //private methods + + #region IDisposable Support + + private bool _disposedValue = false; // To detect redundant calls + + /// + /// Dispose of this object. + /// + /// True if the method is called by the Dispose method, false if called by the finalizer. + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _analysisRunspacePool.Dispose(); + _analysisRunspacePool = null; + } + + _disposedValue = true; + } + } + + /// + /// Clean up all internal resources and dispose of the analysis service. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + + #endregion + + /// + /// Wraps the result of an execution of PowerShell to send back through + /// asynchronous calls. + /// + private class PowerShellResult + { + public PowerShellResult( + PSObject[] output, + ErrorRecord[] errors, + bool hasErrors) + { + Output = output; + Errors = errors; + HasErrors = hasErrors; + } + + public PSObject[] Output { get; } + + public ErrorRecord[] Errors { get; } + + public bool HasErrors { get; } + } + } + + /// + /// Class to catch known failure modes for starting the AnalysisService. + /// + public class AnalysisServiceLoadException : Exception + { + /// + /// Instantiate an AnalysisService error based on a simple message. + /// + /// The message to display to the user detailing the error. + public AnalysisServiceLoadException(string message) + : base(message) + { + } + + /// + /// Instantiate an AnalysisService error based on another error that occurred internally. + /// + /// The message to display to the user detailing the error. + /// The inner exception that occurred to trigger this error. + public AnalysisServiceLoadException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs new file mode 100644 index 000000000..effdd7660 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about a position in a file buffer. All + /// positions are expressed in 1-based positions (i.e. the + /// first line and column in the file is position 1,1). + /// + [DebuggerDisplay("Position = {Line}:{Column}")] + public class BufferPosition + { + #region Properties + + /// + /// Provides an instance that represents a position that has not been set. + /// + public static readonly BufferPosition None = new BufferPosition(-1, -1); + + /// + /// Gets the line number of the position in the buffer. + /// + public int Line { get; private set; } + + /// + /// Gets the column number of the position in the buffer. + /// + public int Column { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the BufferPosition class. + /// + /// The line number of the position. + /// The column number of the position. + public BufferPosition(int line, int column) + { + this.Line = line; + this.Column = column; + } + + #endregion + + #region Public Methods + + /// + /// Compares two instances of the BufferPosition class. + /// + /// The object to which this instance will be compared. + /// True if the positions are equal, false otherwise. + public override bool Equals(object obj) + { + if (!(obj is BufferPosition)) + { + return false; + } + + BufferPosition other = (BufferPosition)obj; + + return + this.Line == other.Line && + this.Column == other.Column; + } + + /// + /// Calculates a unique hash code that represents this instance. + /// + /// A hash code representing this instance. + public override int GetHashCode() + { + return this.Line.GetHashCode() ^ this.Column.GetHashCode(); + } + + /// + /// Compares two positions to check if one is greater than the other. + /// + /// The first position to compare. + /// The second position to compare. + /// True if positionOne is greater than positionTwo. + public static bool operator >(BufferPosition positionOne, BufferPosition positionTwo) + { + return + (positionOne != null && positionTwo == null) || + (positionOne.Line > positionTwo.Line) || + (positionOne.Line == positionTwo.Line && + positionOne.Column > positionTwo.Column); + } + + /// + /// Compares two positions to check if one is less than the other. + /// + /// The first position to compare. + /// The second position to compare. + /// True if positionOne is less than positionTwo. + public static bool operator <(BufferPosition positionOne, BufferPosition positionTwo) + { + return positionTwo > positionOne; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs new file mode 100644 index 000000000..147eed042 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs @@ -0,0 +1,123 @@ +// +// 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.Diagnostics; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about a range between two positions in + /// a file buffer. + /// + [DebuggerDisplay("Start = {Start.Line}:{Start.Column}, End = {End.Line}:{End.Column}")] + public class BufferRange + { + #region Properties + + /// + /// Provides an instance that represents a range that has not been set. + /// + public static readonly BufferRange None = new BufferRange(0, 0, 0, 0); + + /// + /// Gets the start position of the range in the buffer. + /// + public BufferPosition Start { get; private set; } + + /// + /// Gets the end position of the range in the buffer. + /// + public BufferPosition End { get; private set; } + + /// + /// Returns true if the current range is non-zero, i.e. + /// contains valid start and end positions. + /// + public bool HasRange + { + get + { + return this.Equals(BufferRange.None); + } + } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the BufferRange class. + /// + /// The start position of the range. + /// The end position of the range. + public BufferRange(BufferPosition start, BufferPosition end) + { + if (start > end) + { + throw new ArgumentException( + string.Format( + "Start position ({0}, {1}) must come before or be equal to the end position ({2}, {3}).", + start.Line, start.Column, + end.Line, end.Column)); + } + + this.Start = start; + this.End = end; + } + + /// + /// Creates a new instance of the BufferRange class. + /// + /// The 1-based starting line number of the range. + /// The 1-based starting column number of the range. + /// The 1-based ending line number of the range. + /// The 1-based ending column number of the range. + public BufferRange( + int startLine, + int startColumn, + int endLine, + int endColumn) + { + this.Start = new BufferPosition(startLine, startColumn); + this.End = new BufferPosition(endLine, endColumn); + } + + #endregion + + #region Public Methods + + /// + /// Compares two instances of the BufferRange class. + /// + /// The object to which this instance will be compared. + /// True if the ranges are equal, false otherwise. + public override bool Equals(object obj) + { + if (!(obj is BufferRange)) + { + return false; + } + + BufferRange other = (BufferRange)obj; + + return + this.Start.Equals(other.Start) && + this.End.Equals(other.End); + } + + /// + /// Calculates a unique hash code that represents this instance. + /// + /// A hash code representing this instance. + public override int GetHashCode() + { + return this.Start.GetHashCode() ^ this.End.GetHashCode(); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs new file mode 100644 index 000000000..79f6925ea --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs @@ -0,0 +1,45 @@ +// +// 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 +{ + /// + /// Contains details relating to a content change in an open file. + /// + public class FileChange + { + /// + /// The string which is to be inserted in the file. + /// + public string InsertString { get; set; } + + /// + /// The 1-based line number where the change starts. + /// + public int Line { get; set; } + + /// + /// The 1-based column offset where the change starts. + /// + public int Offset { get; set; } + + /// + /// The 1-based line number where the change ends. + /// + public int EndLine { get; set; } + + /// + /// The 1-based column offset where the change ends. + /// + public int EndOffset { get; set; } + + /// + /// Indicates that the InsertString is an overwrite + /// of the content, and all stale content and metadata + /// should be discarded. + /// + public bool IsReload { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs new file mode 100644 index 000000000..a7c9036c7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs @@ -0,0 +1,109 @@ +// +// 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 +{ + /// + /// Provides details and operations for a buffer position in a + /// specific file. + /// + public class FilePosition : BufferPosition + { + #region Private Fields + + private ScriptFile scriptFile; + + #endregion + + #region Constructors + + /// + /// Creates a new FilePosition instance for the 1-based line and + /// column numbers in the specified file. + /// + /// The ScriptFile in which the position is located. + /// The 1-based line number in the file. + /// The 1-based column number in the file. + public FilePosition( + ScriptFile scriptFile, + int line, + int column) + : base(line, column) + { + this.scriptFile = scriptFile; + } + + /// + /// Creates a new FilePosition instance for the specified file by + /// copying the specified BufferPosition + /// + /// The ScriptFile in which the position is located. + /// The original BufferPosition from which the line and column will be copied. + public FilePosition( + ScriptFile scriptFile, + BufferPosition copiedPosition) + : this(scriptFile, copiedPosition.Line, copiedPosition.Column) + { + scriptFile.ValidatePosition(copiedPosition); + } + + #endregion + + #region Public Methods + + /// + /// Gets a FilePosition relative to this position by adding the + /// provided line and column offset relative to the contents of + /// the current file. + /// + /// The line offset to add to this position. + /// The column offset to add to this position. + /// A new FilePosition instance for the calculated position. + public FilePosition AddOffset(int lineOffset, int columnOffset) + { + return this.scriptFile.CalculatePosition( + this, + lineOffset, + columnOffset); + } + + /// + /// Gets a FilePosition for the line and column position + /// of the beginning of the current line after any initial + /// whitespace for indentation. + /// + /// A new FilePosition instance for the calculated position. + public FilePosition GetLineStart() + { + string scriptLine = scriptFile.FileLines[this.Line - 1]; + + int lineStartColumn = 1; + for (int i = 0; i < scriptLine.Length; i++) + { + if (!char.IsWhiteSpace(scriptLine[i])) + { + lineStartColumn = i + 1; + break; + } + } + + return new FilePosition(this.scriptFile, this.Line, lineStartColumn); + } + + /// + /// Gets a FilePosition for the line and column position + /// of the end of the current line. + /// + /// A new FilePosition instance for the calculated position. + public FilePosition GetLineEnd() + { + string scriptLine = scriptFile.FileLines[this.Line - 1]; + return new FilePosition(this.scriptFile, this.Line, scriptLine.Length + 1); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs new file mode 100644 index 000000000..3307d8385 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs @@ -0,0 +1,7 @@ +namespace PowerShellEditorServices.Engine.Services.TextDocument.Handlers +{ + public class TextDocumentNotificationHandler + { + + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs new file mode 100644 index 000000000..a916a1600 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs @@ -0,0 +1,678 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains the details and contents of an open script file. + /// + public class ScriptFile + { + #region Private Fields + + private static readonly string[] s_newlines = new[] + { + "\r\n", + "\n" + }; + + private Version powerShellVersion; + + #endregion + + #region Properties + + /// + /// Gets a unique string that identifies this file. At this time, + /// this property returns a normalized version of the value stored + /// in the FilePath property. + /// + public string Id + { + get { return this.FilePath.ToLower(); } + } + + /// + /// Gets the path at which this file resides. + /// + public string FilePath { get; private set; } + + /// + /// Gets the path which the editor client uses to identify this file. + /// + public string ClientFilePath { get; private set; } + + /// + /// Gets the file path in LSP DocumentUri form. The ClientPath property must not be null. + /// + public string DocumentUri + { + get + { + return this.ClientFilePath == null + ? string.Empty + : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); + } + } + + /// + /// Gets or sets a boolean that determines whether + /// semantic analysis should be enabled for this file. + /// For internal use only. + /// + internal bool IsAnalysisEnabled { get; set; } + + /// + /// Gets a boolean that determines whether this file is + /// in-memory or not (either unsaved or non-file content). + /// + public bool IsInMemory { get; private set; } + + /// + /// Gets a string containing the full contents of the file. + /// + public string Contents + { + get + { + return string.Join(Environment.NewLine, this.FileLines); + } + } + + /// + /// Gets a BufferRange that represents the entire content + /// range of the file. + /// + public BufferRange FileRange { get; private set; } + + /// + /// Gets the list of syntax markers found by parsing this + /// file's contents. + /// + public List DiagnosticMarkers + { + get; + private set; + } + + /// + /// Gets the list of strings for each line of the file. + /// + internal List FileLines + { + get; + private set; + } + + /// + /// Gets the ScriptBlockAst representing the parsed script contents. + /// + public ScriptBlockAst ScriptAst + { + get; + private set; + } + + /// + /// Gets the array of Tokens representing the parsed script contents. + /// + public Token[] ScriptTokens + { + get; + private set; + } + + /// + /// Gets the array of filepaths dot sourced in this ScriptFile + /// + public string[] ReferencedFiles + { + get; + private set; + } + + #endregion + + #region Constructors + + /// + /// Creates a new ScriptFile instance by reading file contents from + /// the given TextReader. + /// + /// The path at which the script file resides. + /// The path which the client uses to identify the file. + /// The TextReader to use for reading the file's contents. + /// The version of PowerShell for which the script is being parsed. + public ScriptFile( + string filePath, + string clientFilePath, + TextReader textReader, + Version powerShellVersion) + { + this.FilePath = filePath; + this.ClientFilePath = clientFilePath; + this.IsAnalysisEnabled = true; + this.IsInMemory = Workspace.IsPathInMemory(filePath); + this.powerShellVersion = powerShellVersion; + + // SetFileContents() calls ParseFileContents() which initializes the rest of the properties. + this.SetFileContents(textReader.ReadToEnd()); + } + + /// + /// Creates a new ScriptFile instance with the specified file contents. + /// + /// The path at which the script file resides. + /// The path which the client uses to identify the file. + /// The initial contents of the script file. + /// The version of PowerShell for which the script is being parsed. + public ScriptFile( + string filePath, + string clientFilePath, + string initialBuffer, + Version powerShellVersion) + : this( + filePath, + clientFilePath, + new StringReader(initialBuffer), + powerShellVersion) + { + } + + /// + /// Creates a new ScriptFile instance with the specified filepath. + /// + /// The path at which the script file resides. + /// The path which the client uses to identify the file. + /// The version of PowerShell for which the script is being parsed. + public ScriptFile( + string filePath, + string clientFilePath, + Version powerShellVersion) + : this( + filePath, + clientFilePath, + File.ReadAllText(filePath), + powerShellVersion) + { + } + + #endregion + + #region Public Methods + + /// + /// Get the lines in a string. + /// + /// Input string to be split up into lines. + /// The lines in the string. + [Obsolete("This method is not designed for public exposure and will be retired in later versions of EditorServices")] + public static IList GetLines(string text) + { + return GetLinesInternal(text); + } + + /// + /// Get the lines in a string. + /// + /// Input string to be split up into lines. + /// The lines in the string. + internal static List GetLinesInternal(string text) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + return new List(text.Split(s_newlines, StringSplitOptions.None)); + } + + /// + /// Deterines whether the supplied path indicates the file is an "untitled:Unitled-X" + /// which has not been saved to file. + /// + /// The path to check. + /// True if the path is an untitled file, false otherwise. + public static bool IsUntitledPath(string path) + { + Validate.IsNotNull(nameof(path), path); + + return path.ToLower().StartsWith("untitled:"); + } + + /// + /// Gets a line from the file's contents. + /// + /// The 1-based line number in the file. + /// The complete line at the given line number. + public string GetLine(int lineNumber) + { + Validate.IsWithinRange( + "lineNumber", lineNumber, + 1, this.FileLines.Count + 1); + + return this.FileLines[lineNumber - 1]; + } + + /// + /// Gets a range of lines from the file's contents. + /// + /// The buffer range from which lines will be extracted. + /// An array of strings from the specified range of the file. + public string[] GetLinesInRange(BufferRange bufferRange) + { + this.ValidatePosition(bufferRange.Start); + this.ValidatePosition(bufferRange.End); + + List linesInRange = new List(); + + int startLine = bufferRange.Start.Line, + endLine = bufferRange.End.Line; + + for (int line = startLine; line <= endLine; line++) + { + string currentLine = this.FileLines[line - 1]; + int startColumn = + line == startLine + ? bufferRange.Start.Column + : 1; + int endColumn = + line == endLine + ? bufferRange.End.Column + : currentLine.Length + 1; + + currentLine = + currentLine.Substring( + startColumn - 1, + endColumn - startColumn); + + linesInRange.Add(currentLine); + } + + return linesInRange.ToArray(); + } + + /// + /// Throws ArgumentOutOfRangeException if the given position is outside + /// of the file's buffer extents. + /// + /// The position in the buffer to be validated. + public void ValidatePosition(BufferPosition bufferPosition) + { + this.ValidatePosition( + bufferPosition.Line, + bufferPosition.Column); + } + + /// + /// Throws ArgumentOutOfRangeException if the given position is outside + /// of the file's buffer extents. + /// + /// The 1-based line to be validated. + /// The 1-based column to be validated. + public void ValidatePosition(int line, int column) + { + int maxLine = this.FileLines.Count; + if (line < 1 || line > maxLine) + { + throw new ArgumentOutOfRangeException($"Position {line}:{column} is outside of the line range of 1 to {maxLine}."); + } + + // The maximum column is either **one past** the length of the string + // or 1 if the string is empty. + string lineString = this.FileLines[line - 1]; + int maxColumn = lineString.Length > 0 ? lineString.Length + 1 : 1; + + if (column < 1 || column > maxColumn) + { + throw new ArgumentOutOfRangeException($"Position {line}:{column} is outside of the column range of 1 to {maxColumn}."); + } + } + + + /// + /// Defunct ValidatePosition method call. The isInsertion parameter is ignored. + /// + /// + /// + /// + [Obsolete("Use ValidatePosition(int, int) instead")] + public void ValidatePosition(int line, int column, bool isInsertion) + { + ValidatePosition(line, column); + } + + /// + /// Applies the provided FileChange to the file's contents + /// + /// The FileChange to apply to the file's contents. + public void ApplyChange(FileChange fileChange) + { + // Break up the change lines + string[] changeLines = fileChange.InsertString.Split('\n'); + + if (fileChange.IsReload) + { + this.FileLines.Clear(); + foreach (var changeLine in changeLines) + { + this.FileLines.Add(changeLine); + } + } + else + { + // VSCode sometimes likes to give the change start line as (FileLines.Count + 1). + // This used to crash EditorServices, but we now treat it as an append. + // See https://github.com/PowerShell/vscode-powershell/issues/1283 + if (fileChange.Line == this.FileLines.Count + 1) + { + foreach (string addedLine in changeLines) + { + string finalLine = addedLine.TrimEnd('\r'); + this.FileLines.Add(finalLine); + } + } + // Similarly, when lines are deleted from the end of the file, + // VSCode likes to give the end line as (FileLines.Count + 1). + else if (fileChange.EndLine == this.FileLines.Count + 1 && String.Empty.Equals(fileChange.InsertString)) + { + int lineIndex = fileChange.Line - 1; + this.FileLines.RemoveRange(lineIndex, this.FileLines.Count - lineIndex); + } + // Otherwise, the change needs to go between existing content + else + { + this.ValidatePosition(fileChange.Line, fileChange.Offset); + this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset); + + // Get the first fragment of the first line + string firstLineFragment = + this.FileLines[fileChange.Line - 1] + .Substring(0, fileChange.Offset - 1); + + // Get the last fragment of the last line + string endLine = this.FileLines[fileChange.EndLine - 1]; + string lastLineFragment = + endLine.Substring( + fileChange.EndOffset - 1, + (this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1); + + // Remove the old lines + for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++) + { + this.FileLines.RemoveAt(fileChange.Line - 1); + } + + // Build and insert the new lines + int currentLineNumber = fileChange.Line; + for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++) + { + // Since we split the lines above using \n, make sure to + // trim the ending \r's off as well. + string finalLine = changeLines[changeIndex].TrimEnd('\r'); + + // Should we add first or last line fragments? + if (changeIndex == 0) + { + // Append the first line fragment + finalLine = firstLineFragment + finalLine; + } + if (changeIndex == changeLines.Length - 1) + { + // Append the last line fragment + finalLine = finalLine + lastLineFragment; + } + + this.FileLines.Insert(currentLineNumber - 1, finalLine); + currentLineNumber++; + } + } + } + + // Parse the script again to be up-to-date + this.ParseFileContents(); + } + + /// + /// Calculates the zero-based character offset of a given + /// line and column position in the file. + /// + /// The 1-based line number from which the offset is calculated. + /// The 1-based column number from which the offset is calculated. + /// The zero-based offset for the given file position. + public int GetOffsetAtPosition(int lineNumber, int columnNumber) + { + Validate.IsWithinRange("lineNumber", lineNumber, 1, this.FileLines.Count); + Validate.IsGreaterThan("columnNumber", columnNumber, 0); + + int offset = 0; + + for (int i = 0; i < lineNumber; i++) + { + if (i == lineNumber - 1) + { + // Subtract 1 to account for 1-based column numbering + offset += columnNumber - 1; + } + else + { + // Add an offset to account for the current platform's newline characters + offset += this.FileLines[i].Length + Environment.NewLine.Length; + } + } + + return offset; + } + + /// + /// Calculates a FilePosition relative to a starting BufferPosition + /// using the given 1-based line and column offset. + /// + /// The original BufferPosition from which an new position should be calculated. + /// The 1-based line offset added to the original position in this file. + /// The 1-based column offset added to the original position in this file. + /// A new FilePosition instance with the resulting line and column number. + public FilePosition CalculatePosition( + BufferPosition originalPosition, + int lineOffset, + int columnOffset) + { + int newLine = originalPosition.Line + lineOffset, + newColumn = originalPosition.Column + columnOffset; + + this.ValidatePosition(newLine, newColumn); + + string scriptLine = this.FileLines[newLine - 1]; + newColumn = Math.Min(scriptLine.Length + 1, newColumn); + + return new FilePosition(this, newLine, newColumn); + } + + /// + /// Calculates the 1-based line and column number position based + /// on the given buffer offset. + /// + /// The buffer offset to convert. + /// A new BufferPosition containing the position of the offset. + public BufferPosition GetPositionAtOffset(int bufferOffset) + { + BufferRange bufferRange = + GetRangeBetweenOffsets( + bufferOffset, bufferOffset); + + return bufferRange.Start; + } + + /// + /// Calculates the 1-based line and column number range based on + /// the given start and end buffer offsets. + /// + /// The start offset of the range. + /// The end offset of the range. + /// A new BufferRange containing the positions in the offset range. + public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset) + { + bool foundStart = false; + int currentOffset = 0; + int searchedOffset = startOffset; + + BufferPosition startPosition = new BufferPosition(0, 0); + BufferPosition endPosition = startPosition; + + int line = 0; + while (line < this.FileLines.Count) + { + if (searchedOffset <= currentOffset + this.FileLines[line].Length) + { + int column = searchedOffset - currentOffset; + + // Have we already found the start position? + if (foundStart) + { + // Assign the end position and end the search + endPosition = new BufferPosition(line + 1, column + 1); + break; + } + else + { + startPosition = new BufferPosition(line + 1, column + 1); + + // Do we only need to find the start position? + if (startOffset == endOffset) + { + endPosition = startPosition; + break; + } + else + { + // Since the end offset can be on the same line, + // skip the line increment and continue searching + // for the end position + foundStart = true; + searchedOffset = endOffset; + continue; + } + } + } + + // Increase the current offset and include newline length + currentOffset += this.FileLines[line].Length + Environment.NewLine.Length; + line++; + } + + return new BufferRange(startPosition, endPosition); + } + + #endregion + + #region Private Methods + + private void SetFileContents(string fileContents) + { + // Split the file contents into lines and trim + // any carriage returns from the strings. + this.FileLines = GetLinesInternal(fileContents); + + // Parse the contents to get syntax tree and errors + this.ParseFileContents(); + } + + /// + /// Parses the current file contents to get the AST, tokens, + /// and parse errors. + /// + private void ParseFileContents() + { + ParseError[] parseErrors = null; + + // First, get the updated file range + int lineCount = this.FileLines.Count; + if (lineCount > 0) + { + this.FileRange = + new BufferRange( + new BufferPosition(1, 1), + new BufferPosition( + lineCount + 1, + this.FileLines[lineCount - 1].Length + 1)); + } + else + { + this.FileRange = BufferRange.None; + } + + try + { + Token[] scriptTokens; + + // This overload appeared with Windows 10 Update 1 + if (this.powerShellVersion.Major >= 6 || + (this.powerShellVersion.Major == 5 && this.powerShellVersion.Build >= 10586)) + { + // Include the file path so that module relative + // paths are evaluated correctly + this.ScriptAst = + Parser.ParseInput( + this.Contents, + this.FilePath, + out scriptTokens, + out parseErrors); + } + else + { + this.ScriptAst = + Parser.ParseInput( + this.Contents, + out scriptTokens, + out parseErrors); + } + + this.ScriptTokens = scriptTokens; + } + catch (RuntimeException ex) + { + var parseError = + new ParseError( + null, + ex.ErrorRecord.FullyQualifiedErrorId, + ex.Message); + + parseErrors = new[] { parseError }; + this.ScriptTokens = new Token[0]; + this.ScriptAst = null; + } + + // Translate parse errors into syntax markers + this.DiagnosticMarkers = + parseErrors + .Select(ScriptFileMarker.FromParseError) + .ToList(); + + // Untitled files have no directory + // Discussed in https://github.com/PowerShell/PowerShellEditorServices/pull/815. + // Rather than working hard to enable things for untitled files like a phantom directory, + // users should save the file. + if (IsUntitledPath(this.FilePath)) + { + // Need to initialize the ReferencedFiles property to an empty array. + this.ReferencedFiles = new string[0]; + return; + } + + // Get all dot sourced referenced files and store them + //this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath)); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs new file mode 100644 index 000000000..6da4086ed --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs @@ -0,0 +1,188 @@ +// +// 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.Management.Automation; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains details for a code correction which can be applied from a ScriptFileMarker. + /// + public class MarkerCorrection + { + /// + /// Gets or sets the display name of the code correction. + /// + public string Name { get; set; } + + /// + /// Gets or sets the list of ScriptRegions that define the edits to be made by the correction. + /// + public ScriptRegion[] Edits { get; set; } + } + + /// + /// Defines the message level of a script file marker. + /// + public enum ScriptFileMarkerLevel + { + ///  +        /// Information: This warning is trivial, but may be useful. They are recommended by PowerShell best practice. +        ///  +        Information = 0, +        ///  +        /// WARNING: This warning may cause a problem or does not follow PowerShell's recommended guidelines. +        ///  +        Warning = 1, +        ///  +        /// ERROR: This warning is likely to cause a problem or does not follow PowerShell's required guidelines. +        ///  +        Error = 2, +        ///  +        /// ERROR: This diagnostic is caused by an actual parsing error, and is generated only by the engine. +        ///  +        ParseError = 3 + }; + + /// + /// Contains details about a marker that should be displayed + /// for the a script file. The marker information could come + /// from syntax parsing or semantic analysis of the script. + /// + public class ScriptFileMarker + { + #region Properties + + /// + /// Gets or sets the marker's message string. + /// + public string Message { get; set; } + + /// + /// Gets or sets the ruleName associated with this marker. + /// + public string RuleName { get; set; } + + /// + /// Gets or sets the marker's message level. + /// + public ScriptFileMarkerLevel Level { get; set; } + + /// + /// Gets or sets the ScriptRegion where the marker should appear. + /// + public ScriptRegion ScriptRegion { get; set; } + + /// + /// Gets or sets an optional code correction that can be applied based on this marker. + /// + public MarkerCorrection Correction { get; set; } + + /// + /// Gets or sets the name of the marker's source like "PowerShell" + /// or "PSScriptAnalyzer". + /// + public string Source { get; set; } + + #endregion + + #region Public Methods + + internal static ScriptFileMarker FromParseError( + ParseError parseError) + { + Validate.IsNotNull("parseError", parseError); + + return new ScriptFileMarker + { + Message = parseError.Message, + Level = ScriptFileMarkerLevel.Error, + ScriptRegion = ScriptRegion.Create(parseError.Extent), + Source = "PowerShell" + }; + } + private static string GetIfExistsString(PSObject psobj, string memberName) + { + if (psobj.Members.Match(memberName).Count > 0) + { + return psobj.Members[memberName].Value != null ? (string)psobj.Members[memberName].Value : ""; + } + else + { + return ""; + } + } + + internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) + { + Validate.IsNotNull("psObject", psObject); + MarkerCorrection correction = null; + + // make sure psobject is of type DiagnosticRecord + if (!psObject.TypeNames.Contains( + "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord", + StringComparer.OrdinalIgnoreCase)) + { + throw new ArgumentException("Input PSObject must of DiagnosticRecord type."); + } + + // casting psobject to dynamic allows us to access + // the diagnostic record's properties directly i.e. . + // without having to go through PSObject's Members property. + var diagnosticRecord = psObject as dynamic; + + if (diagnosticRecord.SuggestedCorrections != null) + { + var suggestedCorrections = diagnosticRecord.SuggestedCorrections as dynamic; + List editRegions = new List(); + string correctionMessage = null; + foreach (var suggestedCorrection in suggestedCorrections) + { + editRegions.Add(new ScriptRegion + { + File = diagnosticRecord.ScriptPath, + Text = suggestedCorrection.Text, + StartLineNumber = suggestedCorrection.StartLineNumber, + StartColumnNumber = suggestedCorrection.StartColumnNumber, + EndLineNumber = suggestedCorrection.EndLineNumber, + EndColumnNumber = suggestedCorrection.EndColumnNumber + }); + correctionMessage = suggestedCorrection.Description; + } + + correction = new MarkerCorrection + { + Name = correctionMessage == null ? diagnosticRecord.Message : correctionMessage, + Edits = editRegions.ToArray() + }; + } + + string severity = diagnosticRecord.Severity.ToString(); + if (!Enum.TryParse(severity, out ScriptFileMarkerLevel level)) + { + throw new ArgumentException( + $"The provided DiagnosticSeverity value '{severity}' is unknown.", + "diagnosticSeverity"); + } + + return new ScriptFileMarker + { + Message = $"{diagnosticRecord.Message as string}", + RuleName = $"{diagnosticRecord.RuleName as string}", + Level = level, + ScriptRegion = ScriptRegion.Create(diagnosticRecord.Extent as IScriptExtent), + Correction = correction, + Source = "PSScriptAnalyzer" + }; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs new file mode 100644 index 000000000..5717b1382 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs @@ -0,0 +1,112 @@ +// +// 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.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains details about a specific region of text in script file. + /// + public sealed class ScriptRegion : IScriptExtent + { + #region Properties + + /// + /// Gets the file path of the script file in which this region is contained. + /// + public string File { get; set; } + + /// + /// Gets or sets the text that is contained within the region. + /// + public string Text { get; set; } + + /// + /// Gets or sets the starting line number of the region. + /// + public int StartLineNumber { get; set; } + + /// + /// Gets or sets the starting column number of the region. + /// + public int StartColumnNumber { get; set; } + + /// + /// Gets or sets the starting file offset of the region. + /// + public int StartOffset { get; set; } + + /// + /// Gets or sets the ending line number of the region. + /// + public int EndLineNumber { get; set; } + + /// + /// Gets or sets the ending column number of the region. + /// + public int EndColumnNumber { get; set; } + + /// + /// Gets or sets the ending file offset of the region. + /// + public int EndOffset { get; set; } + + /// + /// Gets the starting IScriptPosition in the script. + /// (Currently unimplemented.) + /// + IScriptPosition IScriptExtent.StartScriptPosition => throw new NotImplementedException(); + + /// + /// Gets the ending IScriptPosition in the script. + /// (Currently unimplemented.) + /// + IScriptPosition IScriptExtent.EndScriptPosition => throw new NotImplementedException(); + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ScriptRegion class from an + /// instance of an IScriptExtent implementation. + /// + /// + /// The IScriptExtent to copy into the ScriptRegion. + /// + /// + /// A new ScriptRegion instance with the same details as the IScriptExtent. + /// + public static ScriptRegion Create(IScriptExtent scriptExtent) + { + // IScriptExtent throws an ArgumentOutOfRange exception if Text is null + string scriptExtentText; + try + { + scriptExtentText = scriptExtent.Text; + } + catch (ArgumentOutOfRangeException) + { + scriptExtentText = string.Empty; + } + + return new ScriptRegion + { + File = scriptExtent.File, + Text = scriptExtentText, + StartLineNumber = scriptExtent.StartLineNumber, + StartColumnNumber = scriptExtent.StartColumnNumber, + StartOffset = scriptExtent.StartOffset, + EndLineNumber = scriptExtent.EndLineNumber, + EndColumnNumber = scriptExtent.EndColumnNumber, + EndOffset = scriptExtent.EndOffset + }; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs new file mode 100644 index 000000000..1ed26a7b2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs @@ -0,0 +1,694 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Security; +using System.Text; +using System.Runtime.InteropServices; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Manages a "workspace" of script files that are open for a particular + /// editing session. Also helps to navigate references between ScriptFiles. + /// + public class Workspace + { + #region Private Fields + + // List of all file extensions considered PowerShell files in the .Net Core Framework. + private static readonly string[] s_psFileExtensionsCoreFramework = + { + ".ps1", + ".psm1", + ".psd1" + }; + + // .Net Core doesn't appear to use the same three letter pattern matching rule although the docs + // suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'. + // ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_ + private static readonly string[] s_psFileExtensionsFullFramework = + { + ".ps1", + ".psm1", + ".psd1", + ".ps1xml" + }; + + // An array of globs which includes everything. + private static readonly string[] s_psIncludeAllGlob = new [] + { + "**/*" + }; + + private ILogger logger; + private Version powerShellVersion; + private Dictionary workspaceFiles = new Dictionary(); + + #endregion + + #region Properties + + /// + /// Gets or sets the root path of the workspace. + /// + public string WorkspacePath { get; set; } + + /// + /// Gets or sets the default list of file globs to exclude during workspace searches. + /// + public List ExcludeFilesGlob { get; set; } + + /// + /// Gets or sets whether the workspace should follow symlinks in search operations. + /// + public bool FollowSymlinks { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the Workspace class. + /// + /// The version of PowerShell for which scripts will be parsed. + /// An ILogger implementation used for writing log messages. + public Workspace(Version powerShellVersion, ILogger logger) + { + this.powerShellVersion = powerShellVersion; + this.logger = logger; + this.ExcludeFilesGlob = new List(); + this.FollowSymlinks = true; + } + + #endregion + + #region Public Methods + + /// + /// Creates a new ScriptFile instance which is identified by the given file + /// path and initially contains the given buffer contents. + /// + /// The file path for which a buffer will be retrieved. + /// The initial buffer contents if there is not an existing ScriptFile for this path. + /// A ScriptFile instance for the specified path. + public ScriptFile CreateScriptFileFromFileBuffer(string filePath, string initialBuffer) + { + Validate.IsNotNullOrEmptyString("filePath", filePath); + + // Resolve the full file path + string resolvedFilePath = this.ResolveFilePath(filePath); + string keyName = resolvedFilePath.ToLower(); + + ScriptFile scriptFile = + new ScriptFile( + resolvedFilePath, + filePath, + initialBuffer, + this.powerShellVersion); + + this.workspaceFiles[keyName] = scriptFile; + + this.logger.LogDebug("Opened file as in-memory buffer: " + resolvedFilePath); + + return scriptFile; + } + + /// + /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. + /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using + /// instead. + /// + /// The file path at which the script resides. + /// + /// is not found. + /// + /// + /// contains a null or empty string. + /// + public ScriptFile GetFile(string filePath) + { + Validate.IsNotNullOrEmptyString("filePath", filePath); + + // Resolve the full file path + string resolvedFilePath = this.ResolveFilePath(filePath); + string keyName = resolvedFilePath.ToLower(); + + // Make sure the file isn't already loaded into the workspace + ScriptFile scriptFile = null; + if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) + { + // This method allows FileNotFoundException to bubble up + // if the file isn't found. + using (FileStream fileStream = new FileStream(resolvedFilePath, FileMode.Open, FileAccess.Read)) + using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8)) + { + scriptFile = + new ScriptFile( + resolvedFilePath, + filePath, + streamReader, + this.powerShellVersion); + + this.workspaceFiles.Add(keyName, scriptFile); + } + + this.logger.LogDebug("Opened file on disk: " + resolvedFilePath); + } + + return scriptFile; + } + + /// + /// Tries to get an open file in the workspace. Returns true if it succeeds, false otherwise. + /// + /// The file path at which the script resides. + /// The out parameter that will contain the ScriptFile object. + public bool TryGetFile(string filePath, out ScriptFile scriptFile) + { + var fileUri = new Uri(filePath); + + switch (fileUri.Scheme) + { + // List supported schemes here + case "file": + case "untitled": + break; + + default: + scriptFile = null; + return false; + } + + try + { + scriptFile = GetFile(filePath); + return true; + } + catch (Exception e) when ( + e is NotSupportedException || + e is FileNotFoundException || + e is DirectoryNotFoundException || + e is PathTooLongException || + e is IOException || + e is SecurityException || + e is UnauthorizedAccessException) + { + this.logger.LogWarning($"Failed to get file for {nameof(filePath)}: '{filePath}'", e); + scriptFile = null; + return false; + } + } + + /// + /// Gets a new ScriptFile instance which is identified by the given file path. + /// + /// The file path for which a buffer will be retrieved. + /// A ScriptFile instance if there is a buffer for the path, null otherwise. + public ScriptFile GetFileBuffer(string filePath) + { + return this.GetFileBuffer(filePath, null); + } + + /// + /// Gets a new ScriptFile instance which is identified by the given file + /// path and initially contains the given buffer contents. + /// + /// The file path for which a buffer will be retrieved. + /// The initial buffer contents if there is not an existing ScriptFile for this path. + /// A ScriptFile instance for the specified path. + public ScriptFile GetFileBuffer(string filePath, string initialBuffer) + { + Validate.IsNotNullOrEmptyString("filePath", filePath); + + // Resolve the full file path + string resolvedFilePath = this.ResolveFilePath(filePath); + string keyName = resolvedFilePath.ToLower(); + + // Make sure the file isn't already loaded into the workspace + ScriptFile scriptFile = null; + if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile) && initialBuffer != null) + { + scriptFile = + new ScriptFile( + resolvedFilePath, + filePath, + initialBuffer, + this.powerShellVersion); + + this.workspaceFiles.Add(keyName, scriptFile); + + this.logger.LogDebug("Opened file as in-memory buffer: " + resolvedFilePath); + } + + return scriptFile; + } + + /// + /// Gets an array of all opened ScriptFiles in the workspace. + /// + /// An array of all opened ScriptFiles in the workspace. + public ScriptFile[] GetOpenedFiles() + { + return workspaceFiles.Values.ToArray(); + } + + /// + /// Closes a currently open script file with the given file path. + /// + /// The file path at which the script resides. + public void CloseFile(ScriptFile scriptFile) + { + Validate.IsNotNull("scriptFile", scriptFile); + + this.workspaceFiles.Remove(scriptFile.Id); + } + + /// + /// Gets all file references by recursively searching + /// through referenced files in a scriptfile + /// + /// Contains the details and contents of an open script file + /// A scriptfile array where the first file + /// in the array is the "root file" of the search + public ScriptFile[] ExpandScriptReferences(ScriptFile scriptFile) + { + Dictionary referencedScriptFiles = new Dictionary(); + List expandedReferences = new List(); + + // add original file so it's not searched for, then find all file references + referencedScriptFiles.Add(scriptFile.Id, scriptFile); + RecursivelyFindReferences(scriptFile, referencedScriptFiles); + + // remove original file from referened file and add it as the first element of the + // expanded referenced list to maintain order so the original file is always first in the list + referencedScriptFiles.Remove(scriptFile.Id); + expandedReferences.Add(scriptFile); + + if (referencedScriptFiles.Count > 0) + { + expandedReferences.AddRange(referencedScriptFiles.Values); + } + + return expandedReferences.ToArray(); + } + + /// + /// Gets the workspace-relative path of the given file path. + /// + /// The original full file path. + /// A relative file path + public string GetRelativePath(string filePath) + { + string resolvedPath = filePath; + + if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(this.WorkspacePath)) + { + Uri workspaceUri = new Uri(this.WorkspacePath); + Uri fileUri = new Uri(filePath); + + resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString(); + + // Convert the directory separators if necessary + if (System.IO.Path.DirectorySeparatorChar == '\\') + { + resolvedPath = resolvedPath.Replace('/', '\\'); + } + } + + return resolvedPath; + } + + /// + /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values. + /// + /// An enumerator over the PowerShell files found in the workspace. + public IEnumerable EnumeratePSFiles() + { + return EnumeratePSFiles( + ExcludeFilesGlob.ToArray(), + s_psIncludeAllGlob, + maxDepth: 64, + ignoreReparsePoints: !FollowSymlinks + ); + } + + /// + /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner. + /// + /// An enumerator over the PowerShell files found in the workspace. + public IEnumerable EnumeratePSFiles( + string[] excludeGlobs, + string[] includeGlobs, + int maxDepth, + bool ignoreReparsePoints + ) + { + if (WorkspacePath == null || !Directory.Exists(WorkspacePath)) + { + yield break; + } + + var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher(); + foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } + foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } + + var fsFactory = new WorkspaceFileSystemWrapperFactory( + WorkspacePath, + maxDepth, + DotNetFacade.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, + ignoreReparsePoints, + logger + ); + var fileMatchResult = matcher.Execute(fsFactory.RootDirectory); + foreach (FilePatternMatch item in fileMatchResult.Files) + { + yield return Path.Combine(WorkspacePath, item.Path); + } + } + + #endregion + + #region Private Methods + /// + /// Recusrively searches through referencedFiles in scriptFiles + /// and builds a Dictonary of the file references + /// + /// Details an contents of "root" script file + /// A Dictionary of referenced script files + private void RecursivelyFindReferences( + ScriptFile scriptFile, + Dictionary referencedScriptFiles) + { + // Get the base path of the current script for use in resolving relative paths + string baseFilePath = + GetBaseFilePath( + scriptFile.FilePath); + + foreach (string referencedFileName in scriptFile.ReferencedFiles) + { + string resolvedScriptPath = + this.ResolveRelativeScriptPath( + baseFilePath, + referencedFileName); + + // If there was an error resolving the string, skip this reference + if (resolvedScriptPath == null) + { + continue; + } + + this.logger.LogDebug( + string.Format( + "Resolved relative path '{0}' to '{1}'", + referencedFileName, + resolvedScriptPath)); + + // Get the referenced file if it's not already in referencedScriptFiles + if (this.TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) + { + // Normalize the resolved script path and add it to the + // referenced files list if it isn't there already + resolvedScriptPath = resolvedScriptPath.ToLower(); + if (!referencedScriptFiles.ContainsKey(resolvedScriptPath)) + { + referencedScriptFiles.Add(resolvedScriptPath, referencedFile); + RecursivelyFindReferences(referencedFile, referencedScriptFiles); + } + } + } + } + + internal string ResolveFilePath(string filePath) + { + if (!IsPathInMemory(filePath)) + { + if (filePath.StartsWith(@"file://")) + { + filePath = Workspace.UnescapeDriveColon(filePath); + // Client sent the path in URI format, extract the local path + filePath = new Uri(filePath).LocalPath; + } + + // Clients could specify paths with escaped space, [ and ] characters which .NET APIs + // will not handle. These paths will get appropriately escaped just before being passed + // into the PowerShell engine. + //filePath = PowerShellContext.UnescapeWildcardEscapedPath(filePath); + + // Get the absolute file path + filePath = Path.GetFullPath(filePath); + } + + this.logger.LogDebug("Resolved path: " + filePath); + + return filePath; + } + + internal static bool IsPathInMemory(string filePath) + { + bool isInMemory = false; + + // In cases where a "virtual" file is displayed in the editor, + // we need to treat the file differently than one that exists + // on disk. A virtual file could be something like a diff + // view of the current file or an untitled file. + try + { + // File system absoulute paths will have a URI scheme of file:. + // Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile. + var uri = new Uri(filePath); + isInMemory = !uri.IsFile; + } + catch (UriFormatException) + { + // Relative file paths cause a UriFormatException. + // In this case, fallback to using Path.GetFullPath(). + try + { + Path.GetFullPath(filePath); + } + catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException) + { + isInMemory = true; + } + catch (PathTooLongException) + { + // If we ever get here, it should be an actual file so, not in memory + } + } + + return isInMemory; + } + + private string GetBaseFilePath(string filePath) + { + if (IsPathInMemory(filePath)) + { + // If the file is in memory, use the workspace path + return this.WorkspacePath; + } + + if (!Path.IsPathRooted(filePath)) + { + // TODO: Assert instead? + throw new InvalidOperationException( + string.Format( + "Must provide a full path for originalScriptPath: {0}", + filePath)); + } + + // Get the directory of the file path + return Path.GetDirectoryName(filePath); + } + + internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath) + { + string combinedPath = null; + Exception resolveException = null; + + try + { + // If the path is already absolute there's no need to resolve it relatively + // to the baseFilePath. + if (Path.IsPathRooted(relativePath)) + { + return relativePath; + } + + // Get the directory of the original script file, combine it + // with the given path and then resolve the absolute file path. + combinedPath = + Path.GetFullPath( + Path.Combine( + baseFilePath, + relativePath)); + } + catch (NotSupportedException e) + { + // Occurs if the path is incorrectly formatted for any reason. One + // instance where this occurred is when a user had curly double-quote + // characters in their source instead of normal double-quotes. + resolveException = e; + } + catch (ArgumentException e) + { + // Occurs if the path contains invalid characters, specifically those + // listed in System.IO.Path.InvalidPathChars. + resolveException = e; + } + + if (resolveException != null) + { + this.logger.LogError( + $"Could not resolve relative script path\r\n" + + $" baseFilePath = {baseFilePath}\r\n " + + $" relativePath = {relativePath}\r\n\r\n" + + $"{resolveException.ToString()}"); + } + + return combinedPath; + } + + /// + /// Takes a file-scheme URI with an escaped colon after the drive letter and unescapes only the colon. + /// VSCode sends escaped colons after drive letters, but System.Uri expects unescaped. + /// + /// The fully-escaped file-scheme URI string. + /// A file-scheme URI string with the drive colon unescaped. + private static string UnescapeDriveColon(string fileUri) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return fileUri; + } + + // Check here that we have something like "file:///C%3A/" as a prefix (caller must check the file:// part) + if (!(fileUri[7] == '/' && + char.IsLetter(fileUri[8]) && + fileUri[9] == '%' && + fileUri[10] == '3' && + fileUri[11] == 'A' && + fileUri[12] == '/')) + { + return fileUri; + } + + var sb = new StringBuilder(fileUri.Length - 2); // We lost "%3A" and gained ":", so length - 2 + sb.Append("file:///"); + sb.Append(fileUri[8]); // The drive letter + sb.Append(':'); + sb.Append(fileUri.Substring(12)); // The rest of the URI after the colon + + return sb.ToString(); + } + + /// + /// Converts a file system path into a DocumentUri required by Language Server Protocol. + /// + /// + /// When sending a document path to a LSP client, the path must be provided as a + /// DocumentUri in order to features like the Problems window or peek definition + /// to be able to open the specified file. + /// + /// + /// A file system path. Note: if the path is already a DocumentUri, it will be returned unmodified. + /// + /// The file system path encoded as a DocumentUri. + public static string ConvertPathToDocumentUri(string path) + { + const string fileUriPrefix = "file:"; + const string untitledUriPrefix = "untitled:"; + + // If path is already in document uri form, there is nothing to convert. + if (path.StartsWith(untitledUriPrefix, StringComparison.Ordinal) || + path.StartsWith(fileUriPrefix, StringComparison.Ordinal)) + { + return path; + } + + string escapedPath = Uri.EscapeDataString(path); + + // Max capacity of the StringBuilder will be the current escapedPath length + // plus extra chars for file:///. + var docUriStrBld = new StringBuilder(escapedPath.Length + fileUriPrefix.Length + 3); + docUriStrBld.Append(fileUriPrefix).Append("//"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // VSCode file URIs on Windows need the drive letter to be lowercase. Search the + // original path for colon since a char search (no string culture involved) is + // faster than a string search. If found, then lowercase the associated drive letter. + if (path.Contains(':')) + { + // A valid, drive-letter based path converted to URI form needs to be prefixed + // with a / to indicate the path is an absolute path. + docUriStrBld.Append("/"); + int prefixLen = docUriStrBld.Length; + + docUriStrBld.Append(escapedPath); + + // Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \. + docUriStrBld.Replace("%5C", "/"); + + // Find the first colon after the "file:///" prefix, skipping the first char after + // the prefix since a Windows path cannot start with a colon. End the check at + // less than docUriStrBld.Length - 2 since we need to look-ahead two characters. + for (int i = prefixLen + 1; i < docUriStrBld.Length - 2; i++) + { + if ((docUriStrBld[i] == '%') && (docUriStrBld[i + 1] == '3') && (docUriStrBld[i + 2] == 'A')) + { + int driveLetterIndex = i - 1; + char driveLetter = char.ToLowerInvariant(docUriStrBld[driveLetterIndex]); + docUriStrBld.Replace(docUriStrBld[driveLetterIndex], driveLetter, driveLetterIndex, 1); + break; + } + } + } + else + { + // This is a Windows path without a drive specifier, must be either a relative or UNC path. + int prefixLen = docUriStrBld.Length; + + docUriStrBld.Append(escapedPath); + + // Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \. + docUriStrBld.Replace("%5C", "/"); + + // The proper URI form for a UNC path is file://server/share. In the case of a UNC + // path, remove the path's leading // because the file:// prefix already provides it. + if ((docUriStrBld.Length > prefixLen + 1) && + (docUriStrBld[prefixLen] == '/') && + (docUriStrBld[prefixLen + 1] == '/')) + { + docUriStrBld.Remove(prefixLen, 2); + } + } + } + else + { + // On non-Windows systems, append the escapedPath and undo the over-aggressive + // escaping of / done by Uri.EscapeDataString. + docUriStrBld.Append(escapedPath).Replace("%2F", "/"); + } + + if (!DotNetFacade.IsNetCore) + { + // ' is not encoded by Uri.EscapeDataString in Windows PowerShell 5.x. + // This is apparently a difference between .NET Framework and .NET Core. + docUriStrBld.Replace("'", "%27"); + } + + return docUriStrBld.ToString(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs new file mode 100644 index 000000000..116f29b38 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs @@ -0,0 +1,381 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + + /// + /// A FileSystem wrapper class which only returns files and directories that the consumer is interested in, + /// with a maximum recursion depth and silently ignores most file system errors. Typically this is used by the + /// Microsoft.Extensions.FileSystemGlobbing library. + /// + public class WorkspaceFileSystemWrapperFactory + { + private readonly DirectoryInfoBase _rootDirectory; + private readonly string[] _allowedExtensions; + private readonly bool _ignoreReparsePoints; + + /// + /// Gets the maximum depth of the directories that will be searched + /// + internal int MaxRecursionDepth { get; } + + /// + /// Gets the logging facility + /// + internal ILogger Logger { get; } + + /// + /// Gets the directory where the factory is rooted. Only files and directories at this level, or deeper, will be visible + /// by the wrapper + /// + public DirectoryInfoBase RootDirectory + { + get { return _rootDirectory; } + } + + /// + /// Creates a new FileWrapper Factory + /// + /// The path to the root directory for the factory. + /// The maximum directory depth. + /// An array of file extensions that will be visible from the factory. For example [".ps1", ".psm1"] + /// Whether objects which are Reparse Points should be ignored. https://docs.microsoft.com/en-us/windows/desktop/fileio/reparse-points + /// An ILogger implementation used for writing log messages. + public WorkspaceFileSystemWrapperFactory(String rootPath, int recursionDepthLimit, string[] allowedExtensions, bool ignoreReparsePoints, ILogger logger) + { + MaxRecursionDepth = recursionDepthLimit; + _rootDirectory = new WorkspaceFileSystemDirectoryWrapper(this, new DirectoryInfo(rootPath), 0); + _allowedExtensions = allowedExtensions; + _ignoreReparsePoints = ignoreReparsePoints; + Logger = logger; + } + + /// + /// Creates a wrapped object from . + /// + internal DirectoryInfoBase CreateDirectoryInfoWrapper(DirectoryInfo dirInfo, int depth) => + new WorkspaceFileSystemDirectoryWrapper(this, dirInfo, depth >= 0 ? depth : 0); + + /// + /// Creates a wrapped object from . + /// + internal FileInfoBase CreateFileInfoWrapper(FileInfo fileInfo, int depth) => + new WorkspaceFileSystemFileInfoWrapper(this, fileInfo, depth >= 0 ? depth : 0); + + /// + /// Enumerates all objects in the specified directory and ignores most errors + /// + internal IEnumerable SafeEnumerateFileSystemInfos(DirectoryInfo dirInfo) + { + // Find the subdirectories + string[] subDirs; + try + { + subDirs = Directory.GetDirectories(dirInfo.FullName, "*", SearchOption.TopDirectoryOnly); + } + catch (DirectoryNotFoundException e) + { + Logger.LogHandledException( + $"Could not enumerate directories in the path '{dirInfo.FullName}' due to it being an invalid path", + e); + + yield break; + } + catch (PathTooLongException e) + { + Logger.LogHandledException( + $"Could not enumerate directories in the path '{dirInfo.FullName}' due to the path being too long", + e); + + yield break; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + Logger.LogHandledException( + $"Could not enumerate directories in the path '{dirInfo.FullName}' due to the path not being accessible", + e); + + yield break; + } + catch (Exception e) + { + Logger.LogHandledException( + $"Could not enumerate directories in the path '{dirInfo.FullName}' due to an exception", + e); + + yield break; + } + foreach (string dirPath in subDirs) + { + var subDirInfo = new DirectoryInfo(dirPath); + if (_ignoreReparsePoints && (subDirInfo.Attributes & FileAttributes.ReparsePoint) != 0) { continue; } + yield return subDirInfo; + } + + // Find the files + string[] filePaths; + try + { + filePaths = Directory.GetFiles(dirInfo.FullName, "*", SearchOption.TopDirectoryOnly); + } + catch (DirectoryNotFoundException e) + { + Logger.LogHandledException( + $"Could not enumerate files in the path '{dirInfo.FullName}' due to it being an invalid path", + e); + + yield break; + } + catch (PathTooLongException e) + { + Logger.LogHandledException( + $"Could not enumerate files in the path '{dirInfo.FullName}' due to the path being too long", + e); + + yield break; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + Logger.LogHandledException( + $"Could not enumerate files in the path '{dirInfo.FullName}' due to the path not being accessible", + e); + + yield break; + } + catch (Exception e) + { + Logger.LogHandledException( + $"Could not enumerate files in the path '{dirInfo.FullName}' due to an exception", + e); + + yield break; + } + foreach (string filePath in filePaths) + { + var fileInfo = new FileInfo(filePath); + if (_allowedExtensions == null || _allowedExtensions.Length == 0) { yield return fileInfo; continue; } + if (_ignoreReparsePoints && (fileInfo.Attributes & FileAttributes.ReparsePoint) != 0) { continue; } + foreach (string extension in _allowedExtensions) + { + if (fileInfo.Extension == extension) { yield return fileInfo; break; } + } + } + } + } + + /// + /// Wraps an instance of and provides implementation of + /// . + /// Based on https://github.com/aspnet/Extensions/blob/c087cadf1dfdbd2b8785ef764e5ef58a1a7e5ed0/src/FileSystemGlobbing/src/Abstractions/DirectoryInfoWrapper.cs + /// + public class WorkspaceFileSystemDirectoryWrapper : DirectoryInfoBase + { + private readonly DirectoryInfo _concreteDirectoryInfo; + private readonly bool _isParentPath; + private readonly WorkspaceFileSystemWrapperFactory _fsWrapperFactory; + private readonly int _depth; + + /// + /// Initializes an instance of . + /// + public WorkspaceFileSystemDirectoryWrapper(WorkspaceFileSystemWrapperFactory factory, DirectoryInfo directoryInfo, int depth) + { + _concreteDirectoryInfo = directoryInfo; + _isParentPath = (depth == 0); + _fsWrapperFactory = factory; + _depth = depth; + } + + /// + public override IEnumerable EnumerateFileSystemInfos() + { + if (!_concreteDirectoryInfo.Exists || _depth >= _fsWrapperFactory.MaxRecursionDepth) { yield break; } + foreach (FileSystemInfo fileSystemInfo in _fsWrapperFactory.SafeEnumerateFileSystemInfos(_concreteDirectoryInfo)) + { + switch (fileSystemInfo) + { + case DirectoryInfo dirInfo: + yield return _fsWrapperFactory.CreateDirectoryInfoWrapper(dirInfo, _depth + 1); + break; + case FileInfo fileInfo: + yield return _fsWrapperFactory.CreateFileInfoWrapper(fileInfo, _depth); + break; + default: + // We should NEVER get here, but if we do just continue on + break; + } + } + } + + /// + /// Returns an instance of that represents a subdirectory. + /// + /// + /// If equals '..', this returns the parent directory. + /// + /// The directory name. + /// The directory + public override DirectoryInfoBase GetDirectory(string name) + { + bool isParentPath = string.Equals(name, "..", StringComparison.Ordinal); + + if (isParentPath) { return ParentDirectory; } + + var dirs = _concreteDirectoryInfo.GetDirectories(name); + + if (dirs.Length == 1) { return _fsWrapperFactory.CreateDirectoryInfoWrapper(dirs[0], _depth + 1); } + if (dirs.Length == 0) { return null; } + // This shouldn't happen. The parameter name isn't supposed to contain wild card. + throw new InvalidOperationException( + string.Format( + System.Globalization.CultureInfo.CurrentCulture, + "More than one sub directories are found under {0} with name {1}.", + _concreteDirectoryInfo.FullName, name)); + } + + /// + public override FileInfoBase GetFile(string name) => _fsWrapperFactory.CreateFileInfoWrapper(new FileInfo(Path.Combine(_concreteDirectoryInfo.FullName, name)), _depth); + + /// + public override string Name => _isParentPath ? ".." : _concreteDirectoryInfo.Name; + + /// + /// Returns the full path to the directory. + /// + public override string FullName => _concreteDirectoryInfo.FullName; + + /// + /// Safely calculates the parent of this directory, swallowing most errors. + /// + private DirectoryInfoBase SafeParentDirectory() + { + try + { + return _fsWrapperFactory.CreateDirectoryInfoWrapper(_concreteDirectoryInfo.Parent, _depth - 1); + } + catch (DirectoryNotFoundException e) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to it being an invalid path", + e); + } + catch (PathTooLongException e) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to the path being too long", + e); + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to the path not being accessible", + e); + } + catch (Exception e) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to an exception", + e); + } + return null; + } + + /// + /// Returns the parent directory. (Overrides ). + /// + public override DirectoryInfoBase ParentDirectory + { + get + { + return SafeParentDirectory(); + } + } + } + + /// + /// Wraps an instance of to provide implementation of . + /// + public class WorkspaceFileSystemFileInfoWrapper : FileInfoBase + { + private readonly FileInfo _concreteFileInfo; + private readonly WorkspaceFileSystemWrapperFactory _fsWrapperFactory; + private readonly int _depth; + + /// + /// Initializes instance of to wrap the specified object . + /// + public WorkspaceFileSystemFileInfoWrapper(WorkspaceFileSystemWrapperFactory factory, FileInfo fileInfo, int depth) + { + _fsWrapperFactory = factory; + _concreteFileInfo = fileInfo; + _depth = depth; + } + + /// + /// The file name. (Overrides ). + /// + public override string Name => _concreteFileInfo.Name; + + /// + /// The full path of the file. (Overrides ). + /// + public override string FullName => _concreteFileInfo.FullName; + + /// + /// Safely calculates the parent of this file, swallowing most errors. + /// + private DirectoryInfoBase SafeParentDirectory() + { + try + { + return _fsWrapperFactory.CreateDirectoryInfoWrapper(_concreteFileInfo.Directory, _depth); + } + catch (DirectoryNotFoundException e) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteFileInfo.FullName}' due to it being an invalid path", + e); + } + catch (PathTooLongException e) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteFileInfo.FullName}' due to the path being too long", + e); + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteFileInfo.FullName}' due to the path not being accessible", + e); + } + catch (Exception e) + { + _fsWrapperFactory.Logger.LogHandledException( + $"Could not get parent of '{_concreteFileInfo.FullName}' due to an exception", + e); + } + return null; + } + + /// + /// The directory containing the file. (Overrides ). + /// + public override DirectoryInfoBase ParentDirectory + { + get + { + return SafeParentDirectory(); + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Utility/Utility.cs b/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs similarity index 82% rename from src/PowerShellEditorServices.Engine/Utility/Utility.cs rename to src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs index 14233c528..78bc22a08 100644 --- a/src/PowerShellEditorServices.Engine/Utility/Utility.cs +++ b/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs @@ -1,12 +1,12 @@ using System; using System.Runtime.InteropServices; -namespace Microsoft.PowerShell.EditorServices.Engine +namespace Microsoft.PowerShell.EditorServices { /// /// General purpose common utilities to prevent reimplementation. /// - internal static class Utility + internal static class DotNetFacade { /// /// True if we are running on .NET Core, false otherwise. diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs new file mode 100644 index 000000000..768b26f07 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -0,0 +1,12 @@ +using System; + +namespace PowerShellEditorServices.Engine.Utility +{ + internal class PathUtils + { + public string WildcardUnescapePath(string path) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Utility/Validate.cs b/src/PowerShellEditorServices.Engine/Utility/Validate.cs index a8bb12e1c..a595bd6c9 100644 --- a/src/PowerShellEditorServices.Engine/Utility/Validate.cs +++ b/src/PowerShellEditorServices.Engine/Utility/Validate.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.PowerShell.EditorServices.Engine +namespace Microsoft.PowerShell.EditorServices { /// /// Provides common validation methods to simplify method diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index e063877be..f6a51b22f 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -180,6 +180,20 @@ public ScriptFile GetFile(string filePath) /// The out parameter that will contain the ScriptFile object. public bool TryGetFile(string filePath, out ScriptFile scriptFile) { + var fileUri = new Uri(filePath); + + switch (fileUri.Scheme) + { + // List supported schemes here + case "file": + case "untitled": + break; + + default: + scriptFile = null; + return false; + } + try { if (filePath.Contains(":/") // Quick heuristic to determine if we might have a URI From ea390975d1a3b9988549cfd8e6ff256fb49f629d Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 4 Jul 2019 00:19:19 -0700 Subject: [PATCH 06/47] add dummy workspace symbols handler --- PowerShellEditorServices.sln | 15 +++ .../Hosting/EditorServicesHost.cs | 6 +- .../LanguageServer/OmnisharpLanguageServer.cs | 8 +- .../Handlers/WorkspaceSymbolsHandler.cs | 125 ++++++++++++++++++ .../EditorServices.Integration.Tests.ps1 | 11 ++ 5 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index 6c3671bb0..fce19ffef 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -28,6 +28,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.VSCode", "src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj", "{3B38E8DA-8BFF-4264-AF16-47929E6398A3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Engine", "src\PowerShellEditorServices.Engine\PowerShellEditorServices.Engine.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +136,18 @@ Global {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x64.Build.0 = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.ActiveCfg = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x64.Build.0 = Debug|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x86.Build.0 = Debug|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|Any CPU.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x64.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -147,5 +161,6 @@ Global {F8A0946A-5D25-4651-8079-B8D5776916FB} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {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} EndGlobalSection EndGlobal diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index cf88fbcb6..93db81e19 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -12,6 +12,7 @@ using System.Management.Automation.Host; using System.Reflection; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -236,7 +237,10 @@ public void StartLanguageService( _logger.LogInformation("Starting language server"); - Task.Run(_languageServer.StartAsync); + Task.Factory.StartNew(() => _languageServer.StartAsync(), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); _logger.LogInformation( string.Format( diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index d0873fefa..171eb1978 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Logging; using OS = OmniSharp.Extensions.LanguageServer.Server; using System.Security.AccessControl; +using OmniSharp.Extensions.LanguageServer.Server; +using PowerShellEditorServices.Engine.Services.Workspace.Handlers; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -61,14 +63,14 @@ public async Task StartAsync() } options.Input = namedPipe; - options.Output = outNamedPipe != null - ? outNamedPipe - : namedPipe; + options.Output = outNamedPipe ?? namedPipe; options.LoggerFactory = _configuration.LoggerFactory; options.MinimumLogLevel = _configuration.MinimumLogLevel; options.Services = _configuration.Services; + options.WithHandler(); }); + _serverStart.SetResult(true); } diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs new file mode 100644 index 000000000..97848cd2c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +{ + public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler + { + private ILogger _logger; + + public WorkspaceSymbolsHandler() { + // _logger = logger; + } + + public object GetRegistrationOptions() + { + return null; + // throw new NotImplementedException(); + } + + public Task Handle(WorkspaceSymbolParams request, CancellationToken cancellationToken) + { + var symbols = new List(); + + // foreach (ScriptFile scriptFile in editorSession.Workspace.GetOpenedFiles()) + // { + // FindOccurrencesResult foundSymbols = + // editorSession.LanguageService.FindSymbolsInFile( + // scriptFile); + + // // TODO: Need to compute a relative path that is based on common path for all workspace files + // string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + // if (foundSymbols != null) + // { + // foreach (SymbolReference foundOccurrence in foundSymbols.FoundOccurrences) + // { + // if (!IsQueryMatch(request.Query, foundOccurrence.SymbolName)) + // { + // continue; + // } + + // var location = new Location + // { + // Uri = GetFileUri(foundOccurrence.FilePath), + // Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) + // }; + + // symbols.Add(new SymbolInformation + // { + // ContainerName = containerName, + // Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, + // Location = location, + // Name = GetDecoratedSymbolName(foundOccurrence) + // }); + // } + // } + // } + // _logger.LogWarning("HELLO FOOLS"); + + return Task.FromResult(new SymbolInformationContainer(symbols)); + } + + public void SetCapability(WorkspaceSymbolCapability capability) + { + // throw new NotImplementedException(); + } + + #region private Methods + + // private bool IsQueryMatch(string query, string symbolName) + // { + // return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + // } + + // private static string GetFileUri(string filePath) + // { + // // If the file isn't untitled, return a URI-style path + // return + // !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") + // ? new Uri("file://" + filePath).AbsoluteUri + // : filePath; + // } + + // private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + // { + // return new Range + // { + // Start = new Position + // { + // Line = scriptRegion.StartLineNumber - 1, + // Character = scriptRegion.StartColumnNumber - 1 + // }, + // End = new Position + // { + // Line = scriptRegion.EndLineNumber - 1, + // Character = scriptRegion.EndColumnNumber - 1 + // } + // }; + // } + + // private static string GetDecoratedSymbolName(SymbolReference symbolReference) + // { + // string name = symbolReference.SymbolName; + + // if (symbolReference.SymbolType == SymbolType.Configuration || + // symbolReference.SymbolType == SymbolType.Function || + // symbolReference.SymbolType == SymbolType.Workflow) + // { + // name += " { }"; + // } + + // return name; + // } + + #endregion + } +} diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index a025320b5..afdf0bb9b 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -78,6 +78,17 @@ Describe "Loading and running PowerShellEditorServices" { #ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } + It "Can handle WorkspaceSymbol request" { + $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 + CheckErrorResponse -Response $response + + # ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client From 0c83a4368589a54b74f9ee1f2ab95558f3a55323 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 8 Jul 2019 15:02:47 -0700 Subject: [PATCH 07/47] use LoggerFactory --- .../Hosting/EditorServicesHost.cs | 14 ++++++-------- .../Workspace/Handlers/WorkspaceSymbolsHandler.cs | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 93db81e19..19fb247f3 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -4,9 +4,7 @@ // using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; @@ -17,7 +15,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; -using Serilog.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -65,7 +62,9 @@ public class EditorServicesHost private ILanguageServer _languageServer; - private Extensions.Logging.ILogger _logger; + private readonly Extensions.Logging.ILogger _logger; + + private readonly ILoggerFactory _factory; #endregion @@ -131,10 +130,8 @@ public EditorServicesHost( Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() .WriteTo.Console() .CreateLogger(); - - _logger = new SerilogLoggerProvider().CreateLogger(nameof(EditorServicesHost)); - - _serviceCollection.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); + _factory = new LoggerFactory().AddSerilog(Log.Logger); + _logger = _factory.CreateLogger(); _hostDetails = hostDetails; @@ -232,6 +229,7 @@ public void StartLanguageService( { NamedPipeName = config.InOutPipeName ?? config.InPipeName, OutNamedPipeName = config.OutPipeName, + LoggerFactory = _factory } .BuildLanguageServer(); diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 97848cd2c..460df9337 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -13,10 +13,10 @@ namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers { public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler { - private ILogger _logger; + private readonly ILogger _logger; - public WorkspaceSymbolsHandler() { - // _logger = logger; + public WorkspaceSymbolsHandler(ILoggerFactory loggerFactory) { + _logger = loggerFactory.CreateLogger(); } public object GetRegistrationOptions() @@ -63,7 +63,7 @@ public Task Handle(WorkspaceSymbolParams request, Ca // } // } // } - // _logger.LogWarning("HELLO FOOLS"); + _logger.LogWarning("Logging in a handler works now."); return Task.FromResult(new SymbolInformationContainer(symbols)); } From 5d744ba0da6a9aacdb01e3497f2a2f822d7ba708 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 8 Jul 2019 17:39:19 -0700 Subject: [PATCH 08/47] A working WorkspaceSymbolsHandler --- .../Hosting/EditorServicesHost.cs | 7 +- .../LanguageServer/OmnisharpLanguageServer.cs | 2 +- .../Services/Symbols/FindSymbolVisitor.cs | 145 ++++++++++++ .../Services/Symbols/FindSymbolsVisitor.cs | 148 ++++++++++++ .../Services/Symbols/FindSymbolsVisitor2.cs | 80 +++++++ .../Symbols/IDocumentSymbolProvider.cs | 24 ++ .../Services/Symbols/IDocumentSymbols.cs | 32 +++ .../Symbols/PesterDocumentSymbolProvider.cs | 223 ++++++++++++++++++ .../Symbols/PsdDocumentSymbolProvider.cs | 88 +++++++ .../Symbols/ScriptDocumentSymbolProvider.cs | 76 ++++++ .../Services/Symbols/ScriptExtent.cs | 109 +++++++++ .../Services/Symbols/SymbolReference.cs | 89 +++++++ .../Services/Symbols/SymbolType.cs | 48 ++++ .../Services/Symbols/SymbolsService.cs | 72 ++++++ .../Services/TextDocument/ScriptFile.cs | 4 +- .../Handlers/WorkspaceSymbolsHandler.cs | 161 ++++++------- .../WorkspaceFileSystemWrapper.cs | 0 .../WorkspaceService.cs} | 45 ++-- .../Utility/DotNetFacade.cs | 16 -- .../Utility/PathUtils.cs | 21 ++ .../Utility/VersionUtils.cs | 50 ++++ 21 files changed, 1316 insertions(+), 124 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs rename src/PowerShellEditorServices.Engine/Services/{TextDocument => Workspace}/WorkspaceFileSystemWrapper.cs (100%) rename src/PowerShellEditorServices.Engine/Services/{TextDocument/Workspace.cs => Workspace/WorkspaceService.cs} (94%) delete mode 100644 src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 19fb247f3..13ba05f78 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -219,12 +219,15 @@ public void StartLanguageService( { while (System.Diagnostics.Debugger.IsAttached) { - Console.WriteLine($"{System.Diagnostics.Process.GetCurrentProcess().Id}"); - System.Threading.Thread.Sleep(2000); + Console.WriteLine($"{Process.GetCurrentProcess().Id}"); + Thread.Sleep(2000); } _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(); + _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) { NamedPipeName = config.InOutPipeName ?? config.InPipeName, diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 171eb1978..3b893746b 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -86,7 +86,7 @@ private static NamedPipeServerStream CreateNamedPipe( out NamedPipeServerStream outPipe) { // .NET Core implementation is simplest so try that first - if (DotNetFacade.IsNetCore) + if (VersionUtils.IsNetCore) { outPipe = outPipeName == null ? null diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs new file mode 100644 index 000000000..06bdf8235 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs @@ -0,0 +1,145 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find the the symbol at a specfic location in the AST + /// + internal class FindSymbolVisitor : AstVisitor + { + private int lineNumber; + private int columnNumber; + private bool includeFunctionDefinitions; + + public SymbolReference FoundSymbolReference { get; private set; } + + public FindSymbolVisitor( + int lineNumber, + int columnNumber, + bool includeFunctionDefinitions) + { + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + this.includeFunctionDefinitions = includeFunctionDefinitions; + } + + /// + /// Checks to see if this command ast is the symbol we are looking for. + /// + /// A CommandAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + Ast commandNameAst = commandAst.CommandElements[0]; + + if (this.IsPositionInExtent(commandNameAst.Extent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Function, + commandNameAst.Extent); + + return AstVisitAction.StopVisit; + } + + return base.VisitCommand(commandAst); + } + + /// + /// Checks to see if this function definition is the symbol we are looking for. + /// + /// A functionDefinitionAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + int startColumnNumber = 1; + + if (!this.includeFunctionDefinitions) + { + startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name) + 1; + } + + IScriptExtent nameExtent = new ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, + StartColumnNumber = startColumnNumber, + EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length + }; + + if (this.IsPositionInExtent(nameExtent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Function, + nameExtent); + + return AstVisitAction.StopVisit; + } + + return base.VisitFunctionDefinition(functionDefinitionAst); + } + + /// + /// Checks to see if this command parameter is the symbol we are looking for. + /// + /// A CommandParameterAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) + { + if (this.IsPositionInExtent(commandParameterAst.Extent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Parameter, + commandParameterAst.Extent); + return AstVisitAction.StopVisit; + } + return AstVisitAction.Continue; + } + + /// + /// Checks to see if this variable expression is the symbol we are looking for. + /// + /// A VariableExpressionAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if (this.IsPositionInExtent(variableExpressionAst.Extent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Variable, + variableExpressionAst.Extent); + + return AstVisitAction.StopVisit; + } + + return AstVisitAction.Continue; + } + + /// + /// Is the position of the given location is in the ast's extent + /// + /// The script extent of the element + /// True if the given position is in the range of the element's extent + private bool IsPositionInExtent(IScriptExtent extent) + { + return (extent.StartLineNumber == lineNumber && + extent.StartColumnNumber <= columnNumber && + extent.EndColumnNumber >= columnNumber); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs new file mode 100644 index 000000000..fd624c429 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs @@ -0,0 +1,148 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find all the symbols (function and class defs) in the AST. + /// + /// + /// Requires PowerShell v3 or higher + /// + internal class FindSymbolsVisitor : AstVisitor + { + public List SymbolReferences { get; private set; } + + public FindSymbolsVisitor() + { + this.SymbolReferences = new List(); + } + + /// + /// Adds each function definition as a + /// + /// A functionDefinitionAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + IScriptExtent nameExtent = new ScriptExtent() { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, + StartColumnNumber = functionDefinitionAst.Extent.StartColumnNumber, + EndColumnNumber = functionDefinitionAst.Extent.EndColumnNumber + }; + + SymbolType symbolType = + functionDefinitionAst.IsWorkflow ? + SymbolType.Workflow : SymbolType.Function; + + this.SymbolReferences.Add( + new SymbolReference( + symbolType, + nameExtent)); + + return AstVisitAction.Continue; + } + + /// + /// Checks to see if this variable expression is the symbol we are looking for. + /// + /// A VariableExpressionAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if (!IsAssignedAtScriptScope(variableExpressionAst)) + { + return AstVisitAction.Continue; + } + + this.SymbolReferences.Add( + new SymbolReference( + SymbolType.Variable, + variableExpressionAst.Extent)); + + return AstVisitAction.Continue; + } + + private bool IsAssignedAtScriptScope(VariableExpressionAst variableExpressionAst) + { + Ast parent = variableExpressionAst.Parent; + if (!(parent is AssignmentStatementAst)) + { + return false; + } + + parent = parent.Parent; + if (parent == null || parent.Parent == null || parent.Parent.Parent == null) + { + return true; + } + + return false; + } + } + + /// + /// Visitor to find all the keys in Hashtable AST + /// + internal class FindHashtableSymbolsVisitor : AstVisitor + { + /// + /// List of symbols (keys) found in the hashtable + /// + public List SymbolReferences { get; private set; } + + /// + /// Initializes a new instance of FindHashtableSymbolsVisitor class + /// + public FindHashtableSymbolsVisitor() + { + SymbolReferences = new List(); + } + + /// + /// Adds keys in the input hashtable to the symbol reference + /// + public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) + { + if (hashtableAst.KeyValuePairs == null) + { + return AstVisitAction.Continue; + } + + foreach (var kvp in hashtableAst.KeyValuePairs) + { + var keyStrConstExprAst = kvp.Item1 as StringConstantExpressionAst; + if (keyStrConstExprAst != null) + { + IScriptExtent nameExtent = new ScriptExtent() + { + Text = keyStrConstExprAst.Value, + StartLineNumber = kvp.Item1.Extent.StartLineNumber, + EndLineNumber = kvp.Item2.Extent.EndLineNumber, + StartColumnNumber = kvp.Item1.Extent.StartColumnNumber, + EndColumnNumber = kvp.Item2.Extent.EndColumnNumber + }; + + SymbolType symbolType = SymbolType.HashtableKey; + + this.SymbolReferences.Add( + new SymbolReference( + symbolType, + nameExtent)); + + } + } + + return AstVisitAction.Continue; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs new file mode 100644 index 000000000..03628ee3e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices +{ + // TODO: Restore this when we figure out how to support multiple + // PS versions in the new PSES-as-a-module world (issue #276) + + ///// + ///// The visitor used to find all the symbols (function and class defs) in the AST. + ///// + ///// + ///// Requires PowerShell v5 or higher + ///// + ///// + //internal class FindSymbolsVisitor2 : AstVisitor2 + //{ + // private FindSymbolsVisitor findSymbolsVisitor; + + // public List SymbolReferences + // { + // get + // { + // return this.findSymbolsVisitor.SymbolReferences; + // } + // } + + // public FindSymbolsVisitor2() + // { + // this.findSymbolsVisitor = new FindSymbolsVisitor(); + // } + + // /// + // /// Adds each function definition as a + // /// + // /// A functionDefinitionAst object in the script's AST + // /// A decision to stop searching if the right symbol was found, + // /// or a decision to continue if it wasn't found + // public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + // { + // return this.findSymbolsVisitor.VisitFunctionDefinition(functionDefinitionAst); + // } + + // /// + // /// Checks to see if this variable expression is the symbol we are looking for. + // /// + // /// A VariableExpressionAst object in the script's AST + // /// A decision to stop searching if the right symbol was found, + // /// or a decision to continue if it wasn't found + // public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + // { + // return this.findSymbolsVisitor.VisitVariableExpression(variableExpressionAst); + // } + + // public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) + // { + // IScriptExtent nameExtent = new ScriptExtent() + // { + // Text = configurationDefinitionAst.InstanceName.Extent.Text, + // StartLineNumber = configurationDefinitionAst.Extent.StartLineNumber, + // EndLineNumber = configurationDefinitionAst.Extent.EndLineNumber, + // StartColumnNumber = configurationDefinitionAst.Extent.StartColumnNumber, + // EndColumnNumber = configurationDefinitionAst.Extent.EndColumnNumber + // }; + + // this.findSymbolsVisitor.SymbolReferences.Add( + // new SymbolReference( + // SymbolType.Configuration, + // nameExtent)); + + // return AstVisitAction.Continue; + // } + //} +} + diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs new file mode 100644 index 000000000..d971b9b38 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Specifies the contract for a document symbols provider. + /// + public interface IDocumentSymbolProvider + { + /// + /// Provides a list of symbols for the given document. + /// + /// + /// The document for which SymbolReferences should be provided. + /// + /// An IEnumerable collection of SymbolReferences. + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs new file mode 100644 index 000000000..42472203e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Specifies the contract for an implementation of + /// the IDocumentSymbols component. + /// + public interface IDocumentSymbols + { + /// + /// Gets the collection of IDocumentSymbolsProvider implementations + /// that are registered with this component. + /// + Collection Providers { get; } + + /// + /// Provides a list of symbols for the given document. + /// + /// + /// The document for which SymbolReferences should be provided. + /// + /// An IEnumerable collection of SymbolReferences. + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs new file mode 100644 index 000000000..87a19778e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating test symbols in Pester test (tests.ps1) files. + /// + public class PesterDocumentSymbolProvider : IDocumentSymbolProvider + { + + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if (!scriptFile.FilePath.EndsWith( + "tests.ps1", + StringComparison.OrdinalIgnoreCase)) + { + return Enumerable.Empty(); + } + + // Find plausible Pester commands + IEnumerable commandAsts = scriptFile.ScriptAst.FindAll(IsNamedCommandWithArguments, true); + + return commandAsts.OfType() + .Where(IsPesterCommand) + .Select(ast => ConvertPesterAstToSymbolReference(scriptFile, ast)); + } + + /// + /// Test if the given Ast is a regular CommandAst with arguments + /// + /// the PowerShell Ast to test + /// true if the Ast represents a PowerShell command with arguments, false otherwise + private static bool IsNamedCommandWithArguments(Ast ast) + { + CommandAst commandAst = ast as CommandAst; + + return commandAst != null && + commandAst.InvocationOperator != TokenKind.Dot && + PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue && + commandAst.CommandElements.Count >= 2; + } + + /// + /// Test whether the given CommandAst represents a Pester command + /// + /// the CommandAst to test + /// true if the CommandAst represents a Pester command, false otherwise + private static bool IsPesterCommand(CommandAst commandAst) + { + if (commandAst == null) + { + return false; + } + + // Ensure the first word is a Pester keyword + if (!PesterSymbolReference.PesterKeywords.ContainsKey(commandAst.GetCommandName())) + { + return false; + } + + // Ensure that the last argument of the command is a scriptblock + if (!(commandAst.CommandElements[commandAst.CommandElements.Count-1] is ScriptBlockExpressionAst)) + { + return false; + } + + return true; + } + + /// + /// Convert a CommandAst known to represent a Pester command and a reference to the scriptfile + /// it is in into symbol representing a Pester call for code lens + /// + /// the scriptfile the Pester call occurs in + /// the CommandAst representing the Pester call + /// a symbol representing the Pester call containing metadata for CodeLens to use + private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFile scriptFile, CommandAst pesterCommandAst) + { + string testLine = scriptFile.GetLine(pesterCommandAst.Extent.StartLineNumber); + PesterCommandType? commandName = PesterSymbolReference.GetCommandType(pesterCommandAst.GetCommandName()); + if (commandName == null) + { + return null; + } + + // Search for a name for the test + // If the test has more than one argument for names, we set it to null + string testName = null; + bool alreadySawName = false; + for (int i = 1; i < pesterCommandAst.CommandElements.Count; i++) + { + CommandElementAst currentCommandElement = pesterCommandAst.CommandElements[i]; + + // Check for an explicit "-Name" parameter + if (currentCommandElement is CommandParameterAst parameterAst) + { + // Found -Name parameter, move to next element which is the argument for -TestName + i++; + + if (!alreadySawName && TryGetTestNameArgument(pesterCommandAst.CommandElements[i], out testName)) + { + alreadySawName = true; + } + + continue; + } + + // Otherwise, if an argument is given with no parameter, we assume it's the name + // If we've already seen a name, we set the name to null + if (!alreadySawName && TryGetTestNameArgument(pesterCommandAst.CommandElements[i], out testName)) + { + alreadySawName = true; + } + } + + return new PesterSymbolReference( + scriptFile, + commandName.Value, + testLine, + testName, + pesterCommandAst.Extent + ); + } + + private static bool TryGetTestNameArgument(CommandElementAst commandElementAst, out string testName) + { + testName = null; + + if (commandElementAst is StringConstantExpressionAst testNameStrAst) + { + testName = testNameStrAst.Value; + return true; + } + + return (commandElementAst is ExpandableStringExpressionAst); + } + } + + /// + /// Defines command types for Pester test blocks. + /// + public enum PesterCommandType + { + /// + /// Identifies a Describe block. + /// + Describe, + + /// + /// Identifies a Context block. + /// + Context, + + /// + /// Identifies an It block. + /// + It + } + + /// + /// Provides a specialization of SymbolReference containing + /// extra information about Pester test symbols. + /// + public class PesterSymbolReference : SymbolReference + { + /// + /// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself + /// + internal static readonly IReadOnlyDictionary PesterKeywords = + Enum.GetValues(typeof(PesterCommandType)) + .Cast() + .ToDictionary(pct => pct.ToString(), pct => pct, StringComparer.OrdinalIgnoreCase); + + private static char[] DefinitionTrimChars = new char[] { ' ', '{' }; + + /// + /// Gets the name of the test + /// + public string TestName { get; private set; } + + /// + /// Gets the test's command type. + /// + public PesterCommandType Command { get; private set; } + + internal PesterSymbolReference( + ScriptFile scriptFile, + PesterCommandType commandType, + string testLine, + string testName, + IScriptExtent scriptExtent) + : base( + SymbolType.Function, + testLine.TrimEnd(DefinitionTrimChars), + scriptExtent, + scriptFile.FilePath, + testLine) + { + this.Command = commandType; + this.TestName = testName; + } + + internal static PesterCommandType? GetCommandType(string commandName) + { + PesterCommandType pesterCommandType; + if (commandName == null || !PesterKeywords.TryGetValue(commandName, out pesterCommandType)) + { + return null; + } + return pesterCommandType; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs new file mode 100644 index 000000000..695ed2c02 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating symbols in .psd1 files. + /// + public class PsdDocumentSymbolProvider : IDocumentSymbolProvider + { + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if ((scriptFile.FilePath != null && + scriptFile.FilePath.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase)) || + IsPowerShellDataFileAst(scriptFile.ScriptAst)) + { + var findHashtableSymbolsVisitor = new FindHashtableSymbolsVisitor(); + scriptFile.ScriptAst.Visit(findHashtableSymbolsVisitor); + return findHashtableSymbolsVisitor.SymbolReferences; + } + + return Enumerable.Empty(); + } + + /// + /// Checks if a given ast represents the root node of a *.psd1 file. + /// + /// The abstract syntax tree of the given script + /// true if the AST represts a *.psd1 file, otherwise false + static public bool IsPowerShellDataFileAst(Ast ast) + { + // sometimes we don't have reliable access to the filename + // so we employ heuristics to check if the contents are + // part of a psd1 file. + return IsPowerShellDataFileAstNode( + new { Item = ast, Children = new List() }, + new Type[] { + typeof(ScriptBlockAst), + typeof(NamedBlockAst), + typeof(PipelineAst), + typeof(CommandExpressionAst), + typeof(HashtableAst) }, + 0); + } + + static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap, int level) + { + var levelAstTypeMatch = node.Item.GetType().Equals(levelAstMap[level]); + if (!levelAstTypeMatch) + { + return false; + } + + if (level == levelAstMap.Length - 1) + { + return levelAstTypeMatch; + } + + var astsFound = (node.Item as Ast).FindAll(a => a is Ast, false); + if (astsFound != null) + { + foreach (var astFound in astsFound) + { + if (!astFound.Equals(node.Item) + && node.Item.Equals(astFound.Parent) + && IsPowerShellDataFileAstNode( + new { Item = astFound, Children = new List() }, + levelAstMap, + level + 1)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs new file mode 100644 index 000000000..2c11747f2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating symbols in script (.psd1, .psm1) files. + /// + public class ScriptDocumentSymbolProvider : IDocumentSymbolProvider + { + private Version powerShellVersion; + + /// + /// Creates an instance of the ScriptDocumentSymbolProvider to + /// target the specified PowerShell version. + /// + /// The target PowerShell version. + public ScriptDocumentSymbolProvider(Version powerShellVersion) + { + this.powerShellVersion = powerShellVersion; + } + + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if (scriptFile != null && + scriptFile.FilePath != null && + (scriptFile.FilePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase) || + scriptFile.FilePath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase))) + { + return + FindSymbolsInDocument( + scriptFile.ScriptAst, + this.powerShellVersion); + } + + return Enumerable.Empty(); + } + + /// + /// Finds all symbols in a script + /// + /// The abstract syntax tree of the given script + /// The PowerShell version the Ast was generated from + /// A collection of SymbolReference objects + static public IEnumerable FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion) + { + IEnumerable symbolReferences = null; + + // TODO: Restore this when we figure out how to support multiple + // PS versions in the new PSES-as-a-module world (issue #276) + // if (powerShellVersion >= new Version(5,0)) + // { + //#if PowerShellv5 + // FindSymbolsVisitor2 findSymbolsVisitor = new FindSymbolsVisitor2(); + // scriptAst.Visit(findSymbolsVisitor); + // symbolReferences = findSymbolsVisitor.SymbolReferences; + //#endif + // } + // else + + FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor(); + scriptAst.Visit(findSymbolsVisitor); + symbolReferences = findSymbolsVisitor.SymbolReferences; + return symbolReferences; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs new file mode 100644 index 000000000..d695de649 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs @@ -0,0 +1,109 @@ +// +// 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.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides a default IScriptExtent implementation + /// containing details about a section of script content + /// in a file. + /// + public class ScriptExtent : IScriptExtent + { + #region Properties + + /// + /// Gets the file path of the script file in which this extent is contained. + /// + public string File + { + get; + set; + } + + /// + /// Gets or sets the starting column number of the extent. + /// + public int StartColumnNumber + { + get; + set; + } + + /// + /// Gets or sets the starting line number of the extent. + /// + public int StartLineNumber + { + get; + set; + } + + /// + /// Gets or sets the starting file offset of the extent. + /// + public int StartOffset + { + get; + set; + } + + /// + /// Gets or sets the starting script position of the extent. + /// + public IScriptPosition StartScriptPosition + { + get { throw new NotImplementedException(); } + } + /// + /// Gets or sets the text that is contained within the extent. + /// + public string Text + { + get; + set; + } + + /// + /// Gets or sets the ending column number of the extent. + /// + public int EndColumnNumber + { + get; + set; + } + + /// + /// Gets or sets the ending line number of the extent. + /// + public int EndLineNumber + { + get; + set; + } + + /// + /// Gets or sets the ending file offset of the extent. + /// + public int EndOffset + { + get; + set; + } + + /// + /// Gets the ending script position of the extent. + /// + public IScriptPosition EndScriptPosition + { + get { throw new NotImplementedException(); } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs new file mode 100644 index 000000000..643ab430b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs @@ -0,0 +1,89 @@ +// +// 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.Diagnostics; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// A class that holds the type, name, script extent, and source line of a symbol + /// + [DebuggerDisplay("SymbolType = {SymbolType}, SymbolName = {SymbolName}")] + public class SymbolReference + { + #region Properties + + /// + /// Gets the symbol's type + /// + public SymbolType SymbolType { get; private set; } + + /// + /// Gets the name of the symbol + /// + public string SymbolName { get; private set; } + + /// + /// Gets the script extent of the symbol + /// + public ScriptRegion ScriptRegion { get; private set; } + + /// + /// Gets the contents of the line the given symbol is on + /// + public string SourceLine { get; internal set; } + + /// + /// Gets the path of the file in which the symbol was found. + /// + public string FilePath { get; internal set; } + + #endregion + + /// + /// Constructs and instance of a SymbolReference + /// + /// The higher level type of the symbol + /// The name of the symbol + /// The script extent of the symbol + /// The file path of the symbol + /// The line contents of the given symbol (defaults to empty string) + public SymbolReference( + SymbolType symbolType, + string symbolName, + IScriptExtent scriptExtent, + string filePath = "", + string sourceLine = "") + { + // TODO: Verify params + this.SymbolType = symbolType; + this.SymbolName = symbolName; + this.ScriptRegion = ScriptRegion.Create(scriptExtent); + this.FilePath = filePath; + this.SourceLine = sourceLine; + + // TODO: Make sure end column number usage is correct + + // Build the display string + //this.DisplayString = + // string.Format( + // "{0} {1}") + } + + /// + /// Constructs and instance of a SymbolReference + /// + /// The higher level type of the symbol + /// The script extent of the symbol + /// The file path of the symbol + /// The line contents of the given symbol (defaults to empty string) + public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent, string filePath = "", string sourceLine = "") + : this(symbolType, scriptExtent.Text, scriptExtent, filePath, sourceLine) + { + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs new file mode 100644 index 000000000..2dba9a0a0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.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 +{ + /// + /// A way to define symbols on a higher level + /// + public enum SymbolType + { + /// + /// The symbol type is unknown + /// + Unknown = 0, + + /// + /// The symbol is a vairable + /// + Variable, + + /// + /// The symbol is a function + /// + Function, + + /// + /// The symbol is a parameter + /// + Parameter, + + /// + /// The symbol is a DSC configuration + /// + Configuration, + + /// + /// The symbol is a workflow + /// + Workflow, + + /// + /// The symbol is a hashtable key + /// + HashtableKey + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs new file mode 100644 index 000000000..1b4a38b19 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a high-level service for performing code completion and + /// navigation operations on PowerShell scripts. + /// + public class SymbolsService + { + #region Private Fields + + const int DefaultWaitTimeoutMilliseconds = 5000; + + private readonly ILogger _logger; + + private readonly IDocumentSymbolProvider[] _documentSymbolProviders; + + #endregion + + #region Constructors + + /// + /// Constructs an instance of the SymbolsService class and uses + /// the given Runspace to execute language service operations. + /// + /// + /// The PowerShellContext in which language service operations will be executed. + /// + /// An ILogger implementation used for writing log messages. + public SymbolsService( + ILoggerFactory factory) + { + _logger = factory.CreateLogger(); + _documentSymbolProviders = new IDocumentSymbolProvider[] + { + new ScriptDocumentSymbolProvider(VersionUtils.PSVersion), + new PsdDocumentSymbolProvider(), + new PesterDocumentSymbolProvider() + }; + } + + #endregion + + + + /// + /// Finds all the symbols in a file. + /// + /// The ScriptFile in which the symbol can be located. + /// + public List FindSymbolsInFile(ScriptFile scriptFile) + { + Validate.IsNotNull(nameof(scriptFile), scriptFile); + + var foundOccurrences = new List(); + foreach (IDocumentSymbolProvider symbolProvider in _documentSymbolProviders) + { + foreach (SymbolReference reference in symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + reference.SourceLine = scriptFile.GetLine(reference.ScriptRegion.StartLineNumber); + reference.FilePath = scriptFile.FilePath; + foundOccurrences.Add(reference); + } + } + + return foundOccurrences; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs index a916a1600..9253b81ad 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs @@ -61,7 +61,7 @@ public string DocumentUri { return this.ClientFilePath == null ? string.Empty - : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); + : WorkspaceService.ConvertPathToDocumentUri(this.ClientFilePath); } } @@ -162,7 +162,7 @@ public ScriptFile( this.FilePath = filePath; this.ClientFilePath = clientFilePath; this.IsAnalysisEnabled = true; - this.IsInMemory = Workspace.IsPathInMemory(filePath); + this.IsInMemory = WorkspaceService.IsPathInMemory(filePath); this.powerShellVersion = powerShellVersion; // SetFileContents() calls ParseFileContents() which initializes the rest of the properties. diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 460df9337..f4594dbfc 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -5,18 +5,24 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Symbols; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers { public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler { private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; - public WorkspaceSymbolsHandler(ILoggerFactory loggerFactory) { + public WorkspaceSymbolsHandler(ILoggerFactory loggerFactory, SymbolsService symbols, WorkspaceService workspace) { _logger = loggerFactory.CreateLogger(); + _symbolsService = symbols; + _workspaceService = workspace; } public object GetRegistrationOptions() @@ -29,40 +35,37 @@ public Task Handle(WorkspaceSymbolParams request, Ca { var symbols = new List(); - // foreach (ScriptFile scriptFile in editorSession.Workspace.GetOpenedFiles()) - // { - // FindOccurrencesResult foundSymbols = - // editorSession.LanguageService.FindSymbolsInFile( - // scriptFile); - - // // TODO: Need to compute a relative path that is based on common path for all workspace files - // string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - - // if (foundSymbols != null) - // { - // foreach (SymbolReference foundOccurrence in foundSymbols.FoundOccurrences) - // { - // if (!IsQueryMatch(request.Query, foundOccurrence.SymbolName)) - // { - // continue; - // } - - // var location = new Location - // { - // Uri = GetFileUri(foundOccurrence.FilePath), - // Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) - // }; - - // symbols.Add(new SymbolInformation - // { - // ContainerName = containerName, - // Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, - // Location = location, - // Name = GetDecoratedSymbolName(foundOccurrence) - // }); - // } - // } - // } + foreach (ScriptFile scriptFile in _workspaceService.GetOpenedFiles()) + { + List foundSymbols = + _symbolsService.FindSymbolsInFile( + scriptFile); + + // TODO: Need to compute a relative path that is based on common path for all workspace files + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + foreach (SymbolReference foundOccurrence in foundSymbols) + { + if (!IsQueryMatch(request.Query, foundOccurrence.SymbolName)) + { + continue; + } + + var location = new Location + { + Uri = PathUtils.ToUri(foundOccurrence.FilePath), + Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) + }; + + symbols.Add(new SymbolInformation + { + ContainerName = containerName, + Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, + Location = location, + Name = GetDecoratedSymbolName(foundOccurrence) + }); + } + } _logger.LogWarning("Logging in a handler works now."); return Task.FromResult(new SymbolInformationContainer(symbols)); @@ -75,50 +78,50 @@ public void SetCapability(WorkspaceSymbolCapability capability) #region private Methods - // private bool IsQueryMatch(string query, string symbolName) - // { - // return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; - // } - - // private static string GetFileUri(string filePath) - // { - // // If the file isn't untitled, return a URI-style path - // return - // !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") - // ? new Uri("file://" + filePath).AbsoluteUri - // : filePath; - // } - - // private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - // { - // return new Range - // { - // Start = new Position - // { - // Line = scriptRegion.StartLineNumber - 1, - // Character = scriptRegion.StartColumnNumber - 1 - // }, - // End = new Position - // { - // Line = scriptRegion.EndLineNumber - 1, - // Character = scriptRegion.EndColumnNumber - 1 - // } - // }; - // } - - // private static string GetDecoratedSymbolName(SymbolReference symbolReference) - // { - // string name = symbolReference.SymbolName; - - // if (symbolReference.SymbolType == SymbolType.Configuration || - // symbolReference.SymbolType == SymbolType.Function || - // symbolReference.SymbolType == SymbolType.Workflow) - // { - // name += " { }"; - // } - - // return name; - // } + private bool IsQueryMatch(string query, string symbolName) + { + return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static string GetFileUri(string filePath) + { + // If the file isn't untitled, return a URI-style path + return + !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") + ? new Uri("file://" + filePath).AbsoluteUri + : filePath; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + + private static string GetDecoratedSymbolName(SymbolReference symbolReference) + { + string name = symbolReference.SymbolName; + + if (symbolReference.SymbolType == SymbolType.Configuration || + symbolReference.SymbolType == SymbolType.Function || + symbolReference.SymbolType == SymbolType.Workflow) + { + name += " { }"; + } + + return name; + } #endregion } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs rename to src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs rename to src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs index 1ed26a7b2..32cde6f7b 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs @@ -11,7 +11,6 @@ using System.Text; using System.Runtime.InteropServices; using Microsoft.Extensions.FileSystemGlobbing; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices @@ -20,7 +19,7 @@ namespace Microsoft.PowerShell.EditorServices /// Manages a "workspace" of script files that are open for a particular /// editing session. Also helps to navigate references between ScriptFiles. /// - public class Workspace + public class WorkspaceService { #region Private Fields @@ -49,9 +48,9 @@ public class Workspace "**/*" }; - private ILogger logger; - private Version powerShellVersion; - private Dictionary workspaceFiles = new Dictionary(); + private readonly ILogger logger; + private readonly Version powerShellVersion; + private readonly Dictionary workspaceFiles = new Dictionary(); #endregion @@ -81,10 +80,10 @@ public class Workspace /// /// The version of PowerShell for which scripts will be parsed. /// An ILogger implementation used for writing log messages. - public Workspace(Version powerShellVersion, ILogger logger) + public WorkspaceService(ILoggerFactory factory) { - this.powerShellVersion = powerShellVersion; - this.logger = logger; + this.powerShellVersion = VersionUtils.PSVersion; + this.logger = factory.CreateLogger(); this.ExcludeFilesGlob = new List(); this.FollowSymlinks = true; } @@ -113,11 +112,11 @@ public ScriptFile CreateScriptFileFromFileBuffer(string filePath, string initial resolvedFilePath, filePath, initialBuffer, - this.powerShellVersion); + powerShellVersion); - this.workspaceFiles[keyName] = scriptFile; + workspaceFiles[keyName] = scriptFile; - this.logger.LogDebug("Opened file as in-memory buffer: " + resolvedFilePath); + logger.LogDebug("Opened file as in-memory buffer: " + resolvedFilePath); return scriptFile; } @@ -125,7 +124,7 @@ public ScriptFile CreateScriptFileFromFileBuffer(string filePath, string initial /// /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using - /// instead. + /// instead. /// /// The file path at which the script resides. /// @@ -143,8 +142,7 @@ public ScriptFile GetFile(string filePath) string keyName = resolvedFilePath.ToLower(); // Make sure the file isn't already loaded into the workspace - ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) + if (!this.workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile)) { // This method allows FileNotFoundException to bubble up // if the file isn't found. @@ -234,8 +232,7 @@ public ScriptFile GetFileBuffer(string filePath, string initialBuffer) string keyName = resolvedFilePath.ToLower(); // Make sure the file isn't already loaded into the workspace - ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile) && initialBuffer != null) + if (!this.workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile) && initialBuffer != null) { scriptFile = new ScriptFile( @@ -357,14 +354,14 @@ bool ignoreReparsePoints yield break; } - var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher(); + var matcher = new Matcher(); foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } var fsFactory = new WorkspaceFileSystemWrapperFactory( WorkspacePath, maxDepth, - DotNetFacade.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, + VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, ignoreReparsePoints, logger ); @@ -379,8 +376,8 @@ bool ignoreReparsePoints #region Private Methods /// - /// Recusrively searches through referencedFiles in scriptFiles - /// and builds a Dictonary of the file references + /// Recursively searches through referencedFiles in scriptFiles + /// and builds a Dictionary of the file references /// /// Details an contents of "root" script file /// A Dictionary of referenced script files @@ -413,7 +410,7 @@ private void RecursivelyFindReferences( resolvedScriptPath)); // Get the referenced file if it's not already in referencedScriptFiles - if (this.TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) + if (TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) { // Normalize the resolved script path and add it to the // referenced files list if it isn't there already @@ -433,7 +430,7 @@ internal string ResolveFilePath(string filePath) { if (filePath.StartsWith(@"file://")) { - filePath = Workspace.UnescapeDriveColon(filePath); + filePath = WorkspaceService.UnescapeDriveColon(filePath); // Client sent the path in URI format, extract the local path filePath = new Uri(filePath).LocalPath; } @@ -462,7 +459,7 @@ internal static bool IsPathInMemory(string filePath) // view of the current file or an untitled file. try { - // File system absoulute paths will have a URI scheme of file:. + // File system absolute paths will have a URI scheme of file:. // Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile. var uri = new Uri(filePath); isInMemory = !uri.IsFile; @@ -679,7 +676,7 @@ public static string ConvertPathToDocumentUri(string path) docUriStrBld.Append(escapedPath).Replace("%2F", "/"); } - if (!DotNetFacade.IsNetCore) + if (!VersionUtils.IsNetCore) { // ' is not encoded by Uri.EscapeDataString in Windows PowerShell 5.x. // This is apparently a difference between .NET Framework and .NET Core. diff --git a/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs b/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs deleted file mode 100644 index 78bc22a08..000000000 --- a/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// General purpose common utilities to prevent reimplementation. - /// - internal static class DotNetFacade - { - /// - /// True if we are running on .NET Core, false otherwise. - /// - public static bool IsNetCore { get; } = RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.Ordinal); -} -} diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index 768b26f07..da9dfa3ad 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -8,5 +8,26 @@ public string WildcardUnescapePath(string path) { throw new NotImplementedException(); } + + public static Uri ToUri(string fileName) + { + fileName = fileName.Replace(":", "%3A").Replace("\\", "/"); + if (!fileName.StartsWith("/")) return new Uri($"file:///{fileName}"); + return new Uri($"file://{fileName}"); + } + + public static string FromUri(Uri uri) + { + if (uri.Segments.Length > 1) + { + // On windows of the Uri contains %3a local path + // doesn't come out as a proper windows path + if (uri.Segments[1].IndexOf("%3a", StringComparison.OrdinalIgnoreCase) > -1) + { + return FromUri(new Uri(uri.AbsoluteUri.Replace("%3a", ":").Replace("%3A", ":"))); + } + } + return uri.LocalPath; + } } } diff --git a/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs b/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs new file mode 100644 index 000000000..6a487e377 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// General purpose common utilities to prevent reimplementation. + /// + internal static class VersionUtils + { + /// + /// True if we are running on .NET Core, false otherwise. + /// + public static bool IsNetCore { get; } = RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.Ordinal); + + /// + /// Get's the Version of PowerShell being used. + /// + public static Version PSVersion { get; } = PowerShellReflectionUtils.PSVersion; + + /// + /// True if we are running in Windows PowerShell, false otherwise. + /// + public static bool IsPS5 { get; } = PSVersion.Major == 5; + + /// + /// True if we are running in PowerShell Core 6, false otherwise. + /// + public static bool IsPS6 { get; } = PSVersion.Major == 6; + + /// + /// True if we are running in PowerShell 7, false otherwise. + /// + public static bool IsPS7 { get; } = PSVersion.Major == 7; + } + + internal static class PowerShellReflectionUtils + { + + private static readonly Assembly _psRuntimeAssembly = typeof(System.Management.Automation.Runspaces.Runspace).Assembly; + private static readonly PropertyInfo _psVersionProperty = _psRuntimeAssembly.GetType("System.Management.Automation.PSVersionInfo") + .GetProperty("PSVersion", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + /// + /// Get's the Version of PowerShell being used. + /// + public static Version PSVersion { get; } = _psVersionProperty.GetValue(null) as Version; + } +} From 90412191e1a18065ac3e019eb95bce62e0bbd399 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 10 Jul 2019 09:10:05 -0700 Subject: [PATCH 09/47] working text document syncer --- .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Services/Symbols/FindSymbolsVisitor.cs | 3 +- .../Handlers/TextDocumentHandler.cs | 175 ++++++++++++++++++ .../TextDocumentNotificationHandler.cs | 7 - .../Handlers/WorkspaceSymbolsHandler.cs | 3 +- .../EditorServices.Integration.Tests.ps1 | 20 +- tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 43 ++++- 8 files changed, 241 insertions(+), 12 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs delete mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 3b893746b..cb6da4423 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -69,6 +69,7 @@ public async Task StartAsync() options.MinimumLogLevel = _configuration.MinimumLogLevel; options.Services = _configuration.Services; options.WithHandler(); + options.WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs index fd624c429..9b2dc0dc6 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs @@ -120,8 +120,7 @@ public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) foreach (var kvp in hashtableAst.KeyValuePairs) { - var keyStrConstExprAst = kvp.Item1 as StringConstantExpressionAst; - if (keyStrConstExprAst != null) + if (kvp.Item1 is StringConstantExpressionAst keyStrConstExprAst) { IScriptExtent nameExtent = new ScriptExtent() { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs new file mode 100644 index 000000000..a322b7d4a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; + +namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +{ + class TextDocumentHandler : ITextDocumentSyncHandler + { + + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private SynchronizationCapability _capability; + + public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental; + + public TextDocumentHandler(ILoggerFactory factory, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + } + + public Task Handle(DidChangeTextDocumentParams notification, CancellationToken token) + { + List changedFiles = new List(); + + // A text change notification can batch multiple change requests + foreach (TextDocumentContentChangeEvent textChange in notification.ContentChanges) + { + ScriptFile changedFile = _workspaceService.GetFile(notification.TextDocument.Uri.ToString()); + + changedFile.ApplyChange( + GetFileChangeDetails( + textChange.Range, + textChange.Text)); + + changedFiles.Add(changedFile); + } + + // TODO: Get all recently edited files in the workspace + // this.RunScriptDiagnosticsAsync( + // changedFiles.ToArray(), + // editorSession, + // eventContext); + return Unit.Task; + } + + TextDocumentChangeRegistrationOptions IRegistration.GetRegistrationOptions() + { + return new TextDocumentChangeRegistrationOptions() + { + // DocumentSelector = _documentSelector, + SyncKind = Change + }; + } + + public void SetCapability(SynchronizationCapability capability) + { + _capability = capability; + } + + public Task Handle(DidOpenTextDocumentParams notification, CancellationToken token) + { + ScriptFile openedFile = + _workspaceService.GetFileBuffer( + notification.TextDocument.Uri.ToString(), + notification.TextDocument.Text); + + // TODO: Get all recently edited files in the workspace + // this.RunScriptDiagnosticsAsync( + // new ScriptFile[] { openedFile }, + // editorSession, + // eventContext); + + _logger.LogTrace("Finished opening document."); + return Unit.Task; + } + + TextDocumentRegistrationOptions IRegistration.GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions() + { + // DocumentSelector = _documentSelector, + }; + } + + public Task Handle(DidCloseTextDocumentParams notification, CancellationToken token) + { + // Find and close the file in the current session + var fileToClose = _workspaceService.GetFile(notification.TextDocument.Uri.ToString()); + + if (fileToClose != null) + { + _workspaceService.CloseFile(fileToClose); + // await ClearMarkersAsync(fileToClose, eventContext); + } + + _logger.LogTrace("Finished closing document."); + return Unit.Task; + } + + public Task Handle(DidSaveTextDocumentParams notification, CancellationToken token) + { + ScriptFile savedFile = + _workspaceService.GetFile( + notification.TextDocument.Uri.ToString()); + + // if (savedFile != null) + // { + // if (this.editorSession.RemoteFileManager.IsUnderRemoteTempPath(savedFile.FilePath)) + // { + // await this.editorSession.RemoteFileManager.SaveRemoteFileAsync( + // savedFile.FilePath); + // } + // } + return Unit.Task; + } + + TextDocumentSaveRegistrationOptions IRegistration.GetRegistrationOptions() + { + return new TextDocumentSaveRegistrationOptions() + { + // DocumentSelector = _documentSelector, + // IncludeText = true + }; + } + public TextDocumentAttributes GetTextDocumentAttributes(Uri uri) + { + return new TextDocumentAttributes(uri, "powershell"); + } + + private static FileChange GetFileChangeDetails(Range changeRange, string insertString) + { + // The protocol's positions are zero-based so add 1 to all offsets + + if (changeRange == null) return new FileChange { InsertString = insertString, IsReload = true }; + + return new FileChange + { + InsertString = insertString, + Line = (int)(changeRange.Start.Line + 1), + Offset = (int)(changeRange.Start.Character + 1), + EndLine = (int)(changeRange.End.Line + 1), + EndOffset = (int)(changeRange.End.Character + 1), + IsReload = false + }; + } + + // private async Task ClearMarkersAsync(ScriptFile scriptFile, EventContext eventContext) + // { + // // send empty diagnostic markers to clear any markers associated with the given file + // await PublishScriptDiagnosticsAsync( + // scriptFile, + // new List(), + // this.codeActionsPerFile, + // eventContext); + // } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs deleted file mode 100644 index 3307d8385..000000000 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentNotificationHandler.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PowerShellEditorServices.Engine.Services.TextDocument.Handlers -{ - public class TextDocumentNotificationHandler - { - - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index f4594dbfc..26f17605a 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -18,6 +18,7 @@ public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler private readonly ILogger _logger; private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; + private WorkspaceSymbolCapability _capability; public WorkspaceSymbolsHandler(ILoggerFactory loggerFactory, SymbolsService symbols, WorkspaceService workspace) { _logger = loggerFactory.CreateLogger(); @@ -73,7 +74,7 @@ public Task Handle(WorkspaceSymbolParams request, Ca public void SetCapability(WorkspaceSymbolCapability capability) { - // throw new NotImplementedException(); + _capability = capability; } #region private Methods diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index afdf0bb9b..b69f6dac5 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -79,11 +79,29 @@ Describe "Loading and running PowerShellEditorServices" { } It "Can handle WorkspaceSymbol request" { + $script = " +function Get-Foo { + Write-Host 'hello' +} +" + + $file = Set-Content -Path TestDrive:\foo.ps1 -Value $script -PassThru + $response = 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 $response.Id | Out-Null + $request = Send-LspRequest -Client $client -Method "workspace/symbol" -Parameters @{ query = "" } - $response = Get-LspResponse -Client $client -Id $request.Id #-WaitMillis 99999 + $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) diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index c102d30c8..6fe8f9b8e 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -74,6 +74,7 @@ FunctionsToExport = @( 'Connect-PsesServer', 'Send-LspRequest', 'Send-LspInitializeRequest', + 'Send-LspDidOpenTextDocumentRequest', 'Send-LspShutdownRequest', 'Get-LspResponse' ) diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index b4b0a247e..ebe8c5a6d 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -252,6 +252,43 @@ function Send-LspInitializeRequest 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 + } + } + + return Send-LspRequest -Client $Client -Method 'textDocument/didOpen' -Parameters $parameters +} + function Send-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] @@ -304,7 +341,11 @@ function Get-LspResponse if ($Client.TryGetResponse($Id, [ref]$lspResponse, $WaitMillis)) { - return $lspResponse + $result = if ($lspResponse.Result) { $lspResponse.Result.ToString() | ConvertFrom-Json } + return [PSCustomObject]@{ + Id = $lspResponse.Id + Result = $result + } } } From da25a555f0e23873fe40317e67377bc08d598c15 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 10 Jul 2019 16:47:33 -0700 Subject: [PATCH 10/47] needed document selector and getVersion handler to work with vscode --- .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Handlers/GetVersionHandler.cs | 50 +++++++++++++++++++ .../Handlers/IGetVersionHandler.cs | 17 +++++++ .../Handlers/TextDocumentHandler.cs | 8 +-- .../Utility/VersionUtils.cs | 21 ++++++-- .../EditorServices.Integration.Tests.ps1 | 10 ++++ 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index cb6da4423..2b5d5c509 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -70,6 +70,7 @@ public async Task StartAsync() options.Services = _configuration.Services; options.WithHandler(); options.WithHandler(); + options.WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs new file mode 100644 index 000000000..a2b2962d2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; + +namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +{ + public class GetVersionHandler : IGetVersionHandler + { + private readonly ILogger _logger; + + public GetVersionHandler(ILoggerFactory factory) + { + _logger = factory.CreateLogger(); + } + + public Task Handle(GetVersionParams request, CancellationToken cancellationToken) + { + var architecture = PowerShellProcessArchitecture.Unknown; + // This should be changed to using a .NET call sometime in the future... but it's just for logging purposes. + string arch = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"); + if (arch != null) + { + if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X64; + } + else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X86; + } + } + + return Task.FromResult(new PowerShellVersionDetails { + Version = VersionUtils.PSVersion.ToString(), + Edition = VersionUtils.PSEdition, + DisplayVersion = VersionUtils.PSVersion.ToString(2), + Architecture = architecture.ToString() + }); + } + + private enum PowerShellProcessArchitecture + { + Unknown, + X86, + X64 + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs new file mode 100644 index 000000000..63f80b8ad --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs @@ -0,0 +1,17 @@ +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +{ + [Serial, Method("powerShell/getVersion")] + public interface IGetVersionHandler : IJsonRpcRequestHandler { } + + public class GetVersionParams : IRequest { } + + public class PowerShellVersionDetails { + public string Version { get; set; } + public string DisplayVersion { get; set; } + public string Edition { get; set; } + public string Architecture { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index a322b7d4a..2d3913833 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -65,7 +65,7 @@ TextDocumentChangeRegistrationOptions IRegistration.G { return new TextDocumentRegistrationOptions() { - // DocumentSelector = _documentSelector, + DocumentSelector = _documentSelector, }; } @@ -136,8 +136,8 @@ TextDocumentSaveRegistrationOptions IRegistration public static Version PSVersion { get; } = PowerShellReflectionUtils.PSVersion; + /// + /// Get's the Edition of PowerShell being used. + /// + public static string PSEdition { get; } = PowerShellReflectionUtils.PSEdition; + /// /// True if we are running in Windows PowerShell, false otherwise. /// @@ -38,13 +43,21 @@ internal static class VersionUtils internal static class PowerShellReflectionUtils { - private static readonly Assembly _psRuntimeAssembly = typeof(System.Management.Automation.Runspaces.Runspace).Assembly; - private static readonly PropertyInfo _psVersionProperty = _psRuntimeAssembly.GetType("System.Management.Automation.PSVersionInfo") + private static readonly Type s_psVersionInfoType = typeof(System.Management.Automation.Runspaces.Runspace).Assembly.GetType("System.Management.Automation.PSVersionInfo"); + private static readonly PropertyInfo s_psVersionProperty = s_psVersionInfoType .GetProperty("PSVersion", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + private static readonly PropertyInfo s_psEditionProperty = s_psVersionInfoType + .GetProperty("PSEdition", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); /// - /// Get's the Version of PowerShell being used. + /// Get's the Version of PowerShell being used. Note: this will get rid of the SemVer 2.0 suffix because apparently + /// that property is added as a note property and it is not there when we reflect. + /// + public static Version PSVersion { get; } = s_psVersionProperty.GetValue(null) as Version; + + /// + /// Get's the Edition of PowerShell being used. /// - public static Version PSVersion { get; } = _psVersionProperty.GetValue(null) as Version; + public static string PSEdition { get; } = s_psEditionProperty.GetValue(null) as string; } } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index b69f6dac5..cc9ec110f 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -78,6 +78,16 @@ Describe "Loading and running PowerShellEditorServices" { #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" { $script = " function Get-Foo { From a5b04bad5184eae1eb16e1263f62724befc88e0f Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 16 Jul 2019 10:53:22 -0700 Subject: [PATCH 11/47] Add diagnostics (#18) --- .../Hosting/EditorServicesHost.cs | 6 + .../LanguageServer/OmnisharpLanguageServer.cs | 2 +- .../Services/Analysis/AnalysisService.cs | 2 +- .../Handlers/GetVersionHandler.cs | 2 +- .../Handlers/IGetVersionHandler.cs | 2 +- .../Handlers/TextDocumentHandler.cs | 271 ++++++++++++++++-- .../Handlers/WorkspaceSymbolsHandler.cs | 2 +- .../EditorServices.Integration.Tests.ps1 | 33 ++- tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 33 ++- 10 files changed, 324 insertions(+), 30 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 13ba05f78..79d8525fd 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -227,6 +227,12 @@ public void StartLanguageService( _serviceCollection.AddSingleton(); _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton( + (provider) => { + // TODO: Fill in settings + return AnalysisService.Create(null, _factory.CreateLogger()); + } + ); _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) { diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 2b5d5c509..19354a0f2 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -7,7 +7,7 @@ using OS = OmniSharp.Extensions.LanguageServer.Server; using System.Security.AccessControl; using OmniSharp.Extensions.LanguageServer.Server; -using PowerShellEditorServices.Engine.Services.Workspace.Handlers; +using PowerShellEditorServices.Engine.Services.Handlers; namespace Microsoft.PowerShell.EditorServices.Engine { diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index 8d5e5a4b0..97a99fd25 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -13,7 +13,7 @@ using System.Collections; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Services +namespace Microsoft.PowerShell.EditorServices { /// /// Provides a high-level service for performing semantic analysis diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs index a2b2962d2..724c921d4 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices; -namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +namespace PowerShellEditorServices.Engine.Services.Handlers { public class GetVersionHandler : IGetVersionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs index 63f80b8ad..c4570d8b8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs @@ -1,7 +1,7 @@ using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +namespace PowerShellEditorServices.Engine.Services.Handlers { [Serial, Method("powerShell/getVersion")] public interface IGetVersionHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index 2d3913833..0555a8d1d 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -11,14 +12,21 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; -namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +namespace PowerShellEditorServices.Engine.Services.Handlers { class TextDocumentHandler : ITextDocumentSyncHandler { private readonly ILogger _logger; + private readonly ILanguageServer _languageServer; + private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; + private Dictionary> codeActionsPerFile = + new Dictionary>(); + + private static CancellationTokenSource s_existingRequestCancellation; + private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { @@ -30,9 +38,11 @@ class TextDocumentHandler : ITextDocumentSyncHandler public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental; - public TextDocumentHandler(ILoggerFactory factory, WorkspaceService workspaceService) + public TextDocumentHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService, WorkspaceService workspaceService) { _logger = factory.CreateLogger(); + _languageServer = languageServer; + _analysisService = analysisService; _workspaceService = workspaceService; } @@ -54,10 +64,7 @@ public Task Handle(DidChangeTextDocumentParams notification, CancellationT } // TODO: Get all recently edited files in the workspace - // this.RunScriptDiagnosticsAsync( - // changedFiles.ToArray(), - // editorSession, - // eventContext); + RunScriptDiagnosticsAsync(changedFiles.ToArray()); return Unit.Task; } @@ -83,10 +90,7 @@ public Task Handle(DidOpenTextDocumentParams notification, CancellationTok notification.TextDocument.Text); // TODO: Get all recently edited files in the workspace - // this.RunScriptDiagnosticsAsync( - // new ScriptFile[] { openedFile }, - // editorSession, - // eventContext); + RunScriptDiagnosticsAsync(new ScriptFile[] { openedFile }); _logger.LogTrace("Finished opening document."); return Unit.Task; @@ -108,7 +112,7 @@ public Task Handle(DidCloseTextDocumentParams notification, CancellationTo if (fileToClose != null) { _workspaceService.CloseFile(fileToClose); - // await ClearMarkersAsync(fileToClose, eventContext); + ClearMarkers(fileToClose); } _logger.LogTrace("Finished closing document."); @@ -162,14 +166,241 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS }; } - // private async Task ClearMarkersAsync(ScriptFile scriptFile, EventContext eventContext) - // { - // // send empty diagnostic markers to clear any markers associated with the given file - // await PublishScriptDiagnosticsAsync( - // scriptFile, - // new List(), - // this.codeActionsPerFile, - // eventContext); - // } + private Task RunScriptDiagnosticsAsync( + ScriptFile[] filesToAnalyze) + { + // If there's an existing task, attempt to cancel it + try + { + if (s_existingRequestCancellation != null) + { + // Try to cancel the request + s_existingRequestCancellation.Cancel(); + + // If cancellation didn't throw an exception, + // clean up the existing token + s_existingRequestCancellation.Dispose(); + s_existingRequestCancellation = null; + } + } + catch (Exception e) + { + // TODO: Catch a more specific exception! + _logger.LogError( + string.Format( + "Exception while canceling analysis task:\n\n{0}", + e.ToString())); + + TaskCompletionSource cancelTask = new TaskCompletionSource(); + cancelTask.SetCanceled(); + return cancelTask.Task; + } + + // If filesToAnalzye is empty, nothing to do so return early. + if (filesToAnalyze.Length == 0) + { + return Task.FromResult(true); + } + + // Create a fresh cancellation token and then start the task. + // We create this on a different TaskScheduler so that we + // don't block the main message loop thread. + // TODO: Is there a better way to do this? + s_existingRequestCancellation = new CancellationTokenSource(); + // TODO use settings service + Task.Factory.StartNew( + () => + DelayThenInvokeDiagnosticsAsync( + 750, + filesToAnalyze, + true, + this.codeActionsPerFile, + _logger, + s_existingRequestCancellation.Token), + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); + + return Task.FromResult(true); + } + + private async Task DelayThenInvokeDiagnosticsAsync( + int delayMilliseconds, + ScriptFile[] filesToAnalyze, + bool isScriptAnalysisEnabled, + Dictionary> correctionIndex, + ILogger Logger, + CancellationToken cancellationToken) + { + // First of all, wait for the desired delay period before + // analyzing the provided list of files + try + { + await Task.Delay(delayMilliseconds, cancellationToken); + } + catch (TaskCanceledException) + { + // If the task is cancelled, exit directly + foreach (var script in filesToAnalyze) + { + PublishScriptDiagnostics( + script, + script.DiagnosticMarkers, + correctionIndex); + } + + return; + } + + // If we've made it past the delay period then we don't care + // about the cancellation token anymore. This could happen + // when the user stops typing for long enough that the delay + // period ends but then starts typing while analysis is going + // on. It makes sense to send back the results from the first + // delay period while the second one is ticking away. + + // Get the requested files + foreach (ScriptFile scriptFile in filesToAnalyze) + { + List semanticMarkers = null; + if (isScriptAnalysisEnabled && _analysisService != null) + { + semanticMarkers = await _analysisService.GetSemanticMarkersAsync(scriptFile); + } + else + { + // Semantic markers aren't available if the AnalysisService + // isn't available + semanticMarkers = new List(); + } + + scriptFile.DiagnosticMarkers.AddRange(semanticMarkers); + + PublishScriptDiagnostics( + scriptFile, + // Concat script analysis errors to any existing parse errors + scriptFile.DiagnosticMarkers, + correctionIndex); + } + } + + private void ClearMarkers(ScriptFile scriptFile) + { + // send empty diagnostic markers to clear any markers associated with the given file + PublishScriptDiagnostics( + scriptFile, + new List(), + this.codeActionsPerFile); + } + + private void PublishScriptDiagnostics( + ScriptFile scriptFile, + List markers, + Dictionary> correctionIndex) + { + List diagnostics = new List(); + + // Hold on to any corrections that may need to be applied later + Dictionary fileCorrections = + new Dictionary(); + + foreach (var marker in markers) + { + // Does the marker contain a correction? + Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); + if (marker.Correction != null) + { + string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); + fileCorrections.Add(diagnosticId, marker.Correction); + } + + diagnostics.Add(markerDiagnostic); + } + + correctionIndex[scriptFile.DocumentUri] = fileCorrections; + + var uriBuilder = new UriBuilder() + { + Scheme = Uri.UriSchemeFile, + Path = scriptFile.FilePath, + Host = string.Empty, + }; + + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + _languageServer.Document.PublishDiagnostics(new PublishDiagnosticsParams() + { + Uri = uriBuilder.Uri, + Diagnostics = new Container(diagnostics), + }); + } + + // Generate a unique id that is used as a key to look up the associated code action (code fix) when + // we receive and process the textDocument/codeAction message. + private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) + { + Position start = diagnostic.Range.Start; + Position end = diagnostic.Range.End; + + var sb = new StringBuilder(256) + .Append(diagnostic.Source ?? "?") + .Append("_") + .Append(diagnostic.Code.ToString()) + .Append("_") + .Append(diagnostic.Severity?.ToString() ?? "?") + .Append("_") + .Append(start.Line) + .Append(":") + .Append(start.Character) + .Append("-") + .Append(end.Line) + .Append(":") + .Append(end.Character); + + var id = sb.ToString(); + return id; + } + + private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) + { + return new Diagnostic + { + Severity = MapDiagnosticSeverity(scriptFileMarker.Level), + Message = scriptFileMarker.Message, + Code = scriptFileMarker.RuleName, + Source = scriptFileMarker.Source, + Range = new Range + { + Start = new Position + { + Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 + } + } + }; + } + + private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) + { + switch (markerLevel) + { + case ScriptFileMarkerLevel.Error: + return DiagnosticSeverity.Error; + + case ScriptFileMarkerLevel.Warning: + return DiagnosticSeverity.Warning; + + case ScriptFileMarkerLevel.Information: + return DiagnosticSeverity.Information; + + default: + return DiagnosticSeverity.Error; + } + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 26f17605a..5e9c36752 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -11,7 +11,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using PowerShellEditorServices.Engine.Utility; -namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers +namespace PowerShellEditorServices.Engine.Services.Handlers { public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler { diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index cc9ec110f..5f3f6006a 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -95,14 +95,14 @@ function Get-Foo { } " - $file = Set-Content -Path TestDrive:\foo.ps1 -Value $script -PassThru - $response = Send-LspDidOpenTextDocumentRequest -Client $client ` + $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 $response.Id | Out-Null + Get-LspResponse -Client $client -Id $request.Id | Out-Null $request = Send-LspRequest -Client $client -Method "workspace/symbol" -Parameters @{ query = "" @@ -117,6 +117,25 @@ function Get-Foo { # ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } + It "Can get Diagnostics" { + $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 + + $notifications = Get-LspNotification -Client $client + $notifications | Should -Not -BeNullOrEmpty + $notifications.Params.diagnostics | Should -Not -BeNullOrEmpty + $notifications.Params.diagnostics.Count | Should -Be 1 + $notifications.Params.diagnostics.code | Should -Be "PSUseDeclaredVarsMoreThanAssignments" + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client @@ -148,12 +167,20 @@ function Get-Foo { 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) { diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 6fe8f9b8e..e0201c50e 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -76,6 +76,7 @@ FunctionsToExport = @( 'Send-LspInitializeRequest', 'Send-LspDidOpenTextDocumentRequest', 'Send-LspShutdownRequest', + 'Get-LspNotification', 'Get-LspResponse' ) diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index ebe8c5a6d..76cab02bf 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -286,7 +286,12 @@ function Send-LspDidOpenTextDocumentRequest } } - return Send-LspRequest -Client $Client -Method 'textDocument/didOpen' -Parameters $parameters + $result = Send-LspRequest -Client $Client -Method 'textDocument/didOpen' -Parameters $parameters + + # Give PSScriptAnalyzer enough time to run + Start-Sleep -Seconds 1 + + $result } function Send-LspShutdownRequest @@ -317,7 +322,13 @@ function Send-LspRequest $Parameters = $null ) - return $Client.WriteRequest($Method, $Parameters) + + $result = $Client.WriteRequest($Method, $Parameters) + + # To allow for result/notification queue to fill up + Start-Sleep 1 + + $result } function Get-LspResponse @@ -349,6 +360,24 @@ function Get-LspResponse } } +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( From 8f1bdc6ff86886228f96661c717982508376e404 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 26 Jul 2019 09:16:03 -0700 Subject: [PATCH 12/47] Add setting support (#19) * Added Diagnostics * didChangeConfiguration message and general settings support * Apply suggestions from code review Co-Authored-By: Robert Holt --- .../Hosting/EditorServicesHost.cs | 7 +- .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Services/Analysis/AnalysisService.cs | 246 ++++++++++- .../Handlers/TextDocumentHandler.cs | 249 +---------- .../Workspace/ConfigurationService.cs | 13 + .../Handlers/ConfigurationHandler.cs | 136 ++++++ .../Workspace/LanguageServerSettings.cs | 389 ++++++++++++++++++ .../EditorServices.Integration.Tests.ps1 | 39 +- tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 25 ++ 10 files changed, 857 insertions(+), 249 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 79d8525fd..4f810bf22 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -227,10 +227,13 @@ public void StartLanguageService( _serviceCollection.AddSingleton(); _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(); _serviceCollection.AddSingleton( (provider) => { - // TODO: Fill in settings - return AnalysisService.Create(null, _factory.CreateLogger()); + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + _factory.CreateLogger()); } ); diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 19354a0f2..92970bef2 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -71,6 +71,7 @@ public async Task StartAsync() options.WithHandler(); options.WithHandler(); options.WithHandler(); + options.WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index 97a99fd25..992fb65ab 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -12,6 +12,9 @@ using System.Text; using System.Collections; using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using System.Threading; namespace Microsoft.PowerShell.EditorServices { @@ -51,6 +54,11 @@ public class AnalysisService : IDisposable private static readonly string[] s_emptyGetRuleResult = new string[0]; + private Dictionary> codeActionsPerFile = + new Dictionary>(); + + private static CancellationTokenSource s_existingRequestCancellation; + /// /// The indentation to add when the logger lists errors. /// @@ -87,6 +95,9 @@ public class AnalysisService : IDisposable /// private PSModuleInfo _pssaModuleInfo; + private readonly ILanguageServer _languageServer; + private readonly ConfigurationService _configurationService; + #endregion // Private Fields #region Properties @@ -126,12 +137,16 @@ private AnalysisService( RunspacePool analysisRunspacePool, string pssaSettingsPath, IEnumerable activeRules, + ILanguageServer languageServer, + ConfigurationService configurationService, ILogger logger, PSModuleInfo pssaModuleInfo = null) { _analysisRunspacePool = analysisRunspacePool; SettingsPath = pssaSettingsPath; ActiveRules = activeRules.ToArray(); + _languageServer = languageServer; + _configurationService = configurationService; _logger = logger; _pssaModuleInfo = pssaModuleInfo; } @@ -150,8 +165,9 @@ private AnalysisService( /// A new analysis service instance with a freshly imported PSScriptAnalyzer module and runspace pool. /// Returns null if problems occur. This method should never throw. /// - public static AnalysisService Create(string settingsPath, ILogger logger) + public static AnalysisService Create(ConfigurationService configurationService, ILanguageServer languageServer, ILogger logger) { + string settingsPath = configurationService.CurrentSettings.ScriptAnalysis.SettingsPath; try { RunspacePool analysisRunspacePool; @@ -184,6 +200,8 @@ public static AnalysisService Create(string settingsPath, ILogger logger) analysisRunspacePool, settingsPath, s_includedRules, + languageServer, + configurationService, logger, pssaModuleInfo); @@ -673,6 +691,232 @@ public PowerShellResult( public bool HasErrors { get; } } + + internal async Task RunScriptDiagnosticsAsync( + ScriptFile[] filesToAnalyze) + { + // If there's an existing task, attempt to cancel it + try + { + if (s_existingRequestCancellation != null) + { + // Try to cancel the request + s_existingRequestCancellation.Cancel(); + + // If cancellation didn't throw an exception, + // clean up the existing token + s_existingRequestCancellation.Dispose(); + s_existingRequestCancellation = null; + } + } + catch (Exception e) + { + // TODO: Catch a more specific exception! + _logger.LogError( + string.Format( + "Exception while canceling analysis task:\n\n{0}", + e.ToString())); + + TaskCompletionSource cancelTask = new TaskCompletionSource(); + cancelTask.SetCanceled(); + return; + } + + // If filesToAnalzye is empty, nothing to do so return early. + if (filesToAnalyze.Length == 0) + { + return; + } + + // Create a fresh cancellation token and then start the task. + // We create this on a different TaskScheduler so that we + // don't block the main message loop thread. + // TODO: Is there a better way to do this? + s_existingRequestCancellation = new CancellationTokenSource(); + await Task.Factory.StartNew( + () => + DelayThenInvokeDiagnosticsAsync( + 750, + filesToAnalyze, + _configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false, + s_existingRequestCancellation.Token), + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); + } + + private async Task DelayThenInvokeDiagnosticsAsync( + int delayMilliseconds, + ScriptFile[] filesToAnalyze, + bool isScriptAnalysisEnabled, + CancellationToken cancellationToken) + { + // First of all, wait for the desired delay period before + // analyzing the provided list of files + try + { + await Task.Delay(delayMilliseconds, cancellationToken); + } + catch (TaskCanceledException) + { + // If the task is cancelled, exit directly + foreach (var script in filesToAnalyze) + { + PublishScriptDiagnostics( + script, + script.DiagnosticMarkers); + } + + return; + } + + // If we've made it past the delay period then we don't care + // about the cancellation token anymore. This could happen + // when the user stops typing for long enough that the delay + // period ends but then starts typing while analysis is going + // on. It makes sense to send back the results from the first + // delay period while the second one is ticking away. + + // Get the requested files + foreach (ScriptFile scriptFile in filesToAnalyze) + { + List semanticMarkers = null; + if (isScriptAnalysisEnabled) + { + semanticMarkers = await GetSemanticMarkersAsync(scriptFile); + } + else + { + // Semantic markers aren't available if the AnalysisService + // isn't available + semanticMarkers = new List(); + } + + scriptFile.DiagnosticMarkers.AddRange(semanticMarkers); + + PublishScriptDiagnostics( + scriptFile, + // Concat script analysis errors to any existing parse errors + scriptFile.DiagnosticMarkers); + } + } + + internal void ClearMarkers(ScriptFile scriptFile) + { + // send empty diagnostic markers to clear any markers associated with the given file + PublishScriptDiagnostics( + scriptFile, + new List()); + } + + private void PublishScriptDiagnostics( + ScriptFile scriptFile, + List markers) + { + List diagnostics = new List(); + + // Hold on to any corrections that may need to be applied later + Dictionary fileCorrections = + new Dictionary(); + + foreach (var marker in markers) + { + // Does the marker contain a correction? + Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); + if (marker.Correction != null) + { + string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); + fileCorrections[diagnosticId] = marker.Correction; + } + + diagnostics.Add(markerDiagnostic); + } + + codeActionsPerFile[scriptFile.DocumentUri] = fileCorrections; + + var uriBuilder = new UriBuilder() + { + Scheme = Uri.UriSchemeFile, + Path = scriptFile.FilePath, + Host = string.Empty, + }; + + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + _languageServer.Document.PublishDiagnostics(new PublishDiagnosticsParams() + { + Uri = uriBuilder.Uri, + Diagnostics = new Container(diagnostics), + }); + } + + // Generate a unique id that is used as a key to look up the associated code action (code fix) when + // we receive and process the textDocument/codeAction message. + private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) + { + Position start = diagnostic.Range.Start; + Position end = diagnostic.Range.End; + + var sb = new StringBuilder(256) + .Append(diagnostic.Source ?? "?") + .Append("_") + .Append(diagnostic.Code.ToString()) + .Append("_") + .Append(diagnostic.Severity?.ToString() ?? "?") + .Append("_") + .Append(start.Line) + .Append(":") + .Append(start.Character) + .Append("-") + .Append(end.Line) + .Append(":") + .Append(end.Character); + + var id = sb.ToString(); + return id; + } + + private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) + { + return new Diagnostic + { + Severity = MapDiagnosticSeverity(scriptFileMarker.Level), + Message = scriptFileMarker.Message, + Code = scriptFileMarker.RuleName, + Source = scriptFileMarker.Source, + Range = new Range + { + Start = new Position + { + Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, + Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 + } + } + }; + } + + private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) + { + switch (markerLevel) + { + case ScriptFileMarkerLevel.Error: + return DiagnosticSeverity.Error; + + case ScriptFileMarkerLevel.Warning: + return DiagnosticSeverity.Warning; + + case ScriptFileMarkerLevel.Information: + return DiagnosticSeverity.Information; + + default: + return DiagnosticSeverity.Error; + } + } } /// diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index 0555a8d1d..2c5ed0a45 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -18,7 +18,6 @@ class TextDocumentHandler : ITextDocumentSyncHandler { private readonly ILogger _logger; - private readonly ILanguageServer _languageServer; private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; @@ -38,10 +37,9 @@ class TextDocumentHandler : ITextDocumentSyncHandler public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental; - public TextDocumentHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService, WorkspaceService workspaceService) + public TextDocumentHandler(ILoggerFactory factory, AnalysisService analysisService, WorkspaceService workspaceService) { _logger = factory.CreateLogger(); - _languageServer = languageServer; _analysisService = analysisService; _workspaceService = workspaceService; } @@ -64,7 +62,7 @@ public Task Handle(DidChangeTextDocumentParams notification, CancellationT } // TODO: Get all recently edited files in the workspace - RunScriptDiagnosticsAsync(changedFiles.ToArray()); + _analysisService.RunScriptDiagnosticsAsync(changedFiles.ToArray()); return Unit.Task; } @@ -90,7 +88,7 @@ public Task Handle(DidOpenTextDocumentParams notification, CancellationTok notification.TextDocument.Text); // TODO: Get all recently edited files in the workspace - RunScriptDiagnosticsAsync(new ScriptFile[] { openedFile }); + _analysisService.RunScriptDiagnosticsAsync(new ScriptFile[] { openedFile }); _logger.LogTrace("Finished opening document."); return Unit.Task; @@ -112,7 +110,7 @@ public Task Handle(DidCloseTextDocumentParams notification, CancellationTo if (fileToClose != null) { _workspaceService.CloseFile(fileToClose); - ClearMarkers(fileToClose); + _analysisService.ClearMarkers(fileToClose); } _logger.LogTrace("Finished closing document."); @@ -124,7 +122,7 @@ public Task Handle(DidSaveTextDocumentParams notification, CancellationTok ScriptFile savedFile = _workspaceService.GetFile( notification.TextDocument.Uri.ToString()); - + // TODO bring back // if (savedFile != null) // { // if (this.editorSession.RemoteFileManager.IsUnderRemoteTempPath(savedFile.FilePath)) @@ -165,242 +163,5 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS IsReload = false }; } - - private Task RunScriptDiagnosticsAsync( - ScriptFile[] filesToAnalyze) - { - // If there's an existing task, attempt to cancel it - try - { - if (s_existingRequestCancellation != null) - { - // Try to cancel the request - s_existingRequestCancellation.Cancel(); - - // If cancellation didn't throw an exception, - // clean up the existing token - s_existingRequestCancellation.Dispose(); - s_existingRequestCancellation = null; - } - } - catch (Exception e) - { - // TODO: Catch a more specific exception! - _logger.LogError( - string.Format( - "Exception while canceling analysis task:\n\n{0}", - e.ToString())); - - TaskCompletionSource cancelTask = new TaskCompletionSource(); - cancelTask.SetCanceled(); - return cancelTask.Task; - } - - // If filesToAnalzye is empty, nothing to do so return early. - if (filesToAnalyze.Length == 0) - { - return Task.FromResult(true); - } - - // Create a fresh cancellation token and then start the task. - // We create this on a different TaskScheduler so that we - // don't block the main message loop thread. - // TODO: Is there a better way to do this? - s_existingRequestCancellation = new CancellationTokenSource(); - // TODO use settings service - Task.Factory.StartNew( - () => - DelayThenInvokeDiagnosticsAsync( - 750, - filesToAnalyze, - true, - this.codeActionsPerFile, - _logger, - s_existingRequestCancellation.Token), - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default); - - return Task.FromResult(true); - } - - private async Task DelayThenInvokeDiagnosticsAsync( - int delayMilliseconds, - ScriptFile[] filesToAnalyze, - bool isScriptAnalysisEnabled, - Dictionary> correctionIndex, - ILogger Logger, - CancellationToken cancellationToken) - { - // First of all, wait for the desired delay period before - // analyzing the provided list of files - try - { - await Task.Delay(delayMilliseconds, cancellationToken); - } - catch (TaskCanceledException) - { - // If the task is cancelled, exit directly - foreach (var script in filesToAnalyze) - { - PublishScriptDiagnostics( - script, - script.DiagnosticMarkers, - correctionIndex); - } - - return; - } - - // If we've made it past the delay period then we don't care - // about the cancellation token anymore. This could happen - // when the user stops typing for long enough that the delay - // period ends but then starts typing while analysis is going - // on. It makes sense to send back the results from the first - // delay period while the second one is ticking away. - - // Get the requested files - foreach (ScriptFile scriptFile in filesToAnalyze) - { - List semanticMarkers = null; - if (isScriptAnalysisEnabled && _analysisService != null) - { - semanticMarkers = await _analysisService.GetSemanticMarkersAsync(scriptFile); - } - else - { - // Semantic markers aren't available if the AnalysisService - // isn't available - semanticMarkers = new List(); - } - - scriptFile.DiagnosticMarkers.AddRange(semanticMarkers); - - PublishScriptDiagnostics( - scriptFile, - // Concat script analysis errors to any existing parse errors - scriptFile.DiagnosticMarkers, - correctionIndex); - } - } - - private void ClearMarkers(ScriptFile scriptFile) - { - // send empty diagnostic markers to clear any markers associated with the given file - PublishScriptDiagnostics( - scriptFile, - new List(), - this.codeActionsPerFile); - } - - private void PublishScriptDiagnostics( - ScriptFile scriptFile, - List markers, - Dictionary> correctionIndex) - { - List diagnostics = new List(); - - // Hold on to any corrections that may need to be applied later - Dictionary fileCorrections = - new Dictionary(); - - foreach (var marker in markers) - { - // Does the marker contain a correction? - Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); - if (marker.Correction != null) - { - string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); - fileCorrections.Add(diagnosticId, marker.Correction); - } - - diagnostics.Add(markerDiagnostic); - } - - correctionIndex[scriptFile.DocumentUri] = fileCorrections; - - var uriBuilder = new UriBuilder() - { - Scheme = Uri.UriSchemeFile, - Path = scriptFile.FilePath, - Host = string.Empty, - }; - - // Always send syntax and semantic errors. We want to - // make sure no out-of-date markers are being displayed. - _languageServer.Document.PublishDiagnostics(new PublishDiagnosticsParams() - { - Uri = uriBuilder.Uri, - Diagnostics = new Container(diagnostics), - }); - } - - // Generate a unique id that is used as a key to look up the associated code action (code fix) when - // we receive and process the textDocument/codeAction message. - private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) - { - Position start = diagnostic.Range.Start; - Position end = diagnostic.Range.End; - - var sb = new StringBuilder(256) - .Append(diagnostic.Source ?? "?") - .Append("_") - .Append(diagnostic.Code.ToString()) - .Append("_") - .Append(diagnostic.Severity?.ToString() ?? "?") - .Append("_") - .Append(start.Line) - .Append(":") - .Append(start.Character) - .Append("-") - .Append(end.Line) - .Append(":") - .Append(end.Character); - - var id = sb.ToString(); - return id; - } - - private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) - { - return new Diagnostic - { - Severity = MapDiagnosticSeverity(scriptFileMarker.Level), - Message = scriptFileMarker.Message, - Code = scriptFileMarker.RuleName, - Source = scriptFileMarker.Source, - Range = new Range - { - Start = new Position - { - Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 - } - } - }; - } - - private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) - { - switch (markerLevel) - { - case ScriptFileMarkerLevel.Error: - return DiagnosticSeverity.Error; - - case ScriptFileMarkerLevel.Warning: - return DiagnosticSeverity.Warning; - - case ScriptFileMarkerLevel.Information: - return DiagnosticSeverity.Information; - - default: - return DiagnosticSeverity.Error; - } - } } } diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs new file mode 100644 index 000000000..888b18ebe --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs @@ -0,0 +1,13 @@ +// +// 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 +{ + public class ConfigurationService + { + // This probably needs some sort of lock... or maybe LanguageServerSettings needs it. + public LanguageServerSettings CurrentSettings { get; } = new LanguageServerSettings(); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs new file mode 100644 index 000000000..b97485dc0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace Microsoft.PowerShell.EditorServices +{ + public class ConfigurationHandler : IDidChangeConfigurationHandler + { + private readonly ILogger _logger; + private readonly AnalysisService _analysisService; + private readonly WorkspaceService _workspaceService; + private readonly ConfigurationService _configurationService; + private DidChangeConfigurationCapability _capability; + + + public ConfigurationHandler(ILoggerFactory factory, WorkspaceService workspaceService, AnalysisService analysisService, ConfigurationService configurationService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _analysisService = analysisService; + _configurationService = configurationService; + } + + public object GetRegistrationOptions() + { + return null; + } + + public async Task Handle(DidChangeConfigurationParams request, CancellationToken cancellationToken) + { + LanguageServerSettingsWrapper incomingSettings = request.Settings.ToObject(); + if(incomingSettings == null) + { + return await Unit.Task; + } + // TODO ADD THIS BACK IN + // bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; + bool oldScriptAnalysisEnabled = + _configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false; + string oldScriptAnalysisSettingsPath = + _configurationService.CurrentSettings.ScriptAnalysis?.SettingsPath; + + _configurationService.CurrentSettings.Update( + incomingSettings.Powershell, + _workspaceService.WorkspacePath, + _logger); + + // TODO ADD THIS BACK IN + // if (!this.profilesLoaded && + // this.currentSettings.EnableProfileLoading && + // oldLoadProfiles != this.currentSettings.EnableProfileLoading) + // { + // await this.editorSession.PowerShellContext.LoadHostProfilesAsync(); + // this.profilesLoaded = true; + // } + + // // Wait until after profiles are loaded (or not, if that's the + // // case) before starting the interactive console. + // if (!this.consoleReplStarted) + // { + // // Start the interactive terminal + // this.editorSession.HostInput.StartCommandLoop(); + // this.consoleReplStarted = true; + // } + + // If there is a new settings file path, restart the analyzer with the new settigs. + bool settingsPathChanged = false; + string newSettingsPath = _configurationService.CurrentSettings.ScriptAnalysis.SettingsPath; + if (!string.Equals(oldScriptAnalysisSettingsPath, newSettingsPath, StringComparison.OrdinalIgnoreCase)) + { + if (_analysisService != null) + { + _analysisService.SettingsPath = newSettingsPath; + settingsPathChanged = true; + } + } + + // If script analysis settings have changed we need to clear & possibly update the current diagnostic records. + if ((oldScriptAnalysisEnabled != _configurationService.CurrentSettings.ScriptAnalysis?.Enable) || settingsPathChanged) + { + // If the user just turned off script analysis or changed the settings path, send a diagnostics + // event to clear the analysis markers that they already have. + if (!_configurationService.CurrentSettings.ScriptAnalysis.Enable.Value || settingsPathChanged) + { + foreach (var scriptFile in _workspaceService.GetOpenedFiles()) + { + _analysisService.ClearMarkers(scriptFile); + } + } + } + + // Convert the editor file glob patterns into an array for the Workspace + // Both the files.exclude and search.exclude hash tables look like (glob-text, is-enabled): + // "files.exclude" : { + // "Makefile": true, + // "*.html": true, + // "build/*": true + // } + var excludeFilePatterns = new List(); + if (incomingSettings.Files?.Exclude != null) + { + foreach(KeyValuePair patternEntry in incomingSettings.Files.Exclude) + { + if (patternEntry.Value) { excludeFilePatterns.Add(patternEntry.Key); } + } + } + if (incomingSettings.Search?.Exclude != null) + { + foreach(KeyValuePair patternEntry in incomingSettings.Files.Exclude) + { + if (patternEntry.Value && !excludeFilePatterns.Contains(patternEntry.Key)) { excludeFilePatterns.Add(patternEntry.Key); } + } + } + _workspaceService.ExcludeFilesGlob = excludeFilePatterns; + + // Convert the editor file search options to Workspace properties + if (incomingSettings.Search?.FollowSymlinks != null) + { + _workspaceService.FollowSymlinks = incomingSettings.Search.FollowSymlinks; + } + + return await Unit.Task; + } + + public void SetCapability(DidChangeConfigurationCapability capability) + { + _capability = capability; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs new file mode 100644 index 000000000..7f2b3dfee --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs @@ -0,0 +1,389 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Security; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + public class LanguageServerSettings + { + private readonly object updateLock = new object(); + public bool EnableProfileLoading { get; set; } + + public ScriptAnalysisSettings ScriptAnalysis { get; set; } + + public CodeFormattingSettings CodeFormatting { get; set; } + + public CodeFoldingSettings CodeFolding { get; set; } + + public LanguageServerSettings() + { + this.ScriptAnalysis = new ScriptAnalysisSettings(); + this.CodeFormatting = new CodeFormattingSettings(); + this.CodeFolding = new CodeFoldingSettings(); + } + + public void Update( + LanguageServerSettings settings, + string workspaceRootPath, + ILogger logger) + { + if (settings != null) + { + lock (updateLock) + { + this.EnableProfileLoading = settings.EnableProfileLoading; + this.ScriptAnalysis.Update( + settings.ScriptAnalysis, + workspaceRootPath, + logger); + this.CodeFormatting = new CodeFormattingSettings(settings.CodeFormatting); + this.CodeFolding.Update(settings.CodeFolding, logger); + } + } + } + } + + public class ScriptAnalysisSettings + { + private readonly object updateLock = new object(); + + public bool? Enable { get; set; } + + public string SettingsPath { get; set; } + + public ScriptAnalysisSettings() + { + this.Enable = true; + } + + public void Update( + ScriptAnalysisSettings settings, + string workspaceRootPath, + ILogger logger) + { + if (settings != null) + { + lock(updateLock) + { + this.Enable = settings.Enable; + + string settingsPath = settings.SettingsPath; + + try + { + if (string.IsNullOrWhiteSpace(settingsPath)) + { + settingsPath = null; + } + else if (!Path.IsPathRooted(settingsPath)) + { + if (string.IsNullOrEmpty(workspaceRootPath)) + { + // The workspace root path could be an empty string + // when the user has opened a PowerShell script file + // without opening an entire folder (workspace) first. + // In this case we should just log an error and let + // the specified settings path go through even though + // it will fail to load. + logger.LogError( + "Could not resolve Script Analyzer settings path due to null or empty workspaceRootPath."); + } + else + { + settingsPath = Path.GetFullPath(Path.Combine(workspaceRootPath, settingsPath)); + } + } + + this.SettingsPath = settingsPath; + logger.LogTrace($"Using Script Analyzer settings path - '{settingsPath ?? ""}'."); + } + catch (Exception ex) when ( + ex is NotSupportedException || + ex is PathTooLongException || + ex is SecurityException) + { + // Invalid chars in path like ${env:HOME} can cause Path.GetFullPath() to throw, catch such errors here + logger.LogException( + $"Invalid Script Analyzer settings path - '{settingsPath}'.", + ex); + + this.SettingsPath = null; + } + } + } + } + } + + /// + /// Code formatting presets. + /// See https://en.wikipedia.org/wiki/Indent_style for details on indent and brace styles. + /// + public enum CodeFormattingPreset + { + /// + /// Use the formatting settings as-is. + /// + Custom, + + /// + /// Configure the formatting settings to resemble the Allman indent/brace style. + /// + Allman, + + /// + /// Configure the formatting settings to resemble the one true brace style variant of K&R indent/brace style. + /// + OTBS, + + /// + /// Configure the formatting settings to resemble the Stroustrup brace style variant of K&R indent/brace style. + /// + Stroustrup + } + + /// + /// Multi-line pipeline style settings. + /// + public enum PipelineIndentationStyle + { + /// + /// After the indentation level only once after the first pipeline and keep this level for the following pipelines. + /// + IncreaseIndentationForFirstPipeline, + + /// + /// After every pipeline, keep increasing the indentation. + /// + IncreaseIndentationAfterEveryPipeline, + + /// + /// Do not increase indentation level at all after pipeline. + /// + NoIndentation + } + + public class CodeFormattingSettings + { + /// + /// Default constructor. + /// > + public CodeFormattingSettings() + { + } + + /// + /// Copy constructor. + /// + /// An instance of type CodeFormattingSettings. + public CodeFormattingSettings(CodeFormattingSettings codeFormattingSettings) + { + if (codeFormattingSettings == null) + { + throw new ArgumentNullException(nameof(codeFormattingSettings)); + } + + foreach (var prop in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + prop.SetValue(this, prop.GetValue(codeFormattingSettings)); + } + } + + public CodeFormattingPreset Preset { get; set; } + public bool OpenBraceOnSameLine { get; set; } + public bool NewLineAfterOpenBrace { get; set; } + public bool NewLineAfterCloseBrace { get; set; } + public PipelineIndentationStyle PipelineIndentationStyle { get; set; } + public bool WhitespaceBeforeOpenBrace { get; set; } + public bool WhitespaceBeforeOpenParen { get; set; } + public bool WhitespaceAroundOperator { get; set; } + public bool WhitespaceAfterSeparator { get; set; } + public bool WhitespaceInsideBrace { get; set; } + public bool WhitespaceAroundPipe { get; set; } + public bool IgnoreOneLineBlock { get; set; } + public bool AlignPropertyValuePairs { get; set; } + public bool UseCorrectCasing { get; set; } + + + /// + /// Get the settings hashtable that will be consumed by PSScriptAnalyzer. + /// + /// The tab size in the number spaces. + /// If true, insert spaces otherwise insert tabs for indentation. + /// + public Hashtable GetPSSASettingsHashtable( + int tabSize, + bool insertSpaces) + { + var settings = GetCustomPSSASettingsHashtable(tabSize, insertSpaces); + var ruleSettings = (Hashtable)(settings["Rules"]); + var closeBraceSettings = (Hashtable)ruleSettings["PSPlaceCloseBrace"]; + var openBraceSettings = (Hashtable)ruleSettings["PSPlaceOpenBrace"]; + switch(Preset) + { + case CodeFormattingPreset.Allman: + openBraceSettings["OnSameLine"] = false; + openBraceSettings["NewLineAfter"] = true; + closeBraceSettings["NewLineAfter"] = true; + break; + + case CodeFormattingPreset.OTBS: + openBraceSettings["OnSameLine"] = true; + openBraceSettings["NewLineAfter"] = true; + closeBraceSettings["NewLineAfter"] = false; + break; + + case CodeFormattingPreset.Stroustrup: + openBraceSettings["OnSameLine"] = true; + openBraceSettings["NewLineAfter"] = true; + closeBraceSettings["NewLineAfter"] = true; + break; + + default: + break; + } + + return settings; + } + + private Hashtable GetCustomPSSASettingsHashtable(int tabSize, bool insertSpaces) + { + return new Hashtable + { + {"IncludeRules", new string[] { + "PSPlaceCloseBrace", + "PSPlaceOpenBrace", + "PSUseConsistentWhitespace", + "PSUseConsistentIndentation", + "PSAlignAssignmentStatement" + }}, + {"Rules", new Hashtable { + {"PSPlaceOpenBrace", new Hashtable { + {"Enable", true}, + {"OnSameLine", OpenBraceOnSameLine}, + {"NewLineAfter", NewLineAfterOpenBrace}, + {"IgnoreOneLineBlock", IgnoreOneLineBlock} + }}, + {"PSPlaceCloseBrace", new Hashtable { + {"Enable", true}, + {"NewLineAfter", NewLineAfterCloseBrace}, + {"IgnoreOneLineBlock", IgnoreOneLineBlock} + }}, + {"PSUseConsistentIndentation", new Hashtable { + {"Enable", true}, + {"IndentationSize", tabSize}, + {"PipelineIndentation", PipelineIndentationStyle }, + {"Kind", insertSpaces ? "space" : "tab"} + }}, + {"PSUseConsistentWhitespace", new Hashtable { + {"Enable", true}, + {"CheckOpenBrace", WhitespaceBeforeOpenBrace}, + {"CheckOpenParen", WhitespaceBeforeOpenParen}, + {"CheckOperator", WhitespaceAroundOperator}, + {"CheckSeparator", WhitespaceAfterSeparator}, + {"CheckInnerBrace", WhitespaceInsideBrace}, + {"CheckPipe", WhitespaceAroundPipe}, + }}, + {"PSAlignAssignmentStatement", new Hashtable { + {"Enable", true}, + {"CheckHashtable", AlignPropertyValuePairs} + }}, + {"PSUseCorrectCasing", new Hashtable { + {"Enable", UseCorrectCasing} + }}, + }} + }; + } + } + + /// + /// Code folding settings + /// + public class CodeFoldingSettings + { + /// + /// Whether the folding is enabled. Default is true as per VSCode + /// + public bool Enable { get; set; } = true; + + /// + /// Whether to show or hide the last line of a folding region. Default is true as per VSCode + /// + public bool ShowLastLine { get; set; } = true; + + /// + /// Update these settings from another settings object + /// + public void Update( + CodeFoldingSettings settings, + ILogger logger) + { + if (settings != null) { + if (this.Enable != settings.Enable) { + this.Enable = settings.Enable; + logger.LogTrace(string.Format("Using Code Folding Enabled - {0}", this.Enable)); + } + if (this.ShowLastLine != settings.ShowLastLine) { + this.ShowLastLine = settings.ShowLastLine; + logger.LogTrace(string.Format("Using Code Folding ShowLastLine - {0}", this.ShowLastLine)); + } + } + } + } + + /// + /// Additional settings from the Language Client that affect Language Server operations but + /// do not exist under the 'powershell' section + /// + public class EditorFileSettings + { + /// + /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the + /// the glob is in effect. + /// + public Dictionary Exclude { get; set; } + } + + /// + /// Additional settings from the Language Client that affect Language Server operations but + /// do not exist under the 'powershell' section + /// + public class EditorSearchSettings + { + /// + /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the + /// the glob is in effect. + /// + public Dictionary Exclude { get; set; } + /// + /// Whether to follow symlinks when searching + /// + public bool FollowSymlinks { get; set; } = true; + } + + public class LanguageServerSettingsWrapper + { + // NOTE: This property is capitalized as 'Powershell' because the + // mode name sent from the client is written as 'powershell' and + // JSON.net is using camelCasing. + public LanguageServerSettings Powershell { get; set; } + + // NOTE: This property is capitalized as 'Files' because the + // mode name sent from the client is written as 'files' and + // JSON.net is using camelCasing. + public EditorFileSettings Files { get; set; } + + // NOTE: This property is capitalized as 'Search' because the + // mode name sent from the client is written as 'search' and + // JSON.net is using camelCasing. + public EditorSearchSettings Search { get; set; } + } +} diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 5f3f6006a..17a65fa9e 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -117,7 +117,7 @@ function Get-Foo { # ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } - It "Can get Diagnostics" { + 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 @@ -129,13 +129,48 @@ function Get-Foo { # to increment the counter. Get-LspResponse -Client $client -Id $request.Id | Out-Null - $notifications = Get-LspNotification -Client $client + # 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" { + $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 + + # Throw out any notifications from the first PSScriptAnalyzer run. + Get-LspNotification -Client $client | Out-Null + + $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 + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index e0201c50e..a639162b3 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -75,6 +75,7 @@ FunctionsToExport = @( 'Send-LspRequest', 'Send-LspInitializeRequest', 'Send-LspDidOpenTextDocumentRequest', + 'Send-LspDidChangeConfigurationRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', 'Get-LspResponse' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 76cab02bf..04f77f605 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -294,6 +294,31 @@ function Send-LspDidOpenTextDocumentRequest $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-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] From 32ef5b9d72e5344dd90b3aff65374f3e4fa41d74 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 26 Jul 2019 10:07:43 -0700 Subject: [PATCH 13/47] Folding support (#20) * Added Diagnostics * didChangeConfiguration message and general settings support * initial folding support * log level trace * folding works with latest omnisharp version * comment typo * added test for folding --- NuGet.Config | 3 + .../Hosting/EditorServicesHost.cs | 3 +- .../LanguageServer/OmnisharpLanguageServer.cs | 10 +- .../PowerShellEditorServices.Engine.csproj | 2 +- .../Services/TextDocument/FoldingReference.cs | 113 +++++++++ .../Handlers/FoldingRangeHandler.cs | 77 +++++++ .../Services/TextDocument/TokenOperations.cs | 216 ++++++++++++++++++ .../EditorServices.Integration.Tests.ps1 | 44 ++++ 8 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs diff --git a/NuGet.Config b/NuGet.Config index 6efc7f7b9..79196aec8 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,4 +3,7 @@ + + + diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 4f810bf22..9512b7918 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -241,7 +241,8 @@ public void StartLanguageService( { NamedPipeName = config.InOutPipeName ?? config.InPipeName, OutNamedPipeName = config.OutPipeName, - LoggerFactory = _factory + LoggerFactory = _factory, + MinimumLogLevel = LogLevel.Trace, } .BuildLanguageServer(); diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 92970bef2..67d030741 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -68,10 +68,12 @@ public async Task StartAsync() options.LoggerFactory = _configuration.LoggerFactory; options.MinimumLogLevel = _configuration.MinimumLogLevel; options.Services = _configuration.Services; - options.WithHandler(); - options.WithHandler(); - options.WithHandler(); - options.WithHandler(); + options + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index 95fb48490..c0fb4a3df 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs new file mode 100644 index 000000000..2bfe4874e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// A class that holds the information for a foldable region of text in a document + /// + public class FoldingReference: IComparable + { + /// + /// The zero-based line number from where the folded range starts. + /// + public int StartLine { get; set; } + + /// + /// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + /// + public int StartCharacter { get; set; } = 0; + + /// + /// The zero-based line number where the folded range ends. + /// + public int EndLine { get; set; } + + /// + /// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. + /// + public int EndCharacter { get; set; } = 0; + + /// + /// Describes the kind of the folding range such as `comment' or 'region'. + /// + public FoldingRangeKind? Kind { get; set; } + + /// + /// A custom comparable method which can properly sort FoldingReference objects + /// + public int CompareTo(FoldingReference that) { + // Initially look at the start line + if (this.StartLine < that.StartLine) { return -1; } + if (this.StartLine > that.StartLine) { return 1; } + + // They have the same start line so now consider the end line. + // The biggest line range is sorted first + if (this.EndLine > that.EndLine) { return -1; } + if (this.EndLine < that.EndLine) { return 1; } + + // They have the same lines, but what about character offsets + if (this.StartCharacter < that.StartCharacter) { return -1; } + if (this.StartCharacter > that.StartCharacter) { return 1; } + if (this.EndCharacter < that.EndCharacter) { return -1; } + if (this.EndCharacter > that.EndCharacter) { return 1; } + + // They're the same range, but what about kind + return that.Kind.Value - this.Kind.Value; + } + } + + /// + /// A class that holds a list of FoldingReferences and ensures that when adding a reference that the + /// folding rules are obeyed, e.g. Only one fold per start line + /// + public class FoldingReferenceList + { + private readonly Dictionary references = new Dictionary(); + + /// + /// Return all references in the list + /// + public IEnumerable References + { + get + { + return references.Values; + } + } + + /// + /// Adds a FoldingReference to the list and enforces ordering rules e.g. Only one fold per start line + /// + public void SafeAdd(FoldingReference item) + { + if (item == null) { return; } + + // Only add the item if it hasn't been seen before or it's the largest range + if (references.TryGetValue(item.StartLine, out FoldingReference currentItem)) + { + if (currentItem.CompareTo(item) == 1) { references[item.StartLine] = item; } + } + else + { + references[item.StartLine] = item; + } + } + + /// + /// Helper method to easily convert the Dictionary Values into an array + /// + public FoldingReference[] ToArray() + { + var result = new FoldingReference[references.Count]; + references.Values.CopyTo(result, 0); + return result; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs new file mode 100644 index 000000000..9aee91b0e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class FoldingRangeHandler : IFoldingRangeHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly ConfigurationService _configurationService; + private readonly WorkspaceService _workspaceService; + + private FoldingRangeCapability _capability; + + public FoldingRangeHandler(ILoggerFactory factory, ConfigurationService configurationService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _configurationService = configurationService; + _workspaceService = workspaceService; + } + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions() + { + DocumentSelector = _documentSelector, + }; + } + + public Task> Handle(FoldingRangeRequestParam request, CancellationToken cancellationToken) + { + // TODO Should be using dynamic registrations + if (!_configurationService.CurrentSettings.CodeFolding.Enable) { return null; } + + // Avoid crash when using untitled: scheme or any other scheme where the document doesn't + // have a backing file. https://github.com/PowerShell/vscode-powershell/issues/1676 + // Perhaps a better option would be to parse the contents of the document as a string + // as opposed to reading a file but the scenario of "no backing file" probably doesn't + // warrant the extra effort. + if (!_workspaceService.TryGetFile(request.TextDocument.Uri.ToString(), out ScriptFile scriptFile)) { return null; } + + var result = new List(); + + // If we're showing the last line, decrement the Endline of all regions by one. + int endLineOffset = _configurationService.CurrentSettings.CodeFolding.ShowLastLine ? -1 : 0; + + foreach (FoldingReference fold in TokenOperations.FoldableReferences(scriptFile.ScriptTokens).References) + { + result.Add(new FoldingRange { + EndCharacter = fold.EndCharacter, + EndLine = fold.EndLine + endLineOffset, + Kind = fold.Kind, + StartCharacter = fold.StartCharacter, + StartLine = fold.StartLine + }); + } + + return Task.FromResult(new Container(result)); + } + + public void SetCapability(FoldingRangeCapability capability) + { + _capability = capability; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs new file mode 100644 index 000000000..bffd0991a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs @@ -0,0 +1,216 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; +using System.Text.RegularExpressions; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices +{ + + /// + /// Provides common operations for the tokens of a parsed script. + /// + internal static class TokenOperations + { + // Region kinds to align with VSCode's region kinds + private const string RegionKindComment = "comment"; + private const string RegionKindRegion = "region"; + private static readonly FoldingRangeKind? RegionKindNone = null; + + // These regular expressions are used to match lines which mark the start and end of region comment in a PowerShell + // script. They are based on the defaults in the VS Code Language Configuration at; + // https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31 + // https://github.com/Microsoft/vscode/issues/49070 + static private readonly Regex s_startRegionTextRegex = new Regex( + @"^\s*#[rR]egion\b", RegexOptions.Compiled); + static private readonly Regex s_endRegionTextRegex = new Regex( + @"^\s*#[eE]nd[rR]egion\b", RegexOptions.Compiled); + + /// + /// Extracts all of the unique foldable regions in a script given the list tokens + /// + internal static FoldingReferenceList FoldableReferences( + Token[] tokens) + { + var refList = new FoldingReferenceList(); + + Stack tokenCurlyStack = new Stack(); + Stack tokenParenStack = new Stack(); + foreach (Token token in tokens) + { + switch (token.Kind) + { + // Find matching braces { -> } + // Find matching hashes @{ -> } + case TokenKind.LCurly: + case TokenKind.AtCurly: + tokenCurlyStack.Push(token); + break; + + case TokenKind.RCurly: + if (tokenCurlyStack.Count > 0) + { + refList.SafeAdd(CreateFoldingReference(tokenCurlyStack.Pop(), token, RegionKindNone)); + } + break; + + // Find matching parentheses ( -> ) + // Find matching array literals @( -> ) + // Find matching subexpressions $( -> ) + case TokenKind.LParen: + case TokenKind.AtParen: + case TokenKind.DollarParen: + tokenParenStack.Push(token); + break; + + case TokenKind.RParen: + if (tokenParenStack.Count > 0) + { + refList.SafeAdd(CreateFoldingReference(tokenParenStack.Pop(), token, RegionKindNone)); + } + break; + + // Find contiguous here strings @' -> '@ + // Find unopinionated variable names ${ \n \n } + // Find contiguous expandable here strings @" -> "@ + case TokenKind.HereStringLiteral: + case TokenKind.Variable: + case TokenKind.HereStringExpandable: + if (token.Extent.StartLineNumber != token.Extent.EndLineNumber) + { + refList.SafeAdd(CreateFoldingReference(token, token, RegionKindNone)); + } + break; + } + } + + // Find matching comment regions #region -> #endregion + // Given a list of tokens, find the tokens that are comments and + // the comment text is either `#region` or `#endregion`, and then use a stack to determine + // the ranges they span + // + // Find blocks of line comments # comment1\n# comment2\n... + // Finding blocks of comment tokens is more complicated as the newline characters are not + // classed as comments. To workaround this we search for valid block comments (See IsBlockCmment) + // and then determine contiguous line numbers from there + // + // Find comments regions <# -> #> + // Match the token start and end of kind TokenKind.Comment + var tokenCommentRegionStack = new Stack(); + Token blockStartToken = null; + int blockNextLine = -1; + + for (int index = 0; index < tokens.Length; index++) + { + Token token = tokens[index]; + if (token.Kind != TokenKind.Comment) { continue; } + + // Processing for comment regions <# -> #> + if (token.Extent.StartLineNumber != token.Extent.EndLineNumber) + { + refList.SafeAdd(CreateFoldingReference(token, token, FoldingRangeKind.Comment)); + continue; + } + + if (!IsBlockComment(index, tokens)) { continue; } + + // Regex's are very expensive. Use them sparingly! + // Processing for #region -> #endregion + if (s_startRegionTextRegex.IsMatch(token.Text)) + { + tokenCommentRegionStack.Push(token); + continue; + } + if (s_endRegionTextRegex.IsMatch(token.Text)) + { + // Mismatched regions in the script can cause bad stacks. + if (tokenCommentRegionStack.Count > 0) + { + refList.SafeAdd(CreateFoldingReference(tokenCommentRegionStack.Pop(), token, FoldingRangeKind.Region)); + } + continue; + } + + // If it's neither a start or end region then it could be block line comment + // Processing for blocks of line comments # comment1\n# comment2\n... + int thisLine = token.Extent.StartLineNumber - 1; + if ((blockStartToken != null) && (thisLine != blockNextLine)) + { + refList.SafeAdd(CreateFoldingReference(blockStartToken, blockNextLine - 1, FoldingRangeKind.Comment)); + blockStartToken = token; + } + if (blockStartToken == null) { blockStartToken = token; } + blockNextLine = thisLine + 1; + } + + // If we exit the token array and we're still processing comment lines, then the + // comment block simply ends at the end of document + if (blockStartToken != null) + { + refList.SafeAdd(CreateFoldingReference(blockStartToken, blockNextLine - 1, FoldingRangeKind.Comment)); + } + + return refList; + } + + /// + /// Creates an instance of a FoldingReference object from a start and end langauge Token + /// Returns null if the line range is invalid + /// + static private FoldingReference CreateFoldingReference( + Token startToken, + Token endToken, + FoldingRangeKind? matchKind) + { + if (endToken.Extent.EndLineNumber == startToken.Extent.StartLineNumber) { return null; } + // Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions + return new FoldingReference { + StartLine = startToken.Extent.StartLineNumber - 1, + StartCharacter = startToken.Extent.StartColumnNumber - 1, + EndLine = endToken.Extent.EndLineNumber - 1, + EndCharacter = endToken.Extent.EndColumnNumber - 1, + Kind = matchKind + }; + } + + /// + /// Creates an instance of a FoldingReference object from a start token and an end line + /// Returns null if the line range is invalid + /// + static private FoldingReference CreateFoldingReference( + Token startToken, + int endLine, + FoldingRangeKind? matchKind) + { + if (endLine == (startToken.Extent.StartLineNumber - 1)) { return null; } + // Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions + return new FoldingReference { + StartLine = startToken.Extent.StartLineNumber - 1, + StartCharacter = startToken.Extent.StartColumnNumber - 1, + EndLine = endLine, + EndCharacter = 0, + Kind = matchKind + }; + } + + /// + /// Returns true if a Token is a block comment; + /// - Must be a TokenKind.comment + /// - Must be preceeded by TokenKind.NewLine + /// - Token text must start with a '#'.false This is because comment regions + /// start with '<#' but have the same TokenKind + /// + static private bool IsBlockComment(int index, Token[] tokens) { + Token thisToken = tokens[index]; + if (thisToken.Kind != TokenKind.Comment) { return false; } + if (index == 0) { return true; } + if (tokens[index - 1].Kind != TokenKind.NewLine) { return false; } + return thisToken.Text.StartsWith("#"); + } + } +} diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 17a65fa9e..c51c62cc0 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -171,6 +171,50 @@ function Get-Foo { $notifications.Params.diagnostics | Should -BeNullOrEmpty } + It "Can handle folding 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 + + # Throw out any notifications from the first PSScriptAnalyzer run. + Get-LspNotification -Client $client | Out-Null + + + + $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($file.PSPath).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 + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client From a48f56a99dc48163fe81347242bd1220505bc855 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 11 Jul 2019 18:17:59 -0700 Subject: [PATCH 14/47] Added Diagnostics --- .../Services/TextDocument/Handlers/TextDocumentHandler.cs | 4 +++- test/Pester/EditorServices.Integration.Tests.ps1 | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index 2c5ed0a45..8ba2e04fb 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -18,6 +18,7 @@ class TextDocumentHandler : ITextDocumentSyncHandler { private readonly ILogger _logger; + private readonly ILanguageServer _languageServer; private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; @@ -37,9 +38,10 @@ class TextDocumentHandler : ITextDocumentSyncHandler public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental; - public TextDocumentHandler(ILoggerFactory factory, AnalysisService analysisService, WorkspaceService workspaceService) + public TextDocumentHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService, WorkspaceService workspaceService) { _logger = factory.CreateLogger(); + _languageServer = languageServer; _analysisService = analysisService; _workspaceService = workspaceService; } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index c51c62cc0..de9ae5102 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -117,7 +117,11 @@ function Get-Foo { # ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } +<<<<<<< HEAD It "Can get Diagnostics after opening a text document" { +======= + It "Can get Diagnostics" { +>>>>>>> Added Diagnostics $script = '$a = 4' $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $script -PassThru -Force From 89904c8a0adeb4e59d3ff9520d2e6ffd0d8296ad Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 16 Jul 2019 10:49:14 -0700 Subject: [PATCH 15/47] didChangeConfiguration message and general settings support --- .../TextDocument/Handlers/TextDocumentHandler.cs | 9 +-------- .../Services/Workspace/LanguageServerSettings.cs | 4 ++++ test/Pester/EditorServices.Integration.Tests.ps1 | 4 ---- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index 8ba2e04fb..34f5358b9 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -18,15 +18,9 @@ class TextDocumentHandler : ITextDocumentSyncHandler { private readonly ILogger _logger; - private readonly ILanguageServer _languageServer; private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; - private Dictionary> codeActionsPerFile = - new Dictionary>(); - - private static CancellationTokenSource s_existingRequestCancellation; - private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { @@ -38,10 +32,9 @@ class TextDocumentHandler : ITextDocumentSyncHandler public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental; - public TextDocumentHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService, WorkspaceService workspaceService) + public TextDocumentHandler(ILoggerFactory factory, AnalysisService analysisService, WorkspaceService workspaceService) { _logger = factory.CreateLogger(); - _languageServer = languageServer; _analysisService = analysisService; _workspaceService = workspaceService; } diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs index 7f2b3dfee..8a9cbfd25 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs @@ -178,6 +178,10 @@ public class CodeFormattingSettings /// > public CodeFormattingSettings() { +<<<<<<< HEAD +======= + +>>>>>>> didChangeConfiguration message and general settings support } /// diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index de9ae5102..c51c62cc0 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -117,11 +117,7 @@ function Get-Foo { # ReportLogErrors -LogPath $psesServer.LogPath -FromIndex ([ref]$logIdx) } -<<<<<<< HEAD It "Can get Diagnostics after opening a text document" { -======= - It "Can get Diagnostics" { ->>>>>>> Added Diagnostics $script = '$a = 4' $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $script -PassThru -Force From aa5d58265acd2a576b3dbcc62c6332c01f771f1d Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 16 Jul 2019 16:17:30 -0700 Subject: [PATCH 16/47] initial folding support --- .../Hosting/EditorServicesHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 9512b7918..b52323d40 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -217,7 +217,7 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - while (System.Diagnostics.Debugger.IsAttached) + while (!System.Diagnostics.Debugger.IsAttached) { Console.WriteLine($"{Process.GetCurrentProcess().Id}"); Thread.Sleep(2000); From 6e0ecbc2aa16ee8e2ca0452f78becd79e588130f Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 18 Jul 2019 15:26:43 -0700 Subject: [PATCH 17/47] added test for folding --- .../Hosting/EditorServicesHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index b52323d40..9512b7918 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -217,7 +217,7 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - while (!System.Diagnostics.Debugger.IsAttached) + while (System.Diagnostics.Debugger.IsAttached) { Console.WriteLine($"{Process.GetCurrentProcess().Id}"); Thread.Sleep(2000); From 9a273f491b1b63d3e505979e0f94b5f9eb12265e Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 26 Jul 2019 10:15:05 -0700 Subject: [PATCH 18/47] formatting support --- .../LanguageServer/OmnisharpLanguageServer.cs | 4 +- .../Handlers/FormattingHandlers.cs | 174 ++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 67d030741..73a6d8186 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -73,7 +73,9 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler() + .WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs new file mode 100644 index 000000000..b66e581de --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class DocumentFormattingHandler : IDocumentFormattingHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly AnalysisService _analysisService; + private readonly ConfigurationService _configurationService; + private readonly WorkspaceService _workspaceService; + private DocumentFormattingCapability _capability; + + public DocumentFormattingHandler(ILoggerFactory factory, AnalysisService analysisService, ConfigurationService configurationService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _analysisService = analysisService; + _configurationService = configurationService; + _workspaceService = workspaceService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector + }; + } + + public async Task Handle(DocumentFormattingParams request, CancellationToken cancellationToken) + { + var scriptFile = _workspaceService.GetFile(request.TextDocument.Uri.ToString()); + var pssaSettings = _configurationService.CurrentSettings.CodeFormatting.GetPSSASettingsHashtable( + (int)request.Options.TabSize, + request.Options.InsertSpaces); + + + // TODO raise an error event in case format returns null; + string formattedScript; + Range editRange; + var extent = scriptFile.ScriptAst.Extent; + + // todo create an extension for converting range to script extent + editRange = new Range + { + Start = new Position + { + Line = extent.StartLineNumber - 1, + Character = extent.StartColumnNumber - 1 + }, + End = new Position + { + Line = extent.EndLineNumber - 1, + Character = extent.EndColumnNumber - 1 + } + }; + + formattedScript = await _analysisService.FormatAsync( + scriptFile.Contents, + pssaSettings, + null); + formattedScript = formattedScript ?? scriptFile.Contents; + + return new TextEditContainer(new TextEdit + { + NewText = formattedScript, + Range = editRange + }); + } + + public void SetCapability(DocumentFormattingCapability capability) + { + _capability = capability; + } + } + + public class DocumentRangeFormattingHandler : IDocumentRangeFormattingHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly AnalysisService _analysisService; + private readonly ConfigurationService _configurationService; + private readonly WorkspaceService _workspaceService; + private DocumentRangeFormattingCapability _capability; + + public DocumentRangeFormattingHandler(ILoggerFactory factory, AnalysisService analysisService, ConfigurationService configurationService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _analysisService = analysisService; + _configurationService = configurationService; + _workspaceService = workspaceService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector + }; + } + + public async Task Handle(DocumentRangeFormattingParams request, CancellationToken cancellationToken) + { + var scriptFile = _workspaceService.GetFile(request.TextDocument.Uri.ToString()); + var pssaSettings = _configurationService.CurrentSettings.CodeFormatting.GetPSSASettingsHashtable( + (int)request.Options.TabSize, + request.Options.InsertSpaces); + + // TODO raise an error event in case format returns null; + string formattedScript; + Range editRange; + var extent = scriptFile.ScriptAst.Extent; + + // TODO create an extension for converting range to script extent + editRange = new Range + { + Start = new Position + { + Line = extent.StartLineNumber - 1, + Character = extent.StartColumnNumber - 1 + }, + End = new Position + { + Line = extent.EndLineNumber - 1, + Character = extent.EndColumnNumber - 1 + } + }; + + Range range = request.Range; + var rangeList = range == null ? null : new int[] { + (int)range.Start.Line + 1, + (int)range.Start.Character + 1, + (int)range.End.Line + 1, + (int)range.End.Character + 1}; + + formattedScript = await _analysisService.FormatAsync( + scriptFile.Contents, + pssaSettings, + rangeList); + formattedScript = formattedScript ?? scriptFile.Contents; + + return new TextEditContainer(new TextEdit + { + NewText = formattedScript, + Range = editRange + }); + } + + public void SetCapability(DocumentRangeFormattingCapability capability) + { + _capability = capability; + } + } +} From 90c440e7b5df3762649cfb95d7eed59d91e005ae Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 29 Jul 2019 10:22:59 -0700 Subject: [PATCH 19/47] remove merge conflict --- .../Services/Workspace/LanguageServerSettings.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs index 8a9cbfd25..7f2b3dfee 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs @@ -178,10 +178,6 @@ public class CodeFormattingSettings /// > public CodeFormattingSettings() { -<<<<<<< HEAD -======= - ->>>>>>> didChangeConfiguration message and general settings support } /// From 8e9ca8a5b8542e822175fd88bb6c0ad50865b6ab Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 29 Jul 2019 11:49:50 -0700 Subject: [PATCH 20/47] add formatting tests --- .../EditorServices.Integration.Tests.ps1 | 117 ++++++++++++------ tools/PsesPsClient/PsesPsClient.psd1 | 2 + tools/PsesPsClient/PsesPsClient.psm1 | 73 +++++++++++ 3 files changed, 152 insertions(+), 40 deletions(-) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index c51c62cc0..2d29d381b 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -55,6 +55,33 @@ Data: throw $msg } +function New-TestFile +{ + param( + [Parameter(Mandatory)] + [string] + $Script + ) + + $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()) + + # 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/../../module/PowerShellEditorServices" @@ -89,21 +116,12 @@ Describe "Loading and running PowerShellEditorServices" { } It "Can handle WorkspaceSymbol request" { - $script = " + New-TestFile -Script " function Get-Foo { Write-Host 'hello' } " - $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 - $request = Send-LspRequest -Client $client -Method "workspace/symbol" -Parameters @{ query = "" } @@ -141,19 +159,7 @@ function Get-Foo { } It "Can get Diagnostics after changing settings" { - $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 - - # Throw out any notifications from the first PSScriptAnalyzer run. - Get-LspNotification -Client $client | Out-Null + $file = New-TestFile -Script 'gci | % { $_ }' $request = Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ PowerShell = @{ @@ -172,7 +178,7 @@ function Get-Foo { } It "Can handle folding request" { - $script = 'gci | % { + $filePath = New-TestFile -Script 'gci | % { $_ @" @@ -180,24 +186,9 @@ $_ "@ }' - $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 - - # Throw out any notifications from the first PSScriptAnalyzer run. - Get-LspNotification -Client $client | Out-Null - - - $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($file.PSPath).AbsoluteUri) + Uri = ([Uri]::new($filePath).AbsoluteUri) } }) @@ -215,6 +206,52 @@ $_ $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." + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index a639162b3..1bba35a5d 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -76,6 +76,8 @@ FunctionsToExport = @( 'Send-LspInitializeRequest', 'Send-LspDidOpenTextDocumentRequest', 'Send-LspDidChangeConfigurationRequest', + 'Send-LspFormattingRequest', + 'Send-LspRangeFormattingRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', 'Get-LspResponse' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 04f77f605..bc825a068 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -319,6 +319,79 @@ function Send-LspDidChangeConfigurationRequest $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-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] From 74f4f2f1b02ec9f331da2b4ae14536a3f0204f86 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 31 Jul 2019 07:47:43 -0700 Subject: [PATCH 21/47] DocumentSymbols and References support (#997) * working formatting * add tests * delete commented out code --- .../LanguageServer/OmnisharpLanguageServer.cs | 4 +- .../Services/Symbols/SymbolsService.cs | 108 ++++++ .../Services/Symbols/Vistors/AstOperations.cs | 335 ++++++++++++++++++ .../Symbols/Vistors/FindCommandVisitor.cs | 87 +++++ .../Symbols/Vistors/FindDeclarationVisitor.cs | 146 ++++++++ .../Symbols/Vistors/FindDotSourcedVisitor.cs | 90 +++++ .../Symbols/Vistors/FindReferencesVisitor.cs | 189 ++++++++++ .../{ => Vistors}/FindSymbolVisitor.cs | 0 .../{ => Vistors}/FindSymbolsVisitor.cs | 0 .../{ => Vistors}/FindSymbolsVisitor2.cs | 0 .../Handlers/DocumentSymbolHandler.cs | 194 ++++++++++ .../Handlers/ReferencesHandler.cs | 100 ++++++ .../Services/TextDocument/ScriptFile.cs | 4 +- .../Utility/PathUtils.cs | 32 ++ .../EditorServices.Integration.Tests.ps1 | 47 +++ tools/PsesPsClient/PsesPsClient.psd1 | 2 + tools/PsesPsClient/PsesPsClient.psm1 | 61 ++++ 17 files changed, 1396 insertions(+), 3 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs rename src/PowerShellEditorServices.Engine/Services/Symbols/{ => Vistors}/FindSymbolVisitor.cs (100%) rename src/PowerShellEditorServices.Engine/Services/Symbols/{ => Vistors}/FindSymbolsVisitor.cs (100%) rename src/PowerShellEditorServices.Engine/Services/Symbols/{ => Vistors}/FindSymbolsVisitor2.cs (100%) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 73a6d8186..ed92f9083 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -75,7 +75,9 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler() + .WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 1b4a38b19..071742dea 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; @@ -68,5 +71,110 @@ public List FindSymbolsInFile(ScriptFile scriptFile) return foundOccurrences; } + + /// + /// Finds the symbol in the script given a file location + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// A SymbolReference of the symbol found at the given location + /// or null if there is no symbol at that location + /// + public SymbolReference FindSymbolAtLocation( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + SymbolReference symbolReference = + AstOperations.FindSymbolAtPosition( + scriptFile.ScriptAst, + lineNumber, + columnNumber); + + if (symbolReference != null) + { + symbolReference.FilePath = scriptFile.FilePath; + } + + return symbolReference; + } + + /// + /// Finds all the references of a symbol + /// + /// The symbol to find all references for + /// An array of scriptFiles too search for references in + /// The workspace that will be searched for symbols + /// FindReferencesResult + public List FindReferencesOfSymbol( + SymbolReference foundSymbol, + ScriptFile[] referencedFiles, + WorkspaceService workspace) + { + if (foundSymbol == null) + { + return null; + } + + int symbolOffset = referencedFiles[0].GetOffsetAtPosition( + foundSymbol.ScriptRegion.StartLineNumber, + foundSymbol.ScriptRegion.StartColumnNumber); + + // NOTE: we use to make sure aliases were loaded but took it out because we needed the pipeline thread. + + // We want to look for references first in referenced files, hence we use ordered dictionary + // TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic + var fileMap = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? new OrderedDictionary() + : new OrderedDictionary(StringComparer.OrdinalIgnoreCase); + + foreach (ScriptFile scriptFile in referencedFiles) + { + fileMap[scriptFile.FilePath] = scriptFile; + } + + foreach (string filePath in workspace.EnumeratePSFiles()) + { + if (!fileMap.Contains(filePath)) + { + if (!workspace.TryGetFile(filePath, out ScriptFile scriptFile)) + { + // If we can't access the file for some reason, just ignore it + continue; + } + + fileMap[filePath] = scriptFile; + } + } + + var symbolReferences = new List(); + foreach (object fileName in fileMap.Keys) + { + var file = (ScriptFile)fileMap[fileName]; + + IEnumerable references = AstOperations.FindReferencesOfSymbol( + file.ScriptAst, + foundSymbol, + needsAliases: false); + + foreach (SymbolReference reference in references) + { + try + { + reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); + } + catch (ArgumentOutOfRangeException e) + { + reference.SourceLine = string.Empty; + _logger.LogException("Found reference is out of range in script file", e); + } + reference.FilePath = file.FilePath; + symbolReferences.Add(reference); + } + } + + return symbolReferences; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs new file mode 100644 index 000000000..9e003c0d4 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs @@ -0,0 +1,335 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides common operations for the syntax tree of a parsed script. + /// + internal static class AstOperations + { + // TODO: When netstandard is upgraded to 2.0, see if + // Delegate.CreateDelegate can be used here instead + //private static readonly MethodInfo s_extentCloneWithNewOffset = typeof(PSObject).GetTypeInfo().Assembly + // .GetType("System.Management.Automation.Language.InternalScriptPosition") + // .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); + + //private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + // TODO: BRING THIS BACK + /// + /// Gets completions for the symbol found in the Ast at + /// the given file offset. + /// + /// + /// The Ast which will be traversed to find a completable symbol. + /// + /// + /// The array of tokens corresponding to the scriptAst parameter. + /// + /// + /// The 1-based file offset at which a symbol will be located. + /// + /// + /// The PowerShellContext to use for gathering completions. + /// + /// An ILogger implementation used for writing log messages. + /// + /// A CancellationToken to cancel completion requests. + /// + /// + /// A CommandCompletion instance that contains completions for the + /// symbol at the given offset. + /// + // static public async Task GetCompletionsAsync( + // Ast scriptAst, + // Token[] currentTokens, + // int fileOffset, + // PowerShellContext powerShellContext, + // ILogger logger, + // CancellationToken cancellationToken) + // { + // if (!s_completionHandle.Wait(0)) + // { + // return null; + // } + + // try + // { + // IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( + // scriptAst.Extent.StartScriptPosition, + // new object[] { fileOffset }); + + // logger.Write( + // LogLevel.Verbose, + // string.Format( + // "Getting completions at offset {0} (line: {1}, column: {2})", + // fileOffset, + // cursorPosition.LineNumber, + // cursorPosition.ColumnNumber)); + + // if (!powerShellContext.IsAvailable) + // { + // return null; + // } + + // var stopwatch = new Stopwatch(); + + // // If the current runspace is out of process we can use + // // CommandCompletion.CompleteInput because PSReadLine won't be taking up the + // // main runspace. + // if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + // { + // using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) + // using (PowerShell powerShell = PowerShell.Create()) + // { + // powerShell.Runspace = runspaceHandle.Runspace; + // stopwatch.Start(); + // try + // { + // return CommandCompletion.CompleteInput( + // scriptAst, + // currentTokens, + // cursorPosition, + // options: null, + // powershell: powerShell); + // } + // finally + // { + // stopwatch.Stop(); + // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + // } + // } + // } + + // CommandCompletion commandCompletion = null; + // await powerShellContext.InvokeOnPipelineThreadAsync( + // pwsh => + // { + // stopwatch.Start(); + // commandCompletion = CommandCompletion.CompleteInput( + // scriptAst, + // currentTokens, + // cursorPosition, + // options: null, + // powershell: pwsh); + // }); + // stopwatch.Stop(); + // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + + // return commandCompletion; + // } + // finally + // { + // s_completionHandle.Release(); + // } + // } + + /// + /// Finds the symbol at a given file location + /// + /// The abstract syntax tree of the given script + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// Includes full function definition ranges in the search. + /// SymbolReference of found symbol + static public SymbolReference FindSymbolAtPosition( + Ast scriptAst, + int lineNumber, + int columnNumber, + bool includeFunctionDefinitions = false) + { + FindSymbolVisitor symbolVisitor = + new FindSymbolVisitor( + lineNumber, + columnNumber, + includeFunctionDefinitions); + + scriptAst.Visit(symbolVisitor); + + return symbolVisitor.FoundSymbolReference; + } + + /// + /// Finds the symbol (always Command type) at a given file location + /// + /// The abstract syntax tree of the given script + /// The line number of the cursor for the given script + /// The column number of the cursor for the given script + /// SymbolReference of found command + static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber) + { + FindCommandVisitor commandVisitor = new FindCommandVisitor(lineNumber, columnNumber); + scriptAst.Visit(commandVisitor); + + return commandVisitor.FoundCommandReference; + } + + /// + /// Finds all references (including aliases) in a script for the given symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for referneces of + /// Dictionary maping cmdlets to aliases for finding alias references + /// Dictionary maping aliases to cmdlets for finding alias references + /// + static public IEnumerable FindReferencesOfSymbol( + Ast scriptAst, + SymbolReference symbolReference, + Dictionary> CmdletToAliasDictionary, + Dictionary AliasToCmdletDictionary) + { + // find the symbol evaluators for the node types we are handling + FindReferencesVisitor referencesVisitor = + new FindReferencesVisitor( + symbolReference, + CmdletToAliasDictionary, + AliasToCmdletDictionary); + scriptAst.Visit(referencesVisitor); + + return referencesVisitor.FoundReferences; + } + + /// + /// Finds all references (not including aliases) in a script for the given symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for referneces of + /// If this reference search needs aliases. + /// This should always be false and used for occurence requests + /// A collection of SymbolReference objects that are refrences to the symbolRefrence + /// not including aliases + static public IEnumerable FindReferencesOfSymbol( + ScriptBlockAst scriptAst, + SymbolReference foundSymbol, + bool needsAliases) + { + FindReferencesVisitor referencesVisitor = + new FindReferencesVisitor(foundSymbol); + scriptAst.Visit(referencesVisitor); + + return referencesVisitor.FoundReferences; + } + + /// + /// Finds the definition of the symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for the definition of + /// A SymbolReference of the definition of the symbolReference + static public SymbolReference FindDefinitionOfSymbol( + Ast scriptAst, + SymbolReference symbolReference) + { + FindDeclarationVisitor declarationVisitor = + new FindDeclarationVisitor( + symbolReference); + scriptAst.Visit(declarationVisitor); + + return declarationVisitor.FoundDeclaration; + } + + /// + /// Finds all symbols in a script + /// + /// The abstract syntax tree of the given script + /// The PowerShell version the Ast was generated from + /// A collection of SymbolReference objects + static public IEnumerable FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion) + { + IEnumerable symbolReferences = null; + + // TODO: Restore this when we figure out how to support multiple + // PS versions in the new PSES-as-a-module world (issue #276) + // if (powerShellVersion >= new Version(5,0)) + // { + //#if PowerShellv5 + // FindSymbolsVisitor2 findSymbolsVisitor = new FindSymbolsVisitor2(); + // scriptAst.Visit(findSymbolsVisitor); + // symbolReferences = findSymbolsVisitor.SymbolReferences; + //#endif + // } + // else + + FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor(); + scriptAst.Visit(findSymbolsVisitor); + symbolReferences = findSymbolsVisitor.SymbolReferences; + return symbolReferences; + } + + /// + /// Checks if a given ast represents the root node of a *.psd1 file. + /// + /// The abstract syntax tree of the given script + /// true if the AST represts a *.psd1 file, otherwise false + static public bool IsPowerShellDataFileAst(Ast ast) + { + // sometimes we don't have reliable access to the filename + // so we employ heuristics to check if the contents are + // part of a psd1 file. + return IsPowerShellDataFileAstNode( + new { Item = ast, Children = new List() }, + new Type[] { + typeof(ScriptBlockAst), + typeof(NamedBlockAst), + typeof(PipelineAst), + typeof(CommandExpressionAst), + typeof(HashtableAst) }, + 0); + } + + static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap, int level) + { + var levelAstTypeMatch = node.Item.GetType().Equals(levelAstMap[level]); + if (!levelAstTypeMatch) + { + return false; + } + + if (level == levelAstMap.Length - 1) + { + return levelAstTypeMatch; + } + + var astsFound = (node.Item as Ast).FindAll(a => a is Ast, false); + if (astsFound != null) + { + foreach (var astFound in astsFound) + { + if (!astFound.Equals(node.Item) + && node.Item.Equals(astFound.Parent) + && IsPowerShellDataFileAstNode( + new { Item = astFound, Children = new List() }, + levelAstMap, + level + 1)) + { + return true; + } + } + } + + return false; + } + + /// + /// Finds all files dot sourced in a script + /// + /// The abstract syntax tree of the given script + /// Pre-calculated value of $PSScriptRoot + /// + static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot) + { + FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(psScriptRoot); + scriptAst.Visit(dotSourcedVisitor); + + return dotSourcedVisitor.DotSourcedFiles.ToArray(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs new file mode 100644 index 000000000..254bf6ba3 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The vistior used to find the commandAst of a specific location in an AST + /// + internal class FindCommandVisitor : AstVisitor + { + private readonly int lineNumber; + private readonly int columnNumber; + + public SymbolReference FoundCommandReference { get; private set; } + + public FindCommandVisitor(int lineNumber, int columnNumber) + { + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) + { + if (this.lineNumber == pipelineAst.Extent.StartLineNumber) + { + // Which command is the cursor in? + foreach (var commandAst in pipelineAst.PipelineElements.OfType()) + { + int trueEndColumnNumber = commandAst.Extent.EndColumnNumber; + string currentLine = commandAst.Extent.StartScriptPosition.Line; + + if (currentLine.Length >= trueEndColumnNumber) + { + // Get the text left in the line after the command's extent + string remainingLine = + currentLine.Substring( + commandAst.Extent.EndColumnNumber); + + // Calculate the "true" end column number by finding out how many + // whitespace characters are between this command and the next (or + // the end of the line). + // NOTE: +1 is added to trueEndColumnNumber to account for the position + // just after the last character in the command string or script line. + int preTrimLength = remainingLine.Length; + int postTrimLength = remainingLine.TrimStart().Length; + trueEndColumnNumber = + commandAst.Extent.EndColumnNumber + + (preTrimLength - postTrimLength) + 1; + } + + if (commandAst.Extent.StartColumnNumber <= columnNumber && + trueEndColumnNumber >= columnNumber) + { + this.FoundCommandReference = + new SymbolReference( + SymbolType.Function, + commandAst.CommandElements[0].Extent); + + return AstVisitAction.StopVisit; + } + } + } + + return base.VisitPipeline(pipelineAst); + } + + /// + /// Is the position of the given location is in the range of the start + /// of the first element to the character before the second element + /// + /// The script extent of the first element of the command ast + /// The script extent of the second element of the command ast + /// True if the given position is in the range of the start of + /// the first element to the character before the second element + private bool IsPositionInExtent(IScriptExtent firstExtent, IScriptExtent secondExtent) + { + return (firstExtent.StartLineNumber == lineNumber && + firstExtent.StartColumnNumber <= columnNumber && + secondExtent.StartColumnNumber >= columnNumber - 1); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs new file mode 100644 index 000000000..c9842c9ef --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs @@ -0,0 +1,146 @@ +// +// 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.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find the definition of a symbol + /// + internal class FindDeclarationVisitor : AstVisitor + { + private SymbolReference symbolRef; + private string variableName; + + public SymbolReference FoundDeclaration{ get; private set; } + + public FindDeclarationVisitor(SymbolReference symbolRef) + { + this.symbolRef = symbolRef; + if (this.symbolRef.SymbolType == SymbolType.Variable) + { + // converts `$varName` to `varName` or of the form ${varName} to varName + variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); + } + } + + /// + /// Decides if the current function definition is the right definition + /// for the symbol being searched for. The definition of the symbol will be a of type + /// SymbolType.Function and have the same name as the symbol + /// + /// A FunctionDefinitionAst in the script's AST + /// A decision to stop searching if the right FunctionDefinitionAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + // Get the start column number of the function name, + // instead of the the start column of 'function' and create new extent for the functionName + int startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name, StringComparison.OrdinalIgnoreCase) + 1; + + IScriptExtent nameExtent = new ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + StartColumnNumber = startColumnNumber, + EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length + }; + + if (symbolRef.SymbolType.Equals(SymbolType.Function) && + nameExtent.Text.Equals(symbolRef.ScriptRegion.Text, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundDeclaration = + new SymbolReference( + SymbolType.Function, + nameExtent); + + return AstVisitAction.StopVisit; + } + + return base.VisitFunctionDefinition(functionDefinitionAst); + } + + /// + /// Check if the left hand side of an assignmentStatementAst is a VariableExpressionAst + /// with the same name as that of symbolRef. + /// + /// An AssignmentStatementAst + /// A decision to stop searching if the right VariableExpressionAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) + { + if (variableName == null) + { + return AstVisitAction.Continue; + } + + // We want to check VariableExpressionAsts from within this AssignmentStatementAst so we visit it. + FindDeclarationVariableExpressionVisitor visitor = new FindDeclarationVariableExpressionVisitor(symbolRef); + assignmentStatementAst.Left.Visit(visitor); + + if (visitor.FoundDeclaration != null) + { + FoundDeclaration = visitor.FoundDeclaration; + return AstVisitAction.StopVisit; + } + return AstVisitAction.Continue; + } + + /// + /// The private visitor used to find the variable expression that matches a symbol + /// + private class FindDeclarationVariableExpressionVisitor : AstVisitor + { + private SymbolReference symbolRef; + private string variableName; + + public SymbolReference FoundDeclaration{ get; private set; } + + public FindDeclarationVariableExpressionVisitor(SymbolReference symbolRef) + { + this.symbolRef = symbolRef; + if (this.symbolRef.SymbolType == SymbolType.Variable) + { + // converts `$varName` to `varName` or of the form ${varName} to varName + variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); + } + } + + /// + /// Check if the VariableExpressionAst has the same name as that of symbolRef. + /// + /// A VariableExpressionAst + /// A decision to stop searching if the right VariableExpressionAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if (variableExpressionAst.VariablePath.UserPath.Equals(variableName, StringComparison.OrdinalIgnoreCase)) + { + // TODO also find instances of set-variable + FoundDeclaration = new SymbolReference(SymbolType.Variable, variableExpressionAst.Extent); + return AstVisitAction.StopVisit; + } + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitMemberExpression(MemberExpressionAst functionDefinitionAst) + { + // We don't want to discover any variables in member expressisons (`$something.Foo`) + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitIndexExpression(IndexExpressionAst functionDefinitionAst) + { + // We don't want to discover any variables in index expressions (`$something[0]`) + return AstVisitAction.SkipChildren; + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs new file mode 100644 index 000000000..de42c7753 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; +using PowerShellEditorServices.Engine.Utility; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The vistor used to find the dont sourced files in an AST + /// + internal class FindDotSourcedVisitor : AstVisitor + { + private readonly string _psScriptRoot; + + /// + /// A hash set of the dot sourced files (because we don't want duplicates) + /// + public HashSet DotSourcedFiles { get; private set; } + + /// + /// Creates a new instance of the FindDotSourcedVisitor class. + /// + /// Pre-calculated value of $PSScriptRoot + public FindDotSourcedVisitor(string psScriptRoot) + { + DotSourcedFiles = new HashSet(StringComparer.CurrentCultureIgnoreCase); + _psScriptRoot = psScriptRoot; + } + + /// + /// Checks to see if the command invocation is a dot + /// in order to find a dot sourced file + /// + /// A CommandAst object in the script's AST + /// A decision to stop searching if the right commandAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + CommandElementAst commandElementAst = commandAst.CommandElements[0]; + if (commandAst.InvocationOperator.Equals(TokenKind.Dot)) + { + string path; + switch (commandElementAst) + { + case StringConstantExpressionAst stringConstantExpressionAst: + path = stringConstantExpressionAst.Value; + break; + + case ExpandableStringExpressionAst expandableStringExpressionAst: + path = GetPathFromExpandableStringExpression(expandableStringExpressionAst); + break; + + default: + path = null; + break; + } + + if (!string.IsNullOrWhiteSpace(path)) + { + DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(path)); + } + } + + return base.VisitCommand(commandAst); + } + + private string GetPathFromExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) + { + var path = expandableStringExpressionAst.Value; + foreach (var nestedExpression in expandableStringExpressionAst.NestedExpressions) + { + // If the string contains the variable $PSScriptRoot, we replace it with the corresponding value. + if (!(nestedExpression is VariableExpressionAst variableAst + && variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase))) + { + return null; // We return null instead of a partially evaluated ExpandableStringExpression. + } + + path = path.Replace(variableAst.ToString(), _psScriptRoot); + } + + return path; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs new file mode 100644 index 000000000..2dfb6d86b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs @@ -0,0 +1,189 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find the references of a symbol in a script's AST + /// + internal class FindReferencesVisitor : AstVisitor + { + private SymbolReference symbolRef; + private Dictionary> CmdletToAliasDictionary; + private Dictionary AliasToCmdletDictionary; + private string symbolRefCommandName; + private bool needsAliases; + + public List FoundReferences { get; set; } + + /// + /// Constructor used when searching for aliases is needed + /// + /// The found symbolReference that other symbols are being compared to + /// Dictionary maping cmdlets to aliases for finding alias references + /// Dictionary maping aliases to cmdlets for finding alias references + public FindReferencesVisitor( + SymbolReference symbolReference, + Dictionary> CmdletToAliasDictionary, + Dictionary AliasToCmdletDictionary) + { + this.symbolRef = symbolReference; + this.FoundReferences = new List(); + this.needsAliases = true; + this.CmdletToAliasDictionary = CmdletToAliasDictionary; + this.AliasToCmdletDictionary = AliasToCmdletDictionary; + + // Try to get the symbolReference's command name of an alias, + // if a command name does not exists (if the symbol isn't an alias to a command) + // set symbolRefCommandName to and empty string value + AliasToCmdletDictionary.TryGetValue(symbolReference.ScriptRegion.Text, out symbolRefCommandName); + if (symbolRefCommandName == null) { symbolRefCommandName = string.Empty; } + + } + + /// + /// Constructor used when searching for aliases is not needed + /// + /// The found symbolReference that other symbols are being compared to + public FindReferencesVisitor(SymbolReference foundSymbol) + { + this.symbolRef = foundSymbol; + this.FoundReferences = new List(); + this.needsAliases = false; + } + + /// + /// Decides if the current command is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Function + /// and have the same name as the symbol + /// + /// A CommandAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + Ast commandNameAst = commandAst.CommandElements[0]; + string commandName = commandNameAst.Extent.Text; + + if(symbolRef.SymbolType.Equals(SymbolType.Function)) + { + if (needsAliases) + { + // Try to get the commandAst's name and aliases, + // if a command does not exists (if the symbol isn't an alias to a command) + // set command to and empty string value string command + // if the aliases do not exist (if the symvol isn't a command that has aliases) + // set aliases to an empty List + string command; + List alaises; + CmdletToAliasDictionary.TryGetValue(commandName, out alaises); + AliasToCmdletDictionary.TryGetValue(commandName, out command); + if (alaises == null) { alaises = new List(); } + if (command == null) { command = string.Empty; } + + if (symbolRef.SymbolType.Equals(SymbolType.Function)) + { + // Check if the found symbol's name is the same as the commandAst's name OR + // if the symbol's name is an alias for this commandAst's name (commandAst is a cmdlet) OR + // if the symbol's name is the same as the commandAst's cmdlet name (commandAst is a alias) + if (commandName.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase) || + alaises.Contains(symbolRef.ScriptRegion.Text.ToLower()) || + command.Equals(symbolRef.ScriptRegion.Text, StringComparison.CurrentCultureIgnoreCase) || + (!command.Equals(string.Empty) && command.Equals(symbolRefCommandName, StringComparison.CurrentCultureIgnoreCase))) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + commandNameAst.Extent)); + } + } + + } + else // search does not include aliases + { + if (commandName.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + commandNameAst.Extent)); + } + } + + } + return base.VisitCommand(commandAst); + } + + /// + /// Decides if the current function definition is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Function and have the same name as the symbol + /// + /// A functionDefinitionAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + // Get the start column number of the function name, + // instead of the the start column of 'function' and create new extent for the functionName + int startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name) + 1; + + IScriptExtent nameExtent = new ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, + StartColumnNumber = startColumnNumber, + EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length + }; + + if (symbolRef.SymbolType.Equals(SymbolType.Function) && + nameExtent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + nameExtent)); + } + return base.VisitFunctionDefinition(functionDefinitionAst); + } + + /// + /// Decides if the current function definition is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Parameter and have the same name as the symbol + /// + /// A commandParameterAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) + { + if (symbolRef.SymbolType.Equals(SymbolType.Parameter) && + commandParameterAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Parameter, + commandParameterAst.Extent)); + } + return AstVisitAction.Continue; + } + + /// + /// Decides if the current function definition is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Variable and have the same name as the symbol + /// + /// A variableExpressionAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if(symbolRef.SymbolType.Equals(SymbolType.Variable) && + variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Variable, + variableExpressionAst.Extent)); + } + return AstVisitAction.Continue; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs rename to src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs rename to src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs rename to src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs new file mode 100644 index 000000000..e5289746c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class DocumentSymbolHandler : IDocumentSymbolHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + + private readonly IDocumentSymbolProvider[] _providers; + + private DocumentSymbolCapability _capability; + + public DocumentSymbolHandler(ILoggerFactory factory, ConfigurationService configurationService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _providers = new IDocumentSymbolProvider[] + { + new ScriptDocumentSymbolProvider( + VersionUtils.PSVersion), + new PsdDocumentSymbolProvider(), + new PesterDocumentSymbolProvider() + }; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions() + { + DocumentSelector = _documentSelector, + }; + } + + public Task Handle(DocumentSymbolParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + IEnumerable foundSymbols = + this.ProvideDocumentSymbols(scriptFile); + + SymbolInformationOrDocumentSymbol[] symbols = null; + + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + if (foundSymbols != null) + { + symbols = + foundSymbols + .Select(r => + { + return new SymbolInformationOrDocumentSymbol(new SymbolInformation + { + ContainerName = containerName, + Kind = GetSymbolKind(r.SymbolType), + Location = new Location + { + Uri = PathUtils.ToUri(r.FilePath), + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = GetDecoratedSymbolName(r) + }); + }) + .ToArray(); + } + else + { + symbols = new SymbolInformationOrDocumentSymbol[0]; + } + + + return Task.FromResult(new SymbolInformationOrDocumentSymbolContainer(symbols)); + } + + public void SetCapability(DocumentSymbolCapability capability) + { + _capability = capability; + } + + private IEnumerable ProvideDocumentSymbols( + ScriptFile scriptFile) + { + return + this.InvokeProviders(p => p.ProvideDocumentSymbols(scriptFile)) + .SelectMany(r => r); + } + + /// + /// Invokes the given function synchronously against all + /// registered providers. + /// + /// The function to be invoked. + /// + /// An IEnumerable containing the results of all providers + /// that were invoked successfully. + /// + protected IEnumerable InvokeProviders( + Func invokeFunc) + { + Stopwatch invokeTimer = new Stopwatch(); + List providerResults = new List(); + + foreach (var provider in this._providers) + { + try + { + invokeTimer.Restart(); + + providerResults.Add(invokeFunc(provider)); + + invokeTimer.Stop(); + + this._logger.LogTrace( + $"Invocation of provider '{provider.GetType().Name}' completed in {invokeTimer.ElapsedMilliseconds}ms."); + } + catch (Exception e) + { + this._logger.LogException( + $"Exception caught while invoking provider {provider.GetType().Name}:", + e); + } + } + + return providerResults; + } + + private static SymbolKind GetSymbolKind(SymbolType symbolType) + { + switch (symbolType) + { + case SymbolType.Configuration: + case SymbolType.Function: + case SymbolType.Workflow: + return SymbolKind.Function; + + default: + return SymbolKind.Variable; + } + } + + private static string GetDecoratedSymbolName(SymbolReference symbolReference) + { + string name = symbolReference.SymbolName; + + if (symbolReference.SymbolType == SymbolType.Configuration || + symbolReference.SymbolType == SymbolType.Function || + symbolReference.SymbolType == SymbolType.Workflow) + { + name += " { }"; + } + + return name; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs new file mode 100644 index 000000000..6e10d5fe8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + class ReferencesHandler : IReferencesHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + private ReferencesCapability _capability; + + public ReferencesHandler(ILoggerFactory factory, SymbolsService symbolsService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector + }; + } + + public async Task Handle(ReferenceParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + SymbolReference foundSymbol = + _symbolsService.FindSymbolAtLocation( + scriptFile, + (int)request.Position.Line + 1, + (int)request.Position.Character + 1); + + List referencesResult = + _symbolsService.FindReferencesOfSymbol( + foundSymbol, + _workspaceService.ExpandScriptReferences(scriptFile), + _workspaceService); + + var locations = new List(); + + if (referencesResult != null) + { + foreach (SymbolReference foundReference in referencesResult) + { + locations.Add(new Location + { + Uri = PathUtils.ToUri(foundReference.FilePath), + Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) + }); + } + } + + return new LocationContainer(locations); + } + + public void SetCapability(ReferencesCapability capability) + { + _capability = capability; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs index 9253b81ad..57f80c9c3 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using System.Runtime.InteropServices; +using Microsoft.PowerShell.EditorServices.Symbols; namespace Microsoft.PowerShell.EditorServices { @@ -670,7 +670,7 @@ private void ParseFileContents() } // Get all dot sourced referenced files and store them - //this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath)); + this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath)); } #endregion diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index da9dfa3ad..17d43f77b 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -1,9 +1,31 @@ using System; +using System.IO; +using System.Runtime.InteropServices; namespace PowerShellEditorServices.Engine.Utility { internal class PathUtils { + /// + /// The default path separator used by the base implementation of the providers. + /// + /// Porting note: IO.Path.DirectorySeparatorChar is correct for all platforms. On Windows, + /// it is '\', and on Linux, it is '/', as expected. + /// + internal static readonly char DefaultPathSeparator = Path.DirectorySeparatorChar; + internal static readonly string DefaultPathSeparatorString = DefaultPathSeparator.ToString(); + + /// + /// The alternate path separator used by the base implementation of the providers. + /// + /// Porting note: we do not use .NET's AlternatePathSeparatorChar here because it correctly + /// states that both the default and alternate are '/' on Linux. However, for PowerShell to + /// be "slash agnostic", we need to use the assumption that a '\' is the alternate path + /// separator on Linux. + /// + internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\'; + internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString(); + public string WildcardUnescapePath(string path) { throw new NotImplementedException(); @@ -29,5 +51,15 @@ public static string FromUri(Uri uri) } return uri.LocalPath; } + + /// + /// Converts all alternate path separators to the current platform's main path separators. + /// + /// The path to normalize. + /// The normalized path. + public static string NormalizePathSeparators(string path) + { + return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); + } } } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 2d29d381b..b363d4ccb 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -252,6 +252,53 @@ Get-Process $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 + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 1bba35a5d..7010f9a3e 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -78,6 +78,8 @@ FunctionsToExport = @( 'Send-LspDidChangeConfigurationRequest', 'Send-LspFormattingRequest', 'Send-LspRangeFormattingRequest', + 'Send-LspDocumentSymbolRequest', + 'Send-LspReferencesRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', 'Get-LspResponse' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index bc825a068..bbfec42a4 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -392,6 +392,67 @@ function Send-LspRangeFormattingRequest 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-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] From dda6258d357efb3609905f55a9fa818f759c0ab9 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 2 Aug 2019 14:02:21 -0700 Subject: [PATCH 22/47] [Omnisharp-LSP] textDocument/documentHighlight support (#999) * Add handler scaffold * More stuff * Make handler work * Add copyright * Add tests, fix bugs * Fix small issues --- .../LanguageServer/OmnisharpLanguageServer.cs | 19 ++- .../Services/Symbols/SymbolsService.cs | 36 ++++- .../Handlers/DocumentHighlightHandler.cs | 86 ++++++++++++ .../Services/TextDocument/ScriptFileMarker.cs | 25 ++-- .../Services/TextDocument/ScriptRegion.cs | 124 ++++++++++++------ .../EditorServices.Integration.Tests.ps1 | 28 ++++ tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 34 +++++ 8 files changed, 298 insertions(+), 55 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index ed92f9083..3ffc3777a 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.IO.Pipes; using System.Reflection; using System.Threading.Tasks; @@ -8,6 +13,7 @@ using System.Security.AccessControl; using OmniSharp.Extensions.LanguageServer.Server; using PowerShellEditorServices.Engine.Services.Handlers; +using Microsoft.PowerShell.EditorServices.TextDocument; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -56,18 +62,26 @@ public async Task StartAsync() _configuration.OutNamedPipeName, out NamedPipeServerStream outNamedPipe); + ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); + + logger.LogInformation("Waiting for connection"); namedPipe.WaitForConnection(); if (outNamedPipe != null) { outNamedPipe.WaitForConnection(); } + logger.LogInformation("Connected"); + options.Input = namedPipe; options.Output = outNamedPipe ?? namedPipe; options.LoggerFactory = _configuration.LoggerFactory; options.MinimumLogLevel = _configuration.MinimumLogLevel; options.Services = _configuration.Services; + + logger.LogInformation("Adding handlers"); + options .WithHandler() .WithHandler() @@ -77,7 +91,10 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler(); + + logger.LogInformation("Handlers added"); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 071742dea..6e9726f2c 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -1,6 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; @@ -47,8 +53,6 @@ public SymbolsService( #endregion - - /// /// Finds all the symbols in a file. /// @@ -176,5 +180,33 @@ public List FindReferencesOfSymbol( return symbolReferences; } + + /// + /// Finds all the occurences of a symbol in the script given a file location + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// FindOccurrencesResult + public IReadOnlyList FindOccurrencesInFile( + ScriptFile file, + int symbolLineNumber, + int symbolColumnNumber) + { + SymbolReference foundSymbol = AstOperations.FindSymbolAtPosition( + file.ScriptAst, + symbolLineNumber, + symbolColumnNumber); + + if (foundSymbol == null) + { + return null; + } + + return AstOperations.FindReferencesOfSymbol( + file.ScriptAst, + foundSymbol, + needsAliases: false).ToArray(); + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs new file mode 100644 index 000000000..aafc1c79f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -0,0 +1,86 @@ +// +// 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.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.TextDocument +{ + public class DocumentHighlightHandler : IDocumentHighlightHandler + { + private static readonly DocumentHighlightContainer s_emptyHighlightContainer = new DocumentHighlightContainer(); + + private readonly ILogger _logger; + + private readonly WorkspaceService _workspaceService; + + private readonly SymbolsService _symbolsService; + + private readonly TextDocumentRegistrationOptions _registrationOptions; + + private DocumentHighlightCapability _capability; + + public DocumentHighlightHandler( + ILoggerFactory loggerFactory, + WorkspaceService workspaceService, + SymbolsService symbolService) + { + _logger = loggerFactory.CreateLogger(); + _workspaceService = workspaceService; + _symbolsService = symbolService; + _registrationOptions = new TextDocumentRegistrationOptions() + { + DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" } ) + }; + _logger.LogInformation("highlight handler loaded"); + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return _registrationOptions; + } + + public Task Handle( + DocumentHighlightParams request, + CancellationToken cancellationToken) + { + ScriptFile scriptFile = _workspaceService.GetFile(PathUtils.FromUri(request.TextDocument.Uri)); + + IReadOnlyList symbolOccurrences = _symbolsService.FindOccurrencesInFile( + scriptFile, + (int)request.Position.Line, + (int)request.Position.Character); + + if (symbolOccurrences == null) + { + return Task.FromResult(s_emptyHighlightContainer); + } + + var highlights = new DocumentHighlight[symbolOccurrences.Count]; + for (int i = 0; i < symbolOccurrences.Count; i++) + { + highlights[i] = new DocumentHighlight + { + Kind = DocumentHighlightKind.Write, // TODO: Which symbol types are writable? + Range = symbolOccurrences[i].ScriptRegion.ToRange() + }; + } + + return Task.FromResult(new DocumentHighlightContainer(highlights)); + } + + public void SetCapability(DocumentHighlightCapability capability) + { + _capability = capability; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs index 6da4086ed..9700464dd 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs @@ -139,20 +139,21 @@ internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) if (diagnosticRecord.SuggestedCorrections != null) { - var suggestedCorrections = diagnosticRecord.SuggestedCorrections as dynamic; - List editRegions = new List(); + var editRegions = new List(); string correctionMessage = null; - foreach (var suggestedCorrection in suggestedCorrections) + foreach (dynamic suggestedCorrection in diagnosticRecord.SuggestedCorrections) { - editRegions.Add(new ScriptRegion - { - File = diagnosticRecord.ScriptPath, - Text = suggestedCorrection.Text, - StartLineNumber = suggestedCorrection.StartLineNumber, - StartColumnNumber = suggestedCorrection.StartColumnNumber, - EndLineNumber = suggestedCorrection.EndLineNumber, - EndColumnNumber = suggestedCorrection.EndColumnNumber - }); + editRegions.Add( + new ScriptRegion( + diagnosticRecord.ScriptPath, + suggestedCorrection.Text, + suggestedCorrection.StartLineNumber, + suggestedCorrection.StartColumnNumber, + startOffset: -1, + suggestedCorrection.EndLineNumber, + suggestedCorrection.EndColumnNumber, + endOffset: -1)); + correctionMessage = suggestedCorrection.Description; } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs index 5717b1382..a40814bba 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using OmniSharp.Extensions.LanguageServer.Protocol.Models; using System; using System.Management.Automation.Language; @@ -13,47 +14,109 @@ namespace Microsoft.PowerShell.EditorServices /// public sealed class ScriptRegion : IScriptExtent { + #region Static Methods + + /// + /// Creates a new instance of the ScriptRegion class from an + /// instance of an IScriptExtent implementation. + /// + /// + /// The IScriptExtent to copy into the ScriptRegion. + /// + /// + /// A new ScriptRegion instance with the same details as the IScriptExtent. + /// + public static ScriptRegion Create(IScriptExtent scriptExtent) + { + // IScriptExtent throws an ArgumentOutOfRange exception if Text is null + string scriptExtentText; + try + { + scriptExtentText = scriptExtent.Text; + } + catch (ArgumentOutOfRangeException) + { + scriptExtentText = string.Empty; + } + + return new ScriptRegion( + scriptExtent.File, + scriptExtentText, + scriptExtent.StartLineNumber, + scriptExtent.StartColumnNumber, + scriptExtent.StartOffset, + scriptExtent.EndLineNumber, + scriptExtent.EndColumnNumber, + scriptExtent.EndOffset); + } + + #endregion + + #region Constructors + + public ScriptRegion( + string file, + string text, + int startLineNumber, + int startColumnNumber, + int startOffset, + int endLineNumber, + int endColumnNumber, + int endOffset) + { + File = file; + Text = text; + StartLineNumber = startLineNumber; + StartColumnNumber = startColumnNumber; + StartOffset = startOffset; + EndLineNumber = endLineNumber; + EndColumnNumber = endColumnNumber; + EndOffset = endOffset; + } + + #endregion + #region Properties /// /// Gets the file path of the script file in which this region is contained. /// - public string File { get; set; } + public string File { get; } /// /// Gets or sets the text that is contained within the region. /// - public string Text { get; set; } + public string Text { get; } /// /// Gets or sets the starting line number of the region. /// - public int StartLineNumber { get; set; } + public int StartLineNumber { get; } /// /// Gets or sets the starting column number of the region. /// - public int StartColumnNumber { get; set; } + public int StartColumnNumber { get; } /// /// Gets or sets the starting file offset of the region. /// - public int StartOffset { get; set; } + public int StartOffset { get; } /// /// Gets or sets the ending line number of the region. /// - public int EndLineNumber { get; set; } + public int EndLineNumber { get; } /// /// Gets or sets the ending column number of the region. /// - public int EndColumnNumber { get; set; } + public int EndColumnNumber { get; } /// /// Gets or sets the ending file offset of the region. /// - public int EndOffset { get; set; } + public int EndOffset { get; } /// /// Gets the starting IScriptPosition in the script. @@ -69,41 +132,22 @@ public sealed class ScriptRegion : IScriptExtent #endregion - #region Constructors + #region Methods - /// - /// Creates a new instance of the ScriptRegion class from an - /// instance of an IScriptExtent implementation. - /// - /// - /// The IScriptExtent to copy into the ScriptRegion. - /// - /// - /// A new ScriptRegion instance with the same details as the IScriptExtent. - /// - public static ScriptRegion Create(IScriptExtent scriptExtent) + public Range ToRange() { - // IScriptExtent throws an ArgumentOutOfRange exception if Text is null - string scriptExtentText; - try - { - scriptExtentText = scriptExtent.Text; - } - catch (ArgumentOutOfRangeException) - { - scriptExtentText = string.Empty; - } - - return new ScriptRegion + return new Range { - File = scriptExtent.File, - Text = scriptExtentText, - StartLineNumber = scriptExtent.StartLineNumber, - StartColumnNumber = scriptExtent.StartColumnNumber, - StartOffset = scriptExtent.StartOffset, - EndLineNumber = scriptExtent.EndLineNumber, - EndColumnNumber = scriptExtent.EndColumnNumber, - EndOffset = scriptExtent.EndOffset + Start = new Position + { + Line = StartLineNumber - 1, + Character = StartColumnNumber - 1 + }, + End = new Position + { + Line = EndLineNumber - 1, + Character = EndColumnNumber - 1 + } }; } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index b363d4ccb..8aaa7511a 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -299,6 +299,34 @@ Get-Bar $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 + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 7010f9a3e..6b9c0987a 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -79,6 +79,7 @@ FunctionsToExport = @( 'Send-LspFormattingRequest', 'Send-LspRangeFormattingRequest', 'Send-LspDocumentSymbolRequest', + 'Send-LspDocumentHighlightRequest', 'Send-LspReferencesRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index bbfec42a4..d15582778 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -453,6 +453,40 @@ function Send-LspReferencesRequest 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-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] From 43128e68641acdc3e69030fc37a1952fc3355aa6 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 5 Aug 2019 11:23:59 -0700 Subject: [PATCH 23/47] codelens support (#1001) * codelens support * address rob's feedback --- .../LanguageServer/OmnisharpLanguageServer.cs | 5 +- .../Services/CodeLens/CodeLensData.cs | 17 ++ .../Services/CodeLens/ICodeLensProvider.cs | 50 +++++ .../Services/CodeLens/ICodeLenses.cs | 33 ++++ .../CodeLens/IScriptExtentExtensions.cs | 30 +++ .../CodeLens/PesterCodeLensProvider.cs | 127 +++++++++++++ .../CodeLens/ReferencesCodeLensProvider.cs | 179 ++++++++++++++++++ .../Services/Symbols/SymbolsService.cs | 33 +++- .../TextDocument/Handlers/CodeLensHandlers.cs | 152 +++++++++++++++ .../Services/TextDocument/ScriptRegion.cs | 21 -- .../EditorServices.Integration.Tests.ps1 | 62 +++++- tools/PsesPsClient/PsesPsClient.psd1 | 2 + tools/PsesPsClient/PsesPsClient.psm1 | 52 +++++ 13 files changed, 734 insertions(+), 29 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 3ffc3777a..73da0e447 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -92,9 +92,10 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler(); - logger.LogInformation("Handlers added"); + logger.LogInformation("Handlers added"); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs new file mode 100644 index 000000000..15fff728d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs @@ -0,0 +1,17 @@ +// +// 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.CodeLenses +{ + /// + /// Represents data expected back in an LSP CodeLens response. + /// + internal class CodeLensData + { + public string Uri { get; set; } + + public string ProviderId { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs new file mode 100644 index 000000000..fab101580 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Specifies the contract for a Code Lens provider. + /// + public interface ICodeLensProvider + { + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + string ProviderId { get; } + + /// + /// Provides a collection of CodeLenses for the given + /// document. + /// + /// + /// The document for which CodeLenses should be provided. + /// + /// An array of CodeLenses. + CodeLens[] ProvideCodeLenses(ScriptFile scriptFile); + + /// + /// Resolves a CodeLens that was created without a Command. + /// + /// + /// The CodeLens to resolve. + /// + /// + /// A CancellationToken which can be used to cancel the + /// request. + /// + /// + /// A Task which returns the resolved CodeLens when completed. + /// + CodeLens ResolveCodeLens( + CodeLens codeLens, + ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs new file mode 100644 index 000000000..6bca0c9be --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Specifies the contract for an implementation of + /// the ICodeLenses component. + /// + public interface ICodeLenses + { + /// + /// Gets the collection of ICodeLensProvider implementations + /// that are registered with this component. + /// + List Providers { get; } + + /// + /// Provides a collection of CodeLenses for the given + /// document. + /// + /// + /// The document for which CodeLenses should be provided. + /// + /// An array of CodeLenses. + CodeLens[] ProvideCodeLenses(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs new file mode 100644 index 000000000..1f8ee1da1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation.Language; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices +{ + internal static class IScriptExtentExtensions + { + public static Range ToRange(this IScriptExtent scriptExtent) + { + return new Range + { + Start = new Position + { + Line = scriptExtent.StartLineNumber - 1, + Character = scriptExtent.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptExtent.EndLineNumber - 1, + Character = scriptExtent.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs new file mode 100644 index 000000000..3d7d85556 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Symbols; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + internal class PesterCodeLensProvider : ICodeLensProvider + { + + /// + /// The symbol provider to get symbols from to build code lenses with. + /// + private readonly IDocumentSymbolProvider _symbolProvider; + + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + public string ProviderId => nameof(PesterCodeLensProvider); + + /// + /// Create a new Pester CodeLens provider for a given editor session. + /// + public PesterCodeLensProvider() + { + _symbolProvider = new PesterDocumentSymbolProvider(); + } + + /// + /// Get the Pester CodeLenses for a given Pester symbol. + /// + /// The Pester symbol to get CodeLenses for. + /// The script file the Pester symbol comes from. + /// All CodeLenses for the given Pester symbol. + private CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile scriptFile) + { + + var codeLensResults = new CodeLens[] + { + new CodeLens() + { + Range = pesterSymbol.ScriptRegion.ToRange(), + Data = JToken.FromObject(new { + Uri = scriptFile.DocumentUri, + ProviderId = nameof(PesterCodeLensProvider) + }), + Command = new Command() + { + Name = "PowerShell.RunPesterTests", + Title = "Run tests", + Arguments = JArray.FromObject(new object[] { + scriptFile.DocumentUri, + false /* No debug */, + pesterSymbol.TestName, + pesterSymbol.ScriptRegion?.StartLineNumber }) + } + }, + + new CodeLens() + { + Range = pesterSymbol.ScriptRegion.ToRange(), + Data = JToken.FromObject(new { + Uri = scriptFile.DocumentUri, + ProviderId = nameof(PesterCodeLensProvider) + }), + Command = new Command() + { + Name = "PowerShell.RunPesterTests", + Title = "Debug tests", + Arguments = JArray.FromObject(new object[] { + scriptFile.DocumentUri, + true /* No debug */, + pesterSymbol.TestName, + pesterSymbol.ScriptRegion?.StartLineNumber }) + } + } + }; + + return codeLensResults; + } + + /// + /// Get all Pester CodeLenses for a given script file. + /// + /// The script file to get Pester CodeLenses for. + /// All Pester CodeLenses for the given script file. + public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + var lenses = new List(); + foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + if (symbol is PesterSymbolReference pesterSymbol) + { + if (pesterSymbol.Command != PesterCommandType.Describe) + { + continue; + } + + lenses.AddRange(GetPesterLens(pesterSymbol, scriptFile)); + } + } + + return lenses.ToArray(); + } + + /// + /// Resolve the CodeLens provision asynchronously -- just wraps the CodeLens argument in a task. + /// + /// The code lens to resolve. + /// The script file. + /// The given CodeLens, wrapped in a task. + public CodeLens ResolveCodeLens(CodeLens codeLens, ScriptFile scriptFile) + { + // This provider has no specific behavior for + // resolving CodeLenses. + return codeLens; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs new file mode 100644 index 000000000..5e1c16581 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -0,0 +1,179 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Symbols; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using PowerShellEditorServices.Engine.Utility; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Provides the "reference" code lens by extracting document symbols. + /// + internal class ReferencesCodeLensProvider : ICodeLensProvider + { + private static readonly Location[] s_emptyLocationArray = new Location[0]; + + /// + /// The document symbol provider to supply symbols to generate the code lenses. + /// + private IDocumentSymbolProvider _symbolProvider; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + public string ProviderId => nameof(ReferencesCodeLensProvider); + + /// + /// Construct a new ReferencesCodeLensProvider for a given EditorSession. + /// + /// + /// + public ReferencesCodeLensProvider(WorkspaceService workspaceService, SymbolsService symbolsService) + { + _workspaceService = workspaceService; + _symbolsService = symbolsService; + // TODO: Pull this from components + _symbolProvider = new ScriptDocumentSymbolProvider( + VersionUtils.PSVersion); + } + + /// + /// Get all reference code lenses for a given script file. + /// + /// The PowerShell script file to get code lenses for. + /// An array of CodeLenses describing all functions in the given script file. + public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + var acc = new List(); + foreach (SymbolReference sym in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + if (sym.SymbolType == SymbolType.Function) + { + //acc.Add(new CodeLens(this, scriptFile, sym.ScriptRegion)); + acc.Add(new CodeLens() + { + Data = JToken.FromObject(new + { + Uri = scriptFile.DocumentUri, + ProviderId = nameof(ReferencesCodeLensProvider) + }), + Range = sym.ScriptRegion.ToRange() + }); + } + } + + return acc.ToArray(); + } + + /// + /// Take a codelens and create a new codelens object with updated references. + /// + /// The old code lens to get updated references for. + /// A new code lens object describing the same data as the old one but with updated references. + public CodeLens ResolveCodeLens(CodeLens codeLens, ScriptFile scriptFile) + { + + ScriptFile[] references = _workspaceService.ExpandScriptReferences( + scriptFile); + + SymbolReference foundSymbol = _symbolsService.FindFunctionDefinitionAtLocation( + scriptFile, + (int)codeLens.Range.Start.Line + 1, + (int)codeLens.Range.Start.Character + 1); + + List referencesResult = _symbolsService.FindReferencesOfSymbol( + foundSymbol, + references, + _workspaceService); + + Location[] referenceLocations; + if (referencesResult == null) + { + referenceLocations = s_emptyLocationArray; + } + else + { + var acc = new List(); + foreach (SymbolReference foundReference in referencesResult) + { + if (!NotReferenceDefinition(foundSymbol, foundReference)) + { + continue; + } + + acc.Add(new Location + { + Uri = PathUtils.ToUri(foundReference.FilePath), + Range = foundReference.ScriptRegion.ToRange() + }); + } + referenceLocations = acc.ToArray(); + } + + return new CodeLens() + { + Data = codeLens.Data, + Range = codeLens.Range, + Command = new Command + { + Name = "editor.action.showReferences", + Title = GetReferenceCountHeader(referenceLocations.Length), + Arguments = JArray.FromObject(new object[] + { + scriptFile.DocumentUri, + codeLens.Range.Start, + referenceLocations + }) + } + }; + } + + /// + /// Check whether a SymbolReference is not a reference to another defined symbol. + /// + /// The symbol definition that may be referenced. + /// The reference symbol to check. + /// True if the reference is not a reference to the definition, false otherwise. + private static bool NotReferenceDefinition( + SymbolReference definition, + SymbolReference reference) + { + return + definition.ScriptRegion.StartLineNumber != reference.ScriptRegion.StartLineNumber + || definition.SymbolType != reference.SymbolType + || !string.Equals(definition.SymbolName, reference.SymbolName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Get the code lens header for the number of references on a definition, + /// given the number of references. + /// + /// The number of references found for a given definition. + /// The header string for the reference code lens. + private static string GetReferenceCountHeader(int referenceCount) + { + if (referenceCount == 1) + { + return "1 reference"; + } + + var sb = new StringBuilder(14); // "100 references".Length = 14 + sb.Append(referenceCount); + sb.Append(" references"); + return sb.ToString(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 6e9726f2c..92508adb2 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -35,10 +35,7 @@ public class SymbolsService /// Constructs an instance of the SymbolsService class and uses /// the given Runspace to execute language service operations. /// - /// - /// The PowerShellContext in which language service operations will be executed. - /// - /// An ILogger implementation used for writing log messages. + /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( ILoggerFactory factory) { @@ -208,5 +205,33 @@ public IReadOnlyList FindOccurrencesInFile( foundSymbol, needsAliases: false).ToArray(); } + + /// Finds a function definition in the script given a file location + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// A SymbolReference of the symbol found at the given location + /// or null if there is no symbol at that location + /// + public SymbolReference FindFunctionDefinitionAtLocation( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + SymbolReference symbolReference = + AstOperations.FindSymbolAtPosition( + scriptFile.ScriptAst, + lineNumber, + columnNumber, + includeFunctionDefinitions: true); + + if (symbolReference != null) + { + symbolReference.FilePath = scriptFile.FilePath; + } + + return symbolReference; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs new file mode 100644 index 000000000..463fbf767 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.CodeLenses; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + + private readonly ICodeLensProvider[] _providers; + + private CodeLensCapability _capability; + + public CodeLensHandlers(ILoggerFactory factory, SymbolsService symbolsService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _symbolsService = symbolsService; + _providers = new ICodeLensProvider[] + { + new ReferencesCodeLensProvider(_workspaceService, _symbolsService), + new PesterCodeLensProvider() + }; + } + + CodeLensRegistrationOptions IRegistration.GetRegistrationOptions() + { + return new CodeLensRegistrationOptions() + { + DocumentSelector = _documentSelector, + ResolveProvider = true + }; + } + + public Task Handle(CodeLensParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + CodeLens[] codeLensResults = ProvideCodeLenses(scriptFile); + + return Task.FromResult(new CodeLensContainer(codeLensResults)); + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions() + { + DocumentSelector = _documentSelector, + }; + } + + public bool CanResolve(CodeLens value) + { + CodeLensData codeLensData = value.Data.ToObject(); + return value?.Data != null && _providers.Any(provider => provider.ProviderId.Equals(codeLensData.ProviderId)); + } + + public Task Handle(CodeLens request, CancellationToken cancellationToken) + { + // TODO: Catch deserializtion exception on bad object + CodeLensData codeLensData = request.Data.ToObject(); + + ICodeLensProvider originalProvider = + _providers.FirstOrDefault( + provider => provider.ProviderId.Equals(codeLensData.ProviderId)); + + ScriptFile scriptFile = + _workspaceService.GetFile( + codeLensData.Uri); + + var resolvedCodeLens = originalProvider.ResolveCodeLens(request, scriptFile); + return Task.FromResult(resolvedCodeLens); + } + + public void SetCapability(CodeLensCapability capability) + { + _capability = capability; + } + + /// + /// Get all the CodeLenses for a given script file. + /// + /// The PowerShell script file to get CodeLenses for. + /// All generated CodeLenses for the given script file. + private CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + return InvokeProviders(provider => provider.ProvideCodeLenses(scriptFile)) + .SelectMany(codeLens => codeLens) + .ToArray(); + } + + /// + /// Invokes the given function synchronously against all + /// registered providers. + /// + /// The function to be invoked. + /// + /// An IEnumerable containing the results of all providers + /// that were invoked successfully. + /// + private IEnumerable InvokeProviders( + Func invokeFunc) + { + Stopwatch invokeTimer = new Stopwatch(); + List providerResults = new List(); + + foreach (var provider in this._providers) + { + try + { + invokeTimer.Restart(); + + providerResults.Add(invokeFunc(provider)); + + invokeTimer.Stop(); + + this._logger.LogTrace( + $"Invocation of provider '{provider.GetType().Name}' completed in {invokeTimer.ElapsedMilliseconds}ms."); + } + catch (Exception e) + { + this._logger.LogException( + $"Exception caught while invoking provider {provider.GetType().Name}:", + e); + } + } + + return providerResults; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs index a40814bba..d91ec865e 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs @@ -131,26 +131,5 @@ public ScriptRegion( IScriptPosition IScriptExtent.EndScriptPosition => throw new NotImplementedException(); #endregion - - #region Methods - - public Range ToRange() - { - return new Range - { - Start = new Position - { - Line = StartLineNumber - 1, - Character = StartColumnNumber - 1 - }, - End = new Position - { - Line = EndLineNumber - 1, - Character = EndColumnNumber - 1 - } - }; - } - - #endregion } } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 8aaa7511a..78d8de1a7 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -60,10 +60,14 @@ function New-TestFile param( [Parameter(Mandatory)] [string] - $Script + $Script, + + [Parameter()] + [string] + $FileName = "$([System.IO.Path]::GetRandomFileName()).ps1" ) - $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $Script -PassThru -Force + $file = Set-Content -Path (Join-Path $TestDrive $FileName) -Value $Script -PassThru -Force $request = Send-LspDidOpenTextDocumentRequest -Client $client ` -Uri ([Uri]::new($file.PSPath).AbsoluteUri) ` @@ -327,6 +331,60 @@ Write-Host 'Goodbye' $response.Result[1].Range.End.Character | Should -BeExactly 10 } + 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' + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 6b9c0987a..822ef602d 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -81,6 +81,8 @@ FunctionsToExport = @( 'Send-LspDocumentSymbolRequest', 'Send-LspDocumentHighlightRequest', 'Send-LspReferencesRequest', + 'Send-LspCodeLensRequest', + 'Send-LspCodeLensResolveRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', 'Get-LspResponse' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index d15582778..059b1ee17 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -487,6 +487,58 @@ function Send-LspDocumentHighlightRequest return Send-LspRequest -Client $Client -Method 'textDocument/documentHighlight' -Parameters $documentHighlightParams } +function Send-LspCodeLensRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Mandatory)] + [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-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] From b6d3c9a820d7aafdfb5960f052b4eb0e0bca5222 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 5 Aug 2019 11:27:50 -0700 Subject: [PATCH 24/47] powerShell/getPSHostProcesses and powerShell/getRunspace (#1002) --- .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Handlers/IGetPSHostProcessesHandler.cs | 21 ++++ .../Handlers/IGetRunspaceHandler.cs | 22 ++++ .../PSHostProcessAndRunspaceHandlers.cs | 104 ++++++++++++++++++ .../EditorServices.Integration.Tests.ps1 | 24 ++++ tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 20 +++- 7 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 73da0e447..8f8792980 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -93,6 +93,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .WithHandler(); logger.LogInformation("Handlers added"); diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs new file mode 100644 index 000000000..f5a366c1d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs @@ -0,0 +1,21 @@ +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/getPSHostProcesses")] + public interface IGetPSHostProcessesHandler : IJsonRpcRequestHandler { } + + public class GetPSHostProcesssesParams : IRequest { } + + public class PSHostProcessResponse + { + public string ProcessName { get; set; } + + public int ProcessId { get; set; } + + public string AppDomainName { get; set; } + + public string MainWindowTitle { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs new file mode 100644 index 000000000..dcd94c1e6 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs @@ -0,0 +1,22 @@ +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/getRunspace")] + public interface IGetRunspaceHandler : IJsonRpcRequestHandler { } + + public class GetRunspaceParams : IRequest + { + public string ProcessId {get; set; } + } + + public class RunspaceResponse + { + public int Id { get; set; } + + public string Name { get; set; } + + public string Availability { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs new file mode 100644 index 000000000..b24e2c7aa --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class PSHostProcessAndRunspaceHandlers : IGetPSHostProcessesHandler, IGetRunspaceHandler + { + private readonly ILogger _logger; + + public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory) + { + _logger = factory.CreateLogger(); + } + + public Task Handle(GetPSHostProcesssesParams request, CancellationToken cancellationToken) + { + var psHostProcesses = new List(); + + int processId = System.Diagnostics.Process.GetCurrentProcess().Id; + + using (var pwsh = PowerShell.Create()) + { + pwsh.AddCommand("Get-PSHostProcessInfo") + .AddCommand("Where-Object") + .AddParameter("Property", "ProcessId") + .AddParameter("NE") + .AddParameter("Value", processId.ToString()); + + var processes = pwsh.Invoke(); + + if (processes != null) + { + foreach (dynamic p in processes) + { + psHostProcesses.Add( + new PSHostProcessResponse + { + ProcessName = p.ProcessName, + ProcessId = p.ProcessId, + AppDomainName = p.AppDomainName, + MainWindowTitle = p.MainWindowTitle + }); + } + } + } + + return Task.FromResult(psHostProcesses.ToArray()); + } + + public Task Handle(GetRunspaceParams request, CancellationToken cancellationToken) + { + IEnumerable runspaces = null; + + if (request.ProcessId == null) { + request.ProcessId = "current"; + } + + // If the processId is a valid int, we need to run Get-Runspace within that process + // otherwise just use the current runspace. + if (int.TryParse(request.ProcessId, out int pid)) + { + // Create a remote runspace that we will invoke Get-Runspace in. + using(var rs = RunspaceFactory.CreateRunspace(new NamedPipeConnectionInfo(pid))) + using(var ps = PowerShell.Create()) + { + rs.Open(); + ps.Runspace = rs; + // Returns deserialized Runspaces. For simpler code, we use PSObject and rely on dynamic later. + runspaces = ps.AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace").Invoke(); + } + } + else + { + // TODO: Bring back + // var psCommand = new PSCommand().AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace"); + // var sb = new StringBuilder(); + // // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. + // runspaces = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand, sb); + } + + var runspaceResponses = new List(); + + if (runspaces != null) + { + foreach (dynamic runspace in runspaces) + { + runspaceResponses.Add( + new RunspaceResponse + { + Id = runspace.Id, + Name = runspace.Name, + Availability = runspace.RunspaceAvailability.ToString() + }); + } + } + + return Task.FromResult(runspaceResponses.ToArray()); + } + } +} diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 78d8de1a7..3903e3f32 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -331,6 +331,30 @@ Write-Host 'Goodbye' $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" { diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 822ef602d..5a296e017 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -81,6 +81,7 @@ FunctionsToExport = @( 'Send-LspDocumentSymbolRequest', 'Send-LspDocumentHighlightRequest', 'Send-LspReferencesRequest', + 'Send-LspGetRunspaceRequest', 'Send-LspCodeLensRequest', 'Send-LspCodeLensResolveRequest', 'Send-LspShutdownRequest', diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 059b1ee17..d2395368a 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -487,7 +487,7 @@ function Send-LspDocumentHighlightRequest return Send-LspRequest -Client $Client -Method 'textDocument/documentHighlight' -Parameters $documentHighlightParams } -function Send-LspCodeLensRequest +function Send-LspGetRunspaceRequest { [OutputType([PsesPsClient.LspRequest])] param( @@ -496,6 +496,24 @@ function Send-LspCodeLensRequest $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 ) From d6227645988fe52f459a4e744f2eaca29142a0bf Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 5 Aug 2019 12:42:57 -0700 Subject: [PATCH 25/47] Test only pester for now (#1003) --- NuGet.Config | 3 ++- PowerShellEditorServices.build.ps1 | 3 ++- src/PowerShellEditorServices/Hosting/BuildInfo.cs | 9 +++++++++ test/Pester/EditorServices.Integration.Tests.ps1 | 2 -- tools/PsesPsClient/PsesPsClient.psm1 | 3 +++ 5 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/PowerShellEditorServices/Hosting/BuildInfo.cs diff --git a/NuGet.Config b/NuGet.Config index 79196aec8..2f64451ed 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -4,6 +4,7 @@ - + + diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index c9c8cd17f..464bfb1f2 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -353,7 +353,8 @@ function DotNetTestFilter { if ($TestFilter) { @("--filter",$TestFilter) } else { "" } } -task Test TestServer,TestProtocol,TestPester +# task Test TestServer,TestProtocol,TestPester +task Test TestPester task TestServer { Set-Location .\test\PowerShellEditorServices.Test\ diff --git a/src/PowerShellEditorServices/Hosting/BuildInfo.cs b/src/PowerShellEditorServices/Hosting/BuildInfo.cs new file mode 100644 index 000000000..020786ae0 --- /dev/null +++ b/src/PowerShellEditorServices/Hosting/BuildInfo.cs @@ -0,0 +1,9 @@ +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public static class BuildInfo + { + public const string BuildVersion = ""; + public const string BuildOrigin = ""; + public static readonly System.DateTime? BuildTime = System.DateTime.Parse("2019-10-03T13:14:51"); + } +} diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 3903e3f32..2dd7079ba 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -88,8 +88,6 @@ function New-TestFile Describe "Loading and running PowerShellEditorServices" { BeforeAll { - Import-Module -Force "$PSScriptRoot/../../module/PowerShellEditorServices" - Import-Module -Force "$PSScriptRoot/../../src/PowerShellEditorServices.Engine/bin/Debug/netstandard2.0/publish/Omnisharp.Extensions.LanguageProtocol.dll" Import-Module -Force "$PSScriptRoot/../../tools/PsesPsClient/out/PsesPsClient" Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index d2395368a..69404f03a 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -6,6 +6,9 @@ $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 From 9e67baa9bf838e0673c2d87fd677fea23db7fb56 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 8 Aug 2019 20:24:15 -0700 Subject: [PATCH 26/47] Implement textDocument/codeAction (#1004) * Add initial handler * Add working codeAction implementation * Crash * Make tests work * Fix issues * Make tests work in WinPS --- .../LanguageServer/OmnisharpLanguageServer.cs | 3 +- .../PowerShellEditorServices.Engine.csproj | 2 +- .../Services/Analysis/AnalysisService.cs | 94 +++++++++++--- .../Handlers/CodeActionHandler.cs | 120 ++++++++++++++++++ .../Handlers/FormattingHandlers.cs | 4 +- .../Services/TextDocument/ScriptFileMarker.cs | 8 +- .../Handlers/ConfigurationHandler.cs | 2 +- .../Server/LanguageServer.cs | 1 - .../EditorServices.Integration.Tests.ps1 | 81 ++++++++++-- tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 70 ++++++++++ 11 files changed, 346 insertions(+), 40 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 8f8792980..ccf221008 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -94,7 +94,8 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler(); logger.LogInformation("Handlers added"); }); diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index c0fb4a3df..032e410d8 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index 992fb65ab..d36a0c987 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -15,6 +15,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; +using System.Collections.Concurrent; namespace Microsoft.PowerShell.EditorServices { @@ -22,7 +23,7 @@ namespace Microsoft.PowerShell.EditorServices /// Provides a high-level service for performing semantic analysis /// of PowerShell scripts. /// - public class AnalysisService : IDisposable + internal class AnalysisService : IDisposable { #region Static fields @@ -54,9 +55,6 @@ public class AnalysisService : IDisposable private static readonly string[] s_emptyGetRuleResult = new string[0]; - private Dictionary> codeActionsPerFile = - new Dictionary>(); - private static CancellationTokenSource s_existingRequestCancellation; /// @@ -96,8 +94,11 @@ public class AnalysisService : IDisposable private PSModuleInfo _pssaModuleInfo; private readonly ILanguageServer _languageServer; + private readonly ConfigurationService _configurationService; + private readonly ConcurrentDictionary)> _mostRecentCorrectionsByFile; + #endregion // Private Fields #region Properties @@ -149,6 +150,7 @@ private AnalysisService( _configurationService = configurationService; _logger = logger; _pssaModuleInfo = pssaModuleInfo; + _mostRecentCorrectionsByFile = new ConcurrentDictionary)>(); } #endregion // constructors @@ -813,26 +815,57 @@ private void PublishScriptDiagnostics( ScriptFile scriptFile, List markers) { - List diagnostics = new List(); + var diagnostics = new List(); - // Hold on to any corrections that may need to be applied later - Dictionary fileCorrections = - new Dictionary(); + // Create the entry for this file if it does not already exist + SemaphoreSlim fileLock; + Dictionary fileCorrections; + bool newEntryNeeded = false; + if (_mostRecentCorrectionsByFile.TryGetValue(scriptFile.DocumentUri, out (SemaphoreSlim, Dictionary) fileCorrectionsEntry)) + { + fileLock = fileCorrectionsEntry.Item1; + fileCorrections = fileCorrectionsEntry.Item2; + } + else + { + fileLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); + fileCorrections = new Dictionary(); + newEntryNeeded = true; + } - foreach (var marker in markers) + fileLock.Wait(); + try { - // Does the marker contain a correction? - Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); - if (marker.Correction != null) + if (newEntryNeeded) { - string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); - fileCorrections[diagnosticId] = marker.Correction; + // If we create a new entry, we should do it after acquiring the lock we just created + // to ensure a competing thread can never acquire it first and read invalid information from it + _mostRecentCorrectionsByFile[scriptFile.DocumentUri] = (fileLock, fileCorrections); } + else + { + // Otherwise we need to clear the stale corrections + fileCorrections.Clear(); + } + + foreach (ScriptFileMarker marker in markers) + { + // Does the marker contain a correction? + Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); + if (marker.Correction != null) + { + string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); + fileCorrections[diagnosticId] = marker.Correction; + } - diagnostics.Add(markerDiagnostic); + diagnostics.Add(markerDiagnostic); + } + } + finally + { + fileLock.Release(); } - codeActionsPerFile[scriptFile.DocumentUri] = fileCorrections; var uriBuilder = new UriBuilder() { @@ -850,9 +883,34 @@ private void PublishScriptDiagnostics( }); } + public async Task> GetMostRecentCodeActionsForFileAsync(string documentUri) + { + if (!_mostRecentCorrectionsByFile.TryGetValue(documentUri, out (SemaphoreSlim fileLock, Dictionary corrections) fileCorrectionsEntry)) + { + return null; + } + + await fileCorrectionsEntry.fileLock.WaitAsync(); + // We must copy the dictionary for thread safety + var corrections = new Dictionary(fileCorrectionsEntry.corrections.Count); + try + { + foreach (KeyValuePair correction in fileCorrectionsEntry.corrections) + { + corrections.Add(correction.Key, correction.Value); + } + + return corrections; + } + finally + { + fileCorrectionsEntry.fileLock.Release(); + } + } + // Generate a unique id that is used as a key to look up the associated code action (code fix) when // we receive and process the textDocument/codeAction message. - private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) + internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) { Position start = diagnostic.Range.Start; Position end = diagnostic.Range.End; @@ -860,7 +918,7 @@ private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) var sb = new StringBuilder(256) .Append(diagnostic.Source ?? "?") .Append("_") - .Append(diagnostic.Code.ToString()) + .Append(diagnostic.Code.IsString ? diagnostic.Code.String : diagnostic.Code.Long.ToString()) .Append("_") .Append(diagnostic.Severity?.ToString() ?? "?") .Append("_") diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs new file mode 100644 index 000000000..64f994423 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Services.Handlers; + +namespace Microsoft.PowerShell.EditorServices.TextDocument +{ + internal class CodeActionHandler : ICodeActionHandler + { + private static readonly CodeActionKind[] s_supportedCodeActions = new[] + { + CodeActionKind.QuickFix + }; + + private readonly CodeActionRegistrationOptions _registrationOptions; + + private readonly ILogger _logger; + + private readonly AnalysisService _analysisService; + + private CodeActionCapability _capability; + + public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService) + { + _logger = factory.CreateLogger(); + _analysisService = analysisService; + _registrationOptions = new CodeActionRegistrationOptions() + { + DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }), + CodeActionKinds = s_supportedCodeActions + }; + } + + public CodeActionRegistrationOptions GetRegistrationOptions() + { + return _registrationOptions; + } + + public void SetCapability(CodeActionCapability capability) + { + _capability = capability; + } + + public async Task Handle(CodeActionParams request, CancellationToken cancellationToken) + { + IReadOnlyDictionary corrections = await _analysisService.GetMostRecentCodeActionsForFileAsync(request.TextDocument.Uri.ToString()); + + if (corrections == null) + { + // TODO: Find out if we can cache this empty value + return new CommandOrCodeActionContainer(); + } + + var codeActions = new List(); + + // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI. + foreach (Diagnostic diagnostic in request.Context.Diagnostics) + { + if (diagnostic.Code.IsLong) + { + _logger.LogWarning( + $"textDocument/codeAction skipping diagnostic with non-string code {diagnostic.Code.Long}: {diagnostic.Source} {diagnostic.Message}"); + } + else if (string.IsNullOrEmpty(diagnostic.Code.String)) + { + _logger.LogWarning( + $"textDocument/codeAction skipping diagnostic with empty Code field: {diagnostic.Source} {diagnostic.Message}"); + + continue; + } + + + string diagnosticId = AnalysisService.GetUniqueIdFromDiagnostic(diagnostic); + if (corrections.TryGetValue(diagnosticId, out MarkerCorrection correction)) + { + codeActions.Add(new Command() + { + Title = correction.Name, + Name = "PowerShell.ApplyCodeActionEdits", + Arguments = JArray.FromObject(correction.Edits) + }); + } + } + + // Add "show documentation" commands last so they appear at the bottom of the client UI. + // These commands do not require code fixes. Sometimes we get a batch of diagnostics + // to create commands for. No need to create multiple show doc commands for the same rule. + var ruleNamesProcessed = new HashSet(); + foreach (Diagnostic diagnostic in request.Context.Diagnostics) + { + if (!diagnostic.Code.IsString || string.IsNullOrEmpty(diagnostic.Code.String)) { continue; } + + if (string.Equals(diagnostic.Source, "PSScriptAnalyzer", StringComparison.OrdinalIgnoreCase) && + !ruleNamesProcessed.Contains(diagnostic.Code.String)) + { + ruleNamesProcessed.Add(diagnostic.Code.String); + + codeActions.Add( + new Command + { + Title = $"Show documentation for \"{diagnostic.Code}\"", + Name = "PowerShell.ShowCodeActionDocumentation", + Arguments = JArray.FromObject(new[] { diagnostic.Code }) + }); + } + } + + return codeActions; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs index b66e581de..57c0744a1 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -10,7 +10,7 @@ namespace PowerShellEditorServices.Engine.Services.Handlers { - public class DocumentFormattingHandler : IDocumentFormattingHandler + internal class DocumentFormattingHandler : IDocumentFormattingHandler { private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() @@ -88,7 +88,7 @@ public void SetCapability(DocumentFormattingCapability capability) } } - public class DocumentRangeFormattingHandler : IDocumentRangeFormattingHandler + internal class DocumentRangeFormattingHandler : IDocumentRangeFormattingHandler { private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs index 9700464dd..a6f518c75 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs @@ -147,11 +147,11 @@ internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) new ScriptRegion( diagnosticRecord.ScriptPath, suggestedCorrection.Text, - suggestedCorrection.StartLineNumber, - suggestedCorrection.StartColumnNumber, + startLineNumber: suggestedCorrection.StartLineNumber, + startColumnNumber: suggestedCorrection.StartColumnNumber, + endLineNumber: suggestedCorrection.EndLineNumber, + endColumnNumber: suggestedCorrection.EndColumnNumber, startOffset: -1, - suggestedCorrection.EndLineNumber, - suggestedCorrection.EndColumnNumber, endOffset: -1)); correctionMessage = suggestedCorrection.Description; diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs index b97485dc0..c1d672999 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.EditorServices { - public class ConfigurationHandler : IDidChangeConfigurationHandler + internal class ConfigurationHandler : IDidChangeConfigurationHandler { private readonly ILogger _logger; private readonly AnalysisService _analysisService; diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index f7b4854ae..040579c43 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1744,7 +1744,6 @@ await DelayThenInvokeDiagnosticsAsync( cancellationToken); } - private static async Task DelayThenInvokeDiagnosticsAsync( int delayMilliseconds, ScriptFile[] filesToAnalyze, diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 2dd7079ba..edc53f684 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -163,20 +163,34 @@ function Get-Foo { It "Can get Diagnostics after changing settings" { $file = New-TestFile -Script 'gci | % { $_ }' - $request = Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ - PowerShell = @{ - ScriptAnalysis = @{ - Enable = $false + 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)) + # 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 + } + } + } } - $notifications | Should -Not -BeNullOrEmpty - $notifications.Params.diagnostics | Should -BeNullOrEmpty } It "Can handle folding request" { @@ -208,7 +222,7 @@ $_ $sortedResults[1].endCharacter | Should -Be 2 } - It "can handle a normal formatting request" { + It "Can handle a normal formatting request" { $filePath = New-TestFile -Script ' gci | % { Get-Process @@ -225,7 +239,7 @@ Get-Process $response.Result.newText.Contains("`t") | Should -BeTrue -Because "We expect a tab." } - It "can handle a range formatting request" { + It "Can handle a range formatting request" { $filePath = New-TestFile -Script ' gci | % { Get-Process @@ -407,6 +421,49 @@ Get-Foo $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 diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 5a296e017..34c3921fe 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -74,6 +74,7 @@ FunctionsToExport = @( 'Connect-PsesServer', 'Send-LspRequest', 'Send-LspInitializeRequest', + 'Send-LspCodeActionRequest', 'Send-LspDidOpenTextDocumentRequest', 'Send-LspDidChangeConfigurationRequest', 'Send-LspFormattingRequest', diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 69404f03a..099007ad9 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -560,6 +560,76 @@ function Send-LspCodeLensResolveRequest 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])] From a7057e3b749e6fe2d81471ae70d7161495f55d37 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 9 Aug 2019 11:30:59 -0700 Subject: [PATCH 27/47] Add powershellcontext (#1005) * Add powershellcontext * using file sink now instead * all the newlines --- PowerShellEditorServices.build.ps1 | 2 +- .../Hosting/EditorServicesHost.cs | 125 +- .../PowerShellEditorServices.Engine.csproj | 19 +- .../Components/ComponentRegistry.cs | 84 + .../Components/IComponentRegistry.cs | 61 + .../IComponentRegistryExtensions.cs | 87 + .../Console/ChoiceDetails.cs | 132 + .../Console/ChoicePromptHandler.cs | 353 +++ .../Console/CollectionFieldDetails.cs | 138 + .../Console/ConsoleChoicePromptHandler.cs | 133 + .../Console/ConsoleInputPromptHandler.cs | 102 + .../PowerShellContext/Console/ConsoleProxy.cs | 196 ++ .../Console/ConsoleReadLine.cs | 616 ++++ .../Console/CredentialFieldDetails.cs | 122 + .../PowerShellContext/Console/FieldDetails.cs | 234 ++ .../Console/IConsoleOperations.cs | 140 + .../Console/InputPromptHandler.cs | 331 +++ .../Console/PromptHandler.cs | 55 + .../Console/TerminalChoicePromptHandler.cs | 62 + .../Console/TerminalInputPromptHandler.cs | 79 + .../Console/UnixConsoleOperations.cs | 298 ++ .../Console/WindowsConsoleOperations.cs | 76 + .../Extensions/EditorCommand.cs | 89 + .../Extensions/EditorCommandAttribute.cs | 33 + .../Extensions/EditorContext.cs | 116 + .../Extensions/EditorObject.cs | 110 + .../Extensions/EditorWindow.cs | 83 + .../Extensions/EditorWorkspace.cs | 75 + .../Extensions/ExtensionService.cs | 216 ++ .../Extensions/FileContext.cs | 279 ++ .../Extensions/IEditorOperations.cs | 127 + .../PowerShellContextService.cs | 2506 +++++++++++++++++ .../Capabilities/DscBreakpointCapability.cs | 166 ++ .../Session/ExecutionOptions.cs | 92 + .../Session/ExecutionStatus.cs | 39 + .../ExecutionStatusChangedEventArgs.cs | 52 + .../Session/ExecutionTarget.cs | 28 + .../Session/Host/EditorServicesPSHost.cs | 373 +++ .../Host/EditorServicesPSHostUserInterface.cs | 1068 +++++++ .../Session/Host/IHostInput.cs | 28 + .../Session/Host/IHostOutput.cs | 175 ++ .../Host/SimplePSHostRawUserInterface.cs | 225 ++ .../Host/TerminalPSHostRawUserInterface.cs | 330 +++ .../Host/TerminalPSHostUserInterface.cs | 180 ++ .../Session/IPromptContext.cs | 67 + .../Session/IRunspaceCapability.cs | 12 + .../Session/IVersionSpecificOperations.cs | 33 + .../Session/InvocationEventQueue.cs | 263 ++ .../Session/LegacyReadLineContext.cs | 56 + .../PowerShellContext/Session/OutputType.cs | 41 + .../Session/OutputWrittenEventArgs.cs | 64 + .../Session/PSReadLinePromptContext.cs | 203 ++ .../Session/PSReadLineProxy.cs | 118 + .../Session/PipelineExecutionRequest.cs | 80 + .../Session/PowerShell5Operations.cs | 106 + .../Session/PowerShellContextState.cs | 46 + .../Session/PowerShellExecutionResult.cs | 39 + .../Session/PowerShellVersionDetails.cs | 166 ++ .../Session/ProgressDetails.cs | 32 + .../PowerShellContext/Session/PromptNest.cs | 564 ++++ .../Session/PromptNestFrame.cs | 137 + .../Session/PromptNestFrameType.cs | 21 + .../Session/RemoteFileManager.cs | 785 ++++++ .../Session/RunspaceChangedEventArgs.cs | 67 + .../Session/RunspaceDetails.cs | 321 +++ .../Session/RunspaceHandle.cs | 60 + .../Session/SessionDetails.cs | 63 + .../Session/SessionStateChangedEventArgs.cs | 47 + .../Session/ThreadController.cs | 131 + .../Handlers/ConfigurationHandler.cs | 45 +- .../Utility/AsyncLock.cs | 127 + .../Utility/AsyncQueue.cs | 223 ++ .../Utility/AsyncUtils.cs | 25 + .../EditorServicesHost.cs | 6 + .../PowerShellEditorServices.Host.csproj | 2 +- 75 files changed, 13728 insertions(+), 57 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 464bfb1f2..972a17766 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -83,7 +83,7 @@ $script:RequiredBuildAssets = @{ 'publish/OmniSharp.Extensions.LanguageServer.dll', 'publish/Serilog.dll', 'publish/Serilog.Extensions.Logging.dll', - 'publish/Serilog.Sinks.Console.dll', + 'publish/Serilog.Sinks.File.dll', 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', 'publish/Microsoft.Extensions.DependencyInjection.dll', 'publish/Microsoft.Extensions.Logging.Abstractions.dll', diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 9512b7918..017f99a60 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -4,10 +4,13 @@ // using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; @@ -60,11 +63,19 @@ public class EditorServicesHost private readonly HostDetails _hostDetails; + private readonly PSHost _internalHost; + + private readonly bool _enableConsoleRepl; + + private readonly HashSet _featureFlags; + + private readonly string[] _additionalModules; + private ILanguageServer _languageServer; - private readonly Extensions.Logging.ILogger _logger; + private Microsoft.Extensions.Logging.ILogger _logger; - private readonly ILoggerFactory _factory; + private ILoggerFactory _factory; #endregion @@ -126,24 +137,15 @@ public EditorServicesHost( Validate.IsNotNull(nameof(internalHost), internalHost); _serviceCollection = new ServiceCollection(); - - Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - _factory = new LoggerFactory().AddSerilog(Log.Logger); - _logger = _factory.CreateLogger(); - _hostDetails = hostDetails; - /* - this.hostDetails = hostDetails; - this.enableConsoleRepl = enableConsoleRepl; - this.bundledModulesPath = bundledModulesPath; - this.additionalModules = additionalModules ?? Array.Empty(); - this.featureFlags = new HashSet(featureFlags ?? Array.Empty(); - this.serverCompletedTask = new TaskCompletionSource(); - this.internalHost = internalHost; - */ + //this._hostDetails = hostDetails; + this._enableConsoleRepl = enableConsoleRepl; + //this.bundledModulesPath = bundledModulesPath; + this._additionalModules = additionalModules ?? Array.Empty(); + this._featureFlags = new HashSet(featureFlags ?? Array.Empty()); + //this.serverCompletedTask = new TaskCompletionSource(); + this._internalHost = internalHost; #if DEBUG if (waitForDebugger) @@ -174,6 +176,12 @@ public EditorServicesHost( /// The minimum level of log messages to be written. public void StartLogging(string logFilePath, PsesLogLevel logLevel) { + Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() + .WriteTo.File(logFilePath) + .CreateLogger(); + _factory = new LoggerFactory().AddSerilog(Log.Logger); + _logger = _factory.CreateLogger(); + FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); @@ -184,7 +192,7 @@ public void StartLogging(string logFilePath, PsesLogLevel logLevel) string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? ""; string logHeader = $@" -PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id} +PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id}) Host application details: @@ -219,23 +227,69 @@ public void StartLanguageService( { while (System.Diagnostics.Debugger.IsAttached) { - Console.WriteLine($"{Process.GetCurrentProcess().Id}"); + System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); Thread.Sleep(2000); } _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton( - (provider) => { - return AnalysisService.Create( - provider.GetService(), - provider.GetService(), - _factory.CreateLogger()); - } - ); + var logger = _factory.CreateLogger(); + var powerShellContext = new PowerShellContextService( + logger, + _featureFlags.Contains("PSReadLine")); + + // TODO: Bring this back + //EditorServicesPSHostUserInterface hostUserInterface = + // _enableConsoleRepl + // ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) + // : new ProtocolPSHostUserInterface(powerShellContext, messageSender, logger); + EditorServicesPSHostUserInterface hostUserInterface = + (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost); + + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + _hostDetails, + hostUserInterface, + logger); + + Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + powerShellContext.ImportCommandsModuleAsync( + Path.Combine( + Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), + @"..\Commands")); + + // TODO: This can be moved to the point after the $psEditor object + // gets initialized when that is done earlier than LanguageServer.Initialize + foreach (string module in this._additionalModules) + { + var command = + new System.Management.Automation.PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("Name", module); + + powerShellContext.ExecuteCommandAsync( + command, + sendOutputToHost: false, + sendErrorToHost: true); + } + + _serviceCollection + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(powerShellContext) + .AddSingleton( + (provider) => { + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + _factory.CreateLogger()); + } + ); _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) { @@ -248,10 +302,11 @@ public void StartLanguageService( _logger.LogInformation("Starting language server"); - Task.Factory.StartNew(() => _languageServer.StartAsync(), - CancellationToken.None, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); + Task.Run(_languageServer.StartAsync); + //Task.Factory.StartNew(() => _languageServer.StartAsync(), + // CancellationToken.None, + // TaskCreationOptions.LongRunning, + // TaskScheduler.Default); _logger.LogInformation( string.Format( diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index 032e410d8..a7d6b35ca 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -10,15 +10,32 @@ Latest + + latest + + + + + + latest + - + + + + + + + + + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs new file mode 100644 index 000000000..9a1de6d01 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Components +{ + /// + /// Provides a default implementation for the IComponentRegistry + /// interface. + /// + public class ComponentRegistry : IComponentRegistry + { + private Dictionary componentRegistry = + new Dictionary(); + + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The component type that the instance represents. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + public object Register(Type componentType, object componentInstance) + { + this.componentRegistry.Add(componentType, componentInstance); + return componentInstance; + } + + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// The implementation of the specified type. + public object Get(Type componentType) + { + return this.componentRegistry[componentType]; + } + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + public bool TryGet(Type componentType, out object componentInstance) + { + componentInstance = null; + + if (this.componentRegistry.TryGetValue(componentType, out componentInstance)) + { + return componentInstance != null; + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs new file mode 100644 index 000000000..c9f99000f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Components +{ + /// + /// Specifies the contract for a registry of component interfaces. + /// + public interface IComponentRegistry + { + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The component type that the instance represents. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + object Register( + Type componentType, + object componentInstance); + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// The implementation of the specified type. + object Get(Type componentType); + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The component type for which an instance will be retrieved. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + bool TryGet(Type componentType, out object componentInstance); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs new file mode 100644 index 000000000..0c6307d5a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs @@ -0,0 +1,87 @@ +// +// 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.Components +{ + /// + /// Provides generic helper methods for working with IComponentRegistry + /// methods. + /// + public static class IComponentRegistryExtensions + { + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The IComponentRegistry instance. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + public static TComponent Register( + this IComponentRegistry componentRegistry, + TComponent componentInstance) + where TComponent : class + { + return + (TComponent)componentRegistry.Register( + typeof(TComponent), + componentInstance); + } + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// + /// The IComponentRegistry instance. + /// + /// The implementation of the specified type. + public static TComponent Get( + this IComponentRegistry componentRegistry) + where TComponent : class + { + return (TComponent)componentRegistry.Get(typeof(TComponent)); + } + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The IComponentRegistry instance. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + public static bool TryGet( + this IComponentRegistry componentRegistry, + out TComponent componentInstance) + where TComponent : class + { + object componentObject = null; + componentInstance = null; + + if (componentRegistry.TryGet(typeof(TComponent), out componentObject)) + { + componentInstance = componentObject as TComponent; + return componentInstance != null; + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs new file mode 100644 index 000000000..d8121b6c1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs @@ -0,0 +1,132 @@ +// +// 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.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains the details about a choice that should be displayed + /// to the user. This class is meant to be serializable to the + /// user's UI. + /// + public class ChoiceDetails + { + #region Private Fields + + private string hotKeyString; + + #endregion + + #region Properties + + /// + /// Gets the label for the choice. + /// + public string Label { get; set; } + + /// + /// Gets the index of the hot key character for the choice. + /// + public int HotKeyIndex { get; set; } + + /// + /// Gets the hot key character. + /// + public char? HotKeyCharacter { get; set; } + + /// + /// Gets the help string that describes the choice. + /// + public string HelpMessage { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ChoiceDetails class with + /// the provided details. + /// + public ChoiceDetails() + { + // Parameterless constructor for deserialization. + } + + /// + /// Creates an instance of the ChoiceDetails class with + /// the provided details. + /// + /// + /// The label of the choice. An ampersand '&' may be inserted + /// before the character that will used as a hot key for the + /// choice. + /// + /// + /// A help message that describes the purpose of the choice. + /// + public ChoiceDetails(string label, string helpMessage) + { + this.HelpMessage = helpMessage; + + this.HotKeyIndex = label.IndexOf('&'); + if (this.HotKeyIndex >= 0) + { + this.Label = label.Remove(this.HotKeyIndex, 1); + + if (this.HotKeyIndex < this.Label.Length) + { + this.hotKeyString = this.Label[this.HotKeyIndex].ToString().ToUpper(); + this.HotKeyCharacter = this.hotKeyString[0]; + } + } + else + { + this.Label = label; + } + } + + /// + /// Creates a new instance of the ChoicePromptDetails class + /// based on a ChoiceDescription from the PowerShell layer. + /// + /// + /// A ChoiceDescription on which this instance will be based. + /// + /// A new ChoicePromptDetails instance. + public static ChoiceDetails Create(ChoiceDescription choiceDescription) + { + return new ChoiceDetails( + choiceDescription.Label, + choiceDescription.HelpMessage); + } + + #endregion + + #region Public Methods + + /// + /// Compares an input string to this choice to determine + /// whether the input string is a match. + /// + /// + /// The input string to compare to the choice. + /// + /// True if the input string is a match for the choice. + public bool MatchesInput(string inputString) + { + // Make sure the input string is trimmed of whitespace + inputString = inputString.Trim(); + + // Is it the hotkey? + return + string.Equals(inputString, this.hotKeyString, StringComparison.CurrentCultureIgnoreCase) || + string.Equals(inputString, this.Label, StringComparison.CurrentCultureIgnoreCase); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs new file mode 100644 index 000000000..b4524789f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs @@ -0,0 +1,353 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Indicates the style of prompt to be displayed. + /// + public enum PromptStyle + { + /// + /// Indicates that the full prompt should be displayed + /// with all relevant details. + /// + Full, + + /// + /// Indicates that a minimal prompt should be displayed, + /// generally used after the full prompt has already been + /// displayed and the options must be displayed again. + /// + Minimal + } + + /// + /// Provides a base implementation for IPromptHandler classes + /// that present the user a set of options from which a selection + /// should be made. + /// + public abstract class ChoicePromptHandler : PromptHandler + { + #region Private Fields + + private CancellationTokenSource promptCancellationTokenSource = + new CancellationTokenSource(); + private TaskCompletionSource> cancelTask = + new TaskCompletionSource>(); + + #endregion + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + public ChoicePromptHandler(ILogger logger) : base(logger) + { + } + + #region Properties + + /// + /// Returns true if the choice prompt allows multiple selections. + /// + protected bool IsMultiChoice { get; private set; } + + /// + /// Gets the caption (title) string to display with the prompt. + /// + protected string Caption { get; private set; } + + /// + /// Gets the descriptive message to display with the prompt. + /// + protected string Message { get; private set; } + + /// + /// Gets the array of choices from which the user must select. + /// + protected ChoiceDetails[] Choices { get; private set; } + + /// + /// Gets the index of the default choice so that the user + /// interface can make it easy to select this option. + /// + protected int[] DefaultChoices { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Prompts the user to make a choice using the provided details. + /// + /// + /// The caption string which will be displayed to the user. + /// + /// + /// The descriptive message which will be displayed to the user. + /// + /// + /// The list of choices from which the user will select. + /// + /// + /// The default choice to highlight for the user. + /// + /// + /// A CancellationToken that can be used to cancel the prompt. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's choice. + /// + public async Task PromptForChoiceAsync( + string promptCaption, + string promptMessage, + ChoiceDetails[] choices, + int defaultChoice, + CancellationToken cancellationToken) + { + // TODO: Guard against multiple calls + + this.Caption = promptCaption; + this.Message = promptMessage; + this.Choices = choices; + + this.DefaultChoices = + defaultChoice == -1 + ? new int[] { } + : new int[] { defaultChoice }; + + // Cancel the TaskCompletionSource if the caller cancels the task + cancellationToken.Register(this.CancelPrompt, true); + + // Convert the int[] result to int + return await this.WaitForTaskAsync( + this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token) + .ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + return this.GetSingleResult(task.Result); + })); + } + + /// + /// Prompts the user to make a choice of one or more options using the + /// provided details. + /// + /// + /// The caption string which will be displayed to the user. + /// + /// + /// The descriptive message which will be displayed to the user. + /// + /// + /// The list of choices from which the user will select. + /// + /// + /// The default choice(s) to highlight for the user. + /// + /// + /// A CancellationToken that can be used to cancel the prompt. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's choices. + /// + public async Task PromptForChoiceAsync( + string promptCaption, + string promptMessage, + ChoiceDetails[] choices, + int[] defaultChoices, + CancellationToken cancellationToken) + { + // TODO: Guard against multiple calls + + this.Caption = promptCaption; + this.Message = promptMessage; + this.Choices = choices; + this.DefaultChoices = defaultChoices; + this.IsMultiChoice = true; + + // Cancel the TaskCompletionSource if the caller cancels the task + cancellationToken.Register(this.CancelPrompt, true); + + return await this.WaitForTaskAsync( + this.StartPromptLoopAsync( + this.promptCancellationTokenSource.Token)); + } + + private async Task WaitForTaskAsync(Task taskToWait) + { + Task finishedTask = + await Task.WhenAny( + this.cancelTask.Task, + taskToWait); + + if (this.cancelTask.Task.IsCanceled) + { + throw new PipelineStoppedException(); + } + + return taskToWait.Result; + } + + private async Task StartPromptLoopAsync( + CancellationToken cancellationToken) + { + int[] choiceIndexes = null; + + // Show the prompt to the user + this.ShowPrompt(PromptStyle.Full); + + while (!cancellationToken.IsCancellationRequested) + { + string responseString = await this.ReadInputStringAsync(cancellationToken); + if (responseString == null) + { + // If the response string is null, the prompt has been cancelled + break; + } + + choiceIndexes = this.HandleResponse(responseString); + + // Return the default choice values if no choices were entered + if (choiceIndexes == null && string.IsNullOrEmpty(responseString)) + { + choiceIndexes = this.DefaultChoices; + } + + // If the user provided no choices, we should prompt again + if (choiceIndexes != null) + { + break; + } + + // The user did not respond with a valid choice, + // show the prompt again to give another chance + this.ShowPrompt(PromptStyle.Minimal); + } + + if (cancellationToken.IsCancellationRequested) + { + // Throw a TaskCanceledException to stop the pipeline + throw new TaskCanceledException(); + } + + return choiceIndexes?.ToArray(); + } + + /// + /// Implements behavior to handle the user's response. + /// + /// The string representing the user's response. + /// + /// True if the prompt is complete, false if the prompt is + /// still waiting for a valid response. + /// + protected virtual int[] HandleResponse(string responseString) + { + List choiceIndexes = new List(); + + // Clean up the response string and split it + var choiceStrings = + responseString.Trim().Split( + new char[] { ',' }, + StringSplitOptions.RemoveEmptyEntries); + + foreach (string choiceString in choiceStrings) + { + for (int i = 0; i < this.Choices.Length; i++) + { + if (this.Choices[i].MatchesInput(choiceString)) + { + choiceIndexes.Add(i); + + // If this is a single-choice prompt, break out after + // the first matched choice + if (!this.IsMultiChoice) + { + break; + } + } + } + } + + if (choiceIndexes.Count == 0) + { + // The user did not respond with a valid choice, + // show the prompt again to give another chance + return null; + } + + return choiceIndexes.ToArray(); + } + + /// + /// Called when the active prompt should be cancelled. + /// + protected override void OnPromptCancelled() + { + // Cancel the prompt task + this.promptCancellationTokenSource.Cancel(); + this.cancelTask.TrySetCanceled(); + } + + #endregion + + #region Abstract Methods + + /// + /// Called when the prompt should be displayed to the user. + /// + /// + /// Indicates the prompt style to use when showing the prompt. + /// + protected abstract void ShowPrompt(PromptStyle promptStyle); + + /// + /// Reads an input string asynchronously from the console. + /// + /// + /// A CancellationToken that can be used to cancel the read. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); + + #endregion + + #region Private Methods + + private int GetSingleResult(int[] choiceArray) + { + return + choiceArray != null + ? choiceArray.DefaultIfEmpty(-1).First() + : -1; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs new file mode 100644 index 000000000..80ae62b5f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details of an colleciton input field shown + /// from an InputPromptHandler. This class is meant to be + /// serializable to the user's UI. + /// + public class CollectionFieldDetails : FieldDetails + { + #region Private Fields + + private bool isArray; + private bool isEntryComplete; + private string fieldName; + private int currentCollectionIndex; + private ArrayList collectionItems = new ArrayList(); + + #endregion + + #region Constructors + + /// + /// Creates an instance of the CollectionFieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The field's value type. + /// If true, marks the field as mandatory. + /// The field's default value. + public CollectionFieldDetails( + string name, + string label, + Type fieldType, + bool isMandatory, + object defaultValue) + : base(name, label, fieldType, isMandatory, defaultValue) + { + this.fieldName = name; + + this.FieldType = typeof(object); + + if (fieldType.IsArray) + { + this.isArray = true; + this.FieldType = fieldType.GetElementType(); + } + + this.Name = + string.Format( + "{0}[{1}]", + this.fieldName, + this.currentCollectionIndex); + } + + #endregion + + #region Public Methods + + /// + /// Gets the next field to display if this is a complex + /// field, otherwise returns null. + /// + /// + /// A FieldDetails object if there's another field to + /// display or if this field is complete. + /// + public override FieldDetails GetNextField() + { + if (!this.isEntryComplete) + { + // Get the next collection field + this.currentCollectionIndex++; + this.Name = + string.Format( + "{0}[{1}]", + this.fieldName, + this.currentCollectionIndex); + + return this; + } + else + { + return null; + } + } + + /// + /// Sets the field's value. + /// + /// The field's value. + /// + /// True if a value has been supplied by the user, false if the user supplied no value. + /// + public override void SetValue(object fieldValue, bool hasValue) + { + if (hasValue) + { + // Add the item to the collection + this.collectionItems.Add(fieldValue); + } + else + { + this.isEntryComplete = true; + } + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + protected override object OnGetValue() + { + object collection = this.collectionItems; + + // Should the result collection be an array? + if (this.isArray) + { + // Convert the ArrayList to an array + collection = + this.collectionItems.ToArray( + this.FieldType); + } + + return collection; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs new file mode 100644 index 000000000..e5ed4472c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs @@ -0,0 +1,133 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of ChoicePromptHandler + /// for use in the interactive console (REPL). + /// + public abstract class ConsoleChoicePromptHandler : ChoicePromptHandler + { + #region Private Fields + + /// + /// The IHostOutput instance to use for this prompt. + /// + protected IHostOutput hostOutput; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleChoicePromptHandler class. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public ConsoleChoicePromptHandler( + IHostOutput hostOutput, + ILogger logger) + : base(logger) + { + this.hostOutput = hostOutput; + } + + #endregion + + /// + /// Called when the prompt should be displayed to the user. + /// + /// + /// Indicates the prompt style to use when showing the prompt. + /// + protected override void ShowPrompt(PromptStyle promptStyle) + { + if (promptStyle == PromptStyle.Full) + { + if (this.Caption != null) + { + this.hostOutput.WriteOutput(this.Caption); + } + + if (this.Message != null) + { + this.hostOutput.WriteOutput(this.Message); + } + } + + foreach (var choice in this.Choices) + { + string hotKeyString = + choice.HotKeyIndex > -1 ? + choice.Label[choice.HotKeyIndex].ToString().ToUpper() : + string.Empty; + + this.hostOutput.WriteOutput( + string.Format( + "[{0}] {1} ", + hotKeyString, + choice.Label), + false); + } + + this.hostOutput.WriteOutput("[?] Help", false); + + var validDefaultChoices = + this.DefaultChoices.Where( + choice => choice > -1 && choice < this.Choices.Length); + + if (validDefaultChoices.Any()) + { + var choiceString = + string.Join( + ", ", + this.DefaultChoices + .Select(choice => this.Choices[choice].Label)); + + this.hostOutput.WriteOutput( + $" (default is \"{choiceString}\"): ", + false); + } + } + + + /// + /// Implements behavior to handle the user's response. + /// + /// The string representing the user's response. + /// + /// True if the prompt is complete, false if the prompt is + /// still waiting for a valid response. + /// + protected override int[] HandleResponse(string responseString) + { + if (responseString.Trim() == "?") + { + // Print help text + foreach (var choice in this.Choices) + { + this.hostOutput.WriteOutput( + string.Format( + "{0} - {1}", + (choice.HotKeyCharacter.HasValue ? + choice.HotKeyCharacter.Value.ToString() : + choice.Label), + choice.HelpMessage)); + } + + return null; + } + + return base.HandleResponse(responseString); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs new file mode 100644 index 000000000..6e804f86e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs @@ -0,0 +1,102 @@ +// +// 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.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of InputPromptHandler + /// for use in the interactive console (REPL). + /// + public abstract class ConsoleInputPromptHandler : InputPromptHandler + { + #region Private Fields + + /// + /// The IHostOutput instance to use for this prompt. + /// + protected IHostOutput hostOutput; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleInputPromptHandler class. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public ConsoleInputPromptHandler( + IHostOutput hostOutput, + ILogger logger) + : base(logger) + { + this.hostOutput = hostOutput; + } + + #endregion + + #region Public Methods + + /// + /// Called when the prompt caption and message should be + /// displayed to the user. + /// + /// The caption string to be displayed. + /// The message string to be displayed. + protected override void ShowPromptMessage(string caption, string message) + { + if (!string.IsNullOrEmpty(caption)) + { + this.hostOutput.WriteOutput(caption, true); + } + + if (!string.IsNullOrEmpty(message)) + { + this.hostOutput.WriteOutput(message, true); + } + } + + /// + /// Called when a prompt should be displayed for a specific + /// input field. + /// + /// The details of the field to be displayed. + protected override void ShowFieldPrompt(FieldDetails fieldDetails) + { + // For a simple prompt there won't be any field name. + // In this case don't write anything + if (!string.IsNullOrEmpty(fieldDetails.Name)) + { + this.hostOutput.WriteOutput( + fieldDetails.Name + ": ", + false); + } + } + + /// + /// Called when an error should be displayed, such as when the + /// user types in a string with an incorrect format for the + /// current field. + /// + /// + /// The Exception containing the error to be displayed. + /// + protected override void ShowErrorMessage(Exception e) + { + this.hostOutput.WriteOutput( + e.Message, + true, + OutputType.Error); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs new file mode 100644 index 000000000..b9312ca7c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs @@ -0,0 +1,196 @@ +// +// 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.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides asynchronous implementations of the API's as well as + /// synchronous implementations that work around platform specific issues. + /// + internal static class ConsoleProxy + { + private static IConsoleOperations s_consoleProxy; + + static ConsoleProxy() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + s_consoleProxy = new WindowsConsoleOperations(); + return; + } + + s_consoleProxy = new UnixConsoleOperations(); + } + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// An object that describes the constant and Unicode character, if any, + /// that correspond to the pressed console key. The object also + /// describes, in a bitwise combination of values, whether + /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. + /// + public static ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) => + s_consoleProxy.ReadKey(intercept, cancellationToken); + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// A task that will complete with a result of the key pressed by the user. + /// + public static Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) => + s_consoleProxy.ReadKeyAsync(intercept, cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The horizontal position of the console cursor. + public static int GetCursorLeft() => + s_consoleProxy.GetCursorLeft(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The horizontal position of the console cursor. + public static int GetCursorLeft(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeft(cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + public static Task GetCursorLeftAsync() => + s_consoleProxy.GetCursorLeftAsync(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorLeftAsync(cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The vertical position of the console cursor. + public static int GetCursorTop() => + s_consoleProxy.GetCursorTop(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The vertical position of the console cursor. + public static int GetCursorTop(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTop(cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + public static Task GetCursorTopAsync() => + s_consoleProxy.GetCursorTopAsync(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + public static Task GetCursorTopAsync(CancellationToken cancellationToken) => + s_consoleProxy.GetCursorTopAsync(cancellationToken); + + /// + /// On Unix platforms this method is sent to PSReadLine as a work around for issues + /// with the System.Console implementation for that platform. Functionally it is the + /// same as System.Console.ReadKey, with the exception that it will not lock the + /// standard input stream. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// true to not display the pressed key; otherwise, false. + /// + /// + /// The that can be used to cancel the request. + /// + /// + /// An object that describes the ConsoleKey constant and Unicode character, if any, + /// that correspond to the pressed console key. The ConsoleKeyInfo object also describes, + /// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt, + /// or Ctrl modifier keys was pressed simultaneously with the console key. + /// + internal static ConsoleKeyInfo UnixReadKey(bool intercept, CancellationToken cancellationToken) + { + try + { + return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken); + } + catch (OperationCanceledException) + { + return default(ConsoleKeyInfo); + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs new file mode 100644 index 000000000..466e10764 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs @@ -0,0 +1,616 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + using System; + using System.Management.Automation; + using System.Management.Automation.Language; + using System.Security; + + internal class ConsoleReadLine + { + #region Private Field + private PowerShellContextService powerShellContext; + + #endregion + + #region Constructors + + public ConsoleReadLine(PowerShellContextService powerShellContext) + { + this.powerShellContext = powerShellContext; + } + + #endregion + + #region Public Methods + + public Task ReadCommandLineAsync(CancellationToken cancellationToken) + { + return this.ReadLineAsync(true, cancellationToken); + } + + public Task ReadSimpleLineAsync(CancellationToken cancellationToken) + { + return this.ReadLineAsync(false, cancellationToken); + } + + public async Task ReadSecureLineAsync(CancellationToken cancellationToken) + { + SecureString secureString = new SecureString(); + + int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + int previousInputLength = 0; + + Console.TreatControlCAsInput = true; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken); + + if ((int)keyInfo.Key == 3 || + keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + throw new PipelineStoppedException(); + } + if (keyInfo.Key == ConsoleKey.Enter) + { + // Break to return the completed string + break; + } + if (keyInfo.Key == ConsoleKey.Tab) + { + continue; + } + if (keyInfo.Key == ConsoleKey.Backspace) + { + if (secureString.Length > 0) + { + secureString.RemoveAt(secureString.Length - 1); + } + } + else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + secureString.AppendChar(keyInfo.KeyChar); + } + + // Re-render the secure string characters + int currentInputLength = secureString.Length; + int consoleWidth = Console.WindowWidth; + + if (currentInputLength > previousInputLength) + { + Console.Write('*'); + } + else if (previousInputLength > 0 && currentInputLength < previousInputLength) + { + int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + + // Back up the cursor before clearing the character + col--; + if (col < 0) + { + col = consoleWidth - 1; + row--; + } + + Console.SetCursorPosition(col, row); + Console.Write(' '); + Console.SetCursorPosition(col, row); + } + + previousInputLength = currentInputLength; + } + } + finally + { + Console.TreatControlCAsInput = false; + } + + return secureString; + } + + #endregion + + #region Private Methods + + private static async Task ReadKeyAsync(CancellationToken cancellationToken) + { + return await ConsoleProxy.ReadKeyAsync(intercept: true, cancellationToken); + } + + private async Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken); + } + + /// + /// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine. + /// This method should be used when PSReadLine is disabled, either by user settings or + /// unsupported PowerShell versions. + /// + /// + /// Indicates whether ReadLine should act like a command line. + /// + /// + /// The cancellation token that will be checked prior to completing the returned task. + /// + /// + /// A task object representing the asynchronus operation. The Result property on + /// the task object returns the user input string. + /// + internal async Task InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + string inputBeforeCompletion = null; + string inputAfterCompletion = null; + CommandCompletion currentCompletion = null; + + int historyIndex = -1; + Collection currentHistory = null; + + StringBuilder inputLine = new StringBuilder(); + + int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); + int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + + int initialWindowLeft = Console.WindowLeft; + int initialWindowTop = Console.WindowTop; + + int currentCursorIndex = 0; + + Console.TreatControlCAsInput = true; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken); + + // Do final position calculation after the key has been pressed + // because the window could have been resized before then + int promptStartCol = initialCursorCol; + int promptStartRow = initialCursorRow; + int consoleWidth = Console.WindowWidth; + + if ((int)keyInfo.Key == 3 || + keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + throw new PipelineStoppedException(); + } + else if (keyInfo.Key == ConsoleKey.Tab && isCommandLine) + { + if (currentCompletion == null) + { + inputBeforeCompletion = inputLine.ToString(); + inputAfterCompletion = null; + + // TODO: This logic should be moved to AstOperations or similar! + + if (this.powerShellContext.IsDebuggerStopped) + { + PSCommand command = new PSCommand(); + command.AddCommand("TabExpansion2"); + command.AddParameter("InputScript", inputBeforeCompletion); + command.AddParameter("CursorColumn", currentCursorIndex); + command.AddParameter("Options", null); + + var results = + await this.powerShellContext.ExecuteCommandAsync(command, false, false); + + currentCompletion = results.FirstOrDefault(); + } + else + { + using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandleAsync()) + using (PowerShell powerShell = PowerShell.Create()) + { + powerShell.Runspace = runspaceHandle.Runspace; + currentCompletion = + CommandCompletion.CompleteInput( + inputBeforeCompletion, + currentCursorIndex, + null, + powerShell); + + if (currentCompletion.CompletionMatches.Count > 0) + { + int replacementEndIndex = + currentCompletion.ReplacementIndex + + currentCompletion.ReplacementLength; + + inputAfterCompletion = + inputLine.ToString( + replacementEndIndex, + inputLine.Length - replacementEndIndex); + } + else + { + currentCompletion = null; + } + } + } + } + + CompletionResult completion = + currentCompletion?.GetNextResult( + !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); + + if (completion != null) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + $"{completion.CompletionText}{inputAfterCompletion}", + currentCursorIndex, + insertIndex: currentCompletion.ReplacementIndex, + replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, + finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); + } + } + else if (keyInfo.Key == ConsoleKey.LeftArrow) + { + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex - 1); + } + } + else if (keyInfo.Key == ConsoleKey.Home) + { + currentCompletion = null; + + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + 0); + } + else if (keyInfo.Key == ConsoleKey.RightArrow) + { + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex + 1); + } + } + else if (keyInfo.Key == ConsoleKey.End) + { + currentCompletion = null; + + currentCursorIndex = + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + inputLine.Length); + } + else if (keyInfo.Key == ConsoleKey.UpArrow && isCommandLine) + { + currentCompletion = null; + + // TODO: Ctrl+Up should allow navigation in multi-line input + + if (currentHistory == null) + { + historyIndex = -1; + + PSCommand command = new PSCommand(); + command.AddCommand("Get-History"); + + currentHistory = + await this.powerShellContext.ExecuteCommandAsync( + command, + false, + false) as Collection; + + if (currentHistory != null) + { + historyIndex = currentHistory.Count; + } + } + + if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) + { + historyIndex--; + + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + } + else if (keyInfo.Key == ConsoleKey.DownArrow && isCommandLine) + { + currentCompletion = null; + + // The down arrow shouldn't cause history to be loaded, + // it's only for navigating an active history array + + if (historyIndex > -1 && historyIndex < currentHistory.Count && + currentHistory != null && currentHistory.Count > 0) + { + historyIndex++; + + if (historyIndex < currentHistory.Count) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + else if (historyIndex == currentHistory.Count) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + } + } + else if (keyInfo.Key == ConsoleKey.Escape) + { + currentCompletion = null; + historyIndex = currentHistory != null ? currentHistory.Count : -1; + + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + else if (keyInfo.Key == ConsoleKey.Backspace) + { + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: currentCursorIndex - 1, + replaceLength: 1, + finalCursorIndex: currentCursorIndex - 1); + } + } + else if (keyInfo.Key == ConsoleKey.Delete) + { + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + replaceLength: 1, + finalCursorIndex: currentCursorIndex); + } + } + else if (keyInfo.Key == ConsoleKey.Enter) + { + string completedInput = inputLine.ToString(); + currentCompletion = null; + currentHistory = null; + + //if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) + //{ + // // TODO: Start a new line! + // continue; + //} + + Parser.ParseInput( + completedInput, + out Token[] tokens, + out ParseError[] parseErrors); + + //if (parseErrors.Any(e => e.IncompleteInput)) + //{ + // // TODO: Start a new line! + // continue; + //} + + return completedInput; + } + else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + // Normal character input + currentCompletion = null; + + currentCursorIndex = + this.InsertInput( + inputLine, + promptStartCol, + promptStartRow, + keyInfo.KeyChar.ToString(), + currentCursorIndex, + finalCursorIndex: currentCursorIndex + 1); + } + } + } + finally + { + Console.TreatControlCAsInput = false; + } + + return null; + } + + private int CalculateIndexFromCursor( + int promptStartCol, + int promptStartRow, + int consoleWidth) + { + return + ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + + ConsoleProxy.GetCursorLeft() - promptStartCol; + } + + private void CalculateCursorFromIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int inputIndex, + out int cursorCol, + out int cursorRow) + { + cursorCol = promptStartCol + inputIndex; + cursorRow = promptStartRow + cursorCol / consoleWidth; + cursorCol = cursorCol % consoleWidth; + } + + private int InsertInput( + StringBuilder inputLine, + int promptStartCol, + int promptStartRow, + string insertedInput, + int cursorIndex, + int insertIndex = -1, + int replaceLength = 0, + int finalCursorIndex = -1) + { + int consoleWidth = Console.WindowWidth; + int previousInputLength = inputLine.Length; + + if (insertIndex == -1) + { + insertIndex = cursorIndex; + } + + // Move the cursor to the new insertion point + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + insertIndex); + + // Edit the input string based on the insertion + if (insertIndex < inputLine.Length) + { + if (replaceLength > 0) + { + inputLine.Remove(insertIndex, replaceLength); + } + + inputLine.Insert(insertIndex, insertedInput); + } + else + { + inputLine.Append(insertedInput); + } + + // Re-render affected section + Console.Write( + inputLine.ToString( + insertIndex, + inputLine.Length - insertIndex)); + + if (inputLine.Length < previousInputLength) + { + Console.Write( + new string( + ' ', + previousInputLength - inputLine.Length)); + } + + // Automatically set the final cursor position to the end + // of the new input string. This is needed if the previous + // input string is longer than the new one and needed to have + // its old contents overwritten. This will position the cursor + // back at the end of the new text + if (finalCursorIndex == -1 && inputLine.Length < previousInputLength) + { + finalCursorIndex = inputLine.Length; + } + + if (finalCursorIndex > -1) + { + // Move the cursor to the final position + return + this.MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + finalCursorIndex); + } + else + { + return inputLine.Length; + } + } + + private int MoveCursorToIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int newCursorIndex) + { + this.CalculateCursorFromIndex( + promptStartCol, + promptStartRow, + consoleWidth, + newCursorIndex, + out int newCursorCol, + out int newCursorRow); + + Console.SetCursorPosition(newCursorCol, newCursorRow); + + return newCursorIndex; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs new file mode 100644 index 000000000..4b4452f2b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs @@ -0,0 +1,122 @@ +// +// 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.Management.Automation; +using System.Security; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details of a PSCredential field shown + /// from an InputPromptHandler. This class is meant to + /// be serializable to the user's UI. + /// + public class CredentialFieldDetails : FieldDetails + { + private string userName; + private SecureString password; + + /// + /// Creates an instance of the CredentialFieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The initial value of the userName field. + public CredentialFieldDetails( + string name, + string label, + string userName) + : this(name, label, typeof(PSCredential), true, null) + { + if (!string.IsNullOrEmpty(userName)) + { + // Call GetNextField to prepare the password field + this.userName = userName; + this.GetNextField(); + } + } + + /// + /// Creates an instance of the CredentialFieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The field's value type. + /// If true, marks the field as mandatory. + /// The field's default value. + public CredentialFieldDetails( + string name, + string label, + Type fieldType, + bool isMandatory, + object defaultValue) + : base(name, label, fieldType, isMandatory, defaultValue) + { + this.Name = "User"; + this.FieldType = typeof(string); + } + + #region Public Methods + + /// + /// Gets the next field to display if this is a complex + /// field, otherwise returns null. + /// + /// + /// A FieldDetails object if there's another field to + /// display or if this field is complete. + /// + public override FieldDetails GetNextField() + { + if (this.password != null) + { + // No more fields to display + return null; + } + else if (this.userName != null) + { + this.Name = $"Password for user {this.userName}"; + this.FieldType = typeof(SecureString); + } + + return this; + } + + /// + /// Sets the field's value. + /// + /// The field's value. + /// + /// True if a value has been supplied by the user, false if the user supplied no value. + /// + public override void SetValue(object fieldValue, bool hasValue) + { + if (hasValue) + { + if (this.userName == null) + { + this.userName = (string)fieldValue; + } + else + { + this.password = (SecureString)fieldValue; + } + } + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + protected override object OnGetValue() + { + return new PSCredential(this.userName, this.password); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs new file mode 100644 index 000000000..9fc80252e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs @@ -0,0 +1,234 @@ +// +// 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.Utility; +using System; +using System.Collections; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Contains the details of an input field shown from an + /// InputPromptHandler. This class is meant to be + /// serializable to the user's UI. + /// + public class FieldDetails + { + #region Private Fields + + private object fieldValue; + + #endregion + + #region Properties + + /// + /// Gets or sets the name of the field. + /// + public string Name { get; set; } + + /// + /// Gets or sets the original name of the field before it was manipulated. + /// + public string OriginalName { get; set; } + + /// + /// Gets or sets the descriptive label for the field. + /// + public string Label { get; set; } + + /// + /// Gets or sets the field's value type. + /// + public Type FieldType { get; set; } + + /// + /// Gets or sets the field's help message. + /// + public string HelpMessage { get; set; } + + /// + /// Gets or sets a boolean that is true if the user + /// must enter a value for the field. + /// + public bool IsMandatory { get; set; } + + /// + /// Gets or sets the default value for the field. + /// + public object DefaultValue { get; set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the FieldDetails class. + /// + /// The field's name. + /// The field's label. + /// The field's value type. + /// If true, marks the field as mandatory. + /// The field's default value. + public FieldDetails( + string name, + string label, + Type fieldType, + bool isMandatory, + object defaultValue) + { + this.OriginalName = name; + this.Name = name; + this.Label = label; + this.FieldType = fieldType; + this.IsMandatory = isMandatory; + this.DefaultValue = defaultValue; + + if (fieldType.GetTypeInfo().IsGenericType) + { + throw new PSArgumentException( + "Generic types are not supported for input fields at this time."); + } + } + + #endregion + + #region Public Methods + + /// + /// Sets the field's value. + /// + /// The field's value. + /// + /// True if a value has been supplied by the user, false if the user supplied no value. + /// + public virtual void SetValue(object fieldValue, bool hasValue) + { + if (hasValue) + { + this.fieldValue = fieldValue; + } + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + public object GetValue(ILogger logger) + { + object fieldValue = this.OnGetValue(); + + if (fieldValue == null) + { + if (!this.IsMandatory) + { + fieldValue = this.DefaultValue; + } + else + { + // This "shoudln't" happen, so log in case it does + logger.LogError( + $"Cannot retrieve value for field {this.Label}"); + } + } + + return fieldValue; + } + + /// + /// Gets the field's final value after the prompt is + /// complete. + /// + /// The field's final value. + protected virtual object OnGetValue() + { + return this.fieldValue; + } + + /// + /// Gets the next field if this field can accept multiple + /// values, like a collection or an object with multiple + /// properties. + /// + /// + /// A new FieldDetails instance if there is a next field + /// or null otherwise. + /// + public virtual FieldDetails GetNextField() + { + return null; + } + + #endregion + + #region Internal Methods + + internal static FieldDetails Create( + FieldDescription fieldDescription, + ILogger logger) + { + Type fieldType = + GetFieldTypeFromTypeName( + fieldDescription.ParameterAssemblyFullName, + logger); + + if (typeof(IList).GetTypeInfo().IsAssignableFrom(fieldType.GetTypeInfo())) + { + return new CollectionFieldDetails( + fieldDescription.Name, + fieldDescription.Label, + fieldType, + fieldDescription.IsMandatory, + fieldDescription.DefaultValue); + } + else if (typeof(PSCredential) == fieldType) + { + return new CredentialFieldDetails( + fieldDescription.Name, + fieldDescription.Label, + fieldType, + fieldDescription.IsMandatory, + fieldDescription.DefaultValue); + } + else + { + return new FieldDetails( + fieldDescription.Name, + fieldDescription.Label, + fieldType, + fieldDescription.IsMandatory, + fieldDescription.DefaultValue); + } + } + + private static Type GetFieldTypeFromTypeName( + string assemblyFullName, + ILogger logger) + { + Type fieldType = typeof(string); + + if (!string.IsNullOrEmpty(assemblyFullName)) + { + if (!LanguagePrimitives.TryConvertTo(assemblyFullName, out fieldType)) + { + logger.LogWarning( + string.Format( + "Could not resolve type of field: {0}", + assemblyFullName)); + } + } + + return fieldType; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs new file mode 100644 index 000000000..b3fb58561 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs @@ -0,0 +1,140 @@ +// +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides platform specific console utilities. + /// + public interface IConsoleOperations + { + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// An object that describes the constant and Unicode character, if any, + /// that correspond to the pressed console key. The object also + /// describes, in a bitwise combination of values, whether + /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. + /// + ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken); + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// A task that will complete with a result of the key pressed by the user. + /// + Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The horizontal position of the console cursor. + int GetCursorLeft(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The horizontal position of the console cursor. + int GetCursorLeft(CancellationToken cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + Task GetCursorLeftAsync(); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// + Task GetCursorLeftAsync(CancellationToken cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The vertical position of the console cursor. + int GetCursorTop(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The vertical position of the console cursor. + int GetCursorTop(CancellationToken cancellationToken); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + Task GetCursorTopAsync(); + + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// + Task GetCursorTopAsync(CancellationToken cancellationToken); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs new file mode 100644 index 000000000..2b5c9bd23 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs @@ -0,0 +1,331 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a base implementation for IPromptHandler classes + /// that present the user a set of fields for which values + /// should be entered. + /// + public abstract class InputPromptHandler : PromptHandler + { + #region Private Fields + + private int currentFieldIndex = -1; + private FieldDetails currentField; + private CancellationTokenSource promptCancellationTokenSource = + new CancellationTokenSource(); + private TaskCompletionSource> cancelTask = + new TaskCompletionSource>(); + + #endregion + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + public InputPromptHandler(ILogger logger) : base(logger) + { + } + + #region Properties + + /// + /// Gets the array of fields for which the user must enter values. + /// + protected FieldDetails[] Fields { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Prompts the user for a line of input without writing any message or caption. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + public Task PromptForInputAsync( + CancellationToken cancellationToken) + { + Task> innerTask = + this.PromptForInputAsync( + null, + null, + new FieldDetails[] { new FieldDetails("", "", typeof(string), false, "") }, + cancellationToken); + + return + innerTask.ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + // Return the value of the sole field + return (string)task.Result[""]; + }); + } + + /// + /// Prompts the user for a line (or lines) of input. + /// + /// + /// A title shown before the series of input fields. + /// + /// + /// A descritpive message shown before the series of input fields. + /// + /// + /// An array of FieldDetails items to be displayed which prompt the + /// user for input of a specific type. + /// + /// + /// A CancellationToken that can be used to cancel the prompt. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + public async Task> PromptForInputAsync( + string promptCaption, + string promptMessage, + FieldDetails[] fields, + CancellationToken cancellationToken) + { + // Cancel the prompt if the caller cancels the task + cancellationToken.Register(this.CancelPrompt, true); + + this.Fields = fields; + + this.ShowPromptMessage(promptCaption, promptMessage); + + Task> promptTask = + this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token); + + Task finishedTask = + await Task.WhenAny( + cancelTask.Task, + promptTask); + + if (this.cancelTask.Task.IsCanceled) + { + throw new PipelineStoppedException(); + } + + return promptTask.Result; + } + + /// + /// Prompts the user for a SecureString without writing any message or caption. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + public Task PromptForSecureInputAsync( + CancellationToken cancellationToken) + { + Task> innerTask = + this.PromptForInputAsync( + null, + null, + new FieldDetails[] { new FieldDetails("", "", typeof(SecureString), false, "") }, + cancellationToken); + + return + innerTask.ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + // Return the value of the sole field + return (SecureString)task.Result?[""]; + }); + } + + /// + /// Called when the active prompt should be cancelled. + /// + protected override void OnPromptCancelled() + { + // Cancel the prompt task + this.promptCancellationTokenSource.Cancel(); + this.cancelTask.TrySetCanceled(); + } + + #endregion + + #region Abstract Methods + + /// + /// Called when the prompt caption and message should be + /// displayed to the user. + /// + /// The caption string to be displayed. + /// The message string to be displayed. + protected abstract void ShowPromptMessage(string caption, string message); + + /// + /// Called when a prompt should be displayed for a specific + /// input field. + /// + /// The details of the field to be displayed. + protected abstract void ShowFieldPrompt(FieldDetails fieldDetails); + + /// + /// Reads an input string asynchronously from the console. + /// + /// + /// A CancellationToken that can be used to cancel the read. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); + + /// + /// Reads a SecureString asynchronously from the console. + /// + /// + /// A CancellationToken that can be used to cancel the read. + /// + /// + /// A Task instance that can be monitored for completion to get + /// the user's input. + /// + protected abstract Task ReadSecureStringAsync(CancellationToken cancellationToken); + + /// + /// Called when an error should be displayed, such as when the + /// user types in a string with an incorrect format for the + /// current field. + /// + /// + /// The Exception containing the error to be displayed. + /// + protected abstract void ShowErrorMessage(Exception e); + + #endregion + + #region Private Methods + + private async Task> StartPromptLoopAsync( + CancellationToken cancellationToken) + { + this.GetNextField(); + + // Loop until there are no more prompts to process + while (this.currentField != null && !cancellationToken.IsCancellationRequested) + { + // Show current prompt + this.ShowFieldPrompt(this.currentField); + + bool enteredValue = false; + object responseValue = null; + string responseString = null; + + // Read input depending on field type + if (this.currentField.FieldType == typeof(SecureString)) + { + SecureString secureString = await this.ReadSecureStringAsync(cancellationToken); + responseValue = secureString; + enteredValue = secureString != null; + } + else + { + responseString = await this.ReadInputStringAsync(cancellationToken); + responseValue = responseString; + enteredValue = responseString != null && responseString.Length > 0; + + try + { + responseValue = + LanguagePrimitives.ConvertTo( + responseString, + this.currentField.FieldType, + CultureInfo.CurrentCulture); + } + catch (PSInvalidCastException e) + { + this.ShowErrorMessage(e.InnerException ?? e); + continue; + } + } + + // Set the field's value and get the next field + this.currentField.SetValue(responseValue, enteredValue); + this.GetNextField(); + } + + if (cancellationToken.IsCancellationRequested) + { + // Throw a TaskCanceledException to stop the pipeline + throw new TaskCanceledException(); + } + + // Return the field values + return this.GetFieldValues(); + } + + private FieldDetails GetNextField() + { + FieldDetails nextField = this.currentField?.GetNextField(); + + if (nextField == null) + { + this.currentFieldIndex++; + + // Have we shown all the prompts already? + if (this.currentFieldIndex < this.Fields.Length) + { + nextField = this.Fields[this.currentFieldIndex]; + } + } + + this.currentField = nextField; + return nextField; + } + + private Dictionary GetFieldValues() + { + Dictionary fieldValues = new Dictionary(); + + foreach (FieldDetails field in this.Fields) + { + fieldValues.Add(field.OriginalName, field.GetValue(this.Logger)); + } + + return fieldValues; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs new file mode 100644 index 000000000..0de8a91b5 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs @@ -0,0 +1,55 @@ +// +// 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.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Defines an abstract base class for prompt handler implementations. + /// + public abstract class PromptHandler + { + /// + /// Gets the ILogger implementation used for this instance. + /// + protected ILogger Logger { get; private set; } + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + public PromptHandler(ILogger logger) + { + this.Logger = logger; + } + + /// + /// Called when the active prompt should be cancelled. + /// + public void CancelPrompt() + { + // Allow the implementation to clean itself up + this.OnPromptCancelled(); + this.PromptCancelled?.Invoke(this, new EventArgs()); + } + + /// + /// An event that gets raised if the prompt is cancelled, either + /// by the user or due to a timeout. + /// + public event EventHandler PromptCancelled; + + /// + /// Implementation classes may override this method to perform + /// cleanup when the CancelPrompt method gets called. + /// + protected virtual void OnPromptCancelled() + { + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs new file mode 100644 index 000000000..174c15a11 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of ChoicePromptHandler + /// for use in the interactive console (REPL). + /// + internal class TerminalChoicePromptHandler : ConsoleChoicePromptHandler + { + #region Private Fields + + private ConsoleReadLine consoleReadLine; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleChoicePromptHandler class. + /// + /// + /// The ConsoleReadLine instance to use for interacting with the terminal. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public TerminalChoicePromptHandler( + ConsoleReadLine consoleReadLine, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) + { + this.hostOutput = hostOutput; + this.consoleReadLine = consoleReadLine; + } + + #endregion + + /// + /// Reads an input string from the user. + /// + /// A CancellationToken that can be used to cancel the prompt. + /// A Task that can be awaited to get the user's response. + protected override async Task ReadInputStringAsync(CancellationToken cancellationToken) + { + string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return inputString; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs new file mode 100644 index 000000000..4d95bc92c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + /// + /// Provides a standard implementation of InputPromptHandler + /// for use in the interactive console (REPL). + /// + internal class TerminalInputPromptHandler : ConsoleInputPromptHandler + { + #region Private Fields + + private ConsoleReadLine consoleReadLine; + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ConsoleInputPromptHandler class. + /// + /// + /// The ConsoleReadLine instance to use for interacting with the terminal. + /// + /// + /// The IHostOutput implementation to use for writing to the + /// console. + /// + /// An ILogger implementation used for writing log messages. + public TerminalInputPromptHandler( + ConsoleReadLine consoleReadLine, + IHostOutput hostOutput, + ILogger logger) + : base(hostOutput, logger) + { + this.consoleReadLine = consoleReadLine; + } + + #endregion + + #region Public Methods + + /// + /// Reads an input string from the user. + /// + /// A CancellationToken that can be used to cancel the prompt. + /// A Task that can be awaited to get the user's response. + protected override async Task ReadInputStringAsync(CancellationToken cancellationToken) + { + string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return inputString; + } + + /// + /// Reads a SecureString from the user. + /// + /// A CancellationToken that can be used to cancel the prompt. + /// A Task that can be awaited to get the user's response. + protected override async Task ReadSecureStringAsync(CancellationToken cancellationToken) + { + SecureString secureString = await this.consoleReadLine.ReadSecureLineAsync(cancellationToken); + this.hostOutput.WriteOutput(string.Empty); + + return secureString; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs new file mode 100644 index 000000000..e51547a10 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs @@ -0,0 +1,298 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; +using UnixConsoleEcho; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + internal class UnixConsoleOperations : IConsoleOperations + { + private const int LongWaitForKeySleepTime = 300; + + private const int ShortWaitForKeyTimeout = 5000; + + private const int ShortWaitForKeySpinUntilSleepTime = 30; + + private static readonly ManualResetEventSlim s_waitHandle = new ManualResetEventSlim(); + + private static readonly SemaphoreSlim s_readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private static readonly SemaphoreSlim s_stdInHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private Func WaitForKeyAvailable; + + private Func> WaitForKeyAvailableAsync; + + internal UnixConsoleOperations() + { + // Switch between long and short wait periods depending on if the + // user has recently (last 5 seconds) pressed a key to avoid preventing + // the CPU from entering low power mode. + WaitForKeyAvailable = LongWaitForKey; + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + } + + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + { + s_readKeyHandle.Wait(cancellationToken); + + // On Unix platforms System.Console.ReadKey has an internal lock on stdin. Because + // of this, if a ReadKey call is pending in one thread and in another thread + // Console.CursorLeft is called, both threads block until a key is pressed. + + // To work around this we wait for a key to be pressed before actually calling Console.ReadKey. + // However, any pressed keys during this time will be echoed to the console. To get around + // this we use the UnixConsoleEcho package to disable echo prior to waiting. + if (VersionUtils.IsPS6) + { + InputEcho.Disable(); + } + + try + { + // The WaitForKeyAvailable delegate switches between a long delay between waits and + // a short timeout depending on how recently a key has been pressed. This allows us + // to let the CPU enter low power mode without compromising responsiveness. + while (!WaitForKeyAvailable(cancellationToken)); + } + finally + { + if (VersionUtils.IsPS6) + { + InputEcho.Disable(); + } + s_readKeyHandle.Release(); + } + + // A key has been pressed, so aquire a lock on our internal stdin handle. This is done + // so any of our calls to cursor position API's do not release ReadKey. + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.ReadKey(intercept); + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + { + await s_readKeyHandle.WaitAsync(cancellationToken); + + // I tried to replace this library with a call to `stty -echo`, but unfortunately + // the library also sets up allowing backspace to trigger `Console.KeyAvailable`. + if (VersionUtils.IsPS6) + { + InputEcho.Disable(); + } + + try + { + while (!await WaitForKeyAvailableAsync(cancellationToken)); + } + finally + { + if (VersionUtils.IsPS6) + { + InputEcho.Enable(); + } + s_readKeyHandle.Release(); + } + + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.ReadKey(intercept); + } + finally + { + s_stdInHandle.Release(); + } + } + + public int GetCursorLeft() + { + return GetCursorLeft(CancellationToken.None); + } + + public int GetCursorLeft(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorLeftAsync() + { + return await GetCursorLeftAsync(CancellationToken.None); + } + + public async Task GetCursorLeftAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorLeft; + } + finally + { + s_stdInHandle.Release(); + } + } + + public int GetCursorTop() + { + return GetCursorTop(CancellationToken.None); + } + + public int GetCursorTop(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + public async Task GetCursorTopAsync() + { + return await GetCursorTopAsync(CancellationToken.None); + } + + public async Task GetCursorTopAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.CursorTop; + } + finally + { + s_stdInHandle.Release(); + } + } + + private bool LongWaitForKey(CancellationToken cancellationToken) + { + // Wait for a key to be buffered (in other words, wait for Console.KeyAvailable to become + // true) with a long delay between checks. + while (!IsKeyAvailable(cancellationToken)) + { + s_waitHandle.Wait(LongWaitForKeySleepTime, cancellationToken); + } + + // As soon as a key is buffered, return true and switch the wait logic to be more + // responsive, but also more expensive. + WaitForKeyAvailable = ShortWaitForKey; + return true; + } + + private async Task LongWaitForKeyAsync(CancellationToken cancellationToken) + { + while (!await IsKeyAvailableAsync(cancellationToken)) + { + await Task.Delay(LongWaitForKeySleepTime, cancellationToken); + } + + WaitForKeyAvailableAsync = ShortWaitForKeyAsync; + return true; + } + + private bool ShortWaitForKey(CancellationToken cancellationToken) + { + // Check frequently for a new key to be buffered. + if (SpinUntilKeyAvailable(ShortWaitForKeyTimeout, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return true; + } + + // If the user has not pressed a key before the end of the SpinUntil timeout then + // the user is idle and we can switch back to long delays between KeyAvailable checks. + cancellationToken.ThrowIfCancellationRequested(); + WaitForKeyAvailable = LongWaitForKey; + return false; + } + + private async Task ShortWaitForKeyAsync(CancellationToken cancellationToken) + { + if (await SpinUntilKeyAvailableAsync(ShortWaitForKeyTimeout, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return true; + } + + cancellationToken.ThrowIfCancellationRequested(); + WaitForKeyAvailableAsync = LongWaitForKeyAsync; + return false; + } + + private bool SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) + { + return SpinWait.SpinUntil( + () => + { + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); + }, + millisecondsTimeout); + } + + private async Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, CancellationToken cancellationToken) + { + return await Task.Factory.StartNew( + () => SpinWait.SpinUntil( + () => + { + // The wait handle is never set, it's just used to enable cancelling the wait. + s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); + return IsKeyAvailable(cancellationToken); + }, + millisecondsTimeout)); + } + + private bool IsKeyAvailable(CancellationToken cancellationToken) + { + s_stdInHandle.Wait(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } + + private async Task IsKeyAvailableAsync(CancellationToken cancellationToken) + { + await s_stdInHandle.WaitAsync(cancellationToken); + try + { + return System.Console.KeyAvailable; + } + finally + { + s_stdInHandle.Release(); + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs new file mode 100644 index 000000000..493e66930 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs @@ -0,0 +1,76 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Console +{ + internal class WindowsConsoleOperations : IConsoleOperations + { + private ConsoleKeyInfo? _bufferedKey; + + private SemaphoreSlim _readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + public int GetCursorLeft() => System.Console.CursorLeft; + + public int GetCursorLeft(CancellationToken cancellationToken) => System.Console.CursorLeft; + + public Task GetCursorLeftAsync() => Task.FromResult(System.Console.CursorLeft); + + public Task GetCursorLeftAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorLeft); + + public int GetCursorTop() => System.Console.CursorTop; + + public int GetCursorTop(CancellationToken cancellationToken) => System.Console.CursorTop; + + public Task GetCursorTopAsync() => Task.FromResult(System.Console.CursorTop); + + public Task GetCursorTopAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorTop); + + public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + { + await _readKeyHandle.WaitAsync(cancellationToken); + try + { + return + _bufferedKey.HasValue + ? _bufferedKey.Value + : await Task.Factory.StartNew( + () => (_bufferedKey = System.Console.ReadKey(intercept)).Value); + } + finally + { + _readKeyHandle.Release(); + + // Throw if we're cancelled so the buffered key isn't cleared. + cancellationToken.ThrowIfCancellationRequested(); + _bufferedKey = null; + } + } + + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + { + _readKeyHandle.Wait(cancellationToken); + try + { + return + _bufferedKey.HasValue + ? _bufferedKey.Value + : (_bufferedKey = System.Console.ReadKey(intercept)).Value; + } + finally + { + _readKeyHandle.Release(); + + // Throw if we're cancelled so the buffered key isn't cleared. + cancellationToken.ThrowIfCancellationRequested(); + _bufferedKey = null; + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs new file mode 100644 index 000000000..8a7b80cef --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides details about a command that has been registered + /// with the editor. + /// + public class EditorCommand + { + #region Properties + + /// + /// Gets the name which uniquely identifies the command. + /// + public string Name { get; private set; } + + /// + /// Gets the display name for the command. + /// + public string DisplayName { get; private set; } + + /// + /// Gets the boolean which determines whether this command's + /// output should be suppressed. + /// + public bool SuppressOutput { get; private set; } + + /// + /// Gets the ScriptBlock which can be used to execute the command. + /// + public ScriptBlock ScriptBlock { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new EditorCommand instance that invokes a cmdlet or + /// function by name. + /// + /// The unique identifier name for the command. + /// The display name for the command. + /// If true, causes output to be suppressed for this command. + /// The name of the cmdlet or function which will be invoked by this command. + public EditorCommand( + string commandName, + string displayName, + bool suppressOutput, + string cmdletName) + : this( + commandName, + displayName, + suppressOutput, + ScriptBlock.Create( + string.Format( + "param($context) {0} $context", + cmdletName))) + { + } + + /// + /// Creates a new EditorCommand instance that invokes a ScriptBlock. + /// + /// The unique identifier name for the command. + /// The display name for the command. + /// If true, causes output to be suppressed for this command. + /// The ScriptBlock which will be invoked by this command. + public EditorCommand( + string commandName, + string displayName, + bool suppressOutput, + ScriptBlock scriptBlock) + { + this.Name = commandName; + this.DisplayName = displayName; + this.SuppressOutput = suppressOutput; + this.ScriptBlock = scriptBlock; + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs new file mode 100644 index 000000000..71b96d300 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs @@ -0,0 +1,33 @@ +using System; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides an attribute that can be used to target PowerShell + /// commands for import as editor commands. + /// + [AttributeUsage(AttributeTargets.Class)] + public class EditorCommandAttribute : Attribute + { + + #region Properties + + /// + /// Gets or sets the name which uniquely identifies the command. + /// + public string Name { get; set; } + + /// + /// Gets or sets the display name for the command. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets a value indicating whether this command's output + /// should be suppressed. + /// + public bool SuppressOutput { get; set; } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs new file mode 100644 index 000000000..83e94a7a5 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides context for the host editor at the time of creation. + /// + public class EditorContext + { + #region Private Fields + + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the FileContext for the active file. + /// + public FileContext CurrentFile { get; private set; } + + /// + /// Gets the BufferRange representing the current selection in the file. + /// + public BufferRange SelectedRange { get; private set; } + + /// + /// Gets the FilePosition representing the current cursor position. + /// + public FilePosition CursorPosition { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the EditorContext class. + /// + /// An IEditorOperations implementation which performs operations in the editor. + /// The ScriptFile that is in the active editor buffer. + /// The position of the user's cursor in the active editor buffer. + /// The range of the user's selection in the active editor buffer. + /// Determines the language of the file.false If it is not specified, then it defaults to "Unknown" + public EditorContext( + IEditorOperations editorOperations, + ScriptFile currentFile, + BufferPosition cursorPosition, + BufferRange selectedRange, + string language = "Unknown") + { + this.editorOperations = editorOperations; + this.CurrentFile = new FileContext(currentFile, this, editorOperations, language); + this.SelectedRange = selectedRange; + this.CursorPosition = new FilePosition(currentFile, cursorPosition); + } + + #endregion + + #region Public Methods + + /// + /// Sets a selection in the host editor's active buffer. + /// + /// The 1-based starting line of the selection. + /// The 1-based starting column of the selection. + /// The 1-based ending line of the selection. + /// The 1-based ending column of the selection. + public void SetSelection( + int startLine, + int startColumn, + int endLine, + int endColumn) + { + this.SetSelection( + new BufferRange( + startLine, startColumn, + endLine, endColumn)); + } + + /// + /// Sets a selection in the host editor's active buffer. + /// + /// The starting position of the selection. + /// The ending position of the selection. + public void SetSelection( + BufferPosition startPosition, + BufferPosition endPosition) + { + this.SetSelection( + new BufferRange( + startPosition, + endPosition)); + } + + /// + /// Sets a selection in the host editor's active buffer. + /// + /// The range of the selection. + public void SetSelection(BufferRange selectionRange) + { + this.editorOperations + .SetSelectionAsync(selectionRange) + .Wait(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs new file mode 100644 index 000000000..39da959cf --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs @@ -0,0 +1,110 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Components; +using System; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides the entry point of the extensibility API, inserted into + /// the PowerShell session as the "$psEditor" variable. + /// + public class EditorObject + { + #region Private Fields + + private ExtensionService extensionService; + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the version of PowerShell Editor Services. + /// + public Version EditorServicesVersion + { + get { return this.GetType().GetTypeInfo().Assembly.GetName().Version; } + } + + /// + /// Gets the workspace interface for the editor API. + /// + public EditorWorkspace Workspace { get; private set; } + + /// + /// Gets the window interface for the editor API. + /// + public EditorWindow Window { get; private set; } + + /// + /// Gets the component registry for the session. + /// + /// + public IComponentRegistry Components { get; private set; } + + #endregion + + /// + /// Creates a new instance of the EditorObject class. + /// + /// An ExtensionService which handles command registration. + /// An IEditorOperations implementation which handles operations in the host editor. + /// An IComponentRegistry instance which provides components in the session. + public EditorObject( + ExtensionService extensionService, + IEditorOperations editorOperations, + IComponentRegistry componentRegistry) + { + this.extensionService = extensionService; + this.editorOperations = editorOperations; + this.Components = componentRegistry; + + // Create API area objects + this.Workspace = new EditorWorkspace(this.editorOperations); + this.Window = new EditorWindow(this.editorOperations); + } + + /// + /// Registers a new command in the editor. + /// + /// The EditorCommand to be registered. + /// True if the command is newly registered, false if the command already exists. + public bool RegisterCommand(EditorCommand editorCommand) + { + return this.extensionService.RegisterCommand(editorCommand); + } + + /// + /// Unregisters an existing EditorCommand based on its registered name. + /// + /// The name of the command to be unregistered. + public void UnregisterCommand(string commandName) + { + this.extensionService.UnregisterCommand(commandName); + } + + /// + /// Returns all registered EditorCommands. + /// + /// An Array of all registered EditorCommands. + public EditorCommand[] GetCommands() + { + return this.extensionService.GetCommands(); + } + /// + /// Gets the EditorContext which contains the state of the editor + /// at the time this method is invoked. + /// + /// A instance of the EditorContext class. + public EditorContext GetEditorContext() + { + return this.editorOperations.GetEditorContextAsync().Result; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs new file mode 100644 index 000000000..8d241559a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs @@ -0,0 +1,83 @@ +// +// 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.Extensions +{ + /// + /// Provides a PowerShell-facing API which allows scripts to + /// interact with the editor's window. + /// + public class EditorWindow + { + #region Private Fields + + private IEditorOperations editorOperations; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the EditorWindow class. + /// + /// An IEditorOperations implementation which handles operations in the host editor. + internal EditorWindow(IEditorOperations editorOperations) + { + this.editorOperations = editorOperations; + } + + #endregion + + #region Public Methods + + /// + /// Shows an informational message to the user. + /// + /// The message to be shown. + public void ShowInformationMessage(string message) + { + this.editorOperations.ShowInformationMessageAsync(message).Wait(); + } + + /// + /// Shows an error message to the user. + /// + /// The message to be shown. + public void ShowErrorMessage(string message) + { + this.editorOperations.ShowErrorMessageAsync(message).Wait(); + } + + /// + /// Shows a warning message to the user. + /// + /// The message to be shown. + public void ShowWarningMessage(string message) + { + this.editorOperations.ShowWarningMessageAsync(message).Wait(); + } + + /// + /// Sets the status bar message in the editor UI (if applicable). + /// + /// The message to be shown. + public void SetStatusBarMessage(string message) + { + this.editorOperations.SetStatusBarMessageAsync(message, null).Wait(); + } + + /// + /// Sets the status bar message in the editor UI (if applicable). + /// + /// The message to be shown. + /// A timeout in milliseconds for how long the message should remain visible. + public void SetStatusBarMessage(string message, int timeout) + { + this.editorOperations.SetStatusBarMessageAsync(message, timeout).Wait(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs new file mode 100644 index 000000000..4eb22de4a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs @@ -0,0 +1,75 @@ +// +// 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.Extensions +{ + /// + /// Provides a PowerShell-facing API which allows scripts to + /// interact with the editor's workspace. + /// + public class EditorWorkspace + { + #region Private Fields + + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the current workspace path if there is one or null otherwise. + /// + public string Path + { + get { return this.editorOperations.GetWorkspacePath(); } + } + + #endregion + + #region Constructors + + internal EditorWorkspace(IEditorOperations editorOperations) + { + this.editorOperations = editorOperations; + } + + #endregion + + #region Public Methods + + /// + /// Creates a new file in the editor + /// + public void NewFile() + { + this.editorOperations.NewFileAsync().Wait(); + } + + /// + /// Opens a file in the workspace. If the file is already open + /// its buffer will be made active. + /// + /// The path to the file to be opened. + public void OpenFile(string filePath) + { + this.editorOperations.OpenFileAsync(filePath).Wait(); + } + + /// + /// Opens a file in the workspace. If the file is already open + /// its buffer will be made active. + /// You can specify whether the file opens as a preview or as a durable editor. + /// + /// The path to the file to be opened. + /// Determines wether the file is opened as a preview or as a durable editor. + public void OpenFile(string filePath, bool preview) + { + this.editorOperations.OpenFileAsync(filePath, preview).Wait(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs new file mode 100644 index 000000000..b96ac9e12 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs @@ -0,0 +1,216 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Components; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides a high-level service which enables PowerShell scripts + /// and modules to extend the behavior of the host editor. + /// + public class ExtensionService + { + #region Fields + + private Dictionary editorCommands = + new Dictionary(); + + #endregion + + #region Properties + + /// + /// Gets the IEditorOperations implementation used to invoke operations + /// in the host editor. + /// + public IEditorOperations EditorOperations { get; private set; } + + /// + /// Gets the EditorObject which exists in the PowerShell session as the + /// '$psEditor' variable. + /// + public EditorObject EditorObject { get; private set; } + + /// + /// Gets the PowerShellContext in which extension code will be executed. + /// + public PowerShellContextService PowerShellContext { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ExtensionService which uses the provided + /// PowerShellContext for loading and executing extension code. + /// + /// A PowerShellContext used to execute extension code. + public ExtensionService(PowerShellContextService powerShellContext) + { + this.PowerShellContext = powerShellContext; + } + + #endregion + + #region Public Methods + + /// + /// Initializes this ExtensionService using the provided IEditorOperations + /// implementation for future interaction with the host editor. + /// + /// An IEditorOperations implementation. + /// An IComponentRegistry instance which provides components in the session. + /// A Task that can be awaited for completion. + public async Task InitializeAsync( + IEditorOperations editorOperations, + IComponentRegistry componentRegistry) + { + this.EditorObject = + new EditorObject( + this, + editorOperations, + componentRegistry); + + // Register the editor object in the runspace + PSCommand variableCommand = new PSCommand(); + using (RunspaceHandle handle = await this.PowerShellContext.GetRunspaceHandleAsync()) + { + handle.Runspace.SessionStateProxy.PSVariable.Set( + "psEditor", + this.EditorObject); + } + } + + /// + /// Invokes the specified editor command against the provided EditorContext. + /// + /// The unique name of the command to be invoked. + /// The context in which the command is being invoked. + /// A Task that can be awaited for completion. + public async Task InvokeCommandAsync(string commandName, EditorContext editorContext) + { + EditorCommand editorCommand; + + if (this.editorCommands.TryGetValue(commandName, out editorCommand)) + { + PSCommand executeCommand = new PSCommand(); + executeCommand.AddCommand("Invoke-Command"); + executeCommand.AddParameter("ScriptBlock", editorCommand.ScriptBlock); + executeCommand.AddParameter("ArgumentList", new object[] { editorContext }); + + await this.PowerShellContext.ExecuteCommandAsync( + executeCommand, + !editorCommand.SuppressOutput, + true); + } + else + { + throw new KeyNotFoundException( + string.Format( + "Editor command not found: '{0}'", + commandName)); + } + } + + /// + /// Registers a new EditorCommand with the ExtensionService and + /// causes its details to be sent to the host editor. + /// + /// The details about the editor command to be registered. + /// True if the command is newly registered, false if the command already exists. + public bool RegisterCommand(EditorCommand editorCommand) + { + bool commandExists = + this.editorCommands.ContainsKey( + editorCommand.Name); + + // Add or replace the editor command + this.editorCommands[editorCommand.Name] = editorCommand; + + if (!commandExists) + { + this.OnCommandAdded(editorCommand); + } + else + { + this.OnCommandUpdated(editorCommand); + } + + return !commandExists; + } + + /// + /// Unregisters an existing EditorCommand based on its registered name. + /// + /// The name of the command to be unregistered. + public void UnregisterCommand(string commandName) + { + EditorCommand existingCommand = null; + if (this.editorCommands.TryGetValue(commandName, out existingCommand)) + { + this.editorCommands.Remove(commandName); + this.OnCommandRemoved(existingCommand); + } + else + { + throw new KeyNotFoundException( + string.Format( + "Command '{0}' is not registered", + commandName)); + } + } + + /// + /// Returns all registered EditorCommands. + /// + /// An Array of all registered EditorCommands. + public EditorCommand[] GetCommands() + { + EditorCommand[] commands = new EditorCommand[this.editorCommands.Count]; + this.editorCommands.Values.CopyTo(commands,0); + return commands; + } + #endregion + + #region Events + + /// + /// Raised when a new editor command is added. + /// + public event EventHandler CommandAdded; + + private void OnCommandAdded(EditorCommand command) + { + this.CommandAdded?.Invoke(this, command); + } + + /// + /// Raised when an existing editor command is updated. + /// + public event EventHandler CommandUpdated; + + private void OnCommandUpdated(EditorCommand command) + { + this.CommandUpdated?.Invoke(this, command); + } + + /// + /// Raised when an existing editor command is removed. + /// + public event EventHandler CommandRemoved; + + private void OnCommandRemoved(EditorCommand command) + { + this.CommandRemoved?.Invoke(this, command); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs new file mode 100644 index 000000000..ace8f5271 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs @@ -0,0 +1,279 @@ +// +// 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; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides context for a file that is open in the editor. + /// + public class FileContext + { + #region Private Fields + + internal ScriptFile scriptFile; + private EditorContext editorContext; + private IEditorOperations editorOperations; + + #endregion + + #region Properties + + /// + /// Gets the parsed abstract syntax tree for the file. + /// + public Ast Ast + { + get { return this.scriptFile.ScriptAst; } + } + + /// + /// Gets a BufferRange which represents the entire content + /// range of the file. + /// + public BufferRange FileRange + { + get { return this.scriptFile.FileRange; } + } + + /// + /// Gets the language of the file. + /// + public string Language { get; private set; } + + /// + /// Gets the filesystem path of the file. + /// + public string Path + { + get { return this.scriptFile.FilePath; } + } + + /// + /// Gets the parsed token list for the file. + /// + public Token[] Tokens + { + get { return this.scriptFile.ScriptTokens; } + } + + /// + /// Gets the workspace-relative path of the file. + /// + public string WorkspacePath + { + get + { + return + this.editorOperations.GetWorkspaceRelativePath( + this.scriptFile.FilePath); + } + } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the FileContext class. + /// + /// The ScriptFile to which this file refers. + /// The EditorContext to which this file relates. + /// An IEditorOperations implementation which performs operations in the editor. + /// Determines the language of the file.false If it is not specified, then it defaults to "Unknown" + public FileContext( + ScriptFile scriptFile, + EditorContext editorContext, + IEditorOperations editorOperations, + string language = "Unknown") + { + if (string.IsNullOrWhiteSpace(language)) + { + language = "Unknown"; + } + + this.scriptFile = scriptFile; + this.editorContext = editorContext; + this.editorOperations = editorOperations; + this.Language = language; + } + + #endregion + + #region Text Accessors + + /// + /// Gets the complete file content as a string. + /// + /// A string containing the complete file content. + public string GetText() + { + return this.scriptFile.Contents; + } + + /// + /// Gets the file content in the specified range as a string. + /// + /// The buffer range for which content will be extracted. + /// A string with the specified range of content. + public string GetText(BufferRange bufferRange) + { + return + string.Join( + Environment.NewLine, + this.GetTextLines(bufferRange)); + } + + /// + /// Gets the complete file content as an array of strings. + /// + /// An array of strings, each representing a line in the file. + public string[] GetTextLines() + { + return this.scriptFile.FileLines.ToArray(); + } + + /// + /// Gets the file content in the specified range as an array of strings. + /// + /// The buffer range for which content will be extracted. + /// An array of strings, each representing a line in the file within the specified range. + public string[] GetTextLines(BufferRange bufferRange) + { + return this.scriptFile.GetLinesInRange(bufferRange); + } + + #endregion + + #region Text Manipulation + + /// + /// Inserts a text string at the current cursor position represented by + /// the parent EditorContext's CursorPosition property. + /// + /// The text string to insert. + public void InsertText(string textToInsert) + { + // Is there a selection? + if (this.editorContext.SelectedRange.HasRange) + { + this.InsertText( + textToInsert, + this.editorContext.SelectedRange); + } + else + { + this.InsertText( + textToInsert, + this.editorContext.CursorPosition); + } + } + + /// + /// Inserts a text string at the specified buffer position. + /// + /// The text string to insert. + /// The position at which the text will be inserted. + public void InsertText(string textToInsert, BufferPosition insertPosition) + { + this.InsertText( + textToInsert, + new BufferRange(insertPosition, insertPosition)); + } + + /// + /// Inserts a text string at the specified line and column numbers. + /// + /// The text string to insert. + /// The 1-based line number at which the text will be inserted. + /// The 1-based column number at which the text will be inserted. + public void InsertText(string textToInsert, int insertLine, int insertColumn) + { + this.InsertText( + textToInsert, + new BufferPosition(insertLine, insertColumn)); + } + + /// + /// Inserts a text string to replace the specified range, represented + /// by starting and ending line and column numbers. Can be used to + /// insert, replace, or delete text depending on the specified range + /// and text to insert. + /// + /// The text string to insert. + /// The 1-based starting line number where text will be replaced. + /// The 1-based starting column number where text will be replaced. + /// The 1-based ending line number where text will be replaced. + /// The 1-based ending column number where text will be replaced. + public void InsertText( + string textToInsert, + int startLine, + int startColumn, + int endLine, + int endColumn) + { + this.InsertText( + textToInsert, + new BufferRange( + startLine, + startColumn, + endLine, + endColumn)); + } + + /// + /// Inserts a text string to replace the specified range. Can be + /// used to insert, replace, or delete text depending on the specified + /// range and text to insert. + /// + /// The text string to insert. + /// The buffer range which will be replaced by the string. + public void InsertText(string textToInsert, BufferRange insertRange) + { + this.editorOperations + .InsertTextAsync(this.scriptFile.ClientFilePath, textToInsert, insertRange) + .Wait(); + } + + #endregion + + #region File Manipulation + + /// + /// Saves this file. + /// + public void Save() + { + this.editorOperations.SaveFileAsync(this.scriptFile.FilePath); + } + + /// + /// Save this file under a new path and open a new editor window on that file. + /// + /// + /// the path where the file should be saved, + /// including the file name with extension as the leaf + /// + public void SaveAs(string newFilePath) + { + // Do some validation here so that we can provide a helpful error if the path won't work + string absolutePath = System.IO.Path.IsPathRooted(newFilePath) ? + newFilePath : + System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(this.scriptFile.FilePath), newFilePath)); + + if (File.Exists(absolutePath)) + { + throw new IOException(String.Format("The file '{0}' already exists", absolutePath)); + } + + this.editorOperations.SaveFileAsync(this.scriptFile.FilePath, newFilePath); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs new file mode 100644 index 000000000..4e3f532fd --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + /// + /// Provides an interface that must be implemented by an editor + /// host to perform operations invoked by extensions written in + /// PowerShell. + /// + public interface IEditorOperations + { + /// + /// Gets the EditorContext for the editor's current state. + /// + /// A new EditorContext object. + Task GetEditorContextAsync(); + + /// + /// Gets the path to the editor's active workspace. + /// + /// The workspace path or null if there isn't one. + string GetWorkspacePath(); + + /// + /// Resolves the given file path relative to the current workspace path. + /// + /// The file path to be resolved. + /// The resolved file path. + string GetWorkspaceRelativePath(string filePath); + + /// + /// Causes a new untitled file to be created in the editor. + /// + /// A task that can be awaited for completion. + Task NewFileAsync(); + + /// + /// Causes a file to be opened in the editor. If the file is + /// already open, the editor must switch to the file. + /// + /// The path of the file to be opened. + /// A Task that can be tracked for completion. + Task OpenFileAsync(string filePath); + + /// + /// Causes a file to be opened in the editor. If the file is + /// already open, the editor must switch to the file. + /// You can specify whether the file opens as a preview or as a durable editor. + /// + /// The path of the file to be opened. + /// Determines wether the file is opened as a preview or as a durable editor. + /// A Task that can be tracked for completion. + Task OpenFileAsync(string filePath, bool preview); + + /// + /// Causes a file to be closed in the editor. + /// + /// The path of the file to be closed. + /// A Task that can be tracked for completion. + Task CloseFileAsync(string filePath); + + /// + /// Causes a file to be saved in the editor. + /// + /// The path of the file to be saved. + /// A Task that can be tracked for completion. + Task SaveFileAsync(string filePath); + + /// + /// Causes a file to be saved as a new file in a new editor window. + /// + /// the path of the current file being saved + /// the path of the new file where the current window content will be saved + /// + Task SaveFileAsync(string oldFilePath, string newFilePath); + + /// + /// Inserts text into the specified range for the file at the specified path. + /// + /// The path of the file which will have text inserted. + /// The text to insert into the file. + /// The range in the file to be replaced. + /// A Task that can be tracked for completion. + Task InsertTextAsync(string filePath, string insertText, BufferRange insertRange); + + /// + /// Causes the selection to be changed in the editor's active file buffer. + /// + /// The range over which the selection will be made. + /// A Task that can be tracked for completion. + Task SetSelectionAsync(BufferRange selectionRange); + + /// + /// Shows an informational message to the user. + /// + /// The message to be shown. + /// A Task that can be tracked for completion. + Task ShowInformationMessageAsync(string message); + + /// + /// Shows an error message to the user. + /// + /// The message to be shown. + /// A Task that can be tracked for completion. + Task ShowErrorMessageAsync(string message); + + /// + /// Shows a warning message to the user. + /// + /// The message to be shown. + /// A Task that can be tracked for completion. + Task ShowWarningMessageAsync(string message); + + /// + /// Sets the status bar message in the editor UI (if applicable). + /// + /// The message to be shown. + /// If non-null, a timeout in milliseconds for how long the message should remain visible. + /// A Task that can be tracked for completion. + Task SetStatusBarMessageAsync(string message, int? timeout); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs new file mode 100644 index 000000000..cfe5f93dd --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -0,0 +1,2506 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation.Host; +using System.Management.Automation.Remoting; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices +{ + using System.Management.Automation; + using Microsoft.PowerShell.EditorServices.Engine; + + /// + /// Manages the lifetime and usage of a PowerShell session. + /// Handles nested PowerShell prompts and also manages execution of + /// commands whether inside or outside of the debugger. + /// + public class PowerShellContextService : IDisposable, IHostSupportsInteractiveSession + { + private static readonly Action s_runspaceApartmentStateSetter; + + static PowerShellContextService() + { + // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection + if (!VersionUtils.IsNetCore) + { + MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); + Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); + s_runspaceApartmentStateSetter = (Action)setter; + } + } + + #region Fields + + private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + private bool isPSReadLineEnabled; + private ILogger logger; + private PowerShell powerShell; + private bool ownsInitialRunspace; + private RunspaceDetails initialRunspace; + private SessionDetails mostRecentSessionDetails; + + private ProfilePaths profilePaths; + + private IVersionSpecificOperations versionSpecificOperations; + + private Stack runspaceStack = new Stack(); + + private int isCommandLoopRestarterSet; + + #endregion + + #region Properties + + private IPromptContext PromptContext { get; set; } + + private PromptNest PromptNest { get; set; } + + private InvocationEventQueue InvocationEventQueue { get; set; } + + private EngineIntrinsics EngineIntrinsics { get; set; } + + private PSHost ExternalHost { get; set; } + + /// + /// Gets a boolean that indicates whether the debugger is currently stopped, + /// either at a breakpoint or because the user broke execution. + /// + public bool IsDebuggerStopped => + this.versionSpecificOperations.IsDebuggerStopped( + PromptNest, + CurrentRunspace.Runspace); + + /// + /// Gets the current state of the session. + /// + public PowerShellContextState SessionState + { + get; + private set; + } + + /// + /// Gets the PowerShell version details for the initial local runspace. + /// + public PowerShellVersionDetails LocalPowerShellVersion + { + get; + private set; + } + + /// + /// Gets or sets an IHostOutput implementation for use in + /// writing output to the console. + /// + private IHostOutput ConsoleWriter { get; set; } + + internal IHostInput ConsoleReader { get; private set; } + + /// + /// Gets details pertaining to the current runspace. + /// + public RunspaceDetails CurrentRunspace + { + get; + private set; + } + + /// + /// Gets a value indicating whether the current runspace + /// is ready for a command + /// + public bool IsAvailable => this.SessionState == PowerShellContextState.Ready; + + /// + /// Gets the working directory path the PowerShell context was inititially set when the debugger launches. + /// This path is used to determine whether a script in the call stack is an "external" script. + /// + public string InitialWorkingDirectory { get; private set; } + + #endregion + + #region Constructors + + /// + /// + /// + /// An ILogger implementation used for writing log messages. + /// + /// Indicates whether PSReadLine should be used if possible + /// + public PowerShellContextService(ILogger logger, bool isPSReadLineEnabled) + { + + this.logger = logger; + this.isPSReadLineEnabled = isPSReadLineEnabled; + } + + /// + /// + /// + /// + /// + /// + /// The EditorServicesPSHostUserInterface to use for this instance. + /// + /// An ILogger implementation to use for this instance. + /// + public static Runspace CreateRunspace( + HostDetails hostDetails, + PowerShellContextService powerShellContext, + EditorServicesPSHostUserInterface hostUserInterface, + ILogger logger) + { + var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); + powerShellContext.ConsoleWriter = hostUserInterface; + powerShellContext.ConsoleReader = hostUserInterface; + return CreateRunspace(psHost); + } + + /// + /// + /// + /// + /// + public static Runspace CreateRunspace(PSHost psHost) + { + InitialSessionState initialSessionState; + if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { + initialSessionState = InitialSessionState.CreateDefault(); + } else { + initialSessionState = InitialSessionState.CreateDefault2(); + } + + Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); + + // Windows PowerShell must be hosted in STA mode + // This must be set on the runspace *before* it is opened + if (s_runspaceApartmentStateSetter != null) + { + s_runspaceApartmentStateSetter(runspace, ApartmentState.STA); + } + + runspace.ThreadOptions = PSThreadOptions.ReuseThread; + runspace.Open(); + + return runspace; + } + + /// + /// Initializes a new instance of the PowerShellContext class using + /// an existing runspace for the session. + /// + /// An object containing the profile paths for the session. + /// The initial runspace to use for this instance. + /// If true, the PowerShellContext owns this runspace. + public void Initialize( + ProfilePaths profilePaths, + Runspace initialRunspace, + bool ownsInitialRunspace) + { + this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, null); + } + + /// + /// Initializes a new instance of the PowerShellContext class using + /// an existing runspace for the session. + /// + /// An object containing the profile paths for the session. + /// The initial runspace to use for this instance. + /// If true, the PowerShellContext owns this runspace. + /// An IHostOutput implementation. Optional. + public void Initialize( + ProfilePaths profilePaths, + Runspace initialRunspace, + bool ownsInitialRunspace, + IHostOutput consoleHost) + { + Validate.IsNotNull("initialRunspace", initialRunspace); + + this.ownsInitialRunspace = ownsInitialRunspace; + this.SessionState = PowerShellContextState.NotStarted; + this.ConsoleWriter = consoleHost; + this.ConsoleReader = consoleHost as IHostInput; + + // Get the PowerShell runtime version + this.LocalPowerShellVersion = + PowerShellVersionDetails.GetVersionDetails( + initialRunspace, + this.logger); + + this.powerShell = PowerShell.Create(); + this.powerShell.Runspace = initialRunspace; + + this.initialRunspace = + new RunspaceDetails( + initialRunspace, + this.GetSessionDetailsInRunspace(initialRunspace), + this.LocalPowerShellVersion, + RunspaceLocation.Local, + RunspaceContext.Original, + null); + this.CurrentRunspace = this.initialRunspace; + + // Write out the PowerShell version for tracking purposes + this.logger.LogInformation( + string.Format( + "PowerShell runtime version: {0}, edition: {1}", + this.LocalPowerShellVersion.Version, + this.LocalPowerShellVersion.Edition)); + + Version powerShellVersion = this.LocalPowerShellVersion.Version; + if (powerShellVersion >= new Version(5, 0)) + { + this.versionSpecificOperations = new PowerShell5Operations(); + } + else + { + throw new NotSupportedException( + "This computer has an unsupported version of PowerShell installed: " + + powerShellVersion.ToString()); + } + + if (this.LocalPowerShellVersion.Edition != "Linux") + { + // TODO: Should this be configurable? + this.SetExecutionPolicy(ExecutionPolicy.RemoteSigned); + } + + // Set up the runspace + this.ConfigureRunspace(this.CurrentRunspace); + + // Add runspace capabilities + this.ConfigureRunspaceCapabilities(this.CurrentRunspace); + + // Set the $profile variable in the runspace + this.profilePaths = profilePaths; + if (this.profilePaths != null) + { + this.SetProfileVariableInCurrentRunspace(profilePaths); + } + + // Now that initialization is complete we can watch for InvocationStateChanged + this.SessionState = PowerShellContextState.Ready; + + // EngineIntrinsics is used in some instances to interact with the initial + // runspace without having to wait for PSReadLine to check for events. + this.EngineIntrinsics = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("ExecutionContext") + as EngineIntrinsics; + + // The external host is used to properly exit from a nested prompt that + // was entered by the user. + this.ExternalHost = + initialRunspace + .SessionStateProxy + .PSVariable + .GetValue("Host") + as PSHost; + + // Now that the runspace is ready, enqueue it for first use + this.PromptNest = new PromptNest( + this, + this.powerShell, + this.ConsoleReader, + this.versionSpecificOperations); + this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); + + if (powerShellVersion.Major >= 5 && + this.isPSReadLineEnabled && + PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) + { + this.PromptContext = new PSReadLinePromptContext( + this, + this.PromptNest, + this.InvocationEventQueue, + proxy); + } + else + { + this.PromptContext = new LegacyReadLineContext(this); + } + } + + /// + /// Imports the PowerShellEditorServices.Commands module into + /// the runspace. This method will be moved somewhere else soon. + /// + /// + /// + public Task ImportCommandsModuleAsync(string moduleBasePath) + { + PSCommand importCommand = new PSCommand(); + importCommand + .AddCommand("Import-Module") + .AddArgument( + Path.Combine( + moduleBasePath, + "PowerShellEditorServices.Commands.psd1")); + + return this.ExecuteCommandAsync(importCommand, false, false); + } + + private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) + { + // The only types of runspaces that need to be configured are: + // - Locally created runspaces + // - Local process entered with Enter-PSHostProcess + // - Remote session entered with Enter-PSSession + return + (runspaceDetails.Location == RunspaceLocation.Local && + (runspaceDetails.Context == RunspaceContext.Original || + runspaceDetails.Context == RunspaceContext.EnteredProcess)) || + (runspaceDetails.Location == RunspaceLocation.Remote && runspaceDetails.Context == RunspaceContext.Original); + } + + private void ConfigureRunspace(RunspaceDetails runspaceDetails) + { + runspaceDetails.Runspace.StateChanged += this.HandleRunspaceStateChanged; + if (runspaceDetails.Runspace.Debugger != null) + { + runspaceDetails.Runspace.Debugger.BreakpointUpdated += OnBreakpointUpdated; + runspaceDetails.Runspace.Debugger.DebuggerStop += OnDebuggerStop; + } + + this.versionSpecificOperations.ConfigureDebugger(runspaceDetails.Runspace); + } + + private void CleanupRunspace(RunspaceDetails runspaceDetails) + { + runspaceDetails.Runspace.StateChanged -= this.HandleRunspaceStateChanged; + if (runspaceDetails.Runspace.Debugger != null) + { + runspaceDetails.Runspace.Debugger.BreakpointUpdated -= OnBreakpointUpdated; + runspaceDetails.Runspace.Debugger.DebuggerStop -= OnDebuggerStop; + } + } + + #endregion + + #region Public Methods + + /// + /// Gets a RunspaceHandle for the session's runspace. This + /// handle is used to gain temporary ownership of the runspace + /// so that commands can be executed against it directly. + /// + /// A RunspaceHandle instance that gives access to the session's runspace. + public Task GetRunspaceHandleAsync() + { + return this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine: false); + } + + /// + /// Gets a RunspaceHandle for the session's runspace. This + /// handle is used to gain temporary ownership of the runspace + /// so that commands can be executed against it directly. + /// + /// A CancellationToken that can be used to cancel the request. + /// A RunspaceHandle instance that gives access to the session's runspace. + public Task GetRunspaceHandleAsync(CancellationToken cancellationToken) + { + return this.GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// + /// If true, causes any output written during command execution to be written to the host. + /// + /// + /// If true, causes any errors encountered during command execution to be written to the host. + /// + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public async Task> ExecuteCommandAsync( + PSCommand psCommand, + bool sendOutputToHost = false, + bool sendErrorToHost = true) + { + return await ExecuteCommandAsync(psCommand, null, sendOutputToHost, sendErrorToHost); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// Error messages from PowerShell will be written to the StringBuilder. + /// + /// If true, causes any output written during command execution to be written to the host. + /// + /// + /// If true, causes any errors encountered during command execution to be written to the host. + /// + /// + /// If true, adds the command to the user's command history. + /// + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public Task> ExecuteCommandAsync( + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost = false, + bool sendErrorToHost = true, + bool addToHistory = false) + { + return + this.ExecuteCommandAsync( + psCommand, + errorMessages, + new ExecutionOptions + { + WriteOutputToHost = sendOutputToHost, + WriteErrorsToHost = sendErrorToHost, + AddToHistory = addToHistory + }); + } + + /// + /// Executes a PSCommand against the session's runspace and returns + /// a collection of results of the expected type. + /// + /// The expected result type. + /// The PSCommand to be executed. + /// Error messages from PowerShell will be written to the StringBuilder. + /// Specifies options to be used when executing this command. + /// + /// An awaitable Task which will provide results once the command + /// execution completes. + /// + public async Task> ExecuteCommandAsync( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + // Add history to PSReadLine before cancelling, otherwise it will be restored as the + // cancelled prompt when it's called again. + if (executionOptions.AddToHistory) + { + this.PromptContext.AddToHistory(psCommand.Commands[0].CommandText); + } + + bool hadErrors = false; + RunspaceHandle runspaceHandle = null; + ExecutionTarget executionTarget = ExecutionTarget.PowerShell; + IEnumerable executionResult = Enumerable.Empty(); + var shouldCancelReadLine = + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost; + + // If the debugger is active and the caller isn't on the pipeline + // thread, send the command over to that thread to be executed. + // Determine if execution should take place in a different thread + // using the following criteria: + // 1. The current frame in the prompt nest has a thread controller + // (meaning it is a nested prompt or is in the debugger) + // 2. We aren't already on the thread in question + // 3. The command is not a candidate for background invocation + // via PowerShell eventing + // 4. The command cannot be for a PSReadLine pipeline while we + // are currently in a out of process runspace + var threadController = PromptNest.GetThreadController(); + if (!(threadController == null || + !threadController.IsPipelineThread || + threadController.IsCurrentThread() || + this.ShouldExecuteWithEventing(executionOptions) || + (PromptNest.IsRemote && executionOptions.IsReadLine))) + { + this.logger.LogTrace("Passing command execution to pipeline thread."); + + if (shouldCancelReadLine && PromptNest.IsReadLineBusy()) + { + // If a ReadLine pipeline is running in the debugger then we'll hang here + // if we don't cancel it. Typically we can rely on OnExecutionStatusChanged but + // the pipeline request won't even start without clearing the current task. + this.ConsoleReader?.StopCommandLoop(); + } + + // Send the pipeline execution request to the pipeline thread + return await threadController.RequestPipelineExecutionAsync( + new PipelineExecutionRequest( + this, + psCommand, + errorMessages, + executionOptions)); + } + else + { + try + { + // Instruct PowerShell to send output and errors to the host + if (executionOptions.WriteOutputToHost) + { + psCommand.Commands[0].MergeMyResults( + PipelineResultTypes.Error, + PipelineResultTypes.Output); + + psCommand.Commands.Add( + this.GetOutputCommand( + endOfStatement: false)); + } + + executionTarget = GetExecutionTarget(executionOptions); + + // If a ReadLine pipeline is running we can still execute commands that + // don't write output (e.g. command completion) + if (executionTarget == ExecutionTarget.InvocationEvent) + { + return (await this.InvocationEventQueue.ExecuteCommandOnIdleAsync( + psCommand, + errorMessages, + executionOptions)); + } + + // Prompt is stopped and started based on the execution status, so naturally + // we don't want PSReadLine pipelines to factor in. + if (!executionOptions.IsReadLine) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Running, + executionOptions, + false); + } + + runspaceHandle = await this.GetRunspaceHandleAsync(executionOptions.IsReadLine); + if (executionOptions.WriteInputToHost) + { + this.WriteOutput(psCommand.Commands[0].CommandText, true); + } + + if (executionTarget == ExecutionTarget.Debugger) + { + // Manually change the session state for debugger commands because + // we don't have an invocation state event to attach to. + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Running, + PowerShellExecutionResult.NotFinished, + null)); + } + try + { + return this.ExecuteCommandInDebugger( + psCommand, + executionOptions.WriteOutputToHost); + } + catch (Exception e) + { + logger.LogError( + "Exception occurred while executing debugger command:\r\n\r\n" + e.ToString()); + } + finally + { + if (!executionOptions.IsReadLine) + { + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + } + } + } + + var invocationSettings = new PSInvocationSettings() + { + AddToHistory = executionOptions.AddToHistory + }; + + this.logger.LogTrace( + string.Format( + "Attempting to execute command(s):\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + + + PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); + shell.Commands = psCommand; + + // Don't change our SessionState for ReadLine. + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged += powerShell_InvocationStateChanged; + } + + shell.Runspace = executionOptions.ShouldExecuteInOriginalRunspace + ? this.initialRunspace.Runspace + : this.CurrentRunspace.Runspace; + try + { + // Nested PowerShell instances can't be invoked asynchronously. This occurs + // in nested prompts and pipeline requests from eventing. + if (shell.IsNested) + { + return shell.Invoke(null, invocationSettings); + } + + return await Task.Factory.StartNew>( + () => shell.Invoke(null, invocationSettings), + CancellationToken.None, // Might need a cancellation token + TaskCreationOptions.None, + TaskScheduler.Default); + } + finally + { + if (!executionOptions.IsReadLine) + { + shell.InvocationStateChanged -= powerShell_InvocationStateChanged; + } + + if (shell.HadErrors) + { + var strBld = new StringBuilder(1024); + strBld.AppendFormat("Execution of the following command(s) completed with errors:\r\n\r\n{0}\r\n", + GetStringForPSCommand(psCommand)); + + int i = 1; + foreach (var error in shell.Streams.Error) + { + if (i > 1) strBld.Append("\r\n\r\n"); + strBld.Append($"Error #{i++}:\r\n"); + strBld.Append(error.ToString() + "\r\n"); + strBld.Append("ScriptStackTrace:\r\n"); + strBld.Append((error.ScriptStackTrace ?? "") + "\r\n"); + strBld.Append($"Exception:\r\n {error.Exception?.ToString() ?? ""}"); + Exception innerEx = error.Exception?.InnerException; + while (innerEx != null) + { + strBld.Append($"InnerException:\r\n {innerEx.ToString()}"); + innerEx = innerEx.InnerException; + } + } + + // We've reported these errors, clear them so they don't keep showing up. + shell.Streams.Error.Clear(); + + var errorMessage = strBld.ToString(); + + errorMessages?.Append(errorMessage); + this.logger.LogError(errorMessage); + + hadErrors = true; + } + else + { + this.logger.LogTrace( + "Execution completed successfully."); + } + } + } + catch (PSRemotingDataStructureException e) + { + this.logger.LogError( + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } + catch (PipelineStoppedException e) + { + this.logger.LogError( + "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); + + errorMessages?.Append(e.Message); + } + catch (RuntimeException e) + { + this.logger.LogWarning( + "Runtime exception occurred while executing command:\r\n\r\n" + e.ToString()); + + hadErrors = true; + errorMessages?.Append(e.Message); + + if (executionOptions.WriteErrorsToHost) + { + // Write the error to the host + this.WriteExceptionToHost(e); + } + } + catch (Exception) + { + this.OnExecutionStatusChanged( + ExecutionStatus.Failed, + executionOptions, + true); + + throw; + } + finally + { + // If the RunspaceAvailability is None, it means that the runspace we're in is dead. + // If this is the case, we should abort the execution which will clean up the runspace + // (and clean up the debugger) and then pop it off the stack. + // An example of when this happens is when the "attach" debug config is used and the + // process you're attached to dies randomly. + if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.None) + { + this.AbortExecution(shouldAbortDebugSession: true); + this.PopRunspace(); + } + + // Get the new prompt before releasing the runspace handle + if (executionOptions.WriteOutputToHost) + { + SessionDetails sessionDetails = null; + + // Get the SessionDetails and then write the prompt + if (executionTarget == ExecutionTarget.Debugger) + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + else if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) + { + // This state can happen if the user types a command that causes the + // debugger to exit before we reach this point. No RunspaceHandle + // will exist already so we need to create one and then use it + if (runspaceHandle == null) + { + runspaceHandle = await this.GetRunspaceHandleAsync(); + } + + sessionDetails = this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); + } + else + { + sessionDetails = this.GetSessionDetailsInNestedPipeline(); + } + + // Check if the runspace has changed + this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails); + } + + // Dispose of the execution context + if (runspaceHandle != null) + { + runspaceHandle.Dispose(); + } + + this.OnExecutionStatusChanged( + ExecutionStatus.Completed, + executionOptions, + hadErrors); + } + } + + return executionResult; + } + + /// + /// Executes a PSCommand in the session's runspace without + /// expecting to receive any result. + /// + /// The PSCommand to be executed. + /// + /// An awaitable Task that the caller can use to know when + /// execution completes. + /// + public Task ExecuteCommandAsync(PSCommand psCommand) + { + return this.ExecuteCommandAsync(psCommand); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString) + { + return this.ExecuteScriptStringAsync(scriptString, false, true); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// Error messages from PowerShell will be written to the StringBuilder. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString, + StringBuilder errorMessages) + { + return this.ExecuteScriptStringAsync(scriptString, errorMessages, false, true, false); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString, + bool writeInputToHost, + bool writeOutputToHost) + { + return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, false); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// If true, adds the command to the user's command history. + /// A Task that can be awaited for the script completion. + public Task> ExecuteScriptStringAsync( + string scriptString, + bool writeInputToHost, + bool writeOutputToHost, + bool addToHistory) + { + return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, addToHistory); + } + + /// + /// Executes a script string in the session's runspace. + /// + /// The script string to execute. + /// Error messages from PowerShell will be written to the StringBuilder. + /// If true, causes the script string to be written to the host. + /// If true, causes the script output to be written to the host. + /// If true, adds the command to the user's command history. + /// A Task that can be awaited for the script completion. + public async Task> ExecuteScriptStringAsync( + string scriptString, + StringBuilder errorMessages, + bool writeInputToHost, + bool writeOutputToHost, + bool addToHistory) + { + return await this.ExecuteCommandAsync( + new PSCommand().AddScript(scriptString.Trim()), + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = writeOutputToHost, + AddToHistory = addToHistory, + WriteInputToHost = writeInputToHost + }); + } + + /// + /// Executes a script file at the specified path. + /// + /// The script execute. + /// Arguments to pass to the script. + /// Writes the executed script path and arguments to the host. + /// A Task that can be awaited for completion. + public async Task ExecuteScriptWithArgsAsync(string script, string arguments = null, bool writeInputToHost = false) + { + PSCommand command = new PSCommand(); + + if (arguments != null) + { + // Need to determine If the script string is a path to a script file. + string scriptAbsPath = string.Empty; + try + { + // Assume we can only debug scripts from the FileSystem provider + string workingDir = (await ExecuteCommandAsync( + new PSCommand() + .AddCommand("Microsoft.PowerShell.Management\\Get-Location") + .AddParameter("PSProvider", "FileSystem"), + false, + false)) + .FirstOrDefault() + .ProviderPath; + + workingDir = workingDir.TrimEnd(Path.DirectorySeparatorChar); + scriptAbsPath = workingDir + Path.DirectorySeparatorChar + script; + } + catch (System.Management.Automation.DriveNotFoundException e) + { + this.logger.LogError( + "Could not determine current filesystem location:\r\n\r\n" + e.ToString()); + } + + var strBld = new StringBuilder(); + + // The script parameter can refer to either a "script path" or a "command name". If it is a + // script path, we can determine that by seeing if the path exists. If so, we always single + // quote that path in case it includes special PowerShell characters like ', &, (, ), [, ] and + // . Any embedded single quotes are escaped. + // If the provided path is already quoted, then File.Exists will not find it. + // This keeps us from quoting an already quoted path. + // Related to issue #123. + if (File.Exists(script) || File.Exists(scriptAbsPath)) + { + // Dot-source the launched script path and single quote the path in case it includes + strBld.Append(". ").Append(QuoteEscapeString(script)); + } + else + { + strBld.Append(script); + } + + // Add arguments + strBld.Append(' ').Append(arguments); + + var launchedScript = strBld.ToString(); + this.logger.LogTrace($"Launch script is: {launchedScript}"); + + command.AddScript(launchedScript, false); + } + else + { + // AddCommand can handle script paths including those with special chars e.g.: + // ".\foo & [bar]\foo.ps1" and it can handle arbitrary commands, like "Invoke-Pester" + command.AddCommand(script, false); + } + + if (writeInputToHost) + { + this.WriteOutput( + script + Environment.NewLine, + true); + } + + await this.ExecuteCommandAsync( + command, + null, + sendOutputToHost: true, + addToHistory: true); + } + + /// + /// Forces the to trigger PowerShell event handling, + /// reliquishing control of the pipeline thread during event processing. + /// + /// + /// This method is called automatically by and + /// . Consider using them instead of this method directly when + /// possible. + /// + internal void ForcePSEventHandling() + { + PromptContext.ForcePSEventHandling(); + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + /// + /// This method is called automatically by . Consider using + /// that method instead of calling this directly when possible. + /// + internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) + { + if (this.PromptNest.IsReadLineBusy()) + { + await this.InvocationEventQueue.InvokeOnPipelineThreadAsync(invocationAction); + return; + } + + // If this is invoked when ReadLine isn't busy then there shouldn't be any running + // pipelines. Right now this method is only used by command completion which doesn't + // actually require running on the pipeline thread, as long as nothing else is running. + invocationAction.Invoke(this.PromptNest.GetPowerShell()); + } + + internal async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await PromptContext.InvokeReadLineAsync( + isCommandLine, + cancellationToken); + } + + internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) + { + using (PowerShell pwsh = PowerShell.Create()) + { + pwsh.Runspace = runspace; + IEnumerable results = pwsh.AddScript(scriptToExecute).Invoke(); + return results.DefaultIfEmpty(defaultValue).First(); + } + } + + /// + /// Loads PowerShell profiles for the host from the specified + /// profile locations. Only the profile paths which exist are + /// loaded. + /// + /// A Task that can be awaited for completion. + public async Task LoadHostProfilesAsync() + { + if (this.profilePaths != null) + { + // Load any of the profile paths that exist + foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths()) + { + PSCommand command = new PSCommand(); + command.AddCommand(profilePath, false); + await this.ExecuteCommandAsync(command, true, true); + } + + // Gather the session details (particularly the prompt) after + // loading the user's profiles. + await this.GetSessionDetailsInRunspaceAsync(); + } + } + + /// + /// Causes the most recent execution to be aborted no matter what state + /// it is currently in. + /// + public void AbortExecution() + { + this.AbortExecution(shouldAbortDebugSession: false); + } + + /// + /// Causes the most recent execution to be aborted no matter what state + /// it is currently in. + /// + /// + /// A value indicating whether a debug session should be aborted if one + /// is currently active. + /// + public void AbortExecution(bool shouldAbortDebugSession) + { + if (this.SessionState != PowerShellContextState.Aborting && + this.SessionState != PowerShellContextState.Disposed) + { + this.logger.LogTrace("Execution abort requested..."); + + if (shouldAbortDebugSession) + { + this.ExitAllNestedPrompts(); + } + + if (this.PromptNest.IsInDebugger) + { + if (shouldAbortDebugSession) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + this.ResumeDebugger(DebuggerResumeAction.Stop); + } + else + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + else + { + this.PromptNest.GetPowerShell(isReadLine: false).BeginStop(null, null); + } + + this.SessionState = PowerShellContextState.Aborting; + + this.OnExecutionStatusChanged( + ExecutionStatus.Aborted, + null, + false); + } + else + { + this.logger.LogTrace( + string.Format( + $"Execution abort requested when already aborted (SessionState = {this.SessionState})")); + } + } + + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + internal void ExitAllNestedPrompts() + { + while (this.PromptNest.IsNestedPrompt) + { + this.PromptNest.WaitForCurrentFrameExit(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + + /// + /// Exit all consecutive nested prompts that the user has entered. + /// + /// + /// A task object that represents all nested prompts being exited + /// + internal async Task ExitAllNestedPromptsAsync() + { + while (this.PromptNest.IsNestedPrompt) + { + await this.PromptNest.WaitForCurrentFrameExitAsync(frame => this.ExitNestedPrompt()); + this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); + } + } + + /// + /// Causes the debugger to break execution wherever it currently is. + /// This method is internal because the real Break API is provided + /// by the DebugService. + /// + internal void BreakExecution() + { + this.logger.LogTrace("Debugger break requested..."); + + // Pause the debugger + this.versionSpecificOperations.PauseDebugger( + this.CurrentRunspace.Runspace); + } + + internal void ResumeDebugger(DebuggerResumeAction resumeAction) + { + ResumeDebugger(resumeAction, shouldWaitForExit: true); + } + + private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitForExit) + { + resumeRequestHandle.Wait(); + try + { + if (this.PromptNest.IsNestedPrompt) + { + this.ExitAllNestedPrompts(); + } + + if (this.PromptNest.IsInDebugger) + { + // Set the result so that the execution thread resumes. + // The execution thread will clean up the task. + if (shouldWaitForExit) + { + this.PromptNest.WaitForCurrentFrameExit( + frame => + { + frame.ThreadController.StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + }); + } + else + { + this.PromptNest.GetThreadController().StartThreadExit(resumeAction); + this.ConsoleReader?.StopCommandLoop(); + if (this.SessionState != PowerShellContextState.Ready) + { + this.versionSpecificOperations.StopCommandInDebugger(this); + } + } + } + else + { + this.logger.LogError( + $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); + } + } + finally + { + resumeRequestHandle.Release(); + } + } + + /// + /// Disposes the runspace and any other resources being used + /// by this PowerShellContext. + /// + public void Dispose() + { + this.PromptNest.Dispose(); + this.SessionState = PowerShellContextState.Disposed; + + // Clean up the active runspace + this.CleanupRunspace(this.CurrentRunspace); + + // Push the active runspace so it will be included in the loop + this.runspaceStack.Push(this.CurrentRunspace); + + while (this.runspaceStack.Count > 0) + { + RunspaceDetails poppedRunspace = this.runspaceStack.Pop(); + + // Close the popped runspace if it isn't the initial runspace + // or if it is the initial runspace and we own that runspace + if (this.initialRunspace != poppedRunspace || this.ownsInitialRunspace) + { + this.CloseRunspace(poppedRunspace); + } + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Shutdown, + poppedRunspace, + null)); + } + + this.initialRunspace = null; + } + + private async Task GetRunspaceHandleAsync(bool isReadLine) + { + return await this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + return await this.PromptNest.GetRunspaceHandleAsync(cancellationToken, isReadLine); + } + + private ExecutionTarget GetExecutionTarget(ExecutionOptions options = null) + { + if (options == null) + { + options = new ExecutionOptions(); + } + + var noBackgroundInvocation = + options.InterruptCommandPrompt || + options.WriteOutputToHost || + options.IsReadLine || + PromptNest.IsRemote; + + // Take over the pipeline if PSReadLine is running, we aren't trying to run PSReadLine, and + // we aren't in a remote session. + if (!noBackgroundInvocation && PromptNest.IsReadLineBusy() && PromptNest.IsMainThreadBusy()) + { + return ExecutionTarget.InvocationEvent; + } + + // We can't take the pipeline from PSReadLine if it's in a remote session, so we need to + // invoke locally in that case. + if (IsDebuggerStopped && PromptNest.IsInDebugger && !(options.IsReadLine && PromptNest.IsRemote)) + { + return ExecutionTarget.Debugger; + } + + return ExecutionTarget.PowerShell; + } + + private bool ShouldExecuteWithEventing(ExecutionOptions executionOptions) + { + return + this.PromptNest.IsReadLineBusy() && + this.PromptNest.IsMainThreadBusy() && + !(executionOptions.IsReadLine || + executionOptions.InterruptCommandPrompt || + executionOptions.WriteOutputToHost || + IsCurrentRunspaceOutOfProcess()); + } + + private void CloseRunspace(RunspaceDetails runspaceDetails) + { + string exitCommand = null; + + switch (runspaceDetails.Context) + { + case RunspaceContext.Original: + if (runspaceDetails.Location == RunspaceLocation.Local) + { + runspaceDetails.Runspace.Close(); + runspaceDetails.Runspace.Dispose(); + } + else + { + exitCommand = "Exit-PSSession"; + } + + break; + + case RunspaceContext.EnteredProcess: + exitCommand = "Exit-PSHostProcess"; + break; + + case RunspaceContext.DebuggedRunspace: + // An attached runspace will be detached when the + // running pipeline is aborted + break; + } + + if (exitCommand != null) + { + Exception exitException = null; + + try + { + using (PowerShell ps = PowerShell.Create()) + { + ps.Runspace = runspaceDetails.Runspace; + ps.AddCommand(exitCommand); + ps.Invoke(); + } + } + catch (RemoteException e) + { + exitException = e; + } + catch (RuntimeException e) + { + exitException = e; + } + + if (exitException != null) + { + this.logger.LogError( + $"Caught {exitException.GetType().Name} while exiting {runspaceDetails.Location} runspace:\r\n{exitException.ToString()}"); + } + } + } + + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) + { + Validate.IsNotNull("runspaceHandle", runspaceHandle); + + if (PromptNest.IsMainThreadBusy() || (runspaceHandle.IsReadLine && PromptNest.IsReadLineBusy())) + { + var unusedTask = PromptNest + .ReleaseRunspaceHandleAsync(runspaceHandle) + .ConfigureAwait(false); + } + else + { + // Write the situation to the log since this shouldn't happen + this.logger.LogError( + "ReleaseRunspaceHandle was called when the main thread was not busy."); + } + } + + /// + /// Determines if the current runspace is out of process. + /// + /// + /// A value indicating whether the current runspace is out of process. + /// + internal bool IsCurrentRunspaceOutOfProcess() + { + return + CurrentRunspace.Context == RunspaceContext.EnteredProcess || + CurrentRunspace.Context == RunspaceContext.DebuggedRunspace || + CurrentRunspace.Location == RunspaceLocation.Remote; + } + + /// + /// Called by the external PSHost when $Host.EnterNestedPrompt is called. + /// + internal void EnterNestedPrompt() + { + if (this.IsCurrentRunspaceOutOfProcess()) + { + throw new NotSupportedException(); + } + + this.PromptNest.PushPromptContext(PromptNestFrameType.NestedPrompt); + var localThreadController = this.PromptNest.GetThreadController(); + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + + // Reset command loop mainly for PSReadLine + this.ConsoleReader?.StopCommandLoop(); + this.ConsoleReader?.StartCommandLoop(); + + var localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + var localDebuggerStoppedTask = localThreadController.Exit(); + + // Wait for off-thread pipeline requests and/or ExitNestedPrompt + while (true) + { + int taskIndex = Task.WaitAny( + localPipelineExecutionTask, + localDebuggerStoppedTask); + + if (taskIndex == 0) + { + var localExecutionTask = localPipelineExecutionTask.GetAwaiter().GetResult(); + localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + localExecutionTask.ExecuteAsync().GetAwaiter().GetResult(); + continue; + } + + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.PopPromptContext(); + break; + } + } + + /// + /// Called by the external PSHost when $Host.ExitNestedPrompt is called. + /// + internal void ExitNestedPrompt() + { + if (this.PromptNest.NestedPromptLevel == 1 || !this.PromptNest.IsNestedPrompt) + { + this.logger.LogError( + "ExitNestedPrompt was called outside of a nested prompt."); + return; + } + + // Stop the command input loop so PSReadLine isn't invoked between ExitNestedPrompt + // being invoked and EnterNestedPrompt getting the message to exit. + this.ConsoleReader?.StopCommandLoop(); + this.PromptNest.GetThreadController().StartThreadExit(DebuggerResumeAction.Stop); + } + + /// + /// Sets the current working directory of the powershell context. The path should be + /// unescaped before calling this method. + /// + /// + public async Task SetWorkingDirectoryAsync(string path) + { + await this.SetWorkingDirectoryAsync(path, true); + } + + /// + /// Sets the current working directory of the powershell context. + /// + /// + /// Specify false to have the path escaped, otherwise specify true if the path has already been escaped. + public async Task SetWorkingDirectoryAsync(string path, bool isPathAlreadyEscaped) + { + this.InitialWorkingDirectory = path; + + if (!isPathAlreadyEscaped) + { + path = WildcardEscapePath(path); + } + + await ExecuteCommandAsync( + new PSCommand().AddCommand("Set-Location").AddParameter("Path", path), + null, + sendOutputToHost: false, + sendErrorToHost: false, + addToHistory: false); + } + + /// + /// Fully escape a given path for use in PowerShell script. + /// Note: this will not work with PowerShell.AddParameter() + /// + /// The path to escape. + /// An escaped version of the path that can be embedded in PowerShell script. + internal static string FullyPowerShellEscapePath(string path) + { + string wildcardEscapedPath = WildcardEscapePath(path); + return QuoteEscapeString(wildcardEscapedPath); + } + + /// + /// Wrap a string in quotes to make it safe to use in scripts. + /// + /// The glob-escaped path to wrap in quotes. + /// The given path wrapped in quotes appropriately. + internal static string QuoteEscapeString(string escapedPath) + { + var sb = new StringBuilder(escapedPath.Length + 2); // Length of string plus two quotes + sb.Append('\''); + if (!escapedPath.Contains('\'')) + { + sb.Append(escapedPath); + } + else + { + foreach (char c in escapedPath) + { + if (c == '\'') + { + sb.Append("''"); + continue; + } + + sb.Append(c); + } + } + sb.Append('\''); + return sb.ToString(); + } + + /// + /// Return the given path with all PowerShell globbing characters escaped, + /// plus optionally the whitespace. + /// + /// The path to process. + /// Specify True to escape spaces in the path, otherwise False. + /// The path with [ and ] escaped. + internal static string WildcardEscapePath(string path, bool escapeSpaces = false) + { + var sb = new StringBuilder(); + for (int i = 0; i < path.Length; i++) + { + char curr = path[i]; + switch (curr) + { + // Escape '[', ']', '?' and '*' with '`' + case '[': + case ']': + case '*': + case '?': + case '`': + sb.Append('`').Append(curr); + break; + + default: + // Escape whitespace if required + if (escapeSpaces && char.IsWhiteSpace(curr)) + { + sb.Append('`').Append(curr); + break; + } + sb.Append(curr); + break; + } + } + + return sb.ToString(); + } + + /// + /// Returns the passed in path with the [ and ] characters escaped. Escaping spaces is optional. + /// + /// The path to process. + /// Specify True to escape spaces in the path, otherwise False. + /// The path with [ and ] escaped. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API is not meant for public usage and should not be used.")] + public static string EscapePath(string path, bool escapeSpaces) + { + return WildcardEscapePath(path, escapeSpaces); + } + + internal static string UnescapeWildcardEscapedPath(string wildcardEscapedPath) + { + // Prevent relying on my implementation if we can help it + if (!wildcardEscapedPath.Contains('`')) + { + return wildcardEscapedPath; + } + + var sb = new StringBuilder(wildcardEscapedPath.Length); + for (int i = 0; i < wildcardEscapedPath.Length; i++) + { + // If we see a backtick perform a lookahead + char curr = wildcardEscapedPath[i]; + if (curr == '`' && i + 1 < wildcardEscapedPath.Length) + { + // If the next char is an escapable one, don't add this backtick to the new string + char next = wildcardEscapedPath[i + 1]; + switch (next) + { + case '[': + case ']': + case '?': + case '*': + continue; + + default: + if (char.IsWhiteSpace(next)) + { + continue; + } + break; + } + } + + sb.Append(curr); + } + + return sb.ToString(); + } + + /// + /// Unescapes any escaped [, ] or space characters. Typically use this before calling a + /// .NET API that doesn't understand PowerShell escaped chars. + /// + /// The path to unescape. + /// The path with the ` character before [, ] and spaces removed. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API is not meant for public usage and should not be used.")] + public static string UnescapePath(string path) + { + return UnescapeWildcardEscapedPath(path); + } + + #endregion + + #region Events + + /// + /// Raised when the state of the session has changed. + /// + public event EventHandler SessionStateChanged; + + private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e) + { + if (this.SessionState != PowerShellContextState.Disposed) + { + this.logger.LogTrace( + string.Format( + "Session state changed --\r\n\r\n Old state: {0}\r\n New state: {1}\r\n Result: {2}", + this.SessionState.ToString(), + e.NewSessionState.ToString(), + e.ExecutionResult)); + + this.SessionState = e.NewSessionState; + this.SessionStateChanged?.Invoke(sender, e); + } + else + { + this.logger.LogWarning( + $"Received session state change to {e.NewSessionState} when already disposed"); + } + } + + /// + /// Raised when the runspace changes by entering a remote session or one in a different process. + /// + public event EventHandler RunspaceChanged; + + private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) + { + this.RunspaceChanged?.Invoke(sender, e); + } + + /// + /// Raised when the status of an executed command changes. + /// + public event EventHandler ExecutionStatusChanged; + + private void OnExecutionStatusChanged( + ExecutionStatus executionStatus, + ExecutionOptions executionOptions, + bool hadErrors) + { + this.ExecutionStatusChanged?.Invoke( + this, + new ExecutionStatusChangedEventArgs( + executionStatus, + executionOptions, + hadErrors)); + } + + #endregion + + #region Private Methods + + private IEnumerable ExecuteCommandInDebugger(PSCommand psCommand, bool sendOutputToHost) + { + this.logger.LogTrace( + string.Format( + "Attempting to execute command(s) in the debugger:\r\n\r\n{0}", + GetStringForPSCommand(psCommand))); + + IEnumerable output = + this.versionSpecificOperations.ExecuteCommandInDebugger( + this, + this.CurrentRunspace.Runspace, + psCommand, + sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction); + + if (debuggerResumeAction.HasValue) + { + // Resume the debugger with the specificed action + this.ResumeDebugger( + debuggerResumeAction.Value, + shouldWaitForExit: false); + } + + return output; + } + + internal void WriteOutput(string outputString, bool includeNewLine) + { + this.WriteOutput( + outputString, + includeNewLine, + OutputType.Normal); + } + + internal void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType) + { + if (this.ConsoleWriter != null) + { + this.ConsoleWriter.WriteOutput( + outputString, + includeNewLine, + outputType); + } + } + + private void WriteExceptionToHost(Exception e) + { + const string ExceptionFormat = + "{0}\r\n{1}\r\n + CategoryInfo : {2}\r\n + FullyQualifiedErrorId : {3}"; + + IContainsErrorRecord containsErrorRecord = e as IContainsErrorRecord; + + if (containsErrorRecord == null || + containsErrorRecord.ErrorRecord == null) + { + this.WriteError(e.Message, null, 0, 0); + return; + } + + ErrorRecord errorRecord = containsErrorRecord.ErrorRecord; + if (errorRecord.InvocationInfo == null) + { + this.WriteError(errorRecord.ToString(), String.Empty, 0, 0); + return; + } + + string errorRecordString = errorRecord.ToString(); + if ((errorRecord.InvocationInfo.PositionMessage != null) && + errorRecordString.IndexOf(errorRecord.InvocationInfo.PositionMessage, StringComparison.Ordinal) != -1) + { + this.WriteError(errorRecordString); + return; + } + + string message = + string.Format( + CultureInfo.InvariantCulture, + ExceptionFormat, + errorRecord.ToString(), + errorRecord.InvocationInfo.PositionMessage, + errorRecord.CategoryInfo, + errorRecord.FullyQualifiedErrorId); + + this.WriteError(message); + } + + private void WriteError( + string errorMessage, + string filePath, + int lineNumber, + int columnNumber) + { + const string ErrorLocationFormat = "At {0}:{1} char:{2}"; + + this.WriteError( + errorMessage + + Environment.NewLine + + string.Format( + ErrorLocationFormat, + String.IsNullOrEmpty(filePath) ? "line" : filePath, + lineNumber, + columnNumber)); + } + + private void WriteError(string errorMessage) + { + if (this.ConsoleWriter != null) + { + this.ConsoleWriter.WriteOutput( + errorMessage, + true, + OutputType.Error, + ConsoleColor.Red, + ConsoleColor.Black); + } + } + + void powerShell_InvocationStateChanged(object sender, PSInvocationStateChangedEventArgs e) + { + SessionStateChangedEventArgs eventArgs = TranslateInvocationStateInfo(e.InvocationStateInfo); + this.OnSessionStateChanged(this, eventArgs); + } + + private static SessionStateChangedEventArgs TranslateInvocationStateInfo(PSInvocationStateInfo invocationState) + { + PowerShellContextState newState = PowerShellContextState.Unknown; + PowerShellExecutionResult executionResult = PowerShellExecutionResult.NotFinished; + + switch (invocationState.State) + { + case PSInvocationState.NotStarted: + newState = PowerShellContextState.NotStarted; + break; + + case PSInvocationState.Failed: + newState = PowerShellContextState.Ready; + executionResult = PowerShellExecutionResult.Failed; + break; + + case PSInvocationState.Disconnected: + // TODO: Any extra work to do in this case? + // TODO: Is this a unique state that can be re-connected? + newState = PowerShellContextState.Disposed; + executionResult = PowerShellExecutionResult.Stopped; + break; + + case PSInvocationState.Running: + newState = PowerShellContextState.Running; + break; + + case PSInvocationState.Completed: + newState = PowerShellContextState.Ready; + executionResult = PowerShellExecutionResult.Completed; + break; + + case PSInvocationState.Stopping: + newState = PowerShellContextState.Aborting; + break; + + case PSInvocationState.Stopped: + newState = PowerShellContextState.Ready; + executionResult = PowerShellExecutionResult.Aborted; + break; + + default: + newState = PowerShellContextState.Unknown; + break; + } + + return + new SessionStateChangedEventArgs( + newState, + executionResult, + invocationState.Reason); + } + + private Command GetOutputCommand(bool endOfStatement) + { + Command outputCommand = + new Command( + command: this.PromptNest.IsInDebugger ? "Out-String" : "Out-Default", + isScript: false, + useLocalScope: true); + + if (this.PromptNest.IsInDebugger) + { + // Out-String needs the -Stream parameter added + outputCommand.Parameters.Add("Stream"); + } + + return outputCommand; + } + + private static string GetStringForPSCommand(PSCommand psCommand) + { + StringBuilder stringBuilder = new StringBuilder(); + + foreach (var command in psCommand.Commands) + { + stringBuilder.Append(" "); + stringBuilder.Append(command.CommandText); + foreach (var param in command.Parameters) + { + if (param.Name != null) + { + stringBuilder.Append($" -{param.Name} {param.Value}"); + } + else + { + stringBuilder.Append($" {param.Value}"); + } + } + + stringBuilder.AppendLine(); + } + + return stringBuilder.ToString(); + } + + private void SetExecutionPolicy(ExecutionPolicy desiredExecutionPolicy) + { + var currentPolicy = ExecutionPolicy.Undefined; + + // Get the current execution policy so that we don't set it higher than it already is + this.powerShell.Commands.AddCommand("Get-ExecutionPolicy"); + + var result = this.powerShell.Invoke(); + if (result.Count > 0) + { + currentPolicy = result.FirstOrDefault(); + } + + if (desiredExecutionPolicy < currentPolicy || + desiredExecutionPolicy == ExecutionPolicy.Bypass || + currentPolicy == ExecutionPolicy.Undefined) + { + this.logger.LogTrace( + string.Format( + "Setting execution policy:\r\n Current = ExecutionPolicy.{0}\r\n Desired = ExecutionPolicy.{1}", + currentPolicy, + desiredExecutionPolicy)); + + this.powerShell.Commands.Clear(); + this.powerShell + .AddCommand("Set-ExecutionPolicy") + .AddParameter("ExecutionPolicy", desiredExecutionPolicy) + .AddParameter("Scope", ExecutionPolicyScope.Process) + .AddParameter("Force"); + + try + { + this.powerShell.Invoke(); + } + catch (CmdletInvocationException e) + { + this.logger.LogException( + $"An error occurred while calling Set-ExecutionPolicy, the desired policy of {desiredExecutionPolicy} may not be set.", + e); + } + + this.powerShell.Commands.Clear(); + } + else + { + this.logger.LogTrace( + string.Format( + "Current execution policy: ExecutionPolicy.{0}", + currentPolicy)); + + } + } + + private SessionDetails GetSessionDetails(Func invokeAction) + { + try + { + this.mostRecentSessionDetails = + new SessionDetails( + invokeAction( + SessionDetails.GetDetailsCommand())); + + return this.mostRecentSessionDetails; + } + catch (RuntimeException e) + { + this.logger.LogTrace( + "Runtime exception occurred while gathering runspace info:\r\n\r\n" + e.ToString()); + } + catch (ArgumentNullException) + { + this.logger.LogError( + "Could not retrieve session details but no exception was thrown."); + } + + // TODO: Return a harmless object if necessary + this.mostRecentSessionDetails = null; + return this.mostRecentSessionDetails; + } + + private async Task GetSessionDetailsInRunspaceAsync() + { + using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandleAsync()) + { + return this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); + } + } + + private SessionDetails GetSessionDetailsInRunspace(Runspace runspace) + { + SessionDetails sessionDetails = + this.GetSessionDetails( + command => + { + using (PowerShell powerShell = PowerShell.Create()) + { + powerShell.Runspace = runspace; + powerShell.Commands = command; + + return + powerShell + .Invoke() + .FirstOrDefault(); + } + }); + + return sessionDetails; + } + + private SessionDetails GetSessionDetailsInDebugger() + { + return this.GetSessionDetails( + command => + { + // Use LastOrDefault to get the last item returned. This + // is necessary because advanced prompt functions (like those + // in posh-git) may return multiple objects in the result. + return + this.ExecuteCommandInDebugger(command, false) + .LastOrDefault(); + }); + } + + private SessionDetails GetSessionDetailsInNestedPipeline() + { + // We don't need to check what thread we're on here. If it's a local + // nested pipeline then we will already be on the correct thread, and + // non-debugger nested pipelines aren't supported in remote runspaces. + return this.GetSessionDetails( + command => + { + using (var localPwsh = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + localPwsh.Commands = command; + return localPwsh.Invoke().FirstOrDefault(); + } + }); + } + + private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) + { + // Create the $profile variable + PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.AllUsersAllHosts), + profilePaths.AllUsersAllHosts)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.AllUsersCurrentHost), + profilePaths.AllUsersCurrentHost)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.CurrentUserAllHosts), + profilePaths.CurrentUserAllHosts)); + + profile.Members.Add( + new PSNoteProperty( + nameof(profilePaths.CurrentUserCurrentHost), + profilePaths.CurrentUserCurrentHost)); + + this.logger.LogTrace( + string.Format( + "Setting $profile variable in runspace. Current user host profile path: {0}", + profilePaths.CurrentUserCurrentHost)); + + // Set the variable in the runspace + this.powerShell.Commands.Clear(); + this.powerShell + .AddCommand("Set-Variable") + .AddParameter("Name", "profile") + .AddParameter("Value", profile) + .AddParameter("Option", "None"); + this.powerShell.Invoke(); + this.powerShell.Commands.Clear(); + } + + private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs args) + { + switch (args.RunspaceStateInfo.State) + { + case RunspaceState.Opening: + case RunspaceState.Opened: + // These cases don't matter, just return + return; + + case RunspaceState.Closing: + case RunspaceState.Closed: + case RunspaceState.Broken: + // If the runspace closes or fails, pop the runspace + ((IHostSupportsInteractiveSession)this).PopRunspace(); + break; + } + } + + #endregion + + #region Events + + // NOTE: This event is 'internal' because the DebugService provides + // the publicly consumable event. + internal event EventHandler DebuggerStop; + + /// + /// Raised when the debugger is resumed after it was previously stopped. + /// + public event EventHandler DebuggerResumed; + + private void StartCommandLoopOnRunspaceAvailable() + { + if (Interlocked.CompareExchange(ref this.isCommandLoopRestarterSet, 1, 1) == 1) + { + return; + } + + EventHandler handler = null; + handler = (runspace, eventArgs) => + { + if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || + this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) + { + return; + } + + ((Runspace)runspace).AvailabilityChanged -= handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); + this.ConsoleReader?.StartCommandLoop(); + }; + + this.CurrentRunspace.Runspace.AvailabilityChanged += handler; + Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 1); + } + + private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) + { + if (CurrentRunspace.Context == RunspaceContext.Original) + { + StartCommandLoopOnRunspaceAvailable(); + } + + this.logger.LogTrace("Debugger stopped execution."); + + PromptNest.PushPromptContext( + IsCurrentRunspaceOutOfProcess() + ? PromptNestFrameType.Debug | PromptNestFrameType.Remote + : PromptNestFrameType.Debug); + + ThreadController localThreadController = PromptNest.GetThreadController(); + + // Update the session state + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); + + // Get the session details and push the current + // runspace if the session has changed + SessionDetails sessionDetails = null; + try + { + sessionDetails = this.GetSessionDetailsInDebugger(); + } + catch (InvalidOperationException) + { + this.logger.LogTrace( + "Attempting to get session details failed, most likely due to a running pipeline that is attempting to stop."); + } + + if (!localThreadController.FrameExitTask.Task.IsCompleted) + { + // Push the current runspace if the session has changed + this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); + + // Raise the event for the debugger service + this.DebuggerStop?.Invoke(sender, e); + } + + this.logger.LogTrace("Starting pipeline thread message loop..."); + + Task localPipelineExecutionTask = + localThreadController.TakeExecutionRequestAsync(); + Task localDebuggerStoppedTask = + localThreadController.Exit(); + while (true) + { + int taskIndex = + Task.WaitAny( + localDebuggerStoppedTask, + localPipelineExecutionTask); + + if (taskIndex == 0) + { + // Write a new output line before continuing + this.WriteOutput("", true); + + e.ResumeAction = localDebuggerStoppedTask.GetAwaiter().GetResult(); + this.logger.LogTrace("Received debugger resume action " + e.ResumeAction.ToString()); + + // Notify listeners that the debugger has resumed + this.DebuggerResumed?.Invoke(this, e.ResumeAction); + + // Pop the current RunspaceDetails if we were attached + // to a runspace and the resume action is Stop + if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace && + e.ResumeAction == DebuggerResumeAction.Stop) + { + this.PopRunspace(); + } + else if (e.ResumeAction != DebuggerResumeAction.Stop) + { + // Update the session state + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Running, + PowerShellExecutionResult.NotFinished, + null)); + } + + break; + } + else if (taskIndex == 1) + { + this.logger.LogTrace("Received pipeline thread execution request."); + + IPipelineExecutionRequest executionRequest = localPipelineExecutionTask.Result; + localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); + executionRequest.ExecuteAsync().GetAwaiter().GetResult(); + + this.logger.LogTrace("Pipeline thread execution completed."); + + if (!this.versionSpecificOperations.IsDebuggerStopped( + this.PromptNest, + this.CurrentRunspace.Runspace)) + { + if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace) + { + // Notify listeners that the debugger has resumed + this.DebuggerResumed?.Invoke(this, DebuggerResumeAction.Stop); + + // We're detached from the runspace now, send a runspace update. + this.PopRunspace(); + } + + // If the executed command caused the debugger to exit, break + // from the pipeline loop + break; + } + } + else + { + // TODO: How to handle this? + } + } + + PromptNest.PopPromptContext(); + } + + // NOTE: This event is 'internal' because the DebugService provides + // the publicly consumable event. + internal event EventHandler BreakpointUpdated; + + private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) + { + this.BreakpointUpdated?.Invoke(sender, e); + } + + #endregion + + #region Nested Classes + + private void ConfigureRunspaceCapabilities(RunspaceDetails runspaceDetails) + { + // TODO: Bring this back + //DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); + } + + private void PushRunspace(RunspaceDetails newRunspaceDetails) + { + this.logger.LogTrace( + $"Pushing {this.CurrentRunspace.Location} ({this.CurrentRunspace.Context}), new runspace is {newRunspaceDetails.Location} ({newRunspaceDetails.Context}), connection: {newRunspaceDetails.ConnectionString}"); + + RunspaceDetails previousRunspace = this.CurrentRunspace; + + if (newRunspaceDetails.Context == RunspaceContext.DebuggedRunspace) + { + this.WriteOutput( + $"Entering debugged runspace on {newRunspaceDetails.Location.ToString().ToLower()} machine {newRunspaceDetails.SessionDetails.ComputerName}", + true); + } + + // Switch out event handlers if necessary + if (CheckIfRunspaceNeedsEventHandlers(newRunspaceDetails)) + { + this.CleanupRunspace(previousRunspace); + this.ConfigureRunspace(newRunspaceDetails); + } + + this.runspaceStack.Push(previousRunspace); + this.CurrentRunspace = newRunspaceDetails; + + // Check for runspace capabilities + this.ConfigureRunspaceCapabilities(newRunspaceDetails); + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Enter, + previousRunspace, + this.CurrentRunspace)); + } + + private void UpdateRunspaceDetailsIfSessionChanged(SessionDetails sessionDetails, bool isDebuggerStop = false) + { + RunspaceDetails newRunspaceDetails = null; + + // If we've exited an entered process or debugged runspace, pop what we've + // got before we evaluate where we're at + if ( + (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace && + this.CurrentRunspace.SessionDetails.InstanceId != sessionDetails.InstanceId) || + (this.CurrentRunspace.Context == RunspaceContext.EnteredProcess && + this.CurrentRunspace.SessionDetails.ProcessId != sessionDetails.ProcessId)) + { + this.PopRunspace(); + } + + // Are we in a new session that the PushRunspace command won't + // notify us about? + // + // Possible cases: + // - Debugged runspace in a local or remote session + // - Entered process in a remote session + // + // We don't need additional logic to check for the cases that + // PowerShell would have notified us about because the CurrentRunspace + // will already be updated by PowerShell by the time we reach + // these checks. + + if (this.CurrentRunspace.SessionDetails.InstanceId != sessionDetails.InstanceId && isDebuggerStop) + { + // Are we on a local or remote computer? + bool differentComputer = + !string.Equals( + sessionDetails.ComputerName, + this.initialRunspace.SessionDetails.ComputerName, + StringComparison.CurrentCultureIgnoreCase); + + // We started debugging a runspace + newRunspaceDetails = + RunspaceDetails.CreateFromDebugger( + this.CurrentRunspace, + differentComputer ? RunspaceLocation.Remote : RunspaceLocation.Local, + RunspaceContext.DebuggedRunspace, + sessionDetails); + } + else if (this.CurrentRunspace.SessionDetails.ProcessId != sessionDetails.ProcessId) + { + // We entered a different PowerShell host process + newRunspaceDetails = + RunspaceDetails.CreateFromContext( + this.CurrentRunspace, + RunspaceContext.EnteredProcess, + sessionDetails); + } + + if (newRunspaceDetails != null) + { + this.PushRunspace(newRunspaceDetails); + } + } + + private void PopRunspace() + { + if (this.SessionState != PowerShellContextState.Disposed) + { + if (this.runspaceStack.Count > 0) + { + RunspaceDetails previousRunspace = this.CurrentRunspace; + this.CurrentRunspace = this.runspaceStack.Pop(); + + this.logger.LogTrace( + $"Popping {previousRunspace.Location} ({previousRunspace.Context}), new runspace is {this.CurrentRunspace.Location} ({this.CurrentRunspace.Context}), connection: {this.CurrentRunspace.ConnectionString}"); + + if (previousRunspace.Context == RunspaceContext.DebuggedRunspace) + { + this.WriteOutput( + $"Leaving debugged runspace on {previousRunspace.Location.ToString().ToLower()} machine {previousRunspace.SessionDetails.ComputerName}", + true); + } + + // Switch out event handlers if necessary + if (CheckIfRunspaceNeedsEventHandlers(previousRunspace)) + { + this.CleanupRunspace(previousRunspace); + this.ConfigureRunspace(this.CurrentRunspace); + } + + this.OnRunspaceChanged( + this, + new RunspaceChangedEventArgs( + RunspaceChangeAction.Exit, + previousRunspace, + this.CurrentRunspace)); + } + else + { + this.logger.LogError( + "Caller attempted to pop a runspace when no runspaces are on the stack."); + } + } + } + + #endregion + + #region IHostSupportsInteractiveSession Implementation + + bool IHostSupportsInteractiveSession.IsRunspacePushed + { + get + { + return this.runspaceStack.Count > 0; + } + } + + Runspace IHostSupportsInteractiveSession.Runspace + { + get + { + return this.CurrentRunspace.Runspace; + } + } + + void IHostSupportsInteractiveSession.PushRunspace(Runspace runspace) + { + // Get the session details for the new runspace + SessionDetails sessionDetails = this.GetSessionDetailsInRunspace(runspace); + + this.PushRunspace( + RunspaceDetails.CreateFromRunspace( + runspace, + sessionDetails, + this.logger)); + } + + void IHostSupportsInteractiveSession.PopRunspace() + { + this.PopRunspace(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs new file mode 100644 index 000000000..03ce2abe9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs @@ -0,0 +1,166 @@ +//// +//// Copyright (c) Microsoft. All rights reserved. +//// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//// + +//using System.Linq; +//using System.Threading.Tasks; + +//namespace Microsoft.PowerShell.EditorServices.Session.Capabilities +//{ +// using Microsoft.Extensions.Logging; +// using Microsoft.PowerShell.EditorServices.Utility; +// using System; +// using System.Collections.Generic; +// using System.Collections.ObjectModel; +// using System.Management.Automation; + +// internal class DscBreakpointCapability : IRunspaceCapability +// { +// private string[] dscResourceRootPaths = new string[0]; + +// private Dictionary breakpointsPerFile = +// new Dictionary(); + +// public async Task> SetLineBreakpointsAsync( +// PowerShellContextService powerShellContext, +// string scriptPath, +// BreakpointDetails[] breakpoints) +// { +// List resultBreakpointDetails = +// new List(); + +// // We always get the latest array of breakpoint line numbers +// // so store that for future use +// if (breakpoints.Length > 0) +// { +// // Set the breakpoints for this scriptPath +// this.breakpointsPerFile[scriptPath] = +// breakpoints.Select(b => b.LineNumber).ToArray(); +// } +// else +// { +// // No more breakpoints for this scriptPath, remove it +// this.breakpointsPerFile.Remove(scriptPath); +// } + +// string hashtableString = +// string.Join( +// ", ", +// this.breakpointsPerFile +// .Select(file => $"@{{Path=\"{file.Key}\";Line=@({string.Join(",", file.Value)})}}")); + +// // Run Enable-DscDebug as a script because running it as a PSCommand +// // causes an error which states that the Breakpoint parameter has not +// // been passed. +// await powerShellContext.ExecuteScriptStringAsync( +// hashtableString.Length > 0 +// ? $"Enable-DscDebug -Breakpoint {hashtableString}" +// : "Disable-DscDebug", +// false, +// false); + +// // Verify all the breakpoints and return them +// foreach (var breakpoint in breakpoints) +// { +// breakpoint.Verified = true; +// } + +// return breakpoints.ToList(); +// } + +// public bool IsDscResourcePath(string scriptPath) +// { +// return dscResourceRootPaths.Any( +// dscResourceRootPath => +// scriptPath.StartsWith( +// dscResourceRootPath, +// StringComparison.CurrentCultureIgnoreCase)); +// } + +// public static DscBreakpointCapability CheckForCapability( +// RunspaceDetails runspaceDetails, +// PowerShellContextService powerShellContext, +// ILogger logger) +// { +// DscBreakpointCapability capability = null; + +// // DSC support is enabled only for Windows PowerShell. +// if ((runspaceDetails.PowerShellVersion.Version.Major < 6) && +// (runspaceDetails.Context != RunspaceContext.DebuggedRunspace)) +// { +// using (PowerShell powerShell = PowerShell.Create()) +// { +// powerShell.Runspace = runspaceDetails.Runspace; + +// // Attempt to import the updated DSC module +// powerShell.AddCommand("Import-Module"); +// powerShell.AddArgument(@"C:\Program Files\DesiredStateConfiguration\1.0.0.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psd1"); +// powerShell.AddParameter("PassThru"); +// powerShell.AddParameter("ErrorAction", "Ignore"); + +// PSObject moduleInfo = null; + +// try +// { +// moduleInfo = powerShell.Invoke().FirstOrDefault(); +// } +// catch (RuntimeException e) +// { +// logger.LogException("Could not load the DSC module!", e); +// } + +// if (moduleInfo != null) +// { +// logger.LogTrace("Side-by-side DSC module found, gathering DSC resource paths..."); + +// // The module was loaded, add the breakpoint capability +// capability = new DscBreakpointCapability(); +// runspaceDetails.AddCapability(capability); + +// powerShell.Commands.Clear(); +// powerShell.AddScript("Write-Host \"Gathering DSC resource paths, this may take a while...\""); +// powerShell.Invoke(); + +// // Get the list of DSC resource paths +// powerShell.Commands.Clear(); +// powerShell.AddCommand("Get-DscResource"); +// powerShell.AddCommand("Select-Object"); +// powerShell.AddParameter("ExpandProperty", "ParentPath"); + +// Collection resourcePaths = null; + +// try +// { +// resourcePaths = powerShell.Invoke(); +// } +// catch (CmdletInvocationException e) +// { +// logger.LogException("Get-DscResource failed!", e); +// } + +// if (resourcePaths != null) +// { +// capability.dscResourceRootPaths = +// resourcePaths +// .Select(o => (string)o.BaseObject) +// .ToArray(); + +// logger.LogTrace($"DSC resources found: {resourcePaths.Count}"); +// } +// else +// { +// logger.LogTrace($"No DSC resources found."); +// } +// } +// else +// { +// logger.LogTrace($"Side-by-side DSC module was not found."); +// } +// } +// } + +// return capability; +// } +// } +//} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs new file mode 100644 index 000000000..a1071606f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs @@ -0,0 +1,92 @@ +// +// 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 +{ + /// + /// Defines options for the execution of a command. + /// + public class ExecutionOptions + { + private bool? _shouldExecuteInOriginalRunspace; + + #region Properties + + /// + /// Gets or sets a boolean that determines whether command output + /// should be written to the host. + /// + public bool WriteOutputToHost { get; set; } + + /// + /// Gets or sets a boolean that determines whether command errors + /// should be written to the host. + /// + public bool WriteErrorsToHost { get; set; } + + /// + /// Gets or sets a boolean that determines whether the executed + /// command should be added to the command history. + /// + public bool AddToHistory { get; set; } + + /// + /// Gets or sets a boolean that determines whether the execution + /// of the command should interrupt the command prompt. Should + /// only be set if WriteOutputToHost is false but the command + /// should still interrupt the command prompt. + /// + public bool InterruptCommandPrompt { get; set; } + + /// + /// Gets or sets a value indicating whether the text of the command + /// should be written to the host as if it was ran interactively. + /// + public bool WriteInputToHost { get; set; } + + /// + /// Gets or sets a value indicating whether the command to + /// be executed is a console input prompt, such as the + /// PSConsoleHostReadLine function. + /// + internal bool IsReadLine { get; set; } + + /// + /// Gets or sets a value indicating whether the command should + /// be invoked in the original runspace. In the majority of cases + /// this should remain unset. + /// + internal bool ShouldExecuteInOriginalRunspace + { + get + { + return _shouldExecuteInOriginalRunspace ?? IsReadLine; + } + set + { + _shouldExecuteInOriginalRunspace = value; + } + } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ExecutionOptions class with + /// default settings configured. + /// + public ExecutionOptions() + { + this.WriteOutputToHost = true; + this.WriteErrorsToHost = true; + this.WriteInputToHost = false; + this.AddToHistory = false; + this.InterruptCommandPrompt = false; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs new file mode 100644 index 000000000..233d0499e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs @@ -0,0 +1,39 @@ +// +// 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 +{ + /// + /// Enumerates the possible execution results that can occur after + /// executing a command or script. + /// + public enum ExecutionStatus + { + /// + /// Indicates that execution has not yet started. + /// + Pending, + + /// + /// Indicates that the command is executing. + /// + Running, + + /// + /// Indicates that execution has failed. + /// + Failed, + + /// + /// Indicates that execution was aborted by the user. + /// + Aborted, + + /// + /// Indicates that execution completed successfully. + /// + Completed + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs new file mode 100644 index 000000000..cd2dcaaf2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// Contains details about an executed + /// + public class ExecutionStatusChangedEventArgs + { + #region Properties + + /// + /// Gets the options used when the command was executed. + /// + public ExecutionOptions ExecutionOptions { get; private set; } + + /// + /// Gets the command execution's current status. + /// + public ExecutionStatus ExecutionStatus { get; private set; } + + /// + /// If true, the command execution had errors. + /// + public bool HadErrors { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the ExecutionStatusChangedEventArgs class. + /// + /// The command execution's current status. + /// The options used when the command was executed. + /// If execution has completed, indicates whether there were errors. + public ExecutionStatusChangedEventArgs( + ExecutionStatus executionStatus, + ExecutionOptions executionOptions, + bool hadErrors) + { + this.ExecutionStatus = executionStatus; + this.ExecutionOptions = executionOptions; + this.HadErrors = hadErrors || (executionStatus == ExecutionStatus.Failed); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs new file mode 100644 index 000000000..70ec3cb6f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs @@ -0,0 +1,28 @@ +// +// 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.Session +{ + /// + /// Represents the different API's available for executing commands. + /// + internal enum ExecutionTarget + { + /// + /// Indicates that the command should be invoked through the PowerShell debugger. + /// + Debugger, + + /// + /// Indicates that the command should be invoked via an instance of the PowerShell class. + /// + PowerShell, + + /// + /// Indicates that the command should be invoked through the PowerShell engine's event manager. + /// + InvocationEvent + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs new file mode 100644 index 000000000..52a94daae --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs @@ -0,0 +1,373 @@ +// +// 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.Engine; +using Microsoft.PowerShell.EditorServices.Session; +using System; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an implementation of the PSHost class for the + /// ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession + { + #region Private Fields + + private ILogger Logger; + private HostDetails hostDetails; + private Guid instanceId = Guid.NewGuid(); + private EditorServicesPSHostUserInterface hostUserInterface; + private IHostSupportsInteractiveSession hostSupportsInteractiveSession; + private PowerShellContextService powerShellContext; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHost class + /// with the given IConsoleHost implementation. + /// + /// + /// An implementation of IHostSupportsInteractiveSession for runspace management. + /// + /// + /// Provides details about the host application. + /// + /// + /// The EditorServicesPSHostUserInterface implementation to use for this host. + /// + /// An ILogger implementation to use for this host. + public EditorServicesPSHost( + PowerShellContextService powerShellContext, + HostDetails hostDetails, + EditorServicesPSHostUserInterface hostUserInterface, + ILogger logger) + { + this.Logger = logger; + this.hostDetails = hostDetails; + this.hostUserInterface = hostUserInterface; + this.hostSupportsInteractiveSession = powerShellContext; + this.powerShellContext = powerShellContext; + } + + #endregion + + #region PSHost Implementation + + /// + /// + /// + public override Guid InstanceId + { + get { return this.instanceId; } + } + + /// + /// + /// + public override string Name + { + get { return this.hostDetails.Name; } + } + + internal class ConsoleColorProxy + { + private EditorServicesPSHostUserInterface _hostUserInterface; + + internal ConsoleColorProxy(EditorServicesPSHostUserInterface hostUserInterface) + { + if (hostUserInterface == null) throw new ArgumentNullException("hostUserInterface"); + _hostUserInterface = hostUserInterface; + } + + /// + /// The ForegroundColor for Error + /// + public ConsoleColor ErrorForegroundColor + { + get + { return _hostUserInterface.ErrorForegroundColor; } + set + { _hostUserInterface.ErrorForegroundColor = value; } + } + + /// + /// The BackgroundColor for Error + /// + public ConsoleColor ErrorBackgroundColor + { + get + { return _hostUserInterface.ErrorBackgroundColor; } + set + { _hostUserInterface.ErrorBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Warning + /// + public ConsoleColor WarningForegroundColor + { + get + { return _hostUserInterface.WarningForegroundColor; } + set + { _hostUserInterface.WarningForegroundColor = value; } + } + + /// + /// The BackgroundColor for Warning + /// + public ConsoleColor WarningBackgroundColor + { + get + { return _hostUserInterface.WarningBackgroundColor; } + set + { _hostUserInterface.WarningBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Debug + /// + public ConsoleColor DebugForegroundColor + { + get + { return _hostUserInterface.DebugForegroundColor; } + set + { _hostUserInterface.DebugForegroundColor = value; } + } + + /// + /// The BackgroundColor for Debug + /// + public ConsoleColor DebugBackgroundColor + { + get + { return _hostUserInterface.DebugBackgroundColor; } + set + { _hostUserInterface.DebugBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Verbose + /// + public ConsoleColor VerboseForegroundColor + { + get + { return _hostUserInterface.VerboseForegroundColor; } + set + { _hostUserInterface.VerboseForegroundColor = value; } + } + + /// + /// The BackgroundColor for Verbose + /// + public ConsoleColor VerboseBackgroundColor + { + get + { return _hostUserInterface.VerboseBackgroundColor; } + set + { _hostUserInterface.VerboseBackgroundColor = value; } + } + + /// + /// The ForegroundColor for Progress + /// + public ConsoleColor ProgressForegroundColor + { + get + { return _hostUserInterface.ProgressForegroundColor; } + set + { _hostUserInterface.ProgressForegroundColor = value; } + } + + /// + /// The BackgroundColor for Progress + /// + public ConsoleColor ProgressBackgroundColor + { + get + { return _hostUserInterface.ProgressBackgroundColor; } + set + { _hostUserInterface.ProgressBackgroundColor = value; } + } + } + + /// + /// Return the actual console host object so that the user can get at + /// the unproxied methods. + /// + public override PSObject PrivateData + { + get + { + if (hostUserInterface == null) return null; + return _consoleColorProxy ?? (_consoleColorProxy = PSObject.AsPSObject(new ConsoleColorProxy(hostUserInterface))); + } + } + private PSObject _consoleColorProxy; + + /// + /// + /// + public override Version Version + { + get { return this.hostDetails.Version; } + } + + // TODO: Pull these from IConsoleHost + + /// + /// + /// + public override System.Globalization.CultureInfo CurrentCulture + { + get { return System.Globalization.CultureInfo.CurrentCulture; } + } + + /// + /// + /// + public override System.Globalization.CultureInfo CurrentUICulture + { + get { return System.Globalization.CultureInfo.CurrentUICulture; } + } + + /// + /// + /// + public override PSHostUserInterface UI + { + get { return this.hostUserInterface; } + } + + /// + /// + /// + public override void EnterNestedPrompt() + { + this.powerShellContext.EnterNestedPrompt(); + } + + /// + /// + /// + public override void ExitNestedPrompt() + { + this.powerShellContext.ExitNestedPrompt(); + } + + /// + /// + /// + public override void NotifyBeginApplication() + { + Logger.LogTrace("NotifyBeginApplication() called."); + this.hostUserInterface.IsNativeApplicationRunning = true; + } + + /// + /// + /// + public override void NotifyEndApplication() + { + Logger.LogTrace("NotifyEndApplication() called."); + this.hostUserInterface.IsNativeApplicationRunning = false; + } + + /// + /// + /// + /// + public override void SetShouldExit(int exitCode) + { + if (this.IsRunspacePushed) + { + this.PopRunspace(); + } + } + + #endregion + + #region IHostSupportsInteractiveSession Implementation + + /// + /// + /// + /// + public bool IsRunspacePushed + { + get + { + if (this.hostSupportsInteractiveSession != null) + { + return this.hostSupportsInteractiveSession.IsRunspacePushed; + } + else + { + throw new NotImplementedException(); + } + } + } + + /// + /// + /// + /// + public Runspace Runspace + { + get + { + if (this.hostSupportsInteractiveSession != null) + { + return this.hostSupportsInteractiveSession.Runspace; + } + else + { + throw new NotImplementedException(); + } + } + } + + /// + /// + /// + /// + public void PushRunspace(Runspace runspace) + { + if (this.hostSupportsInteractiveSession != null) + { + this.hostSupportsInteractiveSession.PushRunspace(runspace); + } + else + { + throw new NotImplementedException(); + } + } + + /// + /// + /// + public void PopRunspace() + { + if (this.hostSupportsInteractiveSession != null) + { + this.hostSupportsInteractiveSession.PopRunspace(); + } + else + { + throw new NotImplementedException(); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs new file mode 100644 index 000000000..e8139beed --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs @@ -0,0 +1,1068 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Linq; +using System.Security; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Console; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Session; +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an implementation of the PSHostUserInterface class + /// for the ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + public abstract class EditorServicesPSHostUserInterface : + PSHostUserInterface, + IHostInput, + IHostOutput, + IHostUISupportsMultipleChoiceSelection + { + #region Private Fields + + private readonly ConcurrentDictionary currentProgressMessages = + new ConcurrentDictionary(); + private PromptHandler activePromptHandler; + private PSHostRawUserInterface rawUserInterface; + private CancellationTokenSource commandLoopCancellationToken; + + /// + /// The PowerShellContext to use for executing commands. + /// + protected PowerShellContextService powerShellContext; + + #endregion + + #region Public Constants + + /// + /// Gets a const string for the console's debug message prefix. + /// + public const string DebugMessagePrefix = "DEBUG: "; + + /// + /// Gets a const string for the console's warning message prefix. + /// + public const string WarningMessagePrefix = "WARNING: "; + + /// + /// Gets a const string for the console's verbose message prefix. + /// + public const string VerboseMessagePrefix = "VERBOSE: "; + + #endregion + + #region Properties + +#if !PowerShellv3 && !PowerShellv4 && !PowerShellv5r1 // Only available in Windows 10 Update 1 or higher + /// + /// Returns true if the host supports VT100 output codes. + /// + public override bool SupportsVirtualTerminal => true; +#endif + + /// + /// Returns true if a native application is currently running. + /// + public bool IsNativeApplicationRunning { get; internal set; } + + private bool IsCommandLoopRunning { get; set; } + + /// + /// Gets the ILogger implementation used for this host. + /// + protected ILogger Logger { get; private set; } + + /// + /// Gets a value indicating whether writing progress is supported. + /// + internal protected virtual bool SupportsWriteProgress => false; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The PowerShellContext to use for executing commands. + /// The PSHostRawUserInterface implementation to use for this host. + /// An ILogger implementation to use for this host. + public EditorServicesPSHostUserInterface( + PowerShellContextService powerShellContext, + PSHostRawUserInterface rawUserInterface, + ILogger logger) + { + this.Logger = logger; + this.powerShellContext = powerShellContext; + this.rawUserInterface = rawUserInterface; + + this.powerShellContext.DebuggerStop += PowerShellContext_DebuggerStop; + this.powerShellContext.DebuggerResumed += PowerShellContext_DebuggerResumed; + this.powerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChanged; + } + + #endregion + + #region Public Methods + + /// + /// Starts the host's interactive command loop. + /// + public void StartCommandLoop() + { + if (!this.IsCommandLoopRunning) + { + this.IsCommandLoopRunning = true; + this.ShowCommandPrompt(); + } + } + + /// + /// Stops the host's interactive command loop. + /// + public void StopCommandLoop() + { + if (this.IsCommandLoopRunning) + { + this.IsCommandLoopRunning = false; + this.CancelCommandPrompt(); + } + } + + private void ShowCommandPrompt() + { + if (this.commandLoopCancellationToken == null) + { + this.commandLoopCancellationToken = new CancellationTokenSource(); + + var commandLoopThreadTask = + Task.Factory.StartNew( + async () => + { + await this.StartReplLoopAsync(this.commandLoopCancellationToken.Token); + }); + } + else + { + Logger.LogTrace("StartReadLoop called while read loop is already running"); + } + } + + private void CancelCommandPrompt() + { + if (this.commandLoopCancellationToken != null) + { + // Set this to false so that Ctrl+C isn't trapped by any + // lingering ReadKey + // TOOD: Move this to Terminal impl! + //Console.TreatControlCAsInput = false; + + this.commandLoopCancellationToken.Cancel(); + this.commandLoopCancellationToken = null; + } + } + + /// + /// Cancels the currently executing command or prompt. + /// + public void SendControlC() + { + if (this.activePromptHandler != null) + { + this.activePromptHandler.CancelPrompt(); + } + else + { + // Cancel the current execution + this.powerShellContext.AbortExecution(); + } + } + + #endregion + + #region Abstract Methods + + /// + /// Requests that the HostUI implementation read a command line + /// from the user to be executed in the integrated console command + /// loop. + /// + /// + /// A CancellationToken used to cancel the command line request. + /// + /// A Task that can be awaited for the resulting input string. + protected abstract Task ReadCommandLineAsync(CancellationToken cancellationToken); + + /// + /// Creates an InputPrompt handle to use for displaying input + /// prompts to the user. + /// + /// A new InputPromptHandler instance. + protected abstract InputPromptHandler OnCreateInputPromptHandler(); + + /// + /// Creates a ChoicePromptHandler to use for displaying a + /// choice prompt to the user. + /// + /// A new ChoicePromptHandler instance. + protected abstract ChoicePromptHandler OnCreateChoicePromptHandler(); + + /// + /// 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 abstract void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor); + + /// + /// Sends a progress update event to the user. + /// + /// The source ID of the progress event. + /// The details of the activity's current progress. + protected abstract void UpdateProgress( + long sourceId, + ProgressDetails progressDetails); + + #endregion + + #region IHostInput Implementation + + #endregion + + #region PSHostUserInterface Implementation + + /// + /// + /// + /// + /// + /// + /// + public override Dictionary Prompt( + string promptCaption, + string promptMessage, + Collection fieldDescriptions) + { + FieldDetails[] fields = + fieldDescriptions + .Select(f => { return FieldDetails.Create(f, this.Logger); }) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task> promptTask = + this.CreateInputPromptHandler() + .PromptForInputAsync( + promptCaption, + promptMessage, + fields, + cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "Prompt", + cancellationToken); + + // Convert all values to PSObjects + var psObjectDict = new Dictionary(); + + // The result will be null if the prompt was cancelled + if (promptTask.Result != null) + { + // Convert all values to PSObjects + foreach (var keyValuePair in promptTask.Result) + { + psObjectDict.Add( + keyValuePair.Key, + keyValuePair.Value != null + ? PSObject.AsPSObject(keyValuePair.Value) + : null); + } + } + + // Return the result + return psObjectDict; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override int PromptForChoice( + string promptCaption, + string promptMessage, + Collection choiceDescriptions, + int defaultChoice) + { + ChoiceDetails[] choices = + choiceDescriptions + .Select(ChoiceDetails.Create) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task promptTask = + this.CreateChoicePromptHandler() + .PromptForChoiceAsync( + promptCaption, + promptMessage, + choices, + defaultChoice, + cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "PromptForChoice", + cancellationToken); + + // Return the result + return promptTask.Result; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public override PSCredential PromptForCredential( + string promptCaption, + string promptMessage, + string userName, + string targetName, + PSCredentialTypes allowedCredentialTypes, + PSCredentialUIOptions options) + { + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task> promptTask = + this.CreateInputPromptHandler() + .PromptForInputAsync( + promptCaption, + promptMessage, + new FieldDetails[] { new CredentialFieldDetails("Credential", "Credential", userName) }, + cancellationToken.Token); + + Task unpackTask = + promptTask.ContinueWith( + task => + { + if (task.IsFaulted) + { + throw task.Exception; + } + else if (task.IsCanceled) + { + throw new TaskCanceledException(task); + } + + // Return the value of the sole field + return (PSCredential)task.Result?["Credential"]; + }); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + unpackTask, + "PromptForCredential", + cancellationToken); + + return unpackTask.Result; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override PSCredential PromptForCredential( + string caption, + string message, + string userName, + string targetName) + { + return this.PromptForCredential( + caption, + message, + userName, + targetName, + PSCredentialTypes.Default, + PSCredentialUIOptions.Default); + } + + /// + /// + /// + /// + public override PSHostRawUserInterface RawUI + { + get { return this.rawUserInterface; } + } + + /// + /// + /// + /// + public override string ReadLine() + { + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task promptTask = + this.CreateInputPromptHandler() + .PromptForInputAsync(cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "ReadLine", + cancellationToken); + + return promptTask.Result; + } + + /// + /// + /// + /// + public override SecureString ReadLineAsSecureString() + { + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + Task promptTask = + this.CreateInputPromptHandler() + .PromptForSecureInputAsync(cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "ReadLineAsSecureString", + cancellationToken); + + return promptTask.Result; + } + + /// + /// + /// + /// + /// + /// + public override void Write( + ConsoleColor foregroundColor, + ConsoleColor backgroundColor, + string value) + { + this.WriteOutput( + value, + false, + OutputType.Normal, + foregroundColor, + backgroundColor); + } + + /// + /// + /// + /// + public override void Write(string value) + { + this.WriteOutput( + value, + false, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); + } + + /// + /// + /// + /// + public override void WriteLine(string value) + { + this.WriteOutput( + value, + true, + OutputType.Normal, + this.rawUserInterface.ForegroundColor, + this.rawUserInterface.BackgroundColor); + } + + /// + /// + /// + /// + public override void WriteDebugLine(string message) + { + this.WriteOutput( + DebugMessagePrefix + message, + true, + OutputType.Debug, + foregroundColor: this.DebugForegroundColor, + backgroundColor: this.DebugBackgroundColor); + } + + /// + /// + /// + /// + public override void WriteVerboseLine(string message) + { + this.WriteOutput( + VerboseMessagePrefix + message, + true, + OutputType.Verbose, + foregroundColor: this.VerboseForegroundColor, + backgroundColor: this.VerboseBackgroundColor); + } + + /// + /// + /// + /// + public override void WriteWarningLine(string message) + { + this.WriteOutput( + WarningMessagePrefix + message, + true, + OutputType.Warning, + foregroundColor: this.WarningForegroundColor, + backgroundColor: this.WarningBackgroundColor); + } + + /// + /// + /// + /// + public override void WriteErrorLine(string value) + { + this.WriteOutput( + value, + true, + OutputType.Error, + foregroundColor: this.ErrorForegroundColor, + backgroundColor: this.ErrorBackgroundColor); + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + public sealed override void WriteProgress( + long sourceId, + ProgressRecord record) + { + // Maintain old behavior if this isn't overridden. + if (!this.SupportsWriteProgress) + { + this.UpdateProgress(sourceId, ProgressDetails.Create(record)); + return; + } + + // Keep a list of progress records we write so we can automatically + // clean them up after the pipeline ends. + if (record.RecordType == ProgressRecordType.Completed) + { + this.currentProgressMessages.TryRemove(new ProgressKey(sourceId, record), out _); + } + else + { + // Adding with a value of null here because we don't actually need a dictionary. We're + // only using ConcurrentDictionary<,> becuase there is no ConcurrentHashSet<>. + this.currentProgressMessages.TryAdd(new ProgressKey(sourceId, record), null); + } + + this.WriteProgressImpl(sourceId, record); + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + protected virtual void WriteProgressImpl(long sourceId, ProgressRecord record) + { + } + + internal void ClearProgress() + { + const string nonEmptyString = "noop"; + if (!this.SupportsWriteProgress) + { + return; + } + + foreach (ProgressKey key in this.currentProgressMessages.Keys) + { + // This constructor throws if the activity description is empty even + // with completed records. + var record = new ProgressRecord( + key.ActivityId, + activity: nonEmptyString, + statusDescription: nonEmptyString); + + record.RecordType = ProgressRecordType.Completed; + this.WriteProgressImpl(key.SourceId, record); + } + + this.currentProgressMessages.Clear(); + } + + #endregion + + #region IHostUISupportsMultipleChoiceSelection Implementation + + /// + /// + /// + /// + /// + /// + /// + /// + public Collection PromptForChoice( + string promptCaption, + string promptMessage, + Collection choiceDescriptions, + IEnumerable defaultChoices) + { + ChoiceDetails[] choices = + choiceDescriptions + .Select(ChoiceDetails.Create) + .ToArray(); + + CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Task promptTask = + this.CreateChoicePromptHandler() + .PromptForChoiceAsync( + promptCaption, + promptMessage, + choices, + defaultChoices.ToArray(), + cancellationToken.Token); + + // Run the prompt task and wait for it to return + this.WaitForPromptCompletion( + promptTask, + "PromptForChoice", + cancellationToken); + + // Return the result + return new Collection(promptTask.Result.ToList()); + } + + #endregion + + #region Private Methods + + private Coordinates lastPromptLocation; + + private async Task WritePromptStringToHostAsync(CancellationToken cancellationToken) + { + try + { + if (this.lastPromptLocation != null && + this.lastPromptLocation.X == await ConsoleProxy.GetCursorLeftAsync(cancellationToken) && + this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) + { + return; + } + } + // When output is redirected (like when running tests) attempting to get + // the cursor position will throw. + catch (System.IO.IOException) + { + } + + PSCommand promptCommand = new PSCommand().AddScript("prompt"); + + cancellationToken.ThrowIfCancellationRequested(); + string promptString = + (await this.powerShellContext.ExecuteCommandAsync(promptCommand, false, false)) + .Select(pso => pso.BaseObject) + .OfType() + .FirstOrDefault() ?? "PS> "; + + // Add the [DBG] prefix if we're stopped in the debugger and the prompt doesn't already have [DBG] in it + if (this.powerShellContext.IsDebuggerStopped && !promptString.Contains("[DBG]")) + { + promptString = + string.Format( + CultureInfo.InvariantCulture, + "[DBG]: {0}", + promptString); + } + + // Update the stored prompt string if the session is remote + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) + { + promptString = + string.Format( + CultureInfo.InvariantCulture, + "[{0}]: {1}", + this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo != null + ? this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo.ComputerName + : this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName, + promptString); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Write the prompt string + this.WriteOutput(promptString, false); + this.lastPromptLocation = new Coordinates( + await ConsoleProxy.GetCursorLeftAsync(cancellationToken), + await ConsoleProxy.GetCursorTopAsync(cancellationToken)); + } + + private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) + { + // TODO: What do we display when we don't know why we stopped? + + if (eventArgs.Breakpoints.Count > 0) + { + // The breakpoint classes have nice ToString output so use that + this.WriteOutput( + Environment.NewLine + $"Hit {eventArgs.Breakpoints[0].ToString()}\n", + true, + OutputType.Normal, + ConsoleColor.Blue); + } + } + + internal static ConsoleColor BackgroundColor { get; set; } + + internal ConsoleColor ErrorForegroundColor { get; set; } = ConsoleColor.Red; + internal ConsoleColor ErrorBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor WarningForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor WarningBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor DebugForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor DebugBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor VerboseForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor VerboseBackgroundColor { get; set; } = BackgroundColor; + + internal ConsoleColor ProgressForegroundColor { get; set; } = ConsoleColor.Yellow; + internal ConsoleColor ProgressBackgroundColor { get; set; } = ConsoleColor.DarkCyan; + + private async Task StartReplLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + string commandString = null; + int originalCursorTop = 0; + + try + { + await this.WritePromptStringToHostAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + + try + { + originalCursorTop = await ConsoleProxy.GetCursorTopAsync(cancellationToken); + commandString = await this.ReadCommandLineAsync(cancellationToken); + } + catch (PipelineStoppedException) + { + this.WriteOutput( + "^C", + true, + OutputType.Normal, + foregroundColor: ConsoleColor.Red); + } + // Do nothing here, the while loop condition will exit. + catch (TaskCanceledException) + { } + catch (OperationCanceledException) + { } + catch (Exception e) // Narrow this if possible + { + this.WriteOutput( + $"\n\nAn error occurred while reading input:\n\n{e.ToString()}\n", + true, + OutputType.Error); + + Logger.LogException("Caught exception while reading command line", e); + } + finally + { + if (!cancellationToken.IsCancellationRequested && + originalCursorTop == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) + { + this.WriteLine(); + } + } + + if (!string.IsNullOrWhiteSpace(commandString)) + { + var unusedTask = + this.powerShellContext + .ExecuteScriptStringAsync( + commandString, + writeInputToHost: false, + writeOutputToHost: true, + addToHistory: true) + .ConfigureAwait(continueOnCapturedContext: false); + + break; + } + } + } + + private InputPromptHandler CreateInputPromptHandler() + { + if (this.activePromptHandler != null) + { + Logger.LogError( + "Prompt handler requested while another prompt is already active."); + } + + InputPromptHandler inputPromptHandler = this.OnCreateInputPromptHandler(); + this.activePromptHandler = inputPromptHandler; + this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; + + return inputPromptHandler; + } + + private ChoicePromptHandler CreateChoicePromptHandler() + { + if (this.activePromptHandler != null) + { + Logger.LogError( + "Prompt handler requested while another prompt is already active."); + } + + ChoicePromptHandler choicePromptHandler = this.OnCreateChoicePromptHandler(); + this.activePromptHandler = choicePromptHandler; + this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; + + return choicePromptHandler; + } + + private void activePromptHandler_PromptCancelled(object sender, EventArgs e) + { + // Clean up the existing prompt + this.activePromptHandler.PromptCancelled -= activePromptHandler_PromptCancelled; + this.activePromptHandler = null; + } + private void WaitForPromptCompletion( + Task promptTask, + string promptFunctionName, + CancellationTokenSource cancellationToken) + { + try + { + // This will synchronously block on the prompt task + // method which gets run on another thread. + promptTask.Wait(); + + if (promptTask.Status == TaskStatus.WaitingForActivation) + { + // The Wait() call has timed out, cancel the prompt + cancellationToken.Cancel(); + + this.WriteOutput("\r\nPrompt has been cancelled due to a timeout.\r\n"); + throw new PipelineStoppedException(); + } + } + catch (AggregateException e) + { + // Find the right InnerException + Exception innerException = e.InnerException; + while (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + // Was the task cancelled? + if (innerException is TaskCanceledException) + { + // Stop the pipeline if the prompt was cancelled + throw new PipelineStoppedException(); + } + else if (innerException is PipelineStoppedException) + { + // The prompt is being cancelled, rethrow the exception + throw innerException; + } + else + { + // Rethrow the exception + throw new Exception( + string.Format( + "{0} failed, check inner exception for details", + promptFunctionName), + innerException); + } + } + } + + private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) + { + if (!this.IsCommandLoopRunning) + { + StartCommandLoop(); + return; + } + + // Cancel any existing prompt first + this.CancelCommandPrompt(); + + this.WriteDebuggerBanner(e); + this.ShowCommandPrompt(); + } + + private void PowerShellContext_DebuggerResumed(object sender, System.Management.Automation.DebuggerResumeAction e) + { + this.CancelCommandPrompt(); + } + + private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs) + { + // The command loop should only be manipulated if it's already started + if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) + { + this.ClearProgress(); + + // When aborted, cancel any lingering prompts + if (this.activePromptHandler != null) + { + this.activePromptHandler.CancelPrompt(); + this.WriteOutput(string.Empty); + } + } + else if ( + eventArgs.ExecutionOptions.WriteOutputToHost || + eventArgs.ExecutionOptions.InterruptCommandPrompt) + { + // Any command which writes output to the host will affect + // the display of the prompt + if (eventArgs.ExecutionStatus != ExecutionStatus.Running) + { + this.ClearProgress(); + + // Execution has completed, start the input prompt + this.ShowCommandPrompt(); + StartCommandLoop(); + } + else + { + // A new command was started, cancel the input prompt + StopCommandLoop(); + this.CancelCommandPrompt(); + } + } + else if ( + eventArgs.ExecutionOptions.WriteErrorsToHost && + (eventArgs.ExecutionStatus == ExecutionStatus.Failed || + eventArgs.HadErrors)) + { + this.ClearProgress(); + this.WriteOutput(string.Empty, true); + var unusedTask = this.WritePromptStringToHostAsync(CancellationToken.None); + } + } + + #endregion + + private readonly struct ProgressKey : IEquatable + { + internal readonly long SourceId; + + internal readonly int ActivityId; + + internal readonly int ParentActivityId; + + internal ProgressKey(long sourceId, ProgressRecord record) + { + SourceId = sourceId; + ActivityId = record.ActivityId; + ParentActivityId = record.ParentActivityId; + } + + public bool Equals(ProgressKey other) + { + return SourceId == other.SourceId + && ActivityId == other.ActivityId + && ParentActivityId == other.ParentActivityId; + } + + public override int GetHashCode() + { + // Algorithm from https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations + unchecked + { + int hash = 17; + hash = hash * 31 + SourceId.GetHashCode(); + hash = hash * 31 + ActivityId.GetHashCode(); + hash = hash * 31 + ParentActivityId.GetHashCode(); + return hash; + } + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs new file mode 100644 index 000000000..95da783db --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs @@ -0,0 +1,28 @@ +// +// 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 +{ + /// + /// Provides methods for integrating with the host's input system. + /// + public interface IHostInput + { + /// + /// Starts the host's interactive command loop. + /// + void StartCommandLoop(); + + /// + /// Stops the host's interactive command loop. + /// + void StopCommandLoop(); + + /// + /// Cancels the currently executing command or prompt. + /// + void SendControlC(); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs new file mode 100644 index 000000000..4f36bc54f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a simplified interface for writing output to a + /// PowerShell host implementation. + /// + public interface IHostOutput + { + /// + /// 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. + /// + void WriteOutput( + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor); + } + + /// + /// Provides helpful extension methods for the IHostOutput interface. + /// + public static class IHostOutputExtensions + { + /// + /// Writes normal output with a newline to the user interface. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// The output string to be written. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString) + { + hostOutput.WriteOutput(outputString, true); + } + + /// + /// Writes normal output to the user interface. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// The output string to be written. + /// + /// + /// If true, a newline should be appended to the output's contents. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + bool includeNewLine) + { + hostOutput.WriteOutput( + outputString, + includeNewLine, + OutputType.Normal); + } + + /// + /// Writes output of a particular type to the user interface + /// with a newline ending. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// The output string to be written. + /// + /// + /// Specifies the type of output to be written. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + OutputType outputType) + { + hostOutput.WriteOutput( + outputString, + true, + OutputType.Normal); + } + + /// + /// Writes output of a particular type to the user interface. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// 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. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + bool includeNewLine, + OutputType outputType) + { + hostOutput.WriteOutput( + outputString, + includeNewLine, + outputType, + ConsoleColor.Gray, + (ConsoleColor)(-1)); // -1 indicates the console's raw background color + } + + /// + /// Writes output of a particular type to the user interface using + /// a particular foreground color. + /// + /// + /// The IHostOutput implementation to use for WriteOutput calls. + /// + /// + /// 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. + /// + public static void WriteOutput( + this IHostOutput hostOutput, + string outputString, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor) + { + hostOutput.WriteOutput( + outputString, + includeNewLine, + outputType, + foregroundColor, + (ConsoleColor)(-1)); // -1 indicates the console's raw background color + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs new file mode 100644 index 000000000..3514469e9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs @@ -0,0 +1,225 @@ +// +// 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.Management.Automation.Host; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an simple implementation of the PSHostRawUserInterface class. + /// + public class SimplePSHostRawUserInterface : PSHostRawUserInterface + { + #region Private Fields + + private const int DefaultConsoleHeight = 100; + private const int DefaultConsoleWidth = 120; + + private ILogger Logger; + + private Size currentBufferSize = new Size(DefaultConsoleWidth, DefaultConsoleHeight); + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the SimplePSHostRawUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The ILogger implementation to use for this instance. + public SimplePSHostRawUserInterface(ILogger logger) + { + this.Logger = logger; + this.ForegroundColor = ConsoleColor.White; + this.BackgroundColor = ConsoleColor.Black; + } + + #endregion + + #region PSHostRawUserInterface Implementation + + /// + /// Gets or sets the background color of the console. + /// + public override ConsoleColor BackgroundColor + { + get; + set; + } + + /// + /// Gets or sets the foreground color of the console. + /// + public override ConsoleColor ForegroundColor + { + get; + set; + } + + /// + /// Gets or sets the size of the console buffer. + /// + public override Size BufferSize + { + get + { + return this.currentBufferSize; + } + set + { + this.currentBufferSize = value; + } + } + + /// + /// Gets or sets the cursor's position in the console buffer. + /// + public override Coordinates CursorPosition + { + get; + set; + } + + /// + /// Gets or sets the size of the cursor in the console buffer. + /// + public override int CursorSize + { + get; + set; + } + + /// + /// Gets or sets the position of the console's window. + /// + public override Coordinates WindowPosition + { + get; + set; + } + + /// + /// Gets or sets the size of the console's window. + /// + public override Size WindowSize + { + get; + set; + } + + /// + /// Gets or sets the console window's title. + /// + public override string WindowTitle + { + get; + set; + } + + /// + /// Gets a boolean that determines whether a keypress is available. + /// + public override bool KeyAvailable + { + get { return false; } + } + + /// + /// Gets the maximum physical size of the console window. + /// + public override Size MaxPhysicalWindowSize + { + get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } + } + + /// + /// Gets the maximum size of the console window. + /// + public override Size MaxWindowSize + { + get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } + } + + /// + /// Reads the current key pressed in the console. + /// + /// Options for reading the current keypress. + /// A KeyInfo struct with details about the current keypress. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + Logger.LogWarning( + "PSHostRawUserInterface.ReadKey was called"); + + throw new System.NotImplementedException(); + } + + /// + /// Flushes the current input buffer. + /// + public override void FlushInputBuffer() + { + Logger.LogWarning( + "PSHostRawUserInterface.FlushInputBuffer was called"); + } + + /// + /// Gets the contents of the console buffer in a rectangular area. + /// + /// The rectangle inside which buffer contents will be accessed. + /// A BufferCell array with the requested buffer contents. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + return new BufferCell[0,0]; + } + + /// + /// Scrolls the contents of the console buffer. + /// + /// The source rectangle to scroll. + /// The destination coordinates by which to scroll. + /// The rectangle inside which the scrolling will be clipped. + /// The cell with which the buffer will be filled. + public override void ScrollBufferContents( + Rectangle source, + Coordinates destination, + Rectangle clip, + BufferCell fill) + { + Logger.LogWarning( + "PSHostRawUserInterface.ScrollBufferContents was called"); + } + + /// + /// Sets the contents of the buffer inside the specified rectangle. + /// + /// The rectangle inside which buffer contents will be filled. + /// The BufferCell which will be used to fill the requested space. + public override void SetBufferContents( + Rectangle rectangle, + BufferCell fill) + { + Logger.LogWarning( + "PSHostRawUserInterface.SetBufferContents was called"); + } + + /// + /// Sets the contents of the buffer at the given coordinate. + /// + /// The coordinate at which the buffer will be changed. + /// The new contents for the buffer at the given coordinate. + public override void SetBufferContents( + Coordinates origin, + BufferCell[,] contents) + { + Logger.LogWarning( + "PSHostRawUserInterface.SetBufferContents was called"); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs new file mode 100644 index 000000000..c9e05c7e3 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs @@ -0,0 +1,330 @@ +// +// 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 System; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides an implementation of the PSHostRawUserInterface class + /// for the ConsoleService and routes its calls to an IConsoleHost + /// implementation. + /// + internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface + { + #region Private Fields + + private readonly PSHostRawUserInterface internalRawUI; + private ILogger Logger; + private KeyInfo? lastKeyDown; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the TerminalPSHostRawUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The ILogger implementation to use for this instance. + /// The InternalHost instance from the origin runspace. + public TerminalPSHostRawUserInterface(ILogger logger, PSHost internalHost) + { + this.Logger = logger; + this.internalRawUI = internalHost.UI.RawUI; + } + + #endregion + + #region PSHostRawUserInterface Implementation + + /// + /// Gets or sets the background color of the console. + /// + public override ConsoleColor BackgroundColor + { + get { return System.Console.BackgroundColor; } + set { System.Console.BackgroundColor = value; } + } + + /// + /// Gets or sets the foreground color of the console. + /// + public override ConsoleColor ForegroundColor + { + get { return System.Console.ForegroundColor; } + set { System.Console.ForegroundColor = value; } + } + + /// + /// Gets or sets the size of the console buffer. + /// + public override Size BufferSize + { + get => this.internalRawUI.BufferSize; + set => this.internalRawUI.BufferSize = value; + } + + /// + /// Gets or sets the cursor's position in the console buffer. + /// + public override Coordinates CursorPosition + { + get + { + return new Coordinates( + ConsoleProxy.GetCursorLeft(), + ConsoleProxy.GetCursorTop()); + } + + set => this.internalRawUI.CursorPosition = value; + } + + /// + /// Gets or sets the size of the cursor in the console buffer. + /// + public override int CursorSize + { + get => this.internalRawUI.CursorSize; + set => this.internalRawUI.CursorSize = value; + } + + /// + /// Gets or sets the position of the console's window. + /// + public override Coordinates WindowPosition + { + get => this.internalRawUI.WindowPosition; + set => this.internalRawUI.WindowPosition = value; + } + + /// + /// Gets or sets the size of the console's window. + /// + public override Size WindowSize + { + get => this.internalRawUI.WindowSize; + set => this.internalRawUI.WindowSize = value; + } + + /// + /// Gets or sets the console window's title. + /// + public override string WindowTitle + { + get => this.internalRawUI.WindowTitle; + set => this.internalRawUI.WindowTitle = value; + } + + /// + /// Gets a boolean that determines whether a keypress is available. + /// + public override bool KeyAvailable => this.internalRawUI.KeyAvailable; + + /// + /// Gets the maximum physical size of the console window. + /// + public override Size MaxPhysicalWindowSize => this.internalRawUI.MaxPhysicalWindowSize; + + /// + /// Gets the maximum size of the console window. + /// + public override Size MaxWindowSize => this.internalRawUI.MaxWindowSize; + + /// + /// Reads the current key pressed in the console. + /// + /// Options for reading the current keypress. + /// A KeyInfo struct with details about the current keypress. + public override KeyInfo ReadKey(ReadKeyOptions options) + { + + bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; + + // Key Up was requested and we have a cached key down we can return. + if (includeUp && this.lastKeyDown != null) + { + KeyInfo info = this.lastKeyDown.Value; + this.lastKeyDown = null; + return new KeyInfo( + info.VirtualKeyCode, + info.Character, + info.ControlKeyState, + keyDown: false); + } + + bool intercept = (options & ReadKeyOptions.NoEcho) != 0; + bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0; + if (!(includeDown || includeUp)) + { + throw new PSArgumentException( + "Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.", + nameof(options)); + } + + // Allow ControlC as input so we can emulate pipeline stop requests. We can't actually + // determine if a stop is requested without using non-public API's. + bool oldValue = System.Console.TreatControlCAsInput; + try + { + System.Console.TreatControlCAsInput = true; + ConsoleKeyInfo key = ConsoleProxy.ReadKey(intercept, default(CancellationToken)); + + if (IsCtrlC(key)) + { + // Caller wants CtrlC as input so return it. + if ((options & ReadKeyOptions.AllowCtrlC) != 0) + { + return ProcessKey(key, includeDown); + } + + // Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate + // a real stop. This will not show an exception to a script based caller and it + // will avoid having to return something like default(KeyInfo). + throw new PipelineStoppedException(); + } + + return ProcessKey(key, includeDown); + } + finally + { + System.Console.TreatControlCAsInput = oldValue; + } + } + + /// + /// Flushes the current input buffer. + /// + public override void FlushInputBuffer() + { + Logger.LogWarning( + "PSHostRawUserInterface.FlushInputBuffer was called"); + } + + /// + /// Gets the contents of the console buffer in a rectangular area. + /// + /// The rectangle inside which buffer contents will be accessed. + /// A BufferCell array with the requested buffer contents. + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + return this.internalRawUI.GetBufferContents(rectangle); + } + + /// + /// Scrolls the contents of the console buffer. + /// + /// The source rectangle to scroll. + /// The destination coordinates by which to scroll. + /// The rectangle inside which the scrolling will be clipped. + /// The cell with which the buffer will be filled. + public override void ScrollBufferContents( + Rectangle source, + Coordinates destination, + Rectangle clip, + BufferCell fill) + { + this.internalRawUI.ScrollBufferContents(source, destination, clip, fill); + } + + /// + /// Sets the contents of the buffer inside the specified rectangle. + /// + /// The rectangle inside which buffer contents will be filled. + /// The BufferCell which will be used to fill the requested space. + public override void SetBufferContents( + Rectangle rectangle, + BufferCell fill) + { + // If the rectangle is all -1s then it means clear the visible buffer + if (rectangle.Top == -1 && + rectangle.Bottom == -1 && + rectangle.Left == -1 && + rectangle.Right == -1) + { + System.Console.Clear(); + return; + } + + this.internalRawUI.SetBufferContents(rectangle, fill); + } + + /// + /// Sets the contents of the buffer at the given coordinate. + /// + /// The coordinate at which the buffer will be changed. + /// The new contents for the buffer at the given coordinate. + public override void SetBufferContents( + Coordinates origin, + BufferCell[,] contents) + { + this.internalRawUI.SetBufferContents(origin, contents); + } + + #endregion + + /// + /// Determines if a key press represents the input Ctrl + C. + /// + /// The key to test. + /// + /// if the key represents the input Ctrl + C, + /// otherwise . + /// + private static bool IsCtrlC(ConsoleKeyInfo keyInfo) + { + // In the VSCode terminal Ctrl C is processed as virtual key code "3", which + // is not a named value in the ConsoleKey enum. + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0; + } + + /// + /// Converts objects to objects and caches + /// key down events for the next key up request. + /// + /// The key to convert. + /// + /// A value indicating whether the result should be a key down event. + /// + /// The converted value. + private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) + { + // Translate ConsoleModifiers to ControlKeyStates + ControlKeyStates states = default; + if ((key.Modifiers & ConsoleModifiers.Alt) != 0) + { + states |= ControlKeyStates.LeftAltPressed; + } + + if ((key.Modifiers & ConsoleModifiers.Control) != 0) + { + states |= ControlKeyStates.LeftCtrlPressed; + } + + if ((key.Modifiers & ConsoleModifiers.Shift) != 0) + { + states |= ControlKeyStates.ShiftPressed; + } + + var result = new KeyInfo((int)key.Key, key.KeyChar, states, isDown); + if (isDown) + { + this.lastKeyDown = result; + } + + return result; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs new file mode 100644 index 000000000..1ba2a7fc1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.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 Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices +{ + using System; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + + /// + /// Provides an EditorServicesPSHostUserInterface implementation + /// that integrates with the user's terminal UI. + /// + public class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface + { + #region Private Fields + + private readonly PSHostUserInterface internalHostUI; + private ConsoleReadLine consoleReadLine; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the ConsoleServicePSHostUserInterface + /// class with the given IConsoleHost implementation. + /// + /// The PowerShellContext to use for executing commands. + /// An ILogger implementation to use for this host. + /// The InternalHost instance from the origin runspace. + public TerminalPSHostUserInterface( + PowerShellContextService powerShellContext, + ILogger logger, + PSHost internalHost) + : base( + powerShellContext, + new TerminalPSHostRawUserInterface(logger, internalHost), + logger) + { + this.internalHostUI = internalHost.UI; + this.consoleReadLine = new ConsoleReadLine(powerShellContext); + + // Set the output encoding to UTF-8 so that special + // characters are written to the console correctly + System.Console.OutputEncoding = System.Text.Encoding.UTF8; + + System.Console.CancelKeyPress += + (obj, args) => + { + if (!this.IsNativeApplicationRunning) + { + // We'll handle Ctrl+C + args.Cancel = true; + this.SendControlC(); + } + }; + } + + #endregion + + /// + /// Gets a value indicating whether writing progress is supported. + /// + internal protected override bool SupportsWriteProgress => true; + + /// + /// Requests that the HostUI implementation read a command line + /// from the user to be executed in the integrated console command + /// loop. + /// + /// + /// A CancellationToken used to cancel the command line request. + /// + /// A Task that can be awaited for the resulting input string. + protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) + { + return this.consoleReadLine.ReadCommandLineAsync(cancellationToken); + } + + /// + /// Creates an InputPrompt handle to use for displaying input + /// prompts to the user. + /// + /// A new InputPromptHandler instance. + protected override InputPromptHandler OnCreateInputPromptHandler() + { + return new TerminalInputPromptHandler( + this.consoleReadLine, + this, + this.Logger); + } + + /// + /// Creates a ChoicePromptHandler to use for displaying a + /// choice prompt to the user. + /// + /// A new ChoicePromptHandler instance. + protected override ChoicePromptHandler OnCreateChoicePromptHandler() + { + return new TerminalChoicePromptHandler( + this.consoleReadLine, + this, + this.Logger); + } + + /// + /// 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) + { + ConsoleColor oldForegroundColor = System.Console.ForegroundColor; + ConsoleColor oldBackgroundColor = System.Console.BackgroundColor; + + System.Console.ForegroundColor = foregroundColor; + System.Console.BackgroundColor = ((int)backgroundColor != -1) ? backgroundColor : oldBackgroundColor; + + System.Console.Write(outputString + (includeNewLine ? Environment.NewLine : "")); + + System.Console.ForegroundColor = oldForegroundColor; + System.Console.BackgroundColor = oldBackgroundColor; + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + protected override void WriteProgressImpl(long sourceId, ProgressRecord record) + { + this.internalHostUI.WriteProgress(sourceId, record); + } + + /// + /// 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) + { + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs new file mode 100644 index 000000000..157715e7d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides methods for interacting with implementations of ReadLine. + /// + public interface IPromptContext + { + /// + /// Read a string that has been input by the user. + /// + /// Indicates if ReadLine should act like a command REPL. + /// + /// The cancellation token can be used to cancel reading user input. + /// + /// + /// A task object that represents the completion of reading input. The Result property will + /// return the input string. + /// + Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken); + + /// + /// Performs any additional actions required to cancel the current ReadLine invocation. + /// + void AbortReadLine(); + + /// + /// Creates a task that completes when the current ReadLine invocation has been aborted. + /// + /// + /// A task object that represents the abortion of the current ReadLine invocation. + /// + Task AbortReadLineAsync(); + + /// + /// Blocks until the current ReadLine invocation has exited. + /// + void WaitForReadLineExit(); + + /// + /// Creates a task that completes when the current ReadLine invocation has exited. + /// + /// + /// A task object that represents the exit of the current ReadLine invocation. + /// + Task WaitForReadLineExitAsync(); + + /// + /// Adds the specified command to the history managed by the ReadLine implementation. + /// + /// The command to record. + void AddToHistory(string command); + + /// + /// Forces the prompt handler to trigger PowerShell event handling, reliquishing control + /// of the pipeline thread during event processing. + /// + void ForcePSEventHandling(); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs new file mode 100644 index 000000000..38d14fb96 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs @@ -0,0 +1,12 @@ +// +// 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.Session +{ + internal interface IRunspaceCapability + { + // NOTE: This interface is intentionally empty for now. + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs new file mode 100644 index 000000000..c210ff47b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IVersionSpecificOperations + { + void ConfigureDebugger(Runspace runspace); + + void PauseDebugger(Runspace runspace); + + IEnumerable ExecuteCommandInDebugger( + PowerShellContextService powerShellContext, + Runspace currentRunspace, + PSCommand psCommand, + bool sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction); + + void StopCommandInDebugger(PowerShellContextService powerShellContext); + + bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace); + + void ExitNestedPrompt(PSHost host); + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs new file mode 100644 index 000000000..deeec8939 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs @@ -0,0 +1,263 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Provides the ability to take over the current pipeline in a runspace. + /// + internal class InvocationEventQueue + { + private const string ShouldProcessInExecutionThreadPropertyName = "ShouldProcessInExecutionThread"; + + private static readonly PropertyInfo s_shouldProcessInExecutionThreadProperty = + typeof(PSEventSubscriber) + .GetProperty( + ShouldProcessInExecutionThreadPropertyName, + BindingFlags.Instance | BindingFlags.NonPublic); + + private readonly PromptNest _promptNest; + + private readonly Runspace _runspace; + + private readonly PowerShellContextService _powerShellContext; + + private InvocationRequest _invocationRequest; + + private SemaphoreSlim _lock = AsyncUtils.CreateSimpleLockingSemaphore(); + + private InvocationEventQueue(PowerShellContextService powerShellContext, PromptNest promptNest) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _runspace = powerShellContext.CurrentRunspace.Runspace; + } + + internal static InvocationEventQueue Create(PowerShellContextService powerShellContext, PromptNest promptNest) + { + var eventQueue = new InvocationEventQueue(powerShellContext, promptNest); + eventQueue.CreateInvocationSubscriber(); + return eventQueue; + } + + /// + /// Executes a command on the main pipeline thread through + /// eventing. A event subscriber will + /// be created that creates a nested PowerShell instance for + /// to utilize. + /// + /// + /// Avoid using this method directly if possible. + /// will route commands + /// through this method if required. + /// + /// The expected result type. + /// The to be executed. + /// + /// Error messages from PowerShell will be written to the . + /// + /// Specifies options to be used when executing this command. + /// + /// An awaitable which will provide results once the command + /// execution completes. + /// + internal async Task> ExecuteCommandOnIdleAsync( + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + var request = new PipelineExecutionRequest( + _powerShellContext, + psCommand, + errorMessages, + executionOptions); + + await SetInvocationRequestAsync( + new InvocationRequest( + pwsh => request.ExecuteAsync().GetAwaiter().GetResult())); + + try + { + return await request.Results; + } + finally + { + await SetInvocationRequestAsync(request: null); + } + } + + /// + /// Marshals a to run on the pipeline thread. A new + /// will be created for the invocation. + /// + /// + /// The to invoke on the pipeline thread. The nested + /// instance for the created + /// will be passed as an argument. + /// + /// + /// An awaitable that the caller can use to know when execution completes. + /// + internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) + { + var request = new InvocationRequest(pwsh => + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false)) + { + pwsh.Runspace = _runspace; + invocationAction(pwsh); + } + }); + + await SetInvocationRequestAsync(request); + try + { + await request.Task; + } + finally + { + await SetInvocationRequestAsync(null); + } + } + + private async Task WaitForExistingRequestAsync() + { + InvocationRequest existingRequest; + await _lock.WaitAsync(); + try + { + existingRequest = _invocationRequest; + if (existingRequest == null || existingRequest.Task.IsCompleted) + { + return; + } + } + finally + { + _lock.Release(); + } + + await existingRequest.Task; + } + + private async Task SetInvocationRequestAsync(InvocationRequest request) + { + await WaitForExistingRequestAsync(); + await _lock.WaitAsync(); + try + { + _invocationRequest = request; + } + finally + { + _lock.Release(); + } + + _powerShellContext.ForcePSEventHandling(); + } + + private void OnPowerShellIdle(object sender, EventArgs e) + { + if (!_lock.Wait(0)) + { + return; + } + + InvocationRequest currentRequest = null; + try + { + if (_invocationRequest == null) + { + return; + } + + currentRequest = _invocationRequest; + } + finally + { + _lock.Release(); + } + + _promptNest.PushPromptContext(); + try + { + currentRequest.Invoke(_promptNest.GetPowerShell()); + } + finally + { + _promptNest.PopPromptContext(); + } + } + + private PSEventSubscriber CreateInvocationSubscriber() + { + PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent( + source: null, + eventName: PSEngineEvent.OnIdle, + sourceIdentifier: PSEngineEvent.OnIdle, + data: null, + handlerDelegate: OnPowerShellIdle, + supportEvent: true, + forwardEvent: false); + + SetSubscriberExecutionThreadWithReflection(subscriber); + + subscriber.Unsubscribed += OnInvokerUnsubscribed; + + return subscriber; + } + + private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e) + { + CreateInvocationSubscriber(); + } + + private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber) + { + // We need to create the PowerShell object in the same thread so we can get a nested + // PowerShell. This is the only way to consistently take control of the pipeline. The + // alternative is to make the subscriber a script block and have that create and process + // the PowerShell object, but that puts us in a different SessionState and is a lot slower. + s_shouldProcessInExecutionThreadProperty.SetValue(subscriber, true); + } + + private class InvocationRequest : TaskCompletionSource + { + private readonly Action _invocationAction; + + internal InvocationRequest(Action invocationAction) + { + _invocationAction = invocationAction; + } + + internal void Invoke(PowerShell pwsh) + { + try + { + _invocationAction(pwsh); + + // Ensure the result is set in another thread otherwise the caller + // may take over the pipeline thread. + System.Threading.Tasks.Task.Run(() => SetResult(true)); + } + catch (Exception e) + { + System.Threading.Tasks.Task.Run(() => SetException(e)); + } + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs new file mode 100644 index 000000000..281b410c2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class LegacyReadLineContext : IPromptContext + { + private readonly ConsoleReadLine _legacyReadLine; + + internal LegacyReadLineContext(PowerShellContextService powerShellContext) + { + _legacyReadLine = new ConsoleReadLine(powerShellContext); + } + + public Task AbortReadLineAsync() + { + return Task.FromResult(true); + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + return await _legacyReadLine.InvokeLegacyReadLineAsync(isCommandLine, cancellationToken); + } + + public Task WaitForReadLineExitAsync() + { + return Task.FromResult(true); + } + + public void AddToHistory(string command) + { + // Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine. + } + + public void AbortReadLine() + { + // Do nothing, no additional actions are needed to cancel ReadLine. + } + + public void WaitForReadLineExit() + { + // Do nothing, ReadLine cancellation is instant or not appliciable. + } + + public void ForcePSEventHandling() + { + // Do nothing, the pipeline thread is not occupied by legacy ReadLine. + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs new file mode 100644 index 000000000..ad67f6891 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs @@ -0,0 +1,41 @@ +// +// 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 +{ + /// + /// Enumerates the types of output lines that will be sent + /// to an IConsoleHost implementation. + /// + public enum OutputType + { + /// + /// A normal output line, usually written with the or Write-Host or + /// Write-Output cmdlets. + /// + Normal, + + /// + /// A debug output line, written with the Write-Debug cmdlet. + /// + Debug, + + /// + /// A verbose output line, written with the Write-Verbose cmdlet. + /// + Verbose, + + /// + /// A warning output line, written with the Write-Warning cmdlet. + /// + Warning, + + /// + /// An error output line, written with the Write-Error cmdlet or + /// as a result of some error during PowerShell pipeline execution. + /// + Error + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs new file mode 100644 index 000000000..0e4663094 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about output that has been written to the + /// PowerShell host. + /// + public class OutputWrittenEventArgs + { + /// + /// Gets the text of the output. + /// + public string OutputText { get; private set; } + + /// + /// Gets the type of the output. + /// + public OutputType OutputType { get; private set; } + + /// + /// Gets a boolean which indicates whether a newline + /// should be written after the output. + /// + public bool IncludeNewLine { get; private set; } + + /// + /// Gets the foreground color of the output text. + /// + public ConsoleColor ForegroundColor { get; private set; } + + /// + /// Gets the background color of the output text. + /// + public ConsoleColor BackgroundColor { get; private set; } + + /// + /// Creates an instance of the OutputWrittenEventArgs class. + /// + /// The text of the output. + /// A boolean which indicates whether a newline should be written after the output. + /// The type of the output. + /// The foreground color of the output text. + /// The background color of the output text. + public OutputWrittenEventArgs( + string outputText, + bool includeNewLine, + OutputType outputType, + ConsoleColor foregroundColor, + ConsoleColor backgroundColor) + { + this.OutputText = outputText; + this.IncludeNewLine = includeNewLine; + this.OutputType = outputType; + this.ForegroundColor = foregroundColor; + this.BackgroundColor = backgroundColor; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs new file mode 100644 index 000000000..9058579e8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -0,0 +1,203 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.EditorServices.Console; + +namespace Microsoft.PowerShell.EditorServices.Session { + using System.Management.Automation; + using Microsoft.Extensions.Logging; + + internal class PSReadLinePromptContext : IPromptContext { + private const string ReadLineScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null]::ReadLine( + $Host.Runspace, + $ExecutionContext, + $args[0])"; + + private const string ReadLineInitScript = @" + [System.Diagnostics.DebuggerHidden()] + [System.Diagnostics.DebuggerStepThrough()] + param() + end { + $module = Get-Module -ListAvailable PSReadLine | + Where-Object Version -eq '2.0.0' | + Where-Object { $_.PrivateData.PSData.Prerelease -notin 'beta1','beta2','beta3' } | + Sort-Object -Descending Version | + Select-Object -First 1 + if (-not $module) { + return + } + + Import-Module -ModuleInfo $module + return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null] + }"; + + private readonly PowerShellContextService _powerShellContext; + + private PromptNest _promptNest; + + private InvocationEventQueue _invocationEventQueue; + + private ConsoleReadLine _consoleReadLine; + + private CancellationTokenSource _readLineCancellationSource; + + private PSReadLineProxy _readLineProxy; + + internal PSReadLinePromptContext( + PowerShellContextService powerShellContext, + PromptNest promptNest, + InvocationEventQueue invocationEventQueue, + PSReadLineProxy readLineProxy) + { + _promptNest = promptNest; + _powerShellContext = powerShellContext; + _invocationEventQueue = invocationEventQueue; + _consoleReadLine = new ConsoleReadLine(powerShellContext); + _readLineProxy = readLineProxy; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + _readLineProxy.OverrideReadKey( + intercept => ConsoleProxy.UnixReadKey( + intercept, + _readLineCancellationSource.Token)); + } + + internal static bool TryGetPSReadLineProxy( + ILogger logger, + Runspace runspace, + out PSReadLineProxy readLineProxy) + { + readLineProxy = null; + using (var pwsh = PowerShell.Create()) + { + pwsh.Runspace = runspace; + var psReadLineType = pwsh + .AddScript(ReadLineInitScript) + .Invoke() + .FirstOrDefault(); + + if (psReadLineType == null) + { + return false; + } + + try + { + readLineProxy = new PSReadLineProxy(psReadLineType, logger); + } + catch (InvalidOperationException) + { + // The Type we got back from PowerShell doesn't have the members we expected. + // Could be an older version, a custom build, or something a newer version with + // breaking changes. + return false; + } + } + + return true; + } + + public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) + { + _readLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var localTokenSource = _readLineCancellationSource; + if (localTokenSource.Token.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + try + { + if (!isCommandLine) + { + return await _consoleReadLine.InvokeLegacyReadLineAsync( + false, + _readLineCancellationSource.Token); + } + + var result = (await _powerShellContext.ExecuteCommandAsync( + new PSCommand() + .AddScript(ReadLineScript) + .AddArgument(_readLineCancellationSource.Token), + null, + new ExecutionOptions() + { + WriteErrorsToHost = false, + WriteOutputToHost = false, + InterruptCommandPrompt = false, + AddToHistory = false, + IsReadLine = isCommandLine + })) + .FirstOrDefault(); + + return cancellationToken.IsCancellationRequested + ? string.Empty + : result; + } + finally + { + _readLineCancellationSource = null; + } + } + + public void AbortReadLine() + { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + WaitForReadLineExit(); + } + + public async Task AbortReadLineAsync() { + if (_readLineCancellationSource == null) + { + return; + } + + _readLineCancellationSource.Cancel(); + + await WaitForReadLineExitAsync(); + } + + public void WaitForReadLineExit() + { + using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: true)) + { } + } + + public async Task WaitForReadLineExitAsync() { + using (await _promptNest.GetRunspaceHandleAsync(CancellationToken.None, isReadLine: true)) + { } + } + + public void AddToHistory(string command) + { + _readLineProxy.AddToHistory(command); + } + + public void ForcePSEventHandling() + { + _readLineProxy.ForcePSEventHandling(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs new file mode 100644 index 000000000..494a3f9f7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs @@ -0,0 +1,118 @@ +// +// 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.Reflection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class PSReadLineProxy + { + private const string FieldMemberType = "field"; + + private const string MethodMemberType = "method"; + + private const string AddToHistoryMethodName = "AddToHistory"; + + private const string SetKeyHandlerMethodName = "SetKeyHandler"; + + private const string ReadKeyOverrideFieldName = "_readKeyOverride"; + + private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal"; + + private const string ForcePSEventHandlingMethodName = "ForcePSEventHandling"; + + private static readonly Type[] s_setKeyHandlerTypes = + { + typeof(string[]), + typeof(Action), + typeof(string), + typeof(string) + }; + + private static readonly Type[] s_addToHistoryTypes = { typeof(string) }; + + private readonly FieldInfo _readKeyOverrideField; + + internal PSReadLineProxy(Type psConsoleReadLine, ILogger logger) + { + ForcePSEventHandling = + (Action)psConsoleReadLine.GetMethod( + ForcePSEventHandlingMethodName, + BindingFlags.Static | BindingFlags.NonPublic) + ?.CreateDelegate(typeof(Action)); + + AddToHistory = (Action)psConsoleReadLine.GetMethod( + AddToHistoryMethodName, + s_addToHistoryTypes) + ?.CreateDelegate(typeof(Action)); + + SetKeyHandler = + (Action, string, string>)psConsoleReadLine.GetMethod( + SetKeyHandlerMethodName, + s_setKeyHandlerTypes) + ?.CreateDelegate(typeof(Action, string, string>)); + + _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly + .GetType(VirtualTerminalTypeName) + ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic); + + if (_readKeyOverrideField == null) + { + throw NewInvalidPSReadLineVersionException( + FieldMemberType, + ReadKeyOverrideFieldName, + logger); + } + + if (SetKeyHandler == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + SetKeyHandlerMethodName, + logger); + } + + if (AddToHistory == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + AddToHistoryMethodName, + logger); + } + + if (ForcePSEventHandling == null) + { + throw NewInvalidPSReadLineVersionException( + MethodMemberType, + ForcePSEventHandlingMethodName, + logger); + } + } + + internal Action AddToHistory { get; } + + internal Action, object>, string, string> SetKeyHandler { get; } + + internal Action ForcePSEventHandling { get; } + + internal void OverrideReadKey(Func readKeyFunc) + { + _readKeyOverrideField.SetValue(null, readKeyFunc); + } + + private static InvalidOperationException NewInvalidPSReadLineVersionException( + string memberType, + string memberName, + ILogger logger) + { + logger.LogError( + $"The loaded version of PSReadLine is not supported. The {memberType} \"{memberName}\" was not found."); + + return new InvalidOperationException(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs new file mode 100644 index 000000000..f2d61192c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal interface IPipelineExecutionRequest + { + Task ExecuteAsync(); + + Task WaitTask { get; } + } + + /// + /// Contains details relating to a request to execute a + /// command on the PowerShell pipeline thread. + /// + /// The expected result type of the execution. + internal class PipelineExecutionRequest : IPipelineExecutionRequest + { + private PowerShellContextService _powerShellContext; + private PSCommand _psCommand; + private StringBuilder _errorMessages; + private ExecutionOptions _executionOptions; + private TaskCompletionSource> _resultsTask; + + public Task> Results + { + get { return this._resultsTask.Task; } + } + + public Task WaitTask { get { return Results; } } + + public PipelineExecutionRequest( + PowerShellContextService powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + bool sendOutputToHost) + : this( + powerShellContext, + psCommand, + errorMessages, + new ExecutionOptions() + { + WriteOutputToHost = sendOutputToHost + }) + { } + + + public PipelineExecutionRequest( + PowerShellContextService powerShellContext, + PSCommand psCommand, + StringBuilder errorMessages, + ExecutionOptions executionOptions) + { + _powerShellContext = powerShellContext; + _psCommand = psCommand; + _errorMessages = errorMessages; + _executionOptions = executionOptions; + _resultsTask = new TaskCompletionSource>(); + } + + public async Task ExecuteAsync() + { + var results = + await _powerShellContext.ExecuteCommandAsync( + _psCommand, + _errorMessages, + _executionOptions); + + var unusedTask = Task.Run(() => _resultsTask.SetResult(results)); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs new file mode 100644 index 000000000..a001d84bf --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs @@ -0,0 +1,106 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + internal class PowerShell5Operations : IVersionSpecificOperations + { + public void ConfigureDebugger(Runspace runspace) + { + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + } + } + + public virtual void PauseDebugger(Runspace runspace) + { + if (runspace.Debugger != null) + { + runspace.Debugger.SetDebuggerStepMode(true); + } + } + + public virtual bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) + { + return runspace.Debugger.InBreakpoint || (promptNest.IsRemote && promptNest.IsInDebugger); + } + + public IEnumerable ExecuteCommandInDebugger( + PowerShellContextService powerShellContext, + Runspace currentRunspace, + PSCommand psCommand, + bool sendOutputToHost, + out DebuggerResumeAction? debuggerResumeAction) + { + debuggerResumeAction = null; + PSDataCollection outputCollection = new PSDataCollection(); + + if (sendOutputToHost) + { + outputCollection.DataAdded += + (obj, e) => + { + for (int i = e.Index; i < outputCollection.Count; i++) + { + powerShellContext.WriteOutput( + outputCollection[i].ToString(), + true); + } + }; + } + + DebuggerCommandResults commandResults = + currentRunspace.Debugger.ProcessCommand( + psCommand, + outputCollection); + + // Pass along the debugger's resume action if the user's + // command caused one to be returned + debuggerResumeAction = commandResults.ResumeAction; + + IEnumerable results = null; + if (typeof(TResult) != typeof(PSObject)) + { + results = + outputCollection + .Select(pso => pso.BaseObject) + .Cast(); + } + else + { + results = outputCollection.Cast(); + } + + return results; + } + + public void StopCommandInDebugger(PowerShellContextService powerShellContext) + { + // If the RunspaceAvailability is None, the runspace is dead and we should not try to run anything in it. + if (powerShellContext.CurrentRunspace.Runspace.RunspaceAvailability != RunspaceAvailability.None) + { + powerShellContext.CurrentRunspace.Runspace.Debugger.StopProcessCommand(); + } + } + + public void ExitNestedPrompt(PSHost host) + { + try + { + host.ExitNestedPrompt(); + } + catch (FlowControlException) + { + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs new file mode 100644 index 000000000..6ebdbc947 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs @@ -0,0 +1,46 @@ +// +// 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 +{ + /// + /// Enumerates the possible states for a PowerShellContext. + /// + public enum PowerShellContextState + { + /// + /// Indicates an unknown, potentially uninitialized state. + /// + Unknown = 0, + + /// + /// Indicates the state where the session is starting but + /// not yet fully initialized. + /// + NotStarted, + + /// + /// Indicates that the session is ready to accept commands + /// for execution. + /// + Ready, + + /// + /// Indicates that the session is currently running a command. + /// + Running, + + /// + /// Indicates that the session is aborting the current execution. + /// + Aborting, + + /// + /// Indicates that the session is already disposed and cannot + /// accept further execution requests. + /// + Disposed + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs new file mode 100644 index 000000000..3c941b5aa --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs @@ -0,0 +1,39 @@ +// +// 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 +{ + /// + /// Enumerates the possible execution results that can occur after + /// executing a command or script. + /// + public enum PowerShellExecutionResult + { + /// + /// Indicates that execution is not yet finished. + /// + NotFinished, + + /// + /// Indicates that execution has failed. + /// + Failed, + + /// + /// Indicates that execution was aborted by the user. + /// + Aborted, + + /// + /// Indicates that execution was stopped by the debugger. + /// + Stopped, + + /// + /// Indicates that execution completed successfully. + /// + Completed + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs new file mode 100644 index 000000000..d0b8e56e2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs @@ -0,0 +1,166 @@ +// +// 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 System; +using System.Collections; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Defines the possible enumeration values for the PowerShell process architecture. + /// + public enum PowerShellProcessArchitecture + { + /// + /// The processor architecture is unknown or wasn't accessible. + /// + Unknown, + + /// + /// The processor architecture is 32-bit. + /// + X86, + + /// + /// The processor architecture is 64-bit. + /// + X64 + } + + /// + /// Provides details about the version of the PowerShell runtime. + /// + public class PowerShellVersionDetails + { + #region Properties + + /// + /// Gets the version of the PowerShell runtime. + /// + public Version Version { get; private set; } + + /// + /// Gets the full version string, either the ToString of the Version + /// property or the GitCommitId for open-source PowerShell releases. + /// + public string VersionString { get; private set; } + + /// + /// Gets the PowerShell edition (generally Desktop or Core). + /// + public string Edition { get; private set; } + + /// + /// Gets the architecture of the PowerShell process. + /// + public PowerShellProcessArchitecture Architecture { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the PowerShellVersionDetails class. + /// + /// The version of the PowerShell runtime. + /// A string representation of the PowerShell version. + /// The string representation of the PowerShell edition. + /// The processor architecture. + public PowerShellVersionDetails( + Version version, + string versionString, + string editionString, + PowerShellProcessArchitecture architecture) + { + this.Version = version; + this.VersionString = versionString; + this.Edition = editionString; + this.Architecture = architecture; + } + + #endregion + + #region Public Methods + + /// + /// Gets the PowerShell version details for the given runspace. + /// + /// The runspace for which version details will be gathered. + /// An ILogger implementation used for writing log messages. + /// A new PowerShellVersionDetails instance. + public static PowerShellVersionDetails GetVersionDetails(Runspace runspace, ILogger logger) + { + Version powerShellVersion = new Version(5, 0); + string versionString = null; + string powerShellEdition = "Desktop"; + var architecture = PowerShellProcessArchitecture.Unknown; + + try + { + var psVersionTable = PowerShellContextService.ExecuteScriptAndGetItem("$PSVersionTable", runspace); + if (psVersionTable != null) + { + var edition = psVersionTable["PSEdition"] as string; + if (edition != null) + { + powerShellEdition = edition; + } + + // The PSVersion value will either be of Version or SemanticVersion. + // In the former case, take the value directly. In the latter case, + // generate a Version from its string representation. + var version = psVersionTable["PSVersion"]; + if (version is Version) + { + powerShellVersion = (Version)version; + } + else if (version != null) + { + // Expected version string format is 6.0.0-alpha so build a simpler version from that + powerShellVersion = new Version(version.ToString().Split('-')[0]); + } + + var gitCommitId = psVersionTable["GitCommitId"] as string; + if (gitCommitId != null) + { + versionString = gitCommitId; + } + else + { + versionString = powerShellVersion.ToString(); + } + + var arch = PowerShellContextService.ExecuteScriptAndGetItem("$env:PROCESSOR_ARCHITECTURE", runspace); + if (arch != null) + { + if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X64; + } + else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) + { + architecture = PowerShellProcessArchitecture.X86; + } + } + } + } + catch (Exception ex) + { + logger.LogWarning( + "Failed to look up PowerShell version, defaulting to version 5.\r\n\r\n" + ex.ToString()); + } + + return new PowerShellVersionDetails( + powerShellVersion, + versionString, + powerShellEdition, + architecture); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs new file mode 100644 index 000000000..d2ec4b1bd --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about the progress of a particular activity. + /// + public class ProgressDetails + { + /// + /// Gets the percentage of the activity that has been completed. + /// + public int PercentComplete { get; private set; } + + internal static ProgressDetails Create(ProgressRecord progressRecord) + { + //progressRecord.RecordType == ProgressRecordType.Completed; + //progressRecord.Activity; + //progressRecord. + + return new ProgressDetails + { + PercentComplete = progressRecord.PercentComplete + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs new file mode 100644 index 000000000..a39f25f34 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs @@ -0,0 +1,564 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System; + using System.Management.Automation; + + /// + /// Represents the stack of contexts in which PowerShell commands can be invoked. + /// + internal class PromptNest : IDisposable + { + private ConcurrentStack _frameStack; + + private PromptNestFrame _readLineFrame; + + private IHostInput _consoleReader; + + private PowerShellContextService _powerShellContext; + + private IVersionSpecificOperations _versionSpecificOperations; + + private bool _isDisposed; + + private object _syncObject = new object(); + + private object _disposeSyncObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The to track prompt status for. + /// + /// + /// The instance for the first frame. + /// + /// + /// The input handler. + /// + /// + /// The for the calling + /// instance. + /// + /// + /// This constructor should only be called when + /// is set to the initial runspace. + /// + internal PromptNest( + PowerShellContextService powerShellContext, + PowerShell initialPowerShell, + IHostInput consoleReader, + IVersionSpecificOperations versionSpecificOperations) + { + _versionSpecificOperations = versionSpecificOperations; + _consoleReader = consoleReader; + _powerShellContext = powerShellContext; + _frameStack = new ConcurrentStack(); + _frameStack.Push( + new PromptNestFrame( + initialPowerShell, + NewHandleQueue())); + + var readLineShell = PowerShell.Create(); + readLineShell.Runspace = powerShellContext.CurrentRunspace.Runspace; + _readLineFrame = new PromptNestFrame( + readLineShell, + new AsyncQueue()); + + ReleaseRunspaceHandleImpl(isReadLine: true); + } + + /// + /// Gets a value indicating whether the current frame was created by a debugger stop event. + /// + internal bool IsInDebugger => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Debug); + + /// + /// Gets a value indicating whether the current frame was created for an out of process runspace. + /// + internal bool IsRemote => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Remote); + + /// + /// Gets a value indicating whether the current frame was created by PSHost.EnterNestedPrompt(). + /// + internal bool IsNestedPrompt => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt); + + /// + /// Gets a value indicating the current number of frames managed by this PromptNest. + /// + internal int NestedPromptLevel => _frameStack.Count; + + private PromptNestFrame CurrentFrame + { + get + { + _frameStack.TryPeek(out PromptNestFrame currentFrame); + return _isDisposed ? _readLineFrame : currentFrame; + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + lock (_disposeSyncObject) + { + if (_isDisposed || !disposing) + { + return; + } + + while (NestedPromptLevel > 1) + { + _consoleReader?.StopCommandLoop(); + var currentFrame = CurrentFrame; + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.Debug)) + { + _versionSpecificOperations.StopCommandInDebugger(_powerShellContext); + currentFrame.ThreadController.StartThreadExit(DebuggerResumeAction.Stop); + currentFrame.WaitForFrameExit(CancellationToken.None); + continue; + } + + if (currentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt)) + { + _powerShellContext.ExitAllNestedPrompts(); + continue; + } + + currentFrame.PowerShell.BeginStop(null, null); + currentFrame.WaitForFrameExit(CancellationToken.None); + } + + _consoleReader?.StopCommandLoop(); + _readLineFrame.Dispose(); + CurrentFrame.Dispose(); + _frameStack.Clear(); + _powerShellContext = null; + _consoleReader = null; + _isDisposed = true; + } + } + + /// + /// Gets the for the current frame. + /// + /// + /// The for the current frame, or + /// if the current frame does not have one. + /// + internal ThreadController GetThreadController() + { + if (_isDisposed) + { + return null; + } + + return CurrentFrame.IsThreadController ? CurrentFrame.ThreadController : null; + } + + /// + /// Create a new and set it as the current frame. + /// + internal void PushPromptContext() + { + if (_isDisposed) + { + return; + } + + PushPromptContext(PromptNestFrameType.Normal); + } + + /// + /// Create a new and set it as the current frame. + /// + /// The frame type. + internal void PushPromptContext(PromptNestFrameType frameType) + { + if (_isDisposed) + { + return; + } + + _frameStack.Push( + new PromptNestFrame( + frameType.HasFlag(PromptNestFrameType.Remote) + ? PowerShell.Create() + : PowerShell.Create(RunspaceMode.CurrentRunspace), + NewHandleQueue(), + frameType)); + } + + /// + /// Dispose of the current and revert to the previous frame. + /// + internal void PopPromptContext() + { + PromptNestFrame currentFrame; + lock (_syncObject) + { + if (_isDisposed || _frameStack.Count == 1) + { + return; + } + + _frameStack.TryPop(out currentFrame); + } + + currentFrame.Dispose(); + } + + /// + /// Get the instance for the current + /// . + /// + /// Indicates whether this is for a PSReadLine command. + /// The instance for the current frame. + internal PowerShell GetPowerShell(bool isReadLine = false) + { + if (_isDisposed) + { + return null; + } + + // Typically we want to run PSReadLine on the current nest frame. + // The exception is when the current frame is remote, in which + // case we need to run it in it's own frame because we can't take + // over a remote pipeline through event invocation. + if (NestedPromptLevel > 1 && !IsRemote) + { + return CurrentFrame.PowerShell; + } + + return isReadLine ? _readLineFrame.PowerShell : CurrentFrame.PowerShell; + } + + /// + /// Get the for the current . + /// + /// + /// The that can be used to cancel the request. + /// + /// Indicates whether this is for a PSReadLine command. + /// The for the current frame. + internal RunspaceHandle GetRunspaceHandle(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + GetRunspaceHandleImpl(cancellationToken, isReadLine: false); + } + + return GetRunspaceHandleImpl(cancellationToken, isReadLine); + } + + + /// + /// Get the for the current . + /// + /// + /// The that will be checked prior to + /// completing the returned task. + /// + /// Indicates whether this is for a PSReadLine command. + /// + /// A object representing the asynchronous operation. + /// The property will return the + /// for the current frame. + /// + internal async Task GetRunspaceHandleAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (_isDisposed) + { + return null; + } + + // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace + // is in process. + if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); + } + + return await GetRunspaceHandleImplAsync(cancellationToken, isReadLine); + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + ReleaseRunspaceHandleImpl(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + ReleaseRunspaceHandleImpl(isReadLine: false); + } + } + + /// + /// Releases control of the runspace aquired via the . + /// + /// + /// The representing the control to release. + /// + /// + /// A object representing the release of the + /// . + /// + internal async Task ReleaseRunspaceHandleAsync(RunspaceHandle runspaceHandle) + { + if (_isDisposed) + { + return; + } + + await ReleaseRunspaceHandleImplAsync(runspaceHandle.IsReadLine); + if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + await ReleaseRunspaceHandleImplAsync(isReadLine: false); + } + } + + /// + /// Determines if the current frame is unavailable for commands. + /// + /// + /// A value indicating whether the current frame is unavailable for commands. + /// + internal bool IsMainThreadBusy() + { + return !_isDisposed && CurrentFrame.Queue.IsEmpty; + } + + /// + /// Determines if a PSReadLine command is currently running. + /// + /// + /// A value indicating whether a PSReadLine command is currently running. + /// + internal bool IsReadLineBusy() + { + return !_isDisposed && _readLineFrame.Queue.IsEmpty; + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + internal void WaitForCurrentFrameExit(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + currentFrame.WaitForFrameExit(CancellationToken.None); + } + } + + /// + /// Blocks until the current frame has been disposed. + /// + internal void WaitForCurrentFrameExit() + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Blocks until the current frame has been disposed. + /// + /// + /// The used the exit the block prior to + /// the current frame being disposed. + /// + internal void WaitForCurrentFrameExit(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + CurrentFrame.WaitForFrameExit(cancellationToken); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Func initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + await initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A delegate that when invoked initates the exit of the current frame. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(Action initiator) + { + if (_isDisposed) + { + return; + } + + var currentFrame = CurrentFrame; + try + { + initiator.Invoke(currentFrame); + } + finally + { + await currentFrame.WaitForFrameExitAsync(CancellationToken.None); + } + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync() + { + if (_isDisposed) + { + return; + } + + await WaitForCurrentFrameExitAsync(CancellationToken.None); + } + + /// + /// Creates a task that is completed when the current frame has been disposed. + /// + /// + /// The used the exit the block prior to the current frame being disposed. + /// + /// + /// A object representing the current frame being disposed. + /// + internal async Task WaitForCurrentFrameExitAsync(CancellationToken cancellationToken) + { + if (_isDisposed) + { + return; + } + + await CurrentFrame.WaitForFrameExitAsync(cancellationToken); + } + + private AsyncQueue NewHandleQueue() + { + var queue = new AsyncQueue(); + queue.Enqueue(new RunspaceHandle(_powerShellContext)); + return queue; + } + + private RunspaceHandle GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return _readLineFrame.Queue.Dequeue(cancellationToken); + } + + return CurrentFrame.Queue.Dequeue(cancellationToken); + } + + private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) + { + if (isReadLine) + { + return await _readLineFrame.Queue.DequeueAsync(cancellationToken); + } + + return await CurrentFrame.Queue.DequeueAsync(cancellationToken); + } + + private void ReleaseRunspaceHandleImpl(bool isReadLine) + { + if (isReadLine) + { + _readLineFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, true)); + return; + } + + CurrentFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, false)); + } + + private async Task ReleaseRunspaceHandleImplAsync(bool isReadLine) + { + if (isReadLine) + { + await _readLineFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, true)); + return; + } + + await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false)); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs new file mode 100644 index 000000000..cae7dfb8a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs @@ -0,0 +1,137 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + using System.Management.Automation; + + /// + /// Represents a single frame in the . + /// + internal class PromptNestFrame : IDisposable + { + private const PSInvocationState IndisposableStates = PSInvocationState.Stopping | PSInvocationState.Running; + + private SemaphoreSlim _frameExited = new SemaphoreSlim(initialCount: 0); + + private bool _isDisposed = false; + + /// + /// Gets the instance. + /// + internal PowerShell PowerShell { get; } + + /// + /// Gets the queue that controls command invocation order. + /// + internal AsyncQueue Queue { get; } + + /// + /// Gets the frame type. + /// + internal PromptNestFrameType FrameType { get; } + + /// + /// Gets the . + /// + internal ThreadController ThreadController { get; } + + /// + /// Gets a value indicating whether the frame requires command invocations + /// to be routed to a specific thread. + /// + internal bool IsThreadController { get; } + + internal PromptNestFrame(PowerShell powerShell, AsyncQueue handleQueue) + : this(powerShell, handleQueue, PromptNestFrameType.Normal) + { } + + internal PromptNestFrame( + PowerShell powerShell, + AsyncQueue handleQueue, + PromptNestFrameType frameType) + { + PowerShell = powerShell; + Queue = handleQueue; + FrameType = frameType; + IsThreadController = (frameType & (PromptNestFrameType.Debug | PromptNestFrameType.NestedPrompt)) != 0; + if (!IsThreadController) + { + return; + } + + ThreadController = new ThreadController(this); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + if (IndisposableStates.HasFlag(PowerShell.InvocationStateInfo.State)) + { + PowerShell.BeginStop( + asyncResult => + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + }, + state: null); + } + else + { + PowerShell.Runspace = null; + PowerShell.Dispose(); + } + + _frameExited.Release(); + } + + _isDisposed = true; + } + + /// + /// Blocks until the frame has been disposed. + /// + /// + /// The that will exit the block when cancelled. + /// + internal void WaitForFrameExit(CancellationToken cancellationToken) + { + _frameExited.Wait(cancellationToken); + _frameExited.Release(); + } + + /// + /// Creates a task object that is completed when the frame has been disposed. + /// + /// + /// The that will be checked prior to completing + /// the returned task. + /// + /// + /// A object that represents this frame being disposed. + /// + internal async Task WaitForFrameExitAsync(CancellationToken cancellationToken) + { + await _frameExited.WaitAsync(cancellationToken); + _frameExited.Release(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs new file mode 100644 index 000000000..b42b42098 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + [Flags] + internal enum PromptNestFrameType + { + Normal = 0, + + NestedPrompt = 1, + + Debug = 2, + + Remote = 4 + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs new file mode 100644 index 000000000..0b24dfa48 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs @@ -0,0 +1,785 @@ +// +// 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.Extensions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Manages files that are accessed from a remote PowerShell session. + /// Also manages the registration and handling of the 'psedit' function. + /// + public class RemoteFileManager + { + #region Fields + + private ILogger logger; + private string remoteFilesPath; + private string processTempPath; + private PowerShellContextService powerShellContext; + private IEditorOperations editorOperations; + + private Dictionary filesPerComputer = + new Dictionary(); + + private const string RemoteSessionOpenFile = "PSESRemoteSessionOpenFile"; + + private const string PSEditModule = @"<# + .SYNOPSIS + Opens the specified files in your editor window + .DESCRIPTION + Opens the specified files in your editor window + .EXAMPLE + PS > Open-EditorFile './foo.ps1' + Opens foo.ps1 in your editor + .EXAMPLE + PS > gci ./myDir | Open-EditorFile + Opens everything in 'myDir' in your editor + .INPUTS + Path + an array of files you want to open in your editor + #> + function Open-EditorFile { + param ( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [String[]] + $Path + ) + + begin { + $Paths = @() + } + + process { + $Paths += $Path + } + + end { + if ($Paths.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + foreach ($fileName in $Paths) + { + Microsoft.PowerShell.Management\Get-ChildItem $fileName | Where-Object { ! $_.PSIsContainer } | Foreach-Object { + $filePathName = $_.FullName + + # Get file contents + $params = @{ Path=$filePathName; Raw=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + $contentBytes = Microsoft.PowerShell.Management\Get-Content @params + + # Notify client for file open. + Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes, $preview) > $null + } + } + } + } + + <# + .SYNOPSIS + Creates new files and opens them in your editor window + .DESCRIPTION + Creates new files and opens them in your editor window + .EXAMPLE + PS > New-EditorFile './foo.ps1' + Creates and opens a new foo.ps1 in your editor + .EXAMPLE + PS > Get-Process | New-EditorFile proc.txt + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process + .EXAMPLE + PS > Get-Process | New-EditorFile proc.txt -Force + Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process. Overwrites the file if it already exists + .INPUTS + Path + an array of files you want to open in your editor + Value + The content you want in the new files + Force + Overwrites a file if it exists + #> + function New-EditorFile { + [CmdletBinding()] + param ( + [Parameter()] + [String[]] + [ValidateNotNullOrEmpty()] + $Path, + + [Parameter(ValueFromPipeline=$true)] + $Value, + + [Parameter()] + [switch] + $Force + ) + + begin { + $valueList = @() + } + + process { + $valueList += $Value + } + + end { + if ($Path) { + foreach ($fileName in $Path) + { + if (-not (Microsoft.PowerShell.Management\Test-Path $fileName) -or $Force) { + $valueList > $fileName + + # Get file contents + $params = @{ Path=$fileName; Raw=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + $contentBytes = Microsoft.PowerShell.Management\Get-Content @params + + if ($Path.Count -gt 1) { + $preview = $false + } else { + $preview = $true + } + + # Notify client for file open. + Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($fileName, $contentBytes, $preview) > $null + } else { + $PSCmdlet.WriteError( ( + Microsoft.PowerShell.Utility\New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList @( + [System.Exception]'File already exists.' + $Null + [System.Management.Automation.ErrorCategory]::ResourceExists + $fileName ) ) ) + } + } + } else { + $bytes = [System.Text.Encoding]::UTF8.GetBytes(($valueList | Microsoft.PowerShell.Utility\Out-String)) + Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($null, $bytes) > $null + } + } + } + + Microsoft.PowerShell.Utility\Set-Alias psedit Open-EditorFile -Scope Global + Microsoft.PowerShell.Core\Export-ModuleMember -Function Open-EditorFile, New-EditorFile + "; + + // This script is templated so that the '-Forward' parameter can be added + // to the script when in non-local sessions + private const string CreatePSEditFunctionScript = @" + param ( + [string] $PSEditModule + ) + + Microsoft.PowerShell.Utility\Register-EngineEvent -SourceIdentifier PSESRemoteSessionOpenFile -Forward -SupportEvent + Microsoft.PowerShell.Core\New-Module -ScriptBlock ([Scriptblock]::Create($PSEditModule)) -Name PSEdit | + Microsoft.PowerShell.Core\Import-Module -Global + "; + + private const string RemovePSEditFunctionScript = @" + Microsoft.PowerShell.Core\Get-Module PSEdit | Microsoft.PowerShell.Core\Remove-Module + + Microsoft.PowerShell.Utility\Unregister-Event -SourceIdentifier PSESRemoteSessionOpenFile -Force -ErrorAction Ignore + "; + + private const string SetRemoteContentsScript = @" + param( + [string] $RemoteFilePath, + [byte[]] $Content + ) + + # Set file contents + $params = @{ Path=$RemoteFilePath; Value=$Content; Force=$true } + if ($PSVersionTable.PSEdition -eq 'Core') + { + $params['AsByteStream']=$true + } + else + { + $params['Encoding']='Byte' + } + + Microsoft.PowerShell.Management\Set-Content @params 2>&1 + "; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the RemoteFileManager class. + /// + /// + /// The PowerShellContext to use for file loading operations. + /// + /// + /// The IEditorOperations instance to use for opening/closing files in the editor. + /// + /// An ILogger implementation used for writing log messages. + public RemoteFileManager( + PowerShellContextService powerShellContext, + IEditorOperations editorOperations, + ILogger logger) + { + Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + + this.logger = logger; + this.powerShellContext = powerShellContext; + this.powerShellContext.RunspaceChanged += HandleRunspaceChangedAsync; + + this.editorOperations = editorOperations; + + this.processTempPath = + Path.Combine( + Path.GetTempPath(), + "PSES-" + Process.GetCurrentProcess().Id); + + this.remoteFilesPath = Path.Combine(this.processTempPath, "RemoteFiles"); + + // Delete existing temporary file cache path if it already exists + this.TryDeleteTemporaryPath(); + + // Register the psedit function in the current runspace + this.RegisterPSEditFunction(this.powerShellContext.CurrentRunspace); + } + + #endregion + + #region Public Methods + + /// + /// Opens a remote file, fetching its contents if necessary. + /// + /// + /// The remote file path to be opened. + /// + /// + /// The runspace from which where the remote file will be fetched. + /// + /// + /// The local file path where the remote file's contents have been stored. + /// + public async Task FetchRemoteFileAsync( + string remoteFilePath, + RunspaceDetails runspaceDetails) + { + string localFilePath = null; + + if (!string.IsNullOrEmpty(remoteFilePath)) + { + try + { + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + localFilePath = this.GetMappedPath(remoteFilePath, runspaceDetails); + + if (!pathMappings.IsRemotePathOpened(remoteFilePath)) + { + // Does the local file already exist? + if (!File.Exists(localFilePath)) + { + // Load the file contents from the remote machine and create the buffer + PSCommand command = new PSCommand(); + command.AddCommand("Microsoft.PowerShell.Management\\Get-Content"); + command.AddParameter("Path", remoteFilePath); + command.AddParameter("Raw"); + command.AddParameter("Encoding", "Byte"); + + byte[] fileContent = + (await this.powerShellContext.ExecuteCommandAsync(command, false, false)) + .FirstOrDefault(); + + if (fileContent != null) + { + this.StoreRemoteFile(localFilePath, fileContent, pathMappings); + } + else + { + this.logger.LogWarning( + $"Could not load contents of remote file '{remoteFilePath}'"); + } + } + } + } + catch (IOException e) + { + this.logger.LogError( + $"Caught {e.GetType().Name} while attempting to get remote file at path '{remoteFilePath}'\r\n\r\n{e.ToString()}"); + } + } + + return localFilePath; + } + + /// + /// Saves the contents of the file under the temporary local + /// file cache to its corresponding remote file. + /// + /// + /// The local file whose contents will be saved. It is assumed + /// that the editor has saved the contents of the local cache + /// file to disk before this method is called. + /// + /// A Task to be awaited for completion. + public async Task SaveRemoteFileAsync(string localFilePath) + { + string remoteFilePath = + this.GetMappedPath( + localFilePath, + this.powerShellContext.CurrentRunspace); + + this.logger.LogTrace( + $"Saving remote file {remoteFilePath} (local path: {localFilePath})"); + + byte[] localFileContents = null; + try + { + localFileContents = File.ReadAllBytes(localFilePath); + } + catch (IOException e) + { + this.logger.LogException( + "Failed to read contents of local copy of remote file", + e); + + return; + } + + PSCommand saveCommand = new PSCommand(); + saveCommand + .AddScript(SetRemoteContentsScript) + .AddParameter("RemoteFilePath", remoteFilePath) + .AddParameter("Content", localFileContents); + + StringBuilder errorMessages = new StringBuilder(); + + await this.powerShellContext.ExecuteCommandAsync( + saveCommand, + errorMessages, + false, + false); + + if (errorMessages.Length > 0) + { + this.logger.LogError($"Remote file save failed due to an error:\r\n\r\n{errorMessages}"); + } + } + + /// + /// Creates a temporary file with the given name and contents + /// corresponding to the specified runspace. + /// + /// + /// The name of the file to be created under the session path. + /// + /// + /// The contents of the file to be created. + /// + /// + /// The runspace for which the temporary file relates. + /// + /// The full temporary path of the file if successful, null otherwise. + public string CreateTemporaryFile(string fileName, string fileContents, RunspaceDetails runspaceDetails) + { + string temporaryFilePath = Path.Combine(this.processTempPath, fileName); + + try + { + File.WriteAllText(temporaryFilePath, fileContents); + + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + pathMappings.AddOpenedLocalPath(temporaryFilePath); + } + catch (IOException e) + { + this.logger.LogError( + $"Caught {e.GetType().Name} while attempting to write temporary file at path '{temporaryFilePath}'\r\n\r\n{e.ToString()}"); + + temporaryFilePath = null; + } + + return temporaryFilePath; + } + + /// + /// For a remote or local cache path, get the corresponding local or + /// remote file path. + /// + /// + /// The remote or local file path. + /// + /// + /// The runspace from which the remote file was fetched. + /// + /// The mapped file path. + public string GetMappedPath( + string filePath, + RunspaceDetails runspaceDetails) + { + RemotePathMappings remotePathMappings = this.GetPathMappings(runspaceDetails); + return remotePathMappings.GetMappedPath(filePath); + } + + /// + /// Returns true if the given file path is under the remote files + /// path in the temporary folder. + /// + /// The local file path to check. + /// + /// True if the file path is under the temporary remote files path. + /// + public bool IsUnderRemoteTempPath(string filePath) + { + return filePath.StartsWith( + this.remoteFilesPath, + System.StringComparison.CurrentCultureIgnoreCase); + } + + #endregion + + #region Private Methods + + private string StoreRemoteFile( + string remoteFilePath, + byte[] fileContent, + RunspaceDetails runspaceDetails) + { + RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); + string localFilePath = pathMappings.GetMappedPath(remoteFilePath); + + this.StoreRemoteFile( + localFilePath, + fileContent, + pathMappings); + + return localFilePath; + } + + private void StoreRemoteFile( + string localFilePath, + byte[] fileContent, + RemotePathMappings pathMappings) + { + File.WriteAllBytes(localFilePath, fileContent); + pathMappings.AddOpenedLocalPath(localFilePath); + } + + private RemotePathMappings GetPathMappings(RunspaceDetails runspaceDetails) + { + RemotePathMappings remotePathMappings = null; + string computerName = runspaceDetails.SessionDetails.ComputerName; + + if (!this.filesPerComputer.TryGetValue(computerName, out remotePathMappings)) + { + remotePathMappings = new RemotePathMappings(runspaceDetails, this); + this.filesPerComputer.Add(computerName, remotePathMappings); + } + + return remotePathMappings; + } + + private async void HandleRunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) + { + if (e.ChangeAction == RunspaceChangeAction.Enter) + { + this.RegisterPSEditFunction(e.NewRunspace); + } + else + { + // Close any remote files that were opened + if (e.PreviousRunspace.Location == RunspaceLocation.Remote && + (e.ChangeAction == RunspaceChangeAction.Shutdown || + !string.Equals( + e.NewRunspace.SessionDetails.ComputerName, + e.PreviousRunspace.SessionDetails.ComputerName, + StringComparison.CurrentCultureIgnoreCase))) + { + RemotePathMappings remotePathMappings; + if (this.filesPerComputer.TryGetValue(e.PreviousRunspace.SessionDetails.ComputerName, out remotePathMappings)) + { + foreach (string remotePath in remotePathMappings.OpenedPaths) + { + await this.editorOperations?.CloseFileAsync(remotePath); + } + } + } + + if (e.PreviousRunspace != null) + { + this.RemovePSEditFunction(e.PreviousRunspace); + } + } + } + + private async void HandlePSEventReceivedAsync(object sender, PSEventArgs args) + { + if (string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) + { + try + { + if (args.SourceArgs.Length >= 1) + { + string localFilePath = string.Empty; + string remoteFilePath = args.SourceArgs[0] as string; + + // Is this a local process runspace? Treat as a local file + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Local) + { + localFilePath = remoteFilePath; + } + else + { + byte[] fileContent = null; + + if (args.SourceArgs.Length >= 2) + { + // Try to cast as a PSObject to get the BaseObject, if not, then try to case as a byte[] + PSObject sourceObj = args.SourceArgs[1] as PSObject; + if (sourceObj != null) + { + fileContent = sourceObj.BaseObject as byte[]; + } + else + { + fileContent = args.SourceArgs[1] as byte[]; + } + } + + // If fileContent is still null after trying to + // unpack the contents, just return an empty byte + // array. + fileContent = fileContent ?? new byte[0]; + + if (remoteFilePath != null) + { + localFilePath = + this.StoreRemoteFile( + remoteFilePath, + fileContent, + this.powerShellContext.CurrentRunspace); + } + else + { + await this.editorOperations?.NewFileAsync(); + EditorContext context = await this.editorOperations?.GetEditorContextAsync(); + context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); + } + } + + bool preview = true; + if (args.SourceArgs.Length >= 3) + { + bool? previewCheck = args.SourceArgs[2] as bool?; + preview = previewCheck ?? true; + } + + // Open the file in the editor + this.editorOperations?.OpenFileAsync(localFilePath, preview); + } + } + catch (NullReferenceException e) + { + this.logger.LogException("Could not store null remote file content", e); + } + } + } + + private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) + { + if (runspaceDetails.Location == RunspaceLocation.Remote && + runspaceDetails.Context == RunspaceContext.Original) + { + try + { + runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceivedAsync; + + PSCommand createCommand = new PSCommand(); + createCommand + .AddScript(CreatePSEditFunctionScript) + .AddParameter("PSEditModule", PSEditModule); + + if (runspaceDetails.Context == RunspaceContext.DebuggedRunspace) + { + this.powerShellContext.ExecuteCommandAsync(createCommand).Wait(); + } + else + { + using (var powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.Runspace = runspaceDetails.Runspace; + powerShell.Commands = createCommand; + powerShell.Invoke(); + } + } + } + catch (RemoteException e) + { + this.logger.LogException("Could not create psedit function.", e); + } + } + } + + private void RemovePSEditFunction(RunspaceDetails runspaceDetails) + { + if (runspaceDetails.Location == RunspaceLocation.Remote && + runspaceDetails.Context == RunspaceContext.Original) + { + try + { + if (runspaceDetails.Runspace.Events != null) + { + runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceivedAsync; + } + + if (runspaceDetails.Runspace.RunspaceStateInfo.State == RunspaceState.Opened) + { + using (var powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.Runspace = runspaceDetails.Runspace; + powerShell.Commands.AddScript(RemovePSEditFunctionScript); + powerShell.Invoke(); + } + } + } + catch (Exception e) when (e is RemoteException || e is PSInvalidOperationException) + { + this.logger.LogException("Could not remove psedit function.", e); + } + } + } + + private void TryDeleteTemporaryPath() + { + try + { + if (Directory.Exists(this.processTempPath)) + { + Directory.Delete(this.processTempPath, true); + } + + Directory.CreateDirectory(this.processTempPath); + } + catch (IOException e) + { + this.logger.LogException( + $"Could not delete temporary folder for current process: {this.processTempPath}", e); + } + } + + #endregion + + #region Nested Classes + + private class RemotePathMappings + { + private RunspaceDetails runspaceDetails; + private RemoteFileManager remoteFileManager; + private HashSet openedPaths = new HashSet(); + private Dictionary pathMappings = new Dictionary(); + + public IEnumerable OpenedPaths + { + get { return openedPaths; } + } + + public RemotePathMappings( + RunspaceDetails runspaceDetails, + RemoteFileManager remoteFileManager) + { + this.runspaceDetails = runspaceDetails; + this.remoteFileManager = remoteFileManager; + } + + public void AddPathMapping(string remotePath, string localPath) + { + // Add mappings in both directions + this.pathMappings[localPath.ToLower()] = remotePath; + this.pathMappings[remotePath.ToLower()] = localPath; + } + + public void AddOpenedLocalPath(string openedLocalPath) + { + this.openedPaths.Add(openedLocalPath); + } + + public bool IsRemotePathOpened(string remotePath) + { + return this.openedPaths.Contains(remotePath); + } + + public string GetMappedPath(string filePath) + { + string mappedPath = filePath; + + if (!this.pathMappings.TryGetValue(filePath.ToLower(), out mappedPath)) + { + // If the path isn't mapped yet, generate it + if (!filePath.StartsWith(this.remoteFileManager.remoteFilesPath)) + { + mappedPath = + this.MapRemotePathToLocal( + filePath, + runspaceDetails.SessionDetails.ComputerName); + + this.AddPathMapping(filePath, mappedPath); + } + } + + return mappedPath; + } + + private string MapRemotePathToLocal(string remotePath, string connectionString) + { + // The path generated by this code will look something like + // %TEMP%\PSES-[PID]\RemoteFiles\1205823508\computer-name\MyFile.ps1 + // The "path hash" is just the hashed representation of the remote + // file's full path (sans directory) to try and ensure some amount of + // uniqueness across different files on the remote machine. We put + // the "connection string" after the path slug so that it can be used + // as the differentiator string in editors like VS Code when more than + // one tab has the same filename. + + var sessionDir = Directory.CreateDirectory(this.remoteFileManager.remoteFilesPath); + var pathHashDir = + sessionDir.CreateSubdirectory( + Path.GetDirectoryName(remotePath).GetHashCode().ToString()); + + var remoteFileDir = pathHashDir.CreateSubdirectory(connectionString); + + return + Path.Combine( + remoteFileDir.FullName, + Path.GetFileName(remotePath)); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs new file mode 100644 index 000000000..7efaf57d8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs @@ -0,0 +1,67 @@ +// +// 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.Session +{ + /// + /// Defines the set of actions that will cause the runspace to be changed. + /// + public enum RunspaceChangeAction + { + /// + /// The runspace change was caused by entering a new session. + /// + Enter, + + /// + /// The runspace change was caused by exiting the current session. + /// + Exit, + + /// + /// The runspace change was caused by shutting down the service. + /// + Shutdown + } + + /// + /// Provides arguments for the PowerShellContext.RunspaceChanged event. + /// + public class RunspaceChangedEventArgs + { + /// + /// Gets the RunspaceChangeAction which caused this event. + /// + public RunspaceChangeAction ChangeAction { get; private set; } + + /// + /// Gets a RunspaceDetails object describing the previous runspace. + /// + public RunspaceDetails PreviousRunspace { get; private set; } + + /// + /// Gets a RunspaceDetails object describing the new runspace. + /// + public RunspaceDetails NewRunspace { get; private set; } + + /// + /// Creates a new instance of the RunspaceChangedEventArgs class. + /// + /// The action which caused the runspace to change. + /// The previously active runspace. + /// The newly active runspace. + public RunspaceChangedEventArgs( + RunspaceChangeAction changeAction, + RunspaceDetails previousRunspace, + RunspaceDetails newRunspace) + { + Validate.IsNotNull(nameof(previousRunspace), previousRunspace); + + this.ChangeAction = changeAction; + this.PreviousRunspace = previousRunspace; + this.NewRunspace = newRunspace; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs new file mode 100644 index 000000000..1188d20a1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs @@ -0,0 +1,321 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.CSharp.RuntimeBinder; +using System; +using System.Management.Automation.Runspaces; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Specifies the possible types of a runspace. + /// + public enum RunspaceLocation + { + /// + /// A runspace on the local machine. + /// + Local, + + /// + /// A runspace on a different machine. + /// + Remote + } + + /// + /// Specifies the context in which the runspace was encountered. + /// + public enum RunspaceContext + { + /// + /// The original runspace in a local or remote session. + /// + Original, + + /// + /// A runspace in a process that was entered with Enter-PSHostProcess. + /// + EnteredProcess, + + /// + /// A runspace that is being debugged with Debug-Runspace. + /// + DebuggedRunspace + } + + /// + /// Provides details about a runspace being used in the current + /// editing session. + /// + public class RunspaceDetails + { + #region Private Fields + + private Dictionary capabilities = + new Dictionary(); + + #endregion + + #region Properties + + /// + /// Gets the Runspace instance for which this class contains details. + /// + internal Runspace Runspace { get; private set; } + + /// + /// Gets the PowerShell version of the new runspace. + /// + public PowerShellVersionDetails PowerShellVersion { get; private set; } + + /// + /// Gets the runspace location, either Local or Remote. + /// + public RunspaceLocation Location { get; private set; } + + /// + /// Gets the context in which the runspace was encountered. + /// + public RunspaceContext Context { get; private set; } + + /// + /// Gets the "connection string" for the runspace, generally the + /// ComputerName for a remote runspace or the ProcessId of an + /// "Attach" runspace. + /// + public string ConnectionString { get; private set; } + + /// + /// Gets the details of the runspace's session at the time this + /// RunspaceDetails object was created. + /// + public SessionDetails SessionDetails { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the RunspaceDetails class. + /// + /// + /// The runspace for which this instance contains details. + /// + /// + /// The SessionDetails for the runspace. + /// + /// + /// The PowerShellVersionDetails of the runspace. + /// + /// + /// The RunspaceLocation of the runspace. + /// + /// + /// The RunspaceContext of the runspace. + /// + /// + /// The connection string of the runspace. + /// + public RunspaceDetails( + Runspace runspace, + SessionDetails sessionDetails, + PowerShellVersionDetails powerShellVersion, + RunspaceLocation runspaceLocation, + RunspaceContext runspaceContext, + string connectionString) + { + this.Runspace = runspace; + this.SessionDetails = sessionDetails; + this.PowerShellVersion = powerShellVersion; + this.Location = runspaceLocation; + this.Context = runspaceContext; + this.ConnectionString = connectionString; + } + + #endregion + + #region Public Methods + + internal void AddCapability(TCapability capability) + where TCapability : IRunspaceCapability + { + this.capabilities.Add(typeof(TCapability), capability); + } + + internal TCapability GetCapability() + where TCapability : IRunspaceCapability + { + TCapability capability = default(TCapability); + this.TryGetCapability(out capability); + return capability; + } + + internal bool TryGetCapability(out TCapability capability) + where TCapability : IRunspaceCapability + { + IRunspaceCapability capabilityAsInterface = default(TCapability); + if (this.capabilities.TryGetValue(typeof(TCapability), out capabilityAsInterface)) + { + capability = (TCapability)capabilityAsInterface; + return true; + } + + capability = default(TCapability); + return false; + } + + internal bool HasCapability() + { + return this.capabilities.ContainsKey(typeof(TCapability)); + } + + /// + /// Creates and populates a new RunspaceDetails instance for the given runspace. + /// + /// + /// The runspace for which details will be gathered. + /// + /// + /// The SessionDetails for the runspace. + /// + /// An ILogger implementation used for writing log messages. + /// A new RunspaceDetails instance. + internal static RunspaceDetails CreateFromRunspace( + Runspace runspace, + SessionDetails sessionDetails, + ILogger logger) + { + Validate.IsNotNull(nameof(runspace), runspace); + Validate.IsNotNull(nameof(sessionDetails), sessionDetails); + + var runspaceLocation = RunspaceLocation.Local; + var runspaceContext = RunspaceContext.Original; + var versionDetails = PowerShellVersionDetails.GetVersionDetails(runspace, logger); + + string connectionString = null; + + if (runspace.ConnectionInfo != null) + { + // Use 'dynamic' to avoid missing NamedPipeRunspaceConnectionInfo + // on PS v3 and v4 + try + { + dynamic connectionInfo = runspace.ConnectionInfo; + if (connectionInfo.ProcessId != null) + { + connectionString = connectionInfo.ProcessId.ToString(); + runspaceContext = RunspaceContext.EnteredProcess; + } + } + catch (RuntimeBinderException) + { + // ProcessId property isn't on the object, move on. + } + + // Grab the $host.name which will tell us if we're in a PSRP session or not + string hostName = + PowerShellContextService.ExecuteScriptAndGetItem( + "$Host.Name", + runspace, + defaultValue: string.Empty); + + // hostname is 'ServerRemoteHost' when the user enters a session. + // ex. Enter-PSSession + // Attaching to process currently needs to be marked as a local session + // so we skip this if block if the runspace is from Enter-PSHostProcess + if (hostName.Equals("ServerRemoteHost", StringComparison.Ordinal) + && runspace.OriginalConnectionInfo?.GetType().ToString() != "System.Management.Automation.Runspaces.NamedPipeConnectionInfo") + { + runspaceLocation = RunspaceLocation.Remote; + connectionString = + runspace.ConnectionInfo.ComputerName + + (connectionString != null ? $"-{connectionString}" : string.Empty); + } + } + + return + new RunspaceDetails( + runspace, + sessionDetails, + versionDetails, + runspaceLocation, + runspaceContext, + connectionString); + } + + /// + /// Creates a clone of the given runspace through which another + /// runspace was attached. Sets the IsAttached property of the + /// resulting RunspaceDetails object to true. + /// + /// + /// The RunspaceDetails object which the new object based. + /// + /// + /// The RunspaceContext of the runspace. + /// + /// + /// The SessionDetails for the runspace. + /// + /// + /// A new RunspaceDetails instance for the attached runspace. + /// + public static RunspaceDetails CreateFromContext( + RunspaceDetails runspaceDetails, + RunspaceContext runspaceContext, + SessionDetails sessionDetails) + { + return + new RunspaceDetails( + runspaceDetails.Runspace, + sessionDetails, + runspaceDetails.PowerShellVersion, + runspaceDetails.Location, + runspaceContext, + runspaceDetails.ConnectionString); + } + + /// + /// Creates a new RunspaceDetails object from a remote + /// debugging session. + /// + /// + /// The RunspaceDetails object which the new object based. + /// + /// + /// The RunspaceLocation of the runspace. + /// + /// + /// The RunspaceContext of the runspace. + /// + /// + /// The SessionDetails for the runspace. + /// + /// + /// A new RunspaceDetails instance for the attached runspace. + /// + public static RunspaceDetails CreateFromDebugger( + RunspaceDetails runspaceDetails, + RunspaceLocation runspaceLocation, + RunspaceContext runspaceContext, + SessionDetails sessionDetails) + { + // TODO: Get the PowerShellVersion correctly! + return + new RunspaceDetails( + runspaceDetails.Runspace, + sessionDetails, + runspaceDetails.PowerShellVersion, + runspaceLocation, + runspaceContext, + runspaceDetails.ConnectionString); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs new file mode 100644 index 000000000..ab0906fe6 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs @@ -0,0 +1,60 @@ +// +// 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.Management.Automation.Host; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a handle to the runspace that is managed by + /// a PowerShellContext. The holder of this handle. + /// + public class RunspaceHandle : IDisposable + { + private PowerShellContextService powerShellContext; + + /// + /// Gets the runspace that is held by this handle. + /// + public Runspace Runspace + { + get + { + return ((IHostSupportsInteractiveSession)this.powerShellContext).Runspace; + } + } + + internal bool IsReadLine { get; } + + /// + /// Initializes a new instance of the RunspaceHandle class using the + /// given runspace. + /// + /// The PowerShellContext instance which manages the runspace. + public RunspaceHandle(PowerShellContextService powerShellContext) + : this(powerShellContext, false) + { } + + internal RunspaceHandle(PowerShellContextService powerShellContext, bool isReadLine) + { + this.powerShellContext = powerShellContext; + this.IsReadLine = isReadLine; + } + + /// + /// Disposes the RunspaceHandle once the holder is done using it. + /// Causes the handle to be released back to the PowerShellContext. + /// + public void Dispose() + { + // Release the handle and clear the runspace so that + // no further operations can be performed on it. + this.powerShellContext.ReleaseRunspaceHandle(this); + } + } +} + diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs new file mode 100644 index 000000000..2347ce69a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs @@ -0,0 +1,63 @@ +// +// 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.Management.Automation; +using System.Collections; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides details about the current PowerShell session. + /// + public class SessionDetails + { + /// + /// Gets the process ID of the current process. + /// + public int? ProcessId { get; private set; } + + /// + /// Gets the name of the current computer. + /// + public string ComputerName { get; private set; } + + /// + /// Gets the current PSHost instance ID. + /// + public Guid? InstanceId { get; private set; } + + /// + /// Creates an instance of SessionDetails using the information + /// contained in the PSObject which was obtained using the + /// PSCommand returned by GetDetailsCommand. + /// + /// + public SessionDetails(PSObject detailsObject) + { + Validate.IsNotNull(nameof(detailsObject), detailsObject); + + Hashtable innerHashtable = detailsObject.BaseObject as Hashtable; + + this.ProcessId = (int)innerHashtable["processId"] as int?; + this.ComputerName = innerHashtable["computerName"] as string; + this.InstanceId = innerHashtable["instanceId"] as Guid?; + } + + /// + /// Gets the PSCommand that gathers details from the + /// current session. + /// + /// A PSCommand used to gather session details. + public static PSCommand GetDetailsCommand() + { + PSCommand infoCommand = new PSCommand(); + infoCommand.AddScript( + "@{ 'computerName' = if ([Environment]::MachineName) {[Environment]::MachineName} else {'localhost'}; 'processId' = $PID; 'instanceId' = $host.InstanceId }"); + + return infoCommand; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs new file mode 100644 index 000000000..1d285636e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details about a change in state of a PowerShellContext. + /// + public class SessionStateChangedEventArgs + { + /// + /// Gets the new state for the session. + /// + public PowerShellContextState NewSessionState { get; private set; } + + /// + /// Gets the execution result of the operation that caused + /// the state change. + /// + public PowerShellExecutionResult ExecutionResult { get; private set; } + + /// + /// Gets the exception that caused a failure state or null otherwise. + /// + public Exception ErrorException { get; private set; } + + /// + /// Creates a new instance of the SessionStateChangedEventArgs class. + /// + /// The new session state. + /// The result of the operation that caused the state change. + /// An exception that describes the failure, if any. + public SessionStateChangedEventArgs( + PowerShellContextState newSessionState, + PowerShellExecutionResult executionResult, + Exception errorException) + { + this.NewSessionState = newSessionState; + this.ExecutionResult = executionResult; + this.ErrorException = errorException; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs new file mode 100644 index 000000000..8720e3fa7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Provides the ability to route PowerShell command invocations to a specific thread. + /// + internal class ThreadController + { + private PromptNestFrame _nestFrame; + + internal AsyncQueue PipelineRequestQueue { get; } + + internal TaskCompletionSource FrameExitTask { get; } + + internal int ManagedThreadId { get; } + + internal bool IsPipelineThread { get; } + + /// + /// Initializes an new instance of the ThreadController class. This constructor should only + /// ever been called from the thread it is meant to control. + /// + /// The parent PromptNestFrame object. + internal ThreadController(PromptNestFrame nestFrame) + { + _nestFrame = nestFrame; + PipelineRequestQueue = new AsyncQueue(); + FrameExitTask = new TaskCompletionSource(); + ManagedThreadId = Thread.CurrentThread.ManagedThreadId; + + // If the debugger stop is triggered on a thread with no default runspace we + // shouldn't attempt to route commands to it. + IsPipelineThread = Runspace.DefaultRunspace != null; + } + + /// + /// Determines if the caller is already on the thread that this object maintains. + /// + /// + /// A value indicating if the caller is already on the thread maintained by this object. + /// + internal bool IsCurrentThread() + { + return Thread.CurrentThread.ManagedThreadId == ManagedThreadId; + } + + /// + /// Requests the invocation of a PowerShell command on the thread maintained by this object. + /// + /// The execution request to send. + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the output of the command invocation. + /// + internal async Task> RequestPipelineExecutionAsync( + PipelineExecutionRequest executionRequest) + { + await PipelineRequestQueue.EnqueueAsync(executionRequest); + return await executionRequest.Results; + } + + /// + /// Retrieves the first currently queued execution request. If there are no pending + /// execution requests then the task will be completed when one is requested. + /// + /// + /// A task object representing the asynchronous operation. The Result property will return + /// the retrieved pipeline execution request. + /// + internal async Task TakeExecutionRequestAsync() + { + return await PipelineRequestQueue.DequeueAsync(); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + internal void StartThreadExit(DebuggerResumeAction action) + { + StartThreadExit(action, waitForExit: false); + } + + /// + /// Marks the thread to be exited. + /// + /// + /// The resume action for the debugger. If the frame is not a debugger frame this parameter + /// is ignored. + /// + /// + /// Indicates whether the method should block until the exit is completed. + /// + internal void StartThreadExit(DebuggerResumeAction action, bool waitForExit) + { + Task.Run(() => FrameExitTask.TrySetResult(action)); + if (!waitForExit) + { + return; + } + + _nestFrame.WaitForFrameExit(CancellationToken.None); + } + + /// + /// Creates a task object that completes when the thread has be marked for exit. + /// + /// + /// A task object representing the frame receiving a request to exit. The Result property + /// will return the DebuggerResumeAction supplied with the request. + /// + internal async Task Exit() + { + return await FrameExitTask.Task.ConfigureAwait(false); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs index c1d672999..652bb9ce4 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -16,15 +16,23 @@ internal class ConfigurationHandler : IDidChangeConfigurationHandler private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; private readonly ConfigurationService _configurationService; + private readonly PowerShellContextService _powerShellContextService; private DidChangeConfigurationCapability _capability; + private bool _profilesLoaded; + private bool _consoleReplStarted; - - public ConfigurationHandler(ILoggerFactory factory, WorkspaceService workspaceService, AnalysisService analysisService, ConfigurationService configurationService) + public ConfigurationHandler( + ILoggerFactory factory, + WorkspaceService workspaceService, + AnalysisService analysisService, + ConfigurationService configurationService, + PowerShellContextService powerShellContextService) { _logger = factory.CreateLogger(); _workspaceService = workspaceService; _analysisService = analysisService; _configurationService = configurationService; + _powerShellContextService = powerShellContextService; } public object GetRegistrationOptions() @@ -40,7 +48,7 @@ public async Task Handle(DidChangeConfigurationParams request, Cancellatio return await Unit.Task; } // TODO ADD THIS BACK IN - // bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; + bool oldLoadProfiles = _configurationService.CurrentSettings.EnableProfileLoading; bool oldScriptAnalysisEnabled = _configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false; string oldScriptAnalysisSettingsPath = @@ -51,23 +59,22 @@ public async Task Handle(DidChangeConfigurationParams request, Cancellatio _workspaceService.WorkspacePath, _logger); - // TODO ADD THIS BACK IN - // if (!this.profilesLoaded && - // this.currentSettings.EnableProfileLoading && - // oldLoadProfiles != this.currentSettings.EnableProfileLoading) - // { - // await this.editorSession.PowerShellContext.LoadHostProfilesAsync(); - // this.profilesLoaded = true; - // } + if (!this._profilesLoaded && + _configurationService.CurrentSettings.EnableProfileLoading && + oldLoadProfiles != _configurationService.CurrentSettings.EnableProfileLoading) + { + await _powerShellContextService.LoadHostProfilesAsync(); + this._profilesLoaded = true; + } - // // Wait until after profiles are loaded (or not, if that's the - // // case) before starting the interactive console. - // if (!this.consoleReplStarted) - // { - // // Start the interactive terminal - // this.editorSession.HostInput.StartCommandLoop(); - // this.consoleReplStarted = true; - // } + // Wait until after profiles are loaded (or not, if that's the + // case) before starting the interactive console. + if (!this._consoleReplStarted) + { + // Start the interactive terminal + _powerShellContextService.ConsoleReader.StartCommandLoop(); + this._consoleReplStarted = true; + } // If there is a new settings file path, restart the analyzer with the new settigs. bool settingsPathChanged = false; diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs new file mode 100644 index 000000000..65e92aa4f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs @@ -0,0 +1,127 @@ +// +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a simple wrapper over a SemaphoreSlim to allow + /// synchronization locking inside of async calls. Cannot be + /// used recursively. + /// + public class AsyncLock + { + #region Fields + + private Task lockReleaseTask; + private SemaphoreSlim lockSemaphore = new SemaphoreSlim(1, 1); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the AsyncLock class. + /// + public AsyncLock() + { + this.lockReleaseTask = + Task.FromResult( + (IDisposable)new LockReleaser(this)); + } + + #endregion + + #region Public Methods + + /// + /// Locks + /// + /// A task which has an IDisposable + public Task LockAsync() + { + return this.LockAsync(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public Task LockAsync(CancellationToken cancellationToken) + { + Task waitTask = lockSemaphore.WaitAsync(cancellationToken); + + return waitTask.IsCompleted ? + this.lockReleaseTask : + waitTask.ContinueWith( + (t, releaser) => + { + return (IDisposable)releaser; + }, + this.lockReleaseTask.Result, + cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. + /// + /// + public IDisposable Lock() + { + return Lock(CancellationToken.None); + } + + /// + /// Obtains or waits for a lock which can be used to synchronize + /// access to a resource. The wait may be cancelled with the + /// given CancellationToken. + /// + /// + /// A CancellationToken which can be used to cancel the lock. + /// + /// + public IDisposable Lock(CancellationToken cancellationToken) + { + lockSemaphore.Wait(cancellationToken); + return this.lockReleaseTask.Result; + } + + #endregion + + #region Private Classes + + /// + /// Provides an IDisposable wrapper around an AsyncLock so + /// that it can easily be used inside of a 'using' block. + /// + private class LockReleaser : IDisposable + { + private AsyncLock lockToRelease; + + internal LockReleaser(AsyncLock lockToRelease) + { + this.lockToRelease = lockToRelease; + } + + public void Dispose() + { + this.lockToRelease.lockSemaphore.Release(); + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs new file mode 100644 index 000000000..8125076b7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides a synchronized queue which can be used from within async + /// operations. This is primarily used for producer/consumer scenarios. + /// + /// The type of item contained in the queue. + public class AsyncQueue + { + #region Private Fields + + private AsyncLock queueLock = new AsyncLock(); + private Queue itemQueue; + private Queue> requestQueue; + + #endregion + + #region Properties + + /// + /// Returns true if the queue is currently empty. + /// + public bool IsEmpty { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes an empty instance of the AsyncQueue class. + /// + public AsyncQueue() : this(Enumerable.Empty()) + { + } + + /// + /// Initializes an instance of the AsyncQueue class, pre-populated + /// with the given collection of items. + /// + /// + /// An IEnumerable containing the initial items with which the queue will + /// be populated. + /// + public AsyncQueue(IEnumerable initialItems) + { + this.itemQueue = new Queue(initialItems); + this.requestQueue = new Queue>(); + } + + #endregion + + #region Public Methods + + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + /// + /// A Task which can be awaited until the synchronized enqueue + /// operation completes. + /// + public async Task EnqueueAsync(T item) + { + using (await queueLock.LockAsync()) + { + TaskCompletionSource requestTaskSource = null; + + // Are any requests waiting? + while (this.requestQueue.Count > 0) + { + // Is the next request cancelled already? + requestTaskSource = this.requestQueue.Dequeue(); + if (!requestTaskSource.Task.IsCanceled) + { + // Dispatch the item + requestTaskSource.SetResult(item); + return; + } + } + + // No more requests waiting, queue the item for a later request + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + } + + /// + /// Enqueues an item onto the end of the queue. + /// + /// The item to be added to the queue. + public void Enqueue(T item) + { + using (queueLock.Lock()) + { + while (this.requestQueue.Count > 0) + { + var requestTaskSource = this.requestQueue.Dequeue(); + if (requestTaskSource.Task.IsCanceled) + { + continue; + } + + requestTaskSource.SetResult(item); + return; + } + } + + this.itemQueue.Enqueue(item); + this.IsEmpty = false; + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + /// A Task which can be awaited until a value can be dequeued. + /// + public Task DequeueAsync() + { + return this.DequeueAsync(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + /// A Task which can be awaited until a value can be dequeued. + /// + public async Task DequeueAsync(CancellationToken cancellationToken) + { + Task requestTask; + + using (await queueLock.LockAsync(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + // Items are waiting to be taken so take one immediately + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + else + { + // Queue the request for the next item + var requestTaskSource = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTaskSource); + + // Register the wait task for cancel notifications + cancellationToken.Register( + () => requestTaskSource.TrySetCanceled()); + + requestTask = requestTaskSource.Task; + } + } + + // Wait for the request task to complete outside of the lock + return await requestTask; + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. + /// + /// + public T Dequeue() + { + return Dequeue(CancellationToken.None); + } + + /// + /// Dequeues an item from the queue or waits asynchronously + /// until an item is available. The wait can be cancelled + /// using the given CancellationToken. + /// + /// + /// A CancellationToken with which a dequeue wait can be cancelled. + /// + /// + public T Dequeue(CancellationToken cancellationToken) + { + TaskCompletionSource requestTask; + using (queueLock.Lock(cancellationToken)) + { + if (this.itemQueue.Count > 0) + { + T item = this.itemQueue.Dequeue(); + this.IsEmpty = this.itemQueue.Count == 0; + + return item; + } + + requestTask = new TaskCompletionSource(); + this.requestQueue.Enqueue(requestTask); + + if (cancellationToken.CanBeCanceled) + { + cancellationToken.Register(() => requestTask.TrySetCanceled()); + } + } + + return requestTask.Task.GetAwaiter().GetResult(); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs b/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs new file mode 100644 index 000000000..8da21b942 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Provides utility methods for common asynchronous operations. + /// + internal static class AsyncUtils + { + /// + /// Creates a with an handle initial and + /// max count of one. + /// + /// A simple single handle . + internal static SemaphoreSlim CreateSimpleLockingSemaphore() + { + return new SemaphoreSlim(initialCount: 1, maxCount: 1); + } + } +} diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 4a4acf9c6..15ace52f8 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -147,6 +147,12 @@ public EditorServicesHost( this.serverCompletedTask = new TaskCompletionSource(); this.internalHost = internalHost; + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id); + System.Threading.Thread.Sleep(2000); + } + #if DEBUG if (waitForDebugger) { diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj index 020372b3f..2245a4a22 100644 --- a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj +++ b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj @@ -16,7 +16,7 @@ - + From 9d9f474ec721878174dc87cac58165fb4fa53d00 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 12 Aug 2019 15:27:25 -0700 Subject: [PATCH 28/47] support $psEditor (#1006) * support $psEditor * deleted commented out code * fix initial build failures due to lack of certain assemblies * use different RootPath * wait an extra 5 seconds just in case * refactor initialize script --- PowerShellEditorServices.build.ps1 | 43 ++--- .../PowerShellEditorServices.psm1 | 3 - .../Hosting/EditorServicesHost.cs | 93 +++++---- .../LanguageServer/OmnisharpLanguageServer.cs | 22 ++- .../PowerShellEditorServices.Engine.csproj | 7 - .../Components/ComponentRegistry.cs | 84 -------- .../Components/IComponentRegistry.cs | 61 ------ .../IComponentRegistryExtensions.cs | 87 --------- .../EditorOperationsService.cs | 182 ++++++++++++++++++ .../{Extensions => }/ExtensionService.cs | 51 ++++- .../Extensions/EditorObject.cs | 44 +++-- .../Extensions/EditorRequests.cs | 76 ++++++++ .../IInvokeExtensionCommandHandler.cs | 30 +++ .../Handlers/InvokeExtensionCommandHandler.cs | 40 ++++ .../PSHostProcessAndRunspaceHandlers.cs | 26 +-- .../Services/Symbols/SymbolsService.cs | 4 - .../Handlers/ConfigurationHandler.cs | 2 +- .../EditorServices.Integration.Tests.ps1 | 4 +- tools/PsesPsClient/PsesPsClient.psd1 | 5 +- tools/PsesPsClient/PsesPsClient.psm1 | 2 +- tools/PsesPsClient/build.ps1 | 2 + 21 files changed, 508 insertions(+), 360 deletions(-) delete mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs delete mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs delete mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs rename src/PowerShellEditorServices.Engine/Services/PowerShellContext/{Extensions => }/ExtensionService.cs (80%) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 972a17766..9629dab9b 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -51,46 +51,27 @@ Schema is: #> $script:RequiredBuildAssets = @{ $script:ModuleBinPath = @{ - 'PowerShellEditorServices' = @( - 'publish/Serilog.dll', - 'publish/Serilog.Sinks.Async.dll', - 'publish/Serilog.Sinks.Console.dll', - 'publish/Serilog.Sinks.File.dll', - 'publish/Microsoft.Extensions.FileSystemGlobbing.dll', - 'Microsoft.PowerShell.EditorServices.dll', - 'Microsoft.PowerShell.EditorServices.pdb' - ) - - 'PowerShellEditorServices.Host' = @( - 'publish/UnixConsoleEcho.dll', - 'publish/runtimes/osx-64/native/libdisablekeyecho.dylib', - 'publish/runtimes/linux-64/native/libdisablekeyecho.so', - 'publish/Newtonsoft.Json.dll', - 'Microsoft.PowerShell.EditorServices.Host.dll', - 'Microsoft.PowerShell.EditorServices.Host.pdb' - ) - - 'PowerShellEditorServices.Protocol' = @( - 'Microsoft.PowerShell.EditorServices.Protocol.dll', - 'Microsoft.PowerShell.EditorServices.Protocol.pdb' - ) - 'PowerShellEditorServices.Engine' = @( + 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', + 'publish/Microsoft.Extensions.DependencyInjection.dll', + 'publish/Microsoft.Extensions.FileSystemGlobbing.dll', + 'publish/Microsoft.Extensions.Logging.Abstractions.dll', + 'publish/Microsoft.Extensions.Logging.dll', + 'publish/Microsoft.Extensions.Options.dll', + 'publish/Microsoft.Extensions.Primitives.dll', 'publish/Microsoft.PowerShell.EditorServices.Engine.dll', 'publish/Microsoft.PowerShell.EditorServices.Engine.pdb', + 'publish/Newtonsoft.Json.dll', 'publish/OmniSharp.Extensions.JsonRpc.dll', 'publish/OmniSharp.Extensions.LanguageProtocol.dll', 'publish/OmniSharp.Extensions.LanguageServer.dll', + 'publish/runtimes/linux-64/native/libdisablekeyecho.so', + 'publish/runtimes/osx-64/native/libdisablekeyecho.dylib', 'publish/Serilog.dll', 'publish/Serilog.Extensions.Logging.dll', 'publish/Serilog.Sinks.File.dll', - 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', - 'publish/Microsoft.Extensions.DependencyInjection.dll', - 'publish/Microsoft.Extensions.Logging.Abstractions.dll', - 'publish/Microsoft.Extensions.Logging.dll', - 'publish/Microsoft.Extensions.Options.dll', - 'publish/Microsoft.Extensions.Primitives.dll', - 'publish/System.Reactive.dll' + 'publish/System.Reactive.dll', + 'publish/UnixConsoleEcho.dll' ) } diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index 2d05b73ea..431e208b9 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -10,9 +10,6 @@ if ($PSEdition -eq 'Desktop') { Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.Principal.Windows.dll" } -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.dll" -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Host.dll" -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Protocol.dll" Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Engine.dll" function Start-EditorServicesHost { diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 017f99a60..f67f28c08 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Extensions; using Serilog; namespace Microsoft.PowerShell.EditorServices.Engine @@ -233,6 +234,56 @@ public void StartLanguageService( _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); + var powerShellContext = GetFullyInitializedPowerShellContext(profilePaths); + + _serviceCollection + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(powerShellContext) + .AddSingleton() + .AddSingleton( + (provider) => + { + var extensionService = new ExtensionService( + provider.GetService(), + provider.GetService()); + extensionService.InitializeAsync( + serviceProvider: provider, + editorOperations: provider.GetService()) + .Wait(); + return extensionService; + }) + .AddSingleton( + (provider) => + { + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + _factory.CreateLogger()); + }); + + _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) + { + NamedPipeName = config.InOutPipeName ?? config.InPipeName, + OutNamedPipeName = config.OutPipeName, + LoggerFactory = _factory, + MinimumLogLevel = LogLevel.Trace, + } + .BuildLanguageServer(); + + _logger.LogInformation("Starting language server"); + + Task.Run(_languageServer.StartAsync); + + _logger.LogInformation( + string.Format( + "Language service started, type = {0}, endpoint = {1}", + config.TransportType, config.Endpoint)); + } + + private PowerShellContextService GetFullyInitializedPowerShellContext(ProfilePaths profilePaths) + { var logger = _factory.CreateLogger(); var powerShellContext = new PowerShellContextService( logger, @@ -244,7 +295,7 @@ public void StartLanguageService( // ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) // : new ProtocolPSHostUserInterface(powerShellContext, messageSender, logger); EditorServicesPSHostUserInterface hostUserInterface = - (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost); + new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost); EditorServicesPSHost psHost = @@ -267,51 +318,17 @@ public void StartLanguageService( foreach (string module in this._additionalModules) { var command = - new System.Management.Automation.PSCommand() + new PSCommand() .AddCommand("Microsoft.PowerShell.Core\\Import-Module") .AddParameter("Name", module); - powerShellContext.ExecuteCommandAsync( + powerShellContext.ExecuteCommandAsync( command, sendOutputToHost: false, sendErrorToHost: true); } - _serviceCollection - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(powerShellContext) - .AddSingleton( - (provider) => { - return AnalysisService.Create( - provider.GetService(), - provider.GetService(), - _factory.CreateLogger()); - } - ); - - _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) - { - NamedPipeName = config.InOutPipeName ?? config.InPipeName, - OutNamedPipeName = config.OutPipeName, - LoggerFactory = _factory, - MinimumLogLevel = LogLevel.Trace, - } - .BuildLanguageServer(); - - _logger.LogInformation("Starting language server"); - - Task.Run(_languageServer.StartAsync); - //Task.Factory.StartNew(() => _languageServer.StartAsync(), - // CancellationToken.None, - // TaskCreationOptions.LongRunning, - // TaskScheduler.Default); - - _logger.LogInformation( - string.Format( - "Language service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); + return powerShellContext; } /// diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index ccf221008..3bbf26dff 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -14,6 +14,7 @@ using OmniSharp.Extensions.LanguageServer.Server; using PowerShellEditorServices.Engine.Services.Handlers; using Microsoft.PowerShell.EditorServices.TextDocument; +using System.IO; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -95,7 +96,26 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler() + .OnInitialize( + async (languageServer, request) => + { + var serviceProvider = languageServer.Services; + var workspaceService = serviceProvider.GetService(); + + // Grab the workspace path from the parameters + workspaceService.WorkspacePath = request.RootPath; + + // Set the working directory of the PowerShell session to the workspace path + if (workspaceService.WorkspacePath != null + && Directory.Exists(workspaceService.WorkspacePath)) + { + await serviceProvider.GetService().SetWorkingDirectoryAsync( + workspaceService.WorkspacePath, + isPathAlreadyEscaped: false); + } + }); logger.LogInformation("Handlers added"); }); diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index a7d6b35ca..3c219d986 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -28,14 +28,7 @@ - - - - - - - diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs deleted file mode 100644 index 9a1de6d01..000000000 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/ComponentRegistry.cs +++ /dev/null @@ -1,84 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Components -{ - /// - /// Provides a default implementation for the IComponentRegistry - /// interface. - /// - public class ComponentRegistry : IComponentRegistry - { - private Dictionary componentRegistry = - new Dictionary(); - - /// - /// Registers an instance of the specified component type - /// or throws an ArgumentException if an instance has - /// already been registered. - /// - /// - /// The component type that the instance represents. - /// - /// - /// The instance of the component to be registered. - /// - /// - /// The provided component instance for convenience in assignment - /// statements. - /// - public object Register(Type componentType, object componentInstance) - { - this.componentRegistry.Add(componentType, componentInstance); - return componentInstance; - } - - - /// - /// Gets the registered instance of the specified - /// component type or throws a KeyNotFoundException if - /// no instance has been registered. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// The implementation of the specified type. - public object Get(Type componentType) - { - return this.componentRegistry[componentType]; - } - - /// - /// Attempts to retrieve the instance of the specified - /// component type and, if found, stores it in the - /// componentInstance parameter. - /// - /// - /// The out parameter in which the found instance will be stored. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// - /// True if a registered instance was found, false otherwise. - /// - public bool TryGet(Type componentType, out object componentInstance) - { - componentInstance = null; - - if (this.componentRegistry.TryGetValue(componentType, out componentInstance)) - { - return componentInstance != null; - } - - return false; - } - } -} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs deleted file mode 100644 index c9f99000f..000000000 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistry.cs +++ /dev/null @@ -1,61 +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; - -namespace Microsoft.PowerShell.EditorServices.Components -{ - /// - /// Specifies the contract for a registry of component interfaces. - /// - public interface IComponentRegistry - { - /// - /// Registers an instance of the specified component type - /// or throws an ArgumentException if an instance has - /// already been registered. - /// - /// - /// The component type that the instance represents. - /// - /// - /// The instance of the component to be registered. - /// - /// - /// The provided component instance for convenience in assignment - /// statements. - /// - object Register( - Type componentType, - object componentInstance); - - /// - /// Gets the registered instance of the specified - /// component type or throws a KeyNotFoundException if - /// no instance has been registered. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// The implementation of the specified type. - object Get(Type componentType); - - /// - /// Attempts to retrieve the instance of the specified - /// component type and, if found, stores it in the - /// componentInstance parameter. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// - /// The out parameter in which the found instance will be stored. - /// - /// - /// True if a registered instance was found, false otherwise. - /// - bool TryGet(Type componentType, out object componentInstance); - } -} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs deleted file mode 100644 index 0c6307d5a..000000000 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Components/IComponentRegistryExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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.Components -{ - /// - /// Provides generic helper methods for working with IComponentRegistry - /// methods. - /// - public static class IComponentRegistryExtensions - { - /// - /// Registers an instance of the specified component type - /// or throws an ArgumentException if an instance has - /// already been registered. - /// - /// - /// The IComponentRegistry instance. - /// - /// - /// The instance of the component to be registered. - /// - /// - /// The provided component instance for convenience in assignment - /// statements. - /// - public static TComponent Register( - this IComponentRegistry componentRegistry, - TComponent componentInstance) - where TComponent : class - { - return - (TComponent)componentRegistry.Register( - typeof(TComponent), - componentInstance); - } - - /// - /// Gets the registered instance of the specified - /// component type or throws a KeyNotFoundException if - /// no instance has been registered. - /// - /// - /// The IComponentRegistry instance. - /// - /// The implementation of the specified type. - public static TComponent Get( - this IComponentRegistry componentRegistry) - where TComponent : class - { - return (TComponent)componentRegistry.Get(typeof(TComponent)); - } - - /// - /// Attempts to retrieve the instance of the specified - /// component type and, if found, stores it in the - /// componentInstance parameter. - /// - /// - /// The IComponentRegistry instance. - /// - /// - /// The out parameter in which the found instance will be stored. - /// - /// - /// True if a registered instance was found, false otherwise. - /// - public static bool TryGet( - this IComponentRegistry componentRegistry, - out TComponent componentInstance) - where TComponent : class - { - object componentObject = null; - componentInstance = null; - - if (componentRegistry.TryGet(typeof(TComponent), out componentObject)) - { - componentInstance = componentObject as TComponent; - return componentInstance != null; - } - - return false; - } - } -} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs new file mode 100644 index 000000000..6d651acde --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Services.Handlers; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Extensions +{ + internal class EditorOperationsService : IEditorOperations + { + private const bool DefaultPreviewSetting = true; + + private WorkspaceService _workspaceService; + private ILanguageServer _languageServer; + + public EditorOperationsService( + WorkspaceService workspaceService, + ILanguageServer languageServer) + { + this._workspaceService = workspaceService; + this._languageServer = languageServer; + } + + public async Task GetEditorContextAsync() + { + ClientEditorContext clientContext = + await _languageServer.SendRequest( + "editor/getEditorContext", + new GetEditorContextRequest()); + + return this.ConvertClientEditorContext(clientContext); + } + + public async Task InsertTextAsync(string filePath, string text, BufferRange insertRange) + { + await _languageServer.SendRequest("editor/insertText", new InsertTextRequest + { + FilePath = filePath, + InsertText = text, + InsertRange = + new Range + { + Start = new Position + { + Line = insertRange.Start.Line - 1, + Character = insertRange.Start.Column - 1 + }, + End = new Position + { + Line = insertRange.End.Line - 1, + Character = insertRange.End.Column - 1 + } + } + }); + } + + public async Task SetSelectionAsync(BufferRange selectionRange) + { + + await _languageServer.SendRequest("editor/setSelection", new SetSelectionRequest + { + SelectionRange = + new Range + { + Start = new Position + { + Line = selectionRange.Start.Line - 1, + Character = selectionRange.Start.Column - 1 + }, + End = new Position + { + Line = selectionRange.End.Line - 1, + Character = selectionRange.End.Column - 1 + } + } + }); + } + + public EditorContext ConvertClientEditorContext( + ClientEditorContext clientContext) + { + ScriptFile scriptFile = _workspaceService.CreateScriptFileFromFileBuffer( + clientContext.CurrentFilePath, + clientContext.CurrentFileContent); + + return + new EditorContext( + this, + scriptFile, + new BufferPosition( + (int) clientContext.CursorPosition.Line + 1, + (int) clientContext.CursorPosition.Character + 1), + new BufferRange( + (int) clientContext.SelectionRange.Start.Line + 1, + (int) clientContext.SelectionRange.Start.Character + 1, + (int) clientContext.SelectionRange.End.Line + 1, + (int) clientContext.SelectionRange.End.Character + 1), + clientContext.CurrentFileLanguage); + } + + public async Task NewFileAsync() + { + await _languageServer.SendRequest("editor/newFile", null); + } + + public async Task OpenFileAsync(string filePath) + { + await _languageServer.SendRequest("editor/openFile", new OpenFileDetails + { + FilePath = filePath, + Preview = DefaultPreviewSetting + }); + } + + public async Task OpenFileAsync(string filePath, bool preview) + { + await _languageServer.SendRequest("editor/openFile", new OpenFileDetails + { + FilePath = filePath, + Preview = preview + }); + } + + public async Task CloseFileAsync(string filePath) + { + await _languageServer.SendRequest("editor/closeFile", filePath); + } + + public async Task SaveFileAsync(string filePath) + { + await SaveFileAsync(filePath, null); + } + + public async Task SaveFileAsync(string currentPath, string newSavePath) + { + await _languageServer.SendRequest("editor/saveFile", new SaveFileDetails + { + FilePath = currentPath, + NewPath = newSavePath + }); + } + + public string GetWorkspacePath() + { + return _workspaceService.WorkspacePath; + } + + public string GetWorkspaceRelativePath(string filePath) + { + return _workspaceService.GetRelativePath(filePath); + } + + public async Task ShowInformationMessageAsync(string message) + { + await _languageServer.SendRequest("editor/showInformationMessage", message); + } + + public async Task ShowErrorMessageAsync(string message) + { + await _languageServer.SendRequest("editor/showErrorMessage", message); + } + + public async Task ShowWarningMessageAsync(string message) + { + await _languageServer.SendRequest("editor/showWarningMessage", message); + } + + public async Task SetStatusBarMessageAsync(string message, int? timeout) + { + await _languageServer.SendRequest("editor/setStatusBarMessage", new StatusBarMessageDetails + { + Message = message, + Timeout = timeout + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs similarity index 80% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs rename to src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs index b96ac9e12..8ecc19300 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/ExtensionService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs @@ -3,7 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Components; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System; using System.Collections.Generic; using System.Management.Automation; @@ -22,6 +23,8 @@ public class ExtensionService private Dictionary editorCommands = new Dictionary(); + private readonly ILanguageServer _languageServer; + #endregion #region Properties @@ -52,9 +55,10 @@ public class ExtensionService /// PowerShellContext for loading and executing extension code. /// /// A PowerShellContext used to execute extension code. - public ExtensionService(PowerShellContextService powerShellContext) + public ExtensionService(PowerShellContextService powerShellContext, ILanguageServer languageServer) { this.PowerShellContext = powerShellContext; + _languageServer = languageServer; } #endregion @@ -66,17 +70,21 @@ public ExtensionService(PowerShellContextService powerShellContext) /// implementation for future interaction with the host editor. /// /// An IEditorOperations implementation. - /// An IComponentRegistry instance which provides components in the session. /// A Task that can be awaited for completion. public async Task InitializeAsync( - IEditorOperations editorOperations, - IComponentRegistry componentRegistry) + IServiceProvider serviceProvider, + IEditorOperations editorOperations) { + // Attach to ExtensionService events + this.CommandAdded += ExtensionService_ExtensionAddedAsync; + this.CommandUpdated += ExtensionService_ExtensionUpdatedAsync; + this.CommandRemoved += ExtensionService_ExtensionRemovedAsync; + this.EditorObject = new EditorObject( + serviceProvider, this, - editorOperations, - componentRegistry); + editorOperations); // Register the editor object in the runspace PSCommand variableCommand = new PSCommand(); @@ -177,6 +185,7 @@ public EditorCommand[] GetCommands() this.editorCommands.Values.CopyTo(commands,0); return commands; } + #endregion #region Events @@ -211,6 +220,34 @@ private void OnCommandRemoved(EditorCommand command) this.CommandRemoved?.Invoke(this, command); } + private void ExtensionService_ExtensionAddedAsync(object sender, EditorCommand e) + { + _languageServer.SendNotification("powerShell/extensionCommandAdded", + new ExtensionCommandAddedNotification + { + Name = e.Name, + DisplayName = e.DisplayName + }); + } + + private void ExtensionService_ExtensionUpdatedAsync(object sender, EditorCommand e) + { + _languageServer.SendNotification("powerShell/extensionCommandUpdated", + new ExtensionCommandUpdatedNotification + { + Name = e.Name, + }); + } + + private void ExtensionService_ExtensionRemovedAsync(object sender, EditorCommand e) + { + _languageServer.SendNotification("powerShell/extensionCommandRemoved", + new ExtensionCommandRemovedNotification + { + Name = e.Name, + }); + } + #endregion } } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs index 39da959cf..44a72a6c8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Components; using System; using System.Reflection; @@ -17,8 +16,9 @@ public class EditorObject { #region Private Fields - private ExtensionService extensionService; - private IEditorOperations editorOperations; + private readonly IServiceProvider _serviceProvider; + private readonly ExtensionService _extensionService; + private readonly IEditorOperations _editorOperations; #endregion @@ -42,12 +42,6 @@ public Version EditorServicesVersion /// public EditorWindow Window { get; private set; } - /// - /// Gets the component registry for the session. - /// - /// - public IComponentRegistry Components { get; private set; } - #endregion /// @@ -55,19 +49,18 @@ public Version EditorServicesVersion /// /// An ExtensionService which handles command registration. /// An IEditorOperations implementation which handles operations in the host editor. - /// An IComponentRegistry instance which provides components in the session. public EditorObject( + IServiceProvider serviceProvider, ExtensionService extensionService, - IEditorOperations editorOperations, - IComponentRegistry componentRegistry) + IEditorOperations editorOperations) { - this.extensionService = extensionService; - this.editorOperations = editorOperations; - this.Components = componentRegistry; + this._serviceProvider = serviceProvider; + this._extensionService = extensionService; + this._editorOperations = editorOperations; // Create API area objects - this.Workspace = new EditorWorkspace(this.editorOperations); - this.Window = new EditorWindow(this.editorOperations); + this.Workspace = new EditorWorkspace(this._editorOperations); + this.Window = new EditorWindow(this._editorOperations); } /// @@ -77,7 +70,7 @@ public EditorObject( /// True if the command is newly registered, false if the command already exists. public bool RegisterCommand(EditorCommand editorCommand) { - return this.extensionService.RegisterCommand(editorCommand); + return this._extensionService.RegisterCommand(editorCommand); } /// @@ -86,7 +79,7 @@ public bool RegisterCommand(EditorCommand editorCommand) /// The name of the command to be unregistered. public void UnregisterCommand(string commandName) { - this.extensionService.UnregisterCommand(commandName); + this._extensionService.UnregisterCommand(commandName); } /// @@ -95,7 +88,7 @@ public void UnregisterCommand(string commandName) /// An Array of all registered EditorCommands. public EditorCommand[] GetCommands() { - return this.extensionService.GetCommands(); + return this._extensionService.GetCommands(); } /// /// Gets the EditorContext which contains the state of the editor @@ -104,7 +97,16 @@ public EditorCommand[] GetCommands() /// A instance of the EditorContext class. public EditorContext GetEditorContext() { - return this.editorOperations.GetEditorContextAsync().Result; + return this._editorOperations.GetEditorContextAsync().Result; + } + + /// + /// Get's the desired service which allows for advanced control of PowerShellEditorServices. + /// + /// The singleton service object of the type requested. + public object GetService(Type type) + { + return _serviceProvider.GetService(type); } } } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs new file mode 100644 index 000000000..38e5c80f8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class ExtensionCommandAddedNotification + { + public string Name { get; set; } + + public string DisplayName { get; set; } + } + + public class ExtensionCommandUpdatedNotification + { + public string Name { get; set; } + } + + public class ExtensionCommandRemovedNotification + { + public string Name { get; set; } + } + + + public class GetEditorContextRequest + {} + + public enum EditorCommandResponse + { + Unsupported, + OK + } + + public class InsertTextRequest + { + public string FilePath { get; set; } + + public string InsertText { get; set; } + + public Range InsertRange { get; set; } + } + + public class SetSelectionRequest + { + public Range SelectionRange { get; set; } + } + + public class SetCursorPositionRequest + { + public Position CursorPosition { get; set; } + } + + public class OpenFileDetails + { + public string FilePath { get; set; } + + public bool Preview { get; set; } + } + + public class SaveFileDetails + { + public string FilePath { get; set; } + + public string NewPath { get; set; } + } + + public class StatusBarMessageDetails + { + public string Message { get; set; } + + public int? Timeout { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs new file mode 100644 index 000000000..e44825c95 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs @@ -0,0 +1,30 @@ +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/invokeExtensionCommand")] + public interface IInvokeExtensionCommandHandler : IJsonRpcNotificationHandler { } + + public class InvokeExtensionCommandParams : IRequest + { + public string Name { get; set; } + + public ClientEditorContext Context { get; set; } + } + + public class ClientEditorContext + { + public string CurrentFileContent { get; set; } + + public string CurrentFileLanguage { get; set; } + + public string CurrentFilePath { get; set; } + + public Position CursorPosition { get; set; } + + public Range SelectionRange { get; set; } + + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs new file mode 100644 index 000000000..fee04ff84 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Extensions; +using OmniSharp.Extensions.Embedded.MediatR; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + internal class InvokeExtensionCommandHandler : IInvokeExtensionCommandHandler + { + private readonly ILogger _logger; + private readonly ExtensionService _extensionService; + private readonly EditorOperationsService _editorOperationsService; + + public InvokeExtensionCommandHandler( + ILoggerFactory factory, + ExtensionService extensionService, + EditorOperationsService editorOperationsService + ) + { + _logger = factory.CreateLogger(); + _extensionService = extensionService; + _editorOperationsService = editorOperationsService; + } + + public async Task Handle(InvokeExtensionCommandParams request, CancellationToken cancellationToken) + { + // We can now await here because we handle asynchronous message handling. + EditorContext editorContext = + _editorOperationsService.ConvertClientEditorContext( + request.Context); + + await _extensionService.InvokeCommandAsync( + request.Name, + editorContext); + + return await Unit.Task; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs index b24e2c7aa..52b8b3411 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs @@ -1,19 +1,23 @@ using System.Collections.Generic; using System.Management.Automation; using System.Management.Automation.Runspaces; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; namespace PowerShellEditorServices.Engine.Services.Handlers { public class PSHostProcessAndRunspaceHandlers : IGetPSHostProcessesHandler, IGetRunspaceHandler { private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; - public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory) + public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory, PowerShellContextService powerShellContextService) { _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; } public Task Handle(GetPSHostProcesssesParams request, CancellationToken cancellationToken) @@ -51,11 +55,12 @@ public Task Handle(GetPSHostProcesssesParams request, C return Task.FromResult(psHostProcesses.ToArray()); } - public Task Handle(GetRunspaceParams request, CancellationToken cancellationToken) + public async Task Handle(GetRunspaceParams request, CancellationToken cancellationToken) { IEnumerable runspaces = null; - if (request.ProcessId == null) { + if (request.ProcessId == null) + { request.ProcessId = "current"; } @@ -64,8 +69,8 @@ public Task Handle(GetRunspaceParams request, CancellationTo if (int.TryParse(request.ProcessId, out int pid)) { // Create a remote runspace that we will invoke Get-Runspace in. - using(var rs = RunspaceFactory.CreateRunspace(new NamedPipeConnectionInfo(pid))) - using(var ps = PowerShell.Create()) + using (var rs = RunspaceFactory.CreateRunspace(new NamedPipeConnectionInfo(pid))) + using (var ps = PowerShell.Create()) { rs.Open(); ps.Runspace = rs; @@ -75,11 +80,10 @@ public Task Handle(GetRunspaceParams request, CancellationTo } else { - // TODO: Bring back - // var psCommand = new PSCommand().AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace"); - // var sb = new StringBuilder(); - // // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. - // runspaces = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand, sb); + var psCommand = new PSCommand().AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace"); + var sb = new StringBuilder(); + // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. + runspaces = await _powerShellContextService.ExecuteCommandAsync(psCommand, sb); } var runspaceResponses = new List(); @@ -98,7 +102,7 @@ public Task Handle(GetRunspaceParams request, CancellationTo } } - return Task.FromResult(runspaceResponses.ToArray()); + return runspaceResponses.ToArray(); } } } diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 92508adb2..468bfb62e 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -118,10 +118,6 @@ public List FindReferencesOfSymbol( return null; } - int symbolOffset = referencedFiles[0].GetOffsetAtPosition( - foundSymbol.ScriptRegion.StartLineNumber, - foundSymbol.ScriptRegion.StartColumnNumber); - // NOTE: we use to make sure aliases were loaded but took it out because we needed the pipeline thread. // We want to look for references first in referenced files, hence we use ordered dictionary diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs index 652bb9ce4..c22ea8d90 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -47,7 +47,7 @@ public async Task Handle(DidChangeConfigurationParams request, Cancellatio { return await Unit.Task; } - // TODO ADD THIS BACK IN + bool oldLoadProfiles = _configurationService.CurrentSettings.EnableProfileLoading; bool oldScriptAnalysisEnabled = _configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false; diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index edc53f684..1054c8268 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -98,7 +98,8 @@ Describe "Loading and running PowerShellEditorServices" { # This test MUST be first It "Starts and responds to an initialization request" { - $request = Send-LspInitializeRequest -Client $client + $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 @@ -301,7 +302,6 @@ Get-Bar -Uri ([Uri]::new($filePath).AbsoluteUri) ` -LineNumber 5 ` -CharacterNumber 0 - $response = Get-LspResponse -Client $client -Id $request.Id $response.Result.Count | Should -BeExactly 2 diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 34c3921fe..2d1f65aaa 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -54,7 +54,10 @@ PowerShellVersion = '5.1' # RequiredModules = @() # Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() +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 = @() diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 099007ad9..132e912a5 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -681,7 +681,7 @@ function Get-LspResponse [Parameter()] [int] - $WaitMillis = 5000 + $WaitMillis = 10000 ) $lspResponse = $null diff --git a/tools/PsesPsClient/build.ps1 b/tools/PsesPsClient/build.ps1 index a9da9a778..82874ab3e 100644 --- a/tools/PsesPsClient/build.ps1 +++ b/tools/PsesPsClient/build.ps1 @@ -19,6 +19,8 @@ $script:ModuleComponents = @{ "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" From 95dc4abc94acae2d5d4885ae40e4d861ea52736a Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 19 Aug 2019 16:44:31 -0700 Subject: [PATCH 29/47] Re-add Stdio option and replace Pester tests with xunit tests. (#1008) --- PowerShellEditorServices.build.ps1 | 35 +- PowerShellEditorServices.sln | 15 + .../Hosting/EditorServicesHost.cs | 55 +- .../LanguageServer/OmnisharpLanguageServer.cs | 44 +- .../OmnisharpLanguageServerBuilder.cs | 5 +- .../Session/Host/PromptEvents.cs | 48 + .../Session/Host/PromptHandlers.cs | 180 ++++ .../Host/ProtocolPSHostUserInterface.cs | 101 ++ .../EditorServices.Integration.Tests.ps1 | 532 ----------- .../LanguageServerProtocolMessageTests.cs | 611 +++++++++++++ .../PowerShellEditorServices.Test.E2E.csproj | 28 + .../TestsFixture.cs | 100 ++ .../xunit.runner.json | 4 + tools/PsesPsClient/Client.cs | 594 ------------ tools/PsesPsClient/PsesPsClient.csproj | 16 - tools/PsesPsClient/PsesPsClient.psd1 | 145 --- tools/PsesPsClient/PsesPsClient.psm1 | 865 ------------------ tools/PsesPsClient/build.ps1 | 51 -- 18 files changed, 1163 insertions(+), 2266 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs delete mode 100644 test/Pester/EditorServices.Integration.Tests.ps1 create mode 100644 test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs create mode 100644 test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj create mode 100644 test/PowerShellEditorServices.Test.E2E/TestsFixture.cs create mode 100644 test/PowerShellEditorServices.Test.E2E/xunit.runner.json delete mode 100644 tools/PsesPsClient/Client.cs delete mode 100644 tools/PsesPsClient/PsesPsClient.csproj delete mode 100644 tools/PsesPsClient/PsesPsClient.psd1 delete mode 100644 tools/PsesPsClient/PsesPsClient.psm1 delete mode 100644 tools/PsesPsClient/build.ps1 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 5cde53561..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 -} From b7e25313ecb15b088ac89247f4eae573dd7d6353 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Aug 2019 12:48:17 -0700 Subject: [PATCH 30/47] Completion Support (#1007) * completion support * misc codacy fixes * use BUILD_ARTIFACTSTAGINGDIRECTORY so logs can be uploaded * publish artifacts even if build fails * handle log messages * give PSES a chance to run what it needs to run * switch to using xUnit output helper * treat DynamicKeywords as Keyword --- .vsts-ci/templates/ci-general.yml | 1 + .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Services/Symbols/SymbolsService.cs | 2 - .../Services/Symbols/Vistors/AstOperations.cs | 180 ++++----- .../TextDocument/CompletionResults.cs | 341 ++++++++++++++++++ .../Handlers/CompletionHandler.cs | 295 +++++++++++++++ .../LanguageServerProtocolMessageTests.cs | 36 +- .../TestsFixture.cs | 15 +- 8 files changed, 780 insertions(+), 91 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs diff --git a/.vsts-ci/templates/ci-general.yml b/.vsts-ci/templates/ci-general.yml index 905802378..0ccaff133 100644 --- a/.vsts-ci/templates/ci-general.yml +++ b/.vsts-ci/templates/ci-general.yml @@ -23,3 +23,4 @@ steps: inputs: ArtifactName: PowerShellEditorServices-CI PathtoPublish: '$(Build.ArtifactStagingDirectory)' + condition: succeededOrFailed() diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index e40b1e60d..9f1152270 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -110,6 +110,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 468bfb62e..c9ab22463 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -21,8 +21,6 @@ public class SymbolsService { #region Private Fields - const int DefaultWaitTimeoutMilliseconds = 5000; - private readonly ILogger _logger; private readonly IDocumentSymbolProvider[] _documentSymbolProviders; diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs index 9e003c0d4..ede463fb3 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs @@ -5,8 +5,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Management.Automation; using System.Management.Automation.Language; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Symbols { @@ -17,11 +24,11 @@ internal static class AstOperations { // TODO: When netstandard is upgraded to 2.0, see if // Delegate.CreateDelegate can be used here instead - //private static readonly MethodInfo s_extentCloneWithNewOffset = typeof(PSObject).GetTypeInfo().Assembly - // .GetType("System.Management.Automation.Language.InternalScriptPosition") - // .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); + private static readonly MethodInfo s_extentCloneWithNewOffset = typeof(PSObject).GetTypeInfo().Assembly + .GetType("System.Management.Automation.Language.InternalScriptPosition") + .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); - //private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); // TODO: BRING THIS BACK /// @@ -48,89 +55,88 @@ internal static class AstOperations /// A CommandCompletion instance that contains completions for the /// symbol at the given offset. /// - // static public async Task GetCompletionsAsync( - // Ast scriptAst, - // Token[] currentTokens, - // int fileOffset, - // PowerShellContext powerShellContext, - // ILogger logger, - // CancellationToken cancellationToken) - // { - // if (!s_completionHandle.Wait(0)) - // { - // return null; - // } - - // try - // { - // IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( - // scriptAst.Extent.StartScriptPosition, - // new object[] { fileOffset }); - - // logger.Write( - // LogLevel.Verbose, - // string.Format( - // "Getting completions at offset {0} (line: {1}, column: {2})", - // fileOffset, - // cursorPosition.LineNumber, - // cursorPosition.ColumnNumber)); - - // if (!powerShellContext.IsAvailable) - // { - // return null; - // } - - // var stopwatch = new Stopwatch(); - - // // If the current runspace is out of process we can use - // // CommandCompletion.CompleteInput because PSReadLine won't be taking up the - // // main runspace. - // if (powerShellContext.IsCurrentRunspaceOutOfProcess()) - // { - // using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) - // using (PowerShell powerShell = PowerShell.Create()) - // { - // powerShell.Runspace = runspaceHandle.Runspace; - // stopwatch.Start(); - // try - // { - // return CommandCompletion.CompleteInput( - // scriptAst, - // currentTokens, - // cursorPosition, - // options: null, - // powershell: powerShell); - // } - // finally - // { - // stopwatch.Stop(); - // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - // } - // } - // } - - // CommandCompletion commandCompletion = null; - // await powerShellContext.InvokeOnPipelineThreadAsync( - // pwsh => - // { - // stopwatch.Start(); - // commandCompletion = CommandCompletion.CompleteInput( - // scriptAst, - // currentTokens, - // cursorPosition, - // options: null, - // powershell: pwsh); - // }); - // stopwatch.Stop(); - // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - - // return commandCompletion; - // } - // finally - // { - // s_completionHandle.Release(); - // } - // } + static public async Task GetCompletionsAsync( + Ast scriptAst, + Token[] currentTokens, + int fileOffset, + PowerShellContextService powerShellContext, + ILogger logger, + CancellationToken cancellationToken) + { + if (!s_completionHandle.Wait(0)) + { + return null; + } + + try + { + IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( + scriptAst.Extent.StartScriptPosition, + new object[] { fileOffset }); + + logger.LogTrace( + string.Format( + "Getting completions at offset {0} (line: {1}, column: {2})", + fileOffset, + cursorPosition.LineNumber, + cursorPosition.ColumnNumber)); + + if (!powerShellContext.IsAvailable) + { + return null; + } + + var stopwatch = new Stopwatch(); + + // If the current runspace is out of process we can use + // CommandCompletion.CompleteInput because PSReadLine won't be taking up the + // main runspace. + if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) + using (System.Management.Automation.PowerShell powerShell = System.Management.Automation.PowerShell.Create()) + { + powerShell.Runspace = runspaceHandle.Runspace; + stopwatch.Start(); + try + { + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: powerShell); + } + finally + { + stopwatch.Stop(); + logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + } + } + } + + CommandCompletion commandCompletion = null; + await powerShellContext.InvokeOnPipelineThreadAsync( + pwsh => + { + stopwatch.Start(); + commandCompletion = CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: pwsh); + }); + stopwatch.Stop(); + logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + + return commandCompletion; + } + finally + { + s_completionHandle.Release(); + } + } /// /// Finds the symbol at a given file location diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs new file mode 100644 index 000000000..3f59eccfe --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs @@ -0,0 +1,341 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management.Automation; +using System.Text.RegularExpressions; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides the results of a single code completion request. + /// + public sealed class CompletionResults + { + #region Properties + + /// + /// Gets the completions that were found during the + /// completion request. + /// + public CompletionDetails[] Completions { get; private set; } + + /// + /// Gets the range in the buffer that should be replaced by this + /// completion result. + /// + public BufferRange ReplacedRange { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an empty CompletionResults instance. + /// + public CompletionResults() + { + this.Completions = new CompletionDetails[0]; + this.ReplacedRange = new BufferRange(0, 0, 0, 0); + } + + internal static CompletionResults Create( + ScriptFile scriptFile, + CommandCompletion commandCompletion) + { + BufferRange replacedRange = null; + + // Only calculate the replacement range if there are completion results + if (commandCompletion.CompletionMatches.Count > 0) + { + replacedRange = + scriptFile.GetRangeBetweenOffsets( + commandCompletion.ReplacementIndex, + commandCompletion.ReplacementIndex + commandCompletion.ReplacementLength); + } + + return new CompletionResults + { + Completions = GetCompletionsArray(commandCompletion), + ReplacedRange = replacedRange + }; + } + + #endregion + + #region Private Methods + + private static CompletionDetails[] GetCompletionsArray( + CommandCompletion commandCompletion) + { + IEnumerable completionList = + commandCompletion.CompletionMatches.Select( + CompletionDetails.Create); + + return completionList.ToArray(); + } + + #endregion + } + + /// + /// Enumerates the completion types that may be returned. + /// + public enum CompletionType + { + /// + /// Completion type is unknown, either through being uninitialized or + /// having been created from an unsupported CompletionResult that was + /// returned by the PowerShell engine. + /// + Unknown = 0, + + /// + /// Identifies a completion for a command. + /// + Command, + + /// + /// Identifies a completion for a .NET method. + /// + Method, + + /// + /// Identifies a completion for a command parameter name. + /// + ParameterName, + + /// + /// Identifies a completion for a command parameter value. + /// + ParameterValue, + + /// + /// Identifies a completion for a .NET property. + /// + Property, + + /// + /// Identifies a completion for a variable name. + /// + Variable, + + /// + /// Identifies a completion for a namespace. + /// + Namespace, + + /// + /// Identifies a completion for a .NET type name. + /// + Type, + + /// + /// Identifies a completion for a PowerShell language keyword. + /// + Keyword, + + /// + /// Identifies a completion for a provider path (like a file system path) to a leaf item. + /// + File, + + /// + /// Identifies a completion for a provider path (like a file system path) to a container. + /// + Folder + } + + /// + /// Provides the details about a single completion result. + /// + [DebuggerDisplay("CompletionType = {CompletionType.ToString()}, CompletionText = {CompletionText}")] + public sealed class CompletionDetails + { + #region Properties + + /// + /// Gets the text that will be used to complete the statement + /// at the requested file offset. + /// + public string CompletionText { get; private set; } + + /// + /// Gets the text that should be dispayed in a drop-down completion list. + /// + public string ListItemText { get; private set; } + + /// + /// Gets the text that can be used to display a tooltip for + /// the statement at the requested file offset. + /// + public string ToolTipText { get; private set; } + + /// + /// Gets the name of the type which this symbol represents. + /// If the symbol doesn't have an inherent type, null will + /// be returned. + /// + public string SymbolTypeName { get; private set; } + + /// + /// Gets the CompletionType which identifies the type of this completion. + /// + public CompletionType CompletionType { get; private set; } + + #endregion + + #region Constructors + + internal static CompletionDetails Create(CompletionResult completionResult) + { + Validate.IsNotNull("completionResult", completionResult); + + // Some tooltips may have newlines or whitespace for unknown reasons + string toolTipText = completionResult.ToolTip; + if (toolTipText != null) + { + toolTipText = toolTipText.Trim(); + } + + return new CompletionDetails + { + CompletionText = completionResult.CompletionText, + ListItemText = completionResult.ListItemText, + ToolTipText = toolTipText, + SymbolTypeName = ExtractSymbolTypeNameFromToolTip(completionResult.ToolTip), + CompletionType = + ConvertCompletionResultType( + completionResult.ResultType) + }; + } + + internal static CompletionDetails Create( + string completionText, + CompletionType completionType, + string toolTipText = null, + string symbolTypeName = null, + string listItemText = null) + { + return new CompletionDetails + { + CompletionText = completionText, + CompletionType = completionType, + ListItemText = listItemText, + ToolTipText = toolTipText, + SymbolTypeName = symbolTypeName + }; + } + + #endregion + + #region Public Methods + + /// + /// Compares two CompletionResults instances for equality. + /// + /// The potential CompletionResults instance to compare. + /// True if the CompletionResults instances have the same details. + public override bool Equals(object obj) + { + CompletionDetails otherDetails = obj as CompletionDetails; + if (otherDetails == null) + { + return false; + } + + return + string.Equals(this.CompletionText, otherDetails.CompletionText) && + this.CompletionType == otherDetails.CompletionType && + string.Equals(this.ToolTipText, otherDetails.ToolTipText) && + string.Equals(this.SymbolTypeName, otherDetails.SymbolTypeName); + } + + /// + /// Returns the hash code for this CompletionResults instance. + /// + /// The hash code for this CompletionResults instance. + public override int GetHashCode() + { + return + string.Format( + "{0}{1}{2}{3}{4}", + this.CompletionText, + this.CompletionType, + this.ListItemText, + this.ToolTipText, + this.SymbolTypeName).GetHashCode(); + } + + #endregion + + #region Private Methods + + private static CompletionType ConvertCompletionResultType( + CompletionResultType completionResultType) + { + switch (completionResultType) + { + case CompletionResultType.Command: + return CompletionType.Command; + + case CompletionResultType.Method: + return CompletionType.Method; + + case CompletionResultType.ParameterName: + return CompletionType.ParameterName; + + case CompletionResultType.ParameterValue: + return CompletionType.ParameterValue; + + case CompletionResultType.Property: + return CompletionType.Property; + + case CompletionResultType.Variable: + return CompletionType.Variable; + + case CompletionResultType.Namespace: + return CompletionType.Namespace; + + case CompletionResultType.Type: + return CompletionType.Type; + + case CompletionResultType.Keyword: + case CompletionResultType.DynamicKeyword: + return CompletionType.Keyword; + + case CompletionResultType.ProviderContainer: + return CompletionType.Folder; + + case CompletionResultType.ProviderItem: + return CompletionType.File; + + default: + // TODO: Trace the unsupported CompletionResultType + return CompletionType.Unknown; + } + } + + private static string ExtractSymbolTypeNameFromToolTip(string toolTipText) + { + // Tooltips returned from PowerShell contain the symbol type in + // brackets. Attempt to extract such strings for further processing. + var matches = Regex.Matches(toolTipText, @"^\[(.+)\]"); + + if (matches.Count > 0 && matches[0].Groups.Count > 1) + { + // Return the symbol type name + return matches[0].Groups[1].Value; + } + + return null; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs new file mode 100644 index 000000000..e66a5d709 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs @@ -0,0 +1,295 @@ +// +// 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.Management.Automation; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace Microsoft.PowerShell.EditorServices.TextDocument +{ + internal class CompletionHandler : ICompletionHandler + { + const int DefaultWaitTimeoutMilliseconds = 5000; + private readonly CompletionItem[] s_emptyCompletionResult = new CompletionItem[0]; + + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + private readonly WorkspaceService _workspaceService; + + private CompletionResults _mostRecentCompletions; + + private int _mostRecentRequestLine; + + private int _mostRecentRequestOffest; + + private string _mostRecentRequestFile; + + private CompletionCapability _capability; + + public CompletionHandler( + ILoggerFactory factory, + PowerShellContextService powerShellContextService, + WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + _workspaceService = workspaceService; + } + + public CompletionRegistrationOptions GetRegistrationOptions() + { + return new CompletionRegistrationOptions + { + DocumentSelector = new DocumentSelector(new DocumentFilter { Pattern = "**/*.ps*1" }), + ResolveProvider = true, + TriggerCharacters = new[] { ".", "-", ":", "\\" } + }; + } + + public async Task Handle(CompletionParams request, CancellationToken cancellationToken) + { + int cursorLine = (int) request.Position.Line + 1; + int cursorColumn = (int) request.Position.Character + 1; + + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + CompletionResults completionResults = + await GetCompletionsInFileAsync( + scriptFile, + cursorLine, + cursorColumn); + + CompletionItem[] completionItems = s_emptyCompletionResult; + + if (completionResults != null) + { + completionItems = new CompletionItem[completionResults.Completions.Length]; + for (int i = 0; i < completionItems.Length; i++) + { + completionItems[i] = CreateCompletionItem(completionResults.Completions[i], completionResults.ReplacedRange, i + 1); + } + } + + return new CompletionList(completionItems); + } + + public void SetCapability(CompletionCapability capability) + { + _capability = capability; + } + + /// + /// Gets completions for a statement contained in the given + /// script file at the specified line and column position. + /// + /// + /// The script file in which completions will be gathered. + /// + /// + /// The 1-based line number at which completions will be gathered. + /// + /// + /// The 1-based column number at which completions will be gathered. + /// + /// + /// A CommandCompletion instance completions for the identified statement. + /// + public async Task GetCompletionsInFileAsync( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + Validate.IsNotNull(nameof(scriptFile), scriptFile); + + // Get the offset at the specified position. This method + // will also validate the given position. + int fileOffset = + scriptFile.GetOffsetAtPosition( + lineNumber, + columnNumber); + + CommandCompletion commandCompletion = null; + using (var cts = new CancellationTokenSource(DefaultWaitTimeoutMilliseconds)) + { + commandCompletion = + await AstOperations.GetCompletionsAsync( + scriptFile.ScriptAst, + scriptFile.ScriptTokens, + fileOffset, + _powerShellContextService, + _logger, + cts.Token); + } + + if (commandCompletion == null) + { + return new CompletionResults(); + } + + try + { + CompletionResults completionResults = + CompletionResults.Create( + scriptFile, + commandCompletion); + + // save state of most recent completion + _mostRecentCompletions = completionResults; + _mostRecentRequestFile = scriptFile.Id; + _mostRecentRequestLine = lineNumber; + _mostRecentRequestOffest = columnNumber; + + return completionResults; + } + catch (ArgumentException e) + { + // Bad completion results could return an invalid + // replacement range, catch that here + _logger.LogError( + $"Caught exception while trying to create CompletionResults:\n\n{e.ToString()}"); + + return new CompletionResults(); + } + } + + private static CompletionItem CreateCompletionItem( + CompletionDetails completionDetails, + BufferRange completionRange, + int sortIndex) + { + string detailString = null; + string documentationString = null; + string completionText = completionDetails.CompletionText; + InsertTextFormat insertTextFormat = InsertTextFormat.PlainText; + + if ((completionDetails.CompletionType == CompletionType.Variable) || + (completionDetails.CompletionType == CompletionType.ParameterName)) + { + // Look for type encoded in the tooltip for parameters and variables. + // Display PowerShell type names in [] to be consistent with PowerShell syntax + // and now the debugger displays type names. + var matches = Regex.Matches(completionDetails.ToolTipText, @"^(\[.+\])"); + if ((matches.Count > 0) && (matches[0].Groups.Count > 1)) + { + detailString = matches[0].Groups[1].Value; + } + } + else if ((completionDetails.CompletionType == CompletionType.Method) || + (completionDetails.CompletionType == CompletionType.Property)) + { + // We have a raw signature for .NET members, heck let's display it. It's + // better than nothing. + documentationString = completionDetails.ToolTipText; + } + else if (completionDetails.CompletionType == CompletionType.Command) + { + // For Commands, let's extract the resolved command or the path for an exe + // from the ToolTipText - if there is any ToolTipText. + if (completionDetails.ToolTipText != null) + { + // Fix for #240 - notepad++.exe in tooltip text caused regex parser to throw. + string escapedToolTipText = Regex.Escape(completionDetails.ToolTipText); + + // Don't display ToolTipText if it is the same as the ListItemText. + // Reject command syntax ToolTipText - it's too much to display as a detailString. + if (!completionDetails.ListItemText.Equals( + completionDetails.ToolTipText, + StringComparison.OrdinalIgnoreCase) && + !Regex.IsMatch(completionDetails.ToolTipText, + @"^\s*" + escapedToolTipText + @"\s+\[")) + { + detailString = completionDetails.ToolTipText; + } + } + } + else if ((completionDetails.CompletionType == CompletionType.Folder) && + (completionText.EndsWith("\"") || completionText.EndsWith("'"))) + { + // Insert a final "tab stop" as identified by $0 in the snippet provided for completion. + // For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert + // the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'. + // This causes the editing cursor to be placed *before* the final quote after completion, + // which makes subsequent path completions work. See this part of the LSP spec for details: + // https://microsoft.github.io/language-server-protocol/specification#textDocument_completion + int len = completionDetails.CompletionText.Length; + completionText = completionDetails.CompletionText.Insert(len - 1, "$0"); + insertTextFormat = InsertTextFormat.Snippet; + } + + // Force the client to maintain the sort order in which the + // original completion results were returned. We just need to + // make sure the default order also be the lexicographical order + // which we do by prefixing the ListItemText with a leading 0's + // four digit index. + var sortText = $"{sortIndex:D4}{completionDetails.ListItemText}"; + + return new CompletionItem + { + InsertText = completionText, + InsertTextFormat = insertTextFormat, + Label = completionDetails.ListItemText, + Kind = MapCompletionKind(completionDetails.CompletionType), + Detail = detailString, + Documentation = documentationString, + SortText = sortText, + FilterText = completionDetails.CompletionText, + TextEdit = new TextEdit + { + NewText = completionText, + Range = new Range + { + Start = new Position + { + Line = completionRange.Start.Line - 1, + Character = completionRange.Start.Column - 1 + }, + End = new Position + { + Line = completionRange.End.Line - 1, + Character = completionRange.End.Column - 1 + } + } + } + }; + } + + private static CompletionItemKind MapCompletionKind(CompletionType completionType) + { + switch (completionType) + { + case CompletionType.Command: + return CompletionItemKind.Function; + + case CompletionType.Property: + return CompletionItemKind.Property; + + case CompletionType.Method: + return CompletionItemKind.Method; + + case CompletionType.Variable: + case CompletionType.ParameterName: + return CompletionItemKind.Variable; + + case CompletionType.File: + return CompletionItemKind.File; + + case CompletionType.Folder: + return CompletionItemKind.Folder; + + default: + return CompletionItemKind.Text; + } + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 6c169c1bf..7c5bd52b9 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -11,6 +11,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using PowerShellEditorServices.Engine.Services.Handlers; using Xunit; +using Xunit.Abstractions; using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; namespace PowerShellEditorServices.Test.E2E @@ -20,17 +21,32 @@ public class LanguageServerProtocolMessageTests : IClassFixture, I private readonly static string s_binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + private static bool s_registeredOnLogMessage; + private readonly LanguageClient LanguageClient; private readonly List Diagnostics; private readonly string PwshExe; + private readonly ITestOutputHelper _output; - public LanguageServerProtocolMessageTests(TestsFixture data) + public LanguageServerProtocolMessageTests(ITestOutputHelper output, TestsFixture data) { Diagnostics = new List(); LanguageClient = data.LanguageClient; Diagnostics = data.Diagnostics; PwshExe = TestsFixture.PwshExe; Diagnostics.Clear(); + + _output = output; + + if (!s_registeredOnLogMessage) + { + LanguageClient.Window.OnLogMessage((message, messageType) => + { + _output.WriteLine($"{messageType.ToString()}: {message}"); + }); + + s_registeredOnLogMessage = true; + } } public void Dispose() @@ -55,6 +71,9 @@ private string NewTestFile(string script, bool isPester = false) } }); + // Give PSES a chance to run what it needs to run. + Thread.Sleep(1000); + return filePath; } @@ -607,5 +626,20 @@ await LanguageClient.SendRequest( Assert.Single(commandOrCodeActions, command => command.Command.Name == "PowerShell.ApplyCodeActionEdits"); } + + [Fact] + public async Task CanSendCompletionRequest() + { + string filePath = NewTestFile("Write-H"); + + CompletionList completionItems = await LanguageClient.TextDocument.Completions( + filePath, line: 0, column: 7); + + Assert.Collection(completionItems, + completionItem1 => { + Assert.Equal("Write-Host", completionItem1.Label); + } + ); + } } } diff --git a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs index a9bd12195..092a8799a 100644 --- a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs +++ b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Client; using OmniSharp.Extensions.LanguageServer.Client.Processes; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -27,7 +28,7 @@ public class TestsFixture : IAsyncLifetime $"pses_test_sessiondetails_{Path.GetRandomFileName()}"); private readonly static string s_logPath = Path.Combine( - s_binDir, + Environment.GetEnvironmentVariable("BUILD_ARTIFACTSTAGINGDIRECTORY") ?? s_binDir, $"pses_test_logs_{Path.GetRandomFileName()}"); const string s_logLevel = "Diagnostic"; @@ -81,8 +82,20 @@ public async Task InitializeAsync() DirectoryInfo testdir = Directory.CreateDirectory(Path.Combine(s_binDir, Path.GetRandomFileName())); + await LanguageClient.Initialize(testdir.FullName); + // Make sure Script Analysis is enabled because we'll need it in the tests. + LanguageClient.Workspace.DidChangeConfiguration(JObject.Parse(@" +{ + ""PowerShell"": { + ""ScriptAnalysis"": { + ""Enable"": true + } + } +} +")); + Diagnostics = new List(); LanguageClient.TextDocument.OnPublishDiagnostics((uri, diagnostics) => { From b61952dbca46ec42dbf2ced82656dba886d863be Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Aug 2019 14:35:48 -0700 Subject: [PATCH 31/47] completionresolve support (#1009) * handle log messages * switch to using xUnit output helper * Add completionItem/resolve request * feedback * update build to run update-help for signature help test * removing scope hoping it works in CI * setting to EA silentlycontinue * change to language=powershell --- scripts/azurePipelinesBuild.ps1 | 3 + .../LanguageServer/OmnisharpLanguageServer.cs | 4 +- .../Utilities/CommandHelpers.cs | 112 ++++++++++++++++++ .../Services/Symbols/Vistors/AstOperations.cs | 18 +-- .../TextDocument/CompletionResults.cs | 3 +- .../Handlers/CodeActionHandler.cs | 2 +- .../TextDocument/Handlers/CodeLensHandlers.cs | 2 +- .../Handlers/CompletionHandler.cs | 44 ++++++- .../Handlers/DocumentHighlightHandler.cs | 2 +- .../Handlers/DocumentSymbolHandler.cs | 2 +- .../Handlers/FoldingRangeHandler.cs | 2 +- .../Handlers/FormattingHandlers.cs | 2 +- .../Handlers/ReferencesHandler.cs | 2 +- .../Handlers/TextDocumentHandler.cs | 2 +- .../LanguageServerProtocolMessageTests.cs | 15 ++- 15 files changed, 184 insertions(+), 31 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs diff --git a/scripts/azurePipelinesBuild.ps1 b/scripts/azurePipelinesBuild.ps1 index ab31ed358..774f57815 100644 --- a/scripts/azurePipelinesBuild.ps1 +++ b/scripts/azurePipelinesBuild.ps1 @@ -10,6 +10,9 @@ if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) { Import-Module -Name PackageManagement -MinimumVersion 1.1.7.0 -Force } +# Update help needed for SignatureHelp LSP request. +Update-Help -Force -ErrorAction SilentlyContinue + # Needed for build and docs gen. Install-Module InvokeBuild -MaximumVersion 5.1.0 -Scope CurrentUser Install-Module PlatyPS -RequiredVersion 0.9.0 -Scope CurrentUser diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 9f1152270..807b5bcaa 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -62,6 +62,8 @@ public async Task StartAsync() { _languageServer = await OS.LanguageServer.From(options => { + options.AddDefaultLoggingProvider(); + options.LoggerFactory = _configuration.LoggerFactory; ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); if (_configuration.Stdio) @@ -89,10 +91,8 @@ public async Task StartAsync() options.Output = outNamedPipe ?? namedPipe; } - options.LoggerFactory = _configuration.LoggerFactory; options.MinimumLogLevel = _configuration.MinimumLogLevel; options.Services = _configuration.Services; - logger.LogInformation("Adding handlers"); options diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs new file mode 100644 index 000000000..a73689dd4 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Linq; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides utility methods for working with PowerShell commands. + /// + public static class CommandHelpers + { + private static readonly ConcurrentDictionary NounExclusionList = + new ConcurrentDictionary(); + + static CommandHelpers() + { + NounExclusionList.TryAdd("Module", true); + NounExclusionList.TryAdd("Script", true); + NounExclusionList.TryAdd("Package", true); + NounExclusionList.TryAdd("PackageProvider", true); + NounExclusionList.TryAdd("PackageSource", true); + NounExclusionList.TryAdd("InstalledModule", true); + NounExclusionList.TryAdd("InstalledScript", true); + NounExclusionList.TryAdd("ScriptFileInfo", true); + NounExclusionList.TryAdd("PSRepository", true); + } + + /// + /// Gets the CommandInfo instance for a command with a particular name. + /// + /// The name of the command. + /// The PowerShellContext to use for running Get-Command. + /// A CommandInfo object with details about the specified command. + public static async Task GetCommandInfoAsync( + string commandName, + PowerShellContextService powerShellContext) + { + Validate.IsNotNull(nameof(commandName), commandName); + + // Make sure the command's noun isn't blacklisted. This is + // currently necessary to make sure that Get-Command doesn't + // load PackageManagement or PowerShellGet because they cause + // a major slowdown in IntelliSense. + var commandParts = commandName.Split('-'); + if (commandParts.Length == 2 && NounExclusionList.ContainsKey(commandParts[1])) + { + return null; + } + + PSCommand command = new PSCommand(); + command.AddCommand(@"Microsoft.PowerShell.Core\Get-Command"); + command.AddArgument(commandName); + command.AddParameter("ErrorAction", "Ignore"); + + return + (await powerShellContext + .ExecuteCommandAsync(command, false, false)) + .Select(o => o.BaseObject) + .OfType() + .FirstOrDefault(); + } + + /// + /// Gets the command's "Synopsis" documentation section. + /// + /// The CommandInfo instance for the command. + /// The PowerShellContext to use for getting command documentation. + /// + public static async Task GetCommandSynopsisAsync( + CommandInfo commandInfo, + PowerShellContextService powerShellContext) + { + string synopsisString = string.Empty; + + if (commandInfo != null && + (commandInfo.CommandType == CommandTypes.Cmdlet || + commandInfo.CommandType == CommandTypes.Function || + commandInfo.CommandType == CommandTypes.Filter)) + { + PSCommand command = new PSCommand(); + command.AddCommand(@"Microsoft.PowerShell.Core\Get-Help"); + command.AddArgument(commandInfo); + command.AddParameter("ErrorAction", "Ignore"); + + var results = await powerShellContext.ExecuteCommandAsync(command, false, false); + PSObject helpObject = results.FirstOrDefault(); + + if (helpObject != null) + { + // Extract the synopsis string from the object + synopsisString = + (string)helpObject.Properties["synopsis"].Value ?? + string.Empty; + + // Ignore the placeholder value for this field + if (string.Equals(synopsisString, "SHORT DESCRIPTION", System.StringComparison.CurrentCultureIgnoreCase)) + { + synopsisString = string.Empty; + } + } + } + + return synopsisString; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs index ede463fb3..fd54cc7d1 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs @@ -55,7 +55,7 @@ internal static class AstOperations /// A CommandCompletion instance that contains completions for the /// symbol at the given offset. /// - static public async Task GetCompletionsAsync( + public static async Task GetCompletionsAsync( Ast scriptAst, Token[] currentTokens, int fileOffset, @@ -146,7 +146,7 @@ await powerShellContext.InvokeOnPipelineThreadAsync( /// The coulumn number of the cursor for the given script /// Includes full function definition ranges in the search. /// SymbolReference of found symbol - static public SymbolReference FindSymbolAtPosition( + public static SymbolReference FindSymbolAtPosition( Ast scriptAst, int lineNumber, int columnNumber, @@ -170,7 +170,7 @@ static public SymbolReference FindSymbolAtPosition( /// The line number of the cursor for the given script /// The column number of the cursor for the given script /// SymbolReference of found command - static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber) + public static SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber) { FindCommandVisitor commandVisitor = new FindCommandVisitor(lineNumber, columnNumber); scriptAst.Visit(commandVisitor); @@ -186,7 +186,7 @@ static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumbe /// Dictionary maping cmdlets to aliases for finding alias references /// Dictionary maping aliases to cmdlets for finding alias references /// - static public IEnumerable FindReferencesOfSymbol( + public static IEnumerable FindReferencesOfSymbol( Ast scriptAst, SymbolReference symbolReference, Dictionary> CmdletToAliasDictionary, @@ -212,7 +212,7 @@ static public IEnumerable FindReferencesOfSymbol( /// This should always be false and used for occurence requests /// A collection of SymbolReference objects that are refrences to the symbolRefrence /// not including aliases - static public IEnumerable FindReferencesOfSymbol( + public static IEnumerable FindReferencesOfSymbol( ScriptBlockAst scriptAst, SymbolReference foundSymbol, bool needsAliases) @@ -230,7 +230,7 @@ static public IEnumerable FindReferencesOfSymbol( /// The abstract syntax tree of the given script /// The symbol that we are looking for the definition of /// A SymbolReference of the definition of the symbolReference - static public SymbolReference FindDefinitionOfSymbol( + public static SymbolReference FindDefinitionOfSymbol( Ast scriptAst, SymbolReference symbolReference) { @@ -248,7 +248,7 @@ static public SymbolReference FindDefinitionOfSymbol( /// The abstract syntax tree of the given script /// The PowerShell version the Ast was generated from /// A collection of SymbolReference objects - static public IEnumerable FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion) + public static IEnumerable FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion) { IEnumerable symbolReferences = null; @@ -275,7 +275,7 @@ static public IEnumerable FindSymbolsInDocument(Ast scriptAst, /// /// The abstract syntax tree of the given script /// true if the AST represts a *.psd1 file, otherwise false - static public bool IsPowerShellDataFileAst(Ast ast) + public static bool IsPowerShellDataFileAst(Ast ast) { // sometimes we don't have reliable access to the filename // so we employ heuristics to check if the contents are @@ -330,7 +330,7 @@ static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap /// The abstract syntax tree of the given script /// Pre-calculated value of $PSScriptRoot /// - static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot) + public static string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot) { FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(psScriptRoot); scriptAst.Visit(dotSourcedVisitor); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs index 3f59eccfe..433d4e5c3 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs @@ -243,8 +243,7 @@ internal static CompletionDetails Create( /// True if the CompletionResults instances have the same details. public override bool Equals(object obj) { - CompletionDetails otherDetails = obj as CompletionDetails; - if (otherDetails == null) + if (!(obj is CompletionDetails otherDetails)) { return false; } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs index 64f994423..7afd6f37e 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -35,7 +35,7 @@ public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService _analysisService = analysisService; _registrationOptions = new CodeActionRegistrationOptions() { - DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }), + DocumentSelector = new DocumentSelector(new DocumentFilter() { Language = "powershell" }), CodeActionKinds = s_supportedCodeActions }; } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs index 463fbf767..f83e35d18 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -19,7 +19,7 @@ public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs index e66a5d709..0f4d81406 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerShell.EditorServices.TextDocument { - internal class CompletionHandler : ICompletionHandler + internal class CompletionHandler : ICompletionHandler, ICompletionResolveHandler { const int DefaultWaitTimeoutMilliseconds = 5000; private readonly CompletionItem[] s_emptyCompletionResult = new CompletionItem[0]; @@ -49,7 +49,7 @@ public CompletionRegistrationOptions GetRegistrationOptions() { return new CompletionRegistrationOptions { - DocumentSelector = new DocumentSelector(new DocumentFilter { Pattern = "**/*.ps*1" }), + DocumentSelector = new DocumentSelector(new DocumentFilter { Language = "powershell" }), ResolveProvider = true, TriggerCharacters = new[] { ".", "-", ":", "\\" } }; @@ -84,6 +84,32 @@ await GetCompletionsInFileAsync( return new CompletionList(completionItems); } + public bool CanResolve(CompletionItem value) + { + return value.Kind == CompletionItemKind.Function; + } + + // Handler for "completionItem/resolve". In VSCode this is fired when a completion item is highlighted in the completion list. + public async Task Handle(CompletionItem request, CancellationToken cancellationToken) + { + // Get the documentation for the function + CommandInfo commandInfo = + await CommandHelpers.GetCommandInfoAsync( + request.Label, + _powerShellContextService); + + if (commandInfo != null) + { + request.Documentation = + await CommandHelpers.GetCommandSynopsisAsync( + commandInfo, + _powerShellContextService); + } + + // Send back the updated CompletionItem + return request; + } + public void SetCapability(CompletionCapability capability) { _capability = capability; @@ -213,8 +239,7 @@ private static CompletionItem CreateCompletionItem( } } } - else if ((completionDetails.CompletionType == CompletionType.Folder) && - (completionText.EndsWith("\"") || completionText.EndsWith("'"))) + else if (completionDetails.CompletionType == CompletionType.Folder && EndsWithQuote(completionText)) { // Insert a final "tab stop" as identified by $0 in the snippet provided for completion. // For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert @@ -291,5 +316,16 @@ private static CompletionItemKind MapCompletionKind(CompletionType completionTyp return CompletionItemKind.Text; } } + + private static bool EndsWithQuote(string text) + { + if (string.IsNullOrEmpty(text)) + { + return false; + } + + char lastChar = text[text.Length - 1]; + return lastChar == '"' || lastChar == '\''; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs index aafc1c79f..838d40331 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -39,7 +39,7 @@ public DocumentHighlightHandler( _symbolsService = symbolService; _registrationOptions = new TextDocumentRegistrationOptions() { - DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" } ) + DocumentSelector = new DocumentSelector(new DocumentFilter() { Language = "powershell" } ) }; _logger.LogInformation("highlight handler loaded"); } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index e5289746c..6cbe8ace7 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -20,7 +20,7 @@ public class DocumentSymbolHandler : IDocumentSymbolHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs index 9aee91b0e..928eb5d03 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs @@ -14,7 +14,7 @@ public class FoldingRangeHandler : IFoldingRangeHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs index 57c0744a1..4565d7ff8 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -15,7 +15,7 @@ internal class DocumentFormattingHandler : IDocumentFormattingHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs index 6e10d5fe8..d49f7f8d2 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -16,7 +16,7 @@ class ReferencesHandler : IReferencesHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index 34f5358b9..d7ec17827 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -24,7 +24,7 @@ class TextDocumentHandler : ITextDocumentSyncHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 7c5bd52b9..a29563f70 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -628,18 +628,21 @@ await LanguageClient.SendRequest( } [Fact] - public async Task CanSendCompletionRequest() + public async Task CanSendCompletionAndCompletionResolveRequest() { string filePath = NewTestFile("Write-H"); CompletionList completionItems = await LanguageClient.TextDocument.Completions( filePath, line: 0, column: 7); - Assert.Collection(completionItems, - completionItem1 => { - Assert.Equal("Write-Host", completionItem1.Label); - } - ); + CompletionItem completionItem = Assert.Single(completionItems, + completionItem1 => completionItem1.Label == "Write-Host"); + + CompletionItem updatedCompletionItem = await LanguageClient.SendRequest( + "completionItem/resolve", + completionItem); + + Assert.Contains("Writes customized output to a host", updatedCompletionItem.Documentation.String); } } } From 53b79e627dd29337267db5ee1927410e7037575b Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Aug 2019 20:17:08 -0700 Subject: [PATCH 32/47] hover support (#1010) * handle log messages * switch to using xUnit output helper * add hover handler * move to language=powershell * refactoring for feedback * codacy --- .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Services/Symbols/SymbolDetails.cs | 90 +++++++++++++++ .../Services/Symbols/SymbolsService.cs | 39 ++++++- .../TextDocument/Handlers/HoverHandler.cs | 104 ++++++++++++++++++ .../LanguageServerProtocolMessageTests.cs | 20 ++++ 5 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 807b5bcaa..6908cc766 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -111,6 +111,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs new file mode 100644 index 000000000..3604481cc --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; +using System.Management.Automation; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides detailed information for a given symbol. + /// + [DebuggerDisplay("SymbolReference = {SymbolReference.SymbolType}/{SymbolReference.SymbolName}, DisplayString = {DisplayString}")] + public class SymbolDetails + { + #region Properties + + /// + /// Gets the original symbol reference which was used to gather details. + /// + public SymbolReference SymbolReference { get; private set; } + + /// + /// Gets the display string for this symbol. + /// + public string DisplayString { get; private set; } + + /// + /// Gets the documentation string for this symbol. Returns an + /// empty string if the symbol has no documentation. + /// + public string Documentation { get; private set; } + + #endregion + + #region Constructors + + static internal async Task CreateAsync( + SymbolReference symbolReference, + PowerShellContextService powerShellContext) + { + SymbolDetails symbolDetails = new SymbolDetails + { + SymbolReference = symbolReference + }; + + switch (symbolReference.SymbolType) + { + case SymbolType.Function: + CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( + symbolReference.SymbolName, + powerShellContext); + + if (commandInfo != null) + { + symbolDetails.Documentation = + await CommandHelpers.GetCommandSynopsisAsync( + commandInfo, + powerShellContext); + + if (commandInfo.CommandType == CommandTypes.Application) + { + symbolDetails.DisplayString = "(application) " + symbolReference.SymbolName; + return symbolDetails; + } + } + + symbolDetails.DisplayString = "function " + symbolReference.SymbolName; + return symbolDetails; + + case SymbolType.Parameter: + // TODO: Get parameter help + symbolDetails.DisplayString = "(parameter) " + symbolReference.SymbolName; + return symbolDetails; + + case SymbolType.Variable: + symbolDetails.DisplayString = symbolReference.SymbolName; + return symbolDetails; + + default: + return symbolDetails; + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index c9ab22463..e0c1e57be 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -8,6 +8,7 @@ using System.Collections.Specialized; using System.Linq; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; @@ -22,7 +23,7 @@ public class SymbolsService #region Private Fields private readonly ILogger _logger; - + private readonly PowerShellContextService _powerShellContextService; private readonly IDocumentSymbolProvider[] _documentSymbolProviders; #endregion @@ -35,9 +36,11 @@ public class SymbolsService /// /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( - ILoggerFactory factory) + ILoggerFactory factory, + PowerShellContextService powerShellContextService) { _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; _documentSymbolProviders = new IDocumentSymbolProvider[] { new ScriptDocumentSymbolProvider(VersionUtils.PSVersion), @@ -200,6 +203,7 @@ public IReadOnlyList FindOccurrencesInFile( needsAliases: false).ToArray(); } + /// /// Finds a function definition in the script given a file location /// /// The details and contents of a open script file @@ -227,5 +231,36 @@ public SymbolReference FindFunctionDefinitionAtLocation( return symbolReference; } + + /// + /// Finds the details of the symbol at the given script file location. + /// + /// The ScriptFile in which the symbol can be located. + /// The line number at which the symbol can be located. + /// The column number at which the symbol can be located. + /// + public async Task FindSymbolDetailsAtLocationAsync( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + SymbolReference symbolReference = + AstOperations.FindSymbolAtPosition( + scriptFile.ScriptAst, + lineNumber, + columnNumber); + + if (symbolReference == null) + { + return null; + } + + symbolReference.FilePath = scriptFile.FilePath; + SymbolDetails symbolDetails = await SymbolDetails.CreateAsync( + symbolReference, + _powerShellContextService); + + return symbolDetails; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs new file mode 100644 index 000000000..633ef2900 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class HoverHandler : IHoverHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter + { + Language = "powershell" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + private readonly PowerShellContextService _powerShellContextService; + + private HoverCapability _capability; + + public HoverHandler( + ILoggerFactory factory, + SymbolsService symbolsService, + WorkspaceService workspaceService, + PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + _powerShellContextService = powerShellContextService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector, + }; + } + + public async Task Handle(HoverParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + SymbolDetails symbolDetails = + await _symbolsService.FindSymbolDetailsAtLocationAsync( + scriptFile, + (int) request.Position.Line + 1, + (int) request.Position.Character + 1); + + List symbolInfo = new List(); + Range symbolRange = null; + + if (symbolDetails != null) + { + symbolInfo.Add(new MarkedString("PowerShell", symbolDetails.DisplayString)); + + if (!string.IsNullOrEmpty(symbolDetails.Documentation)) + { + symbolInfo.Add(new MarkedString("markdown", symbolDetails.Documentation)); + } + + symbolRange = GetRangeFromScriptRegion(symbolDetails.SymbolReference.ScriptRegion); + } + + return new Hover + { + Contents = new MarkedStringsOrMarkupContent(symbolInfo), + Range = symbolRange + }; + } + + public void SetCapability(HoverCapability capability) + { + _capability = capability; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index a29563f70..c7593dc4e 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -644,5 +644,25 @@ public async Task CanSendCompletionAndCompletionResolveRequest() Assert.Contains("Writes customized output to a host", updatedCompletionItem.Documentation.String); } + + [Fact] + public async Task CanSendHoverRequest() + { + string filePath = NewTestFile("Write-Host"); + + Hover hover = await LanguageClient.TextDocument.Hover(filePath, line: 0, column: 1); + + Assert.True(hover.Contents.HasMarkedStrings); + Assert.Collection(hover.Contents.MarkedStrings, + str1 => + { + Assert.Equal("function Write-Host", str1.Value); + }, + str2 => + { + Assert.Equal("markdown", str2.Language); + Assert.Equal("Writes customized output to a host.", str2.Value); + }); + } } } From 930e97848e285427259c5556a9fd3ab027580de9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Aug 2019 20:49:24 -0700 Subject: [PATCH 33/47] Omni signaturehelp (#1011) * handle log messages * switch to using xUnit output helper * Support SignatureHelp * concurrentdict --- .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Symbols/ParameterSetSignatures.cs | 154 ++++++++++++++++++ .../Services/Symbols/SymbolsService.cs | 58 +++++++ .../Handlers/SignatureHelpHandler.cs | 116 +++++++++++++ .../LanguageServerProtocolMessageTests.cs | 30 +++- 5 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 6908cc766..709f0d1df 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -112,6 +112,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs new file mode 100644 index 000000000..072917e06 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// A class for containing the commandName, the command's + /// possible signatures, and the script extent of the command + /// + public class ParameterSetSignatures + { + #region Properties + /// + /// Gets the name of the command + /// + public string CommandName { get; internal set; } + + /// + /// Gets the collection of signatures for the command + /// + public ParameterSetSignature[] Signatures { get; internal set; } + + /// + /// Gets the script extent of the command + /// + public ScriptRegion ScriptRegion { get; internal set; } + #endregion + + /// + /// Constructs an instance of a ParameterSetSignatures object + /// + /// Collection of parameter set info + /// The SymbolReference of the command + public ParameterSetSignatures(IEnumerable commandInfoSet, SymbolReference foundSymbol) + { + List paramSetSignatures = new List(); + foreach (CommandParameterSetInfo setInfo in commandInfoSet) + { + paramSetSignatures.Add(new ParameterSetSignature(setInfo)); + } + Signatures = paramSetSignatures.ToArray(); + CommandName = foundSymbol.ScriptRegion.Text; + ScriptRegion = foundSymbol.ScriptRegion; + } + } + + /// + /// A class for containing the signature text and the collection of parameters for a signature + /// + public class ParameterSetSignature + { + private static readonly ConcurrentDictionary commonParameterNames = + new ConcurrentDictionary(); + + static ParameterSetSignature() + { + commonParameterNames.TryAdd("Verbose", true); + commonParameterNames.TryAdd("Debug", true); + commonParameterNames.TryAdd("ErrorAction", true); + commonParameterNames.TryAdd("WarningAction", true); + commonParameterNames.TryAdd("InformationAction", true); + commonParameterNames.TryAdd("ErrorVariable", true); + commonParameterNames.TryAdd("WarningVariable", true); + commonParameterNames.TryAdd("InformationVariable", true); + commonParameterNames.TryAdd("OutVariable", true); + commonParameterNames.TryAdd("OutBuffer", true); + commonParameterNames.TryAdd("PipelineVariable", true); + } + + #region Properties + /// + /// Gets the signature text + /// + public string SignatureText { get; internal set; } + + /// + /// Gets the collection of parameters for the signature + /// + public IEnumerable Parameters { get; internal set; } + #endregion + + /// + /// Constructs an instance of a ParameterSetSignature + /// + /// Collection of parameter info + public ParameterSetSignature(CommandParameterSetInfo commandParamInfoSet) + { + List parameterInfo = new List(); + foreach (CommandParameterInfo commandParameterInfo in commandParamInfoSet.Parameters) + { + if (!commonParameterNames.ContainsKey(commandParameterInfo.Name)) + { + parameterInfo.Add(new ParameterInfo(commandParameterInfo)); + } + } + + SignatureText = commandParamInfoSet.ToString(); + Parameters = parameterInfo.ToArray(); + } + } + + /// + /// A class for containing the parameter info of a parameter + /// + public class ParameterInfo + { + #region Properties + /// + /// Gets the name of the parameter + /// + public string Name { get; internal set; } + + /// + /// Gets the type of the parameter + /// + public string ParameterType { get; internal set; } + + /// + /// Gets the position of the parameter + /// + public int Position { get; internal set; } + + /// + /// Gets a boolean for whetheer or not the parameter is required + /// + public bool IsMandatory { get; internal set; } + + /// + /// Gets the help message of the parameter + /// + public string HelpMessage { get; internal set; } + #endregion + + /// + /// Constructs an instance of a ParameterInfo object + /// + /// Parameter info of the parameter + public ParameterInfo(CommandParameterInfo parameterInfo) + { + this.Name = "-" + parameterInfo.Name; + this.ParameterType = parameterInfo.ParameterType.FullName; + this.Position = parameterInfo.Position; + this.IsMandatory = parameterInfo.IsMandatory; + this.HelpMessage = parameterInfo.HelpMessage; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index e0c1e57be..326bcc6d5 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Management.Automation; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -262,5 +263,62 @@ public async Task FindSymbolDetailsAtLocationAsync( return symbolDetails; } + + /// + /// Finds the parameter set hints of a specific command (determined by a given file location) + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// ParameterSetSignatures + public async Task FindParameterSetsInFileAsync( + ScriptFile file, + int lineNumber, + int columnNumber, + PowerShellContextService powerShellContext) + { + SymbolReference foundSymbol = + AstOperations.FindCommandAtPosition( + file.ScriptAst, + lineNumber, + columnNumber); + + if (foundSymbol == null) + { + return null; + } + + CommandInfo commandInfo = + await CommandHelpers.GetCommandInfoAsync( + foundSymbol.SymbolName, + powerShellContext); + + if (commandInfo == null) + { + return null; + } + + try + { + IEnumerable commandParamSets = commandInfo.ParameterSets; + return new ParameterSetSignatures(commandParamSets, foundSymbol); + } + catch (RuntimeException e) + { + // A RuntimeException will be thrown when an invalid attribute is + // on a parameter binding block and then that command/script has + // its signatures resolved by typing it into a script. + _logger.LogException("RuntimeException encountered while accessing command parameter sets", e); + + return null; + } + catch (InvalidOperationException) + { + // For some commands there are no paramsets (like applications). Until + // the valid command types are better understood, catch this exception + // which gets raised when there are no ParameterSets for the command type. + return null; + } + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs new file mode 100644 index 000000000..a28828464 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class SignatureHelpHandler : ISignatureHelpHandler + { + private static readonly SignatureInformation[] s_emptySignatureResult = new SignatureInformation[0]; + + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + private readonly PowerShellContextService _powerShellContextService; + + private SignatureHelpCapability _capability; + + public SignatureHelpHandler( + ILoggerFactory factory, + SymbolsService symbolsService, + WorkspaceService workspaceService, + PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + _powerShellContextService = powerShellContextService; + } + + public SignatureHelpRegistrationOptions GetRegistrationOptions() + { + return new SignatureHelpRegistrationOptions + { + DocumentSelector = _documentSelector, + // A sane default of " ". We may be able to include others like "-". + TriggerCharacters = new Container(" ") + }; + } + + public async Task Handle(SignatureHelpParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + ParameterSetSignatures parameterSets = + await _symbolsService.FindParameterSetsInFileAsync( + scriptFile, + (int) request.Position.Line + 1, + (int) request.Position.Character + 1, + _powerShellContextService); + + SignatureInformation[] signatures = s_emptySignatureResult; + + if (parameterSets != null) + { + signatures = new SignatureInformation[parameterSets.Signatures.Length]; + for (int i = 0; i < signatures.Length; i++) + { + var parameters = new ParameterInformation[parameterSets.Signatures[i].Parameters.Count()]; + int j = 0; + foreach (ParameterInfo param in parameterSets.Signatures[i].Parameters) + { + parameters[j] = CreateParameterInfo(param); + j++; + } + + signatures[i] = new SignatureInformation + { + Label = parameterSets.CommandName + " " + parameterSets.Signatures[i].SignatureText, + Documentation = null, + Parameters = parameters, + }; + } + } + + return new SignatureHelp + { + Signatures = signatures, + ActiveParameter = null, + ActiveSignature = 0 + }; + } + + public void SetCapability(SignatureHelpCapability capability) + { + _capability = capability; + } + + private static ParameterInformation CreateParameterInfo(ParameterInfo parameterInfo) + { + return new ParameterInformation + { + Label = parameterInfo.Name, + Documentation = string.Empty + }; + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index c7593dc4e..2f41e9cd7 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1,4 +1,9 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -664,5 +669,28 @@ public async Task CanSendHoverRequest() Assert.Equal("Writes customized output to a host.", str2.Value); }); } + + [Fact] + public async Task CanSendSignatureHelpRequest() + { + string filePath = NewTestFile("Get-Date "); + + SignatureHelp signatureHelp = await LanguageClient.SendRequest( + "textDocument/signatureHelp", + new SignatureHelpParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(filePath) + }, + Position = new Position + { + Line = 0, + Character = 9 + } + }); + + Assert.Contains("Get-Date", signatureHelp.Signatures.First().Label); + } } } From cc67daeab1ebee980550d6a83cf511760c369cab Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 22 Aug 2019 13:19:59 -0700 Subject: [PATCH 34/47] Add definition handler (#1013) * add definition handler * codacy * sneak in powerShell/executionStatusChanged * codacy --- .../Hosting/EditorServicesHost.cs | 1 + .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../PowerShellContextService.cs | 24 ++- .../Services/Symbols/SymbolsService.cs | 187 +++++++++++++++++- .../Handlers/DefinitionHandler.cs | 108 ++++++++++ .../Handlers/SignatureHelpHandler.cs | 2 +- .../Utility/PathUtils.cs | 18 +- .../LanguageServerProtocolMessageTests.cs | 36 ++++ 8 files changed, 368 insertions(+), 9 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 55bf2ae2a..33c2d9524 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -310,6 +310,7 @@ private PowerShellContextService GetFullyInitializedPowerShellContext( // issues arise when redirecting stdio. var powerShellContext = new PowerShellContextService( logger, + languageServer, _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); EditorServicesPSHostUserInterface hostUserInterface = diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 709f0d1df..e0e677eec 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -113,6 +113,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs index cfe5f93dd..6d7dc6462 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -17,13 +17,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices { using System.Management.Automation; - using Microsoft.PowerShell.EditorServices.Engine; /// /// Manages the lifetime and usage of a PowerShell session. @@ -49,6 +49,7 @@ static PowerShellContextService() private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private readonly OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer _languageServer; private bool isPSReadLineEnabled; private ILogger logger; private PowerShell powerShell; @@ -145,11 +146,16 @@ public RunspaceDetails CurrentRunspace /// /// Indicates whether PSReadLine should be used if possible /// - public PowerShellContextService(ILogger logger, bool isPSReadLineEnabled) + public PowerShellContextService( + ILogger logger, + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + bool isPSReadLineEnabled) { - + _languageServer = languageServer; this.logger = logger; this.isPSReadLineEnabled = isPSReadLineEnabled; + + ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; } /// @@ -1720,6 +1726,18 @@ private void OnExecutionStatusChanged( hadErrors)); } + /// + /// Event hook on the PowerShell context to listen for changes in script execution status + /// + /// the PowerShell context sending the execution event + /// details of the execution status change + private void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e) + { + _languageServer.SendNotification( + "powerShell/executionStatusChanged", + e); + } + #endregion #region Private Methods diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 326bcc6d5..3a5ef3b07 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -6,12 +6,16 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.IO; using System.Linq; using System.Management.Automation; +using System.Management.Automation.Language; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; +using PowerShellEditorServices.Engine.Utility; namespace Microsoft.PowerShell.EditorServices { @@ -25,6 +29,7 @@ public class SymbolsService private readonly ILogger _logger; private readonly PowerShellContextService _powerShellContextService; + private readonly WorkspaceService _workspaceService; private readonly IDocumentSymbolProvider[] _documentSymbolProviders; #endregion @@ -38,10 +43,12 @@ public class SymbolsService /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( ILoggerFactory factory, - PowerShellContextService powerShellContextService) + PowerShellContextService powerShellContextService, + WorkspaceService workspaceService) { _logger = factory.CreateLogger(); _powerShellContextService = powerShellContextService; + _workspaceService = workspaceService; _documentSymbolProviders = new IDocumentSymbolProvider[] { new ScriptDocumentSymbolProvider(VersionUtils.PSVersion), @@ -320,5 +327,183 @@ await CommandHelpers.GetCommandInfoAsync( return null; } } + + /// + /// Finds the definition of a symbol in the script file or any of the + /// files that it references. + /// + /// The initial script file to be searched for the symbol's definition. + /// The symbol for which a definition will be found. + /// The resulting GetDefinitionResult for the symbol's definition. + public async Task GetDefinitionOfSymbolAsync( + ScriptFile sourceFile, + SymbolReference foundSymbol) + { + Validate.IsNotNull(nameof(sourceFile), sourceFile); + Validate.IsNotNull(nameof(foundSymbol), foundSymbol); + + ScriptFile[] referencedFiles = + _workspaceService.ExpandScriptReferences( + sourceFile); + + var filesSearched = new HashSet(StringComparer.OrdinalIgnoreCase); + + // look through the referenced files until definition is found + // or there are no more file to look through + SymbolReference foundDefinition = null; + foreach (ScriptFile scriptFile in referencedFiles) + { + foundDefinition = + AstOperations.FindDefinitionOfSymbol( + scriptFile.ScriptAst, + foundSymbol); + + filesSearched.Add(scriptFile.FilePath); + if (foundDefinition != null) + { + foundDefinition.FilePath = scriptFile.FilePath; + break; + } + + if (foundSymbol.SymbolType == SymbolType.Function) + { + // Dot-sourcing is parsed as a "Function" Symbol. + string dotSourcedPath = GetDotSourcedPath(foundSymbol, scriptFile); + if (scriptFile.FilePath == dotSourcedPath) + { + foundDefinition = new SymbolReference(SymbolType.Function, foundSymbol.SymbolName, scriptFile.ScriptAst.Extent, scriptFile.FilePath); + break; + } + } + } + + // if the definition the not found in referenced files + // look for it in all the files in the workspace + if (foundDefinition == null) + { + // Get a list of all powershell files in the workspace path + IEnumerable allFiles = _workspaceService.EnumeratePSFiles(); + foreach (string file in allFiles) + { + if (filesSearched.Contains(file)) + { + continue; + } + + foundDefinition = + AstOperations.FindDefinitionOfSymbol( + Parser.ParseFile(file, out Token[] tokens, out ParseError[] parseErrors), + foundSymbol); + + filesSearched.Add(file); + if (foundDefinition != null) + { + foundDefinition.FilePath = file; + break; + } + } + } + + // if definition is not found in file in the workspace + // look for it in the builtin commands + if (foundDefinition == null) + { + CommandInfo cmdInfo = + await CommandHelpers.GetCommandInfoAsync( + foundSymbol.SymbolName, + _powerShellContextService); + + foundDefinition = + FindDeclarationForBuiltinCommand( + cmdInfo, + foundSymbol); + } + + return foundDefinition; + } + + /// + /// Gets a path from a dot-source symbol. + /// + /// The symbol representing the dot-source expression. + /// The script file containing the symbol + /// + private string GetDotSourcedPath(SymbolReference symbol, ScriptFile scriptFile) + { + string cleanedUpSymbol = PathUtils.NormalizePathSeparators(symbol.SymbolName.Trim('\'', '"')); + string psScriptRoot = Path.GetDirectoryName(scriptFile.FilePath); + return _workspaceService.ResolveRelativeScriptPath(psScriptRoot, + Regex.Replace(cleanedUpSymbol, @"\$PSScriptRoot|\${PSScriptRoot}", psScriptRoot, RegexOptions.IgnoreCase)); + } + + private SymbolReference FindDeclarationForBuiltinCommand( + CommandInfo commandInfo, + SymbolReference foundSymbol) + { + if (commandInfo == null) + { + return null; + } + + ScriptFile[] nestedModuleFiles = + GetBuiltinCommandScriptFiles( + commandInfo.Module); + + SymbolReference foundDefinition = null; + foreach (ScriptFile nestedModuleFile in nestedModuleFiles) + { + foundDefinition = AstOperations.FindDefinitionOfSymbol( + nestedModuleFile.ScriptAst, + foundSymbol); + + if (foundDefinition != null) + { + foundDefinition.FilePath = nestedModuleFile.FilePath; + break; + } + } + + return foundDefinition; + } + + private ScriptFile[] GetBuiltinCommandScriptFiles( + PSModuleInfo moduleInfo) + { + if (moduleInfo == null) + { + return new ScriptFile[0]; + } + + string modPath = moduleInfo.Path; + List scriptFiles = new List(); + ScriptFile newFile; + + // find any files where the moduleInfo's path ends with ps1 or psm1 + // and add it to allowed script files + if (modPath.EndsWith(@".ps1", StringComparison.OrdinalIgnoreCase) || + modPath.EndsWith(@".psm1", StringComparison.OrdinalIgnoreCase)) + { + newFile = _workspaceService.GetFile(modPath); + newFile.IsAnalysisEnabled = false; + scriptFiles.Add(newFile); + } + + if (moduleInfo.NestedModules.Count > 0) + { + foreach (PSModuleInfo nestedInfo in moduleInfo.NestedModules) + { + string nestedModPath = nestedInfo.Path; + if (nestedModPath.EndsWith(@".ps1", StringComparison.OrdinalIgnoreCase) || + nestedModPath.EndsWith(@".psm1", StringComparison.OrdinalIgnoreCase)) + { + newFile = _workspaceService.GetFile(nestedModPath); + newFile.IsAnalysisEnabled = false; + scriptFiles.Add(newFile); + } + } + } + + return scriptFiles.ToArray(); + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs new file mode 100644 index 000000000..407408cf9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class DefinitionHandler : IDefinitionHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter + { + Language = "powershell" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + + private DefinitionCapability _capability; + + public DefinitionHandler( + ILoggerFactory factory, + SymbolsService symbolsService, + WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector + }; + } + + public async Task Handle(DefinitionParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + SymbolReference foundSymbol = + _symbolsService.FindSymbolAtLocation( + scriptFile, + (int) request.Position.Line + 1, + (int) request.Position.Character + 1); + + List definitionLocations = new List(); + if (foundSymbol != null) + { + SymbolReference foundDefinition = await _symbolsService.GetDefinitionOfSymbolAsync( + scriptFile, + foundSymbol); + + if (foundDefinition != null) + { + definitionLocations.Add( + new LocationOrLocationLink( + new Location + { + Uri = PathUtils.ToUri(foundDefinition.FilePath), + Range = GetRangeFromScriptRegion(foundDefinition.ScriptRegion) + })); + } + } + + return new LocationOrLocationLinks(definitionLocations); + } + + public void SetCapability(DefinitionCapability capability) + { + _capability = capability; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs index a28828464..6636da589 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -21,7 +21,7 @@ public class SignatureHelpHandler : ISignatureHelpHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index 17d43f77b..108c7d05f 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -31,11 +31,21 @@ public string WildcardUnescapePath(string path) throw new NotImplementedException(); } - public static Uri ToUri(string fileName) + public static Uri ToUri(string filePath) { - fileName = fileName.Replace(":", "%3A").Replace("\\", "/"); - if (!fileName.StartsWith("/")) return new Uri($"file:///{fileName}"); - return new Uri($"file://{fileName}"); + if (filePath.StartsWith("untitled", StringComparison.OrdinalIgnoreCase) || + filePath.StartsWith("inmemory", StringComparison.OrdinalIgnoreCase)) + { + return new Uri(filePath); + } + + filePath = filePath.Replace(":", "%3A").Replace("\\", "/"); + if (!filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return new Uri($"file:///{filePath}"); + } + + return new Uri($"file://{filePath}"); } public static string FromUri(Uri uri) diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 2f41e9cd7..d66b35f38 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -692,5 +692,41 @@ public async Task CanSendSignatureHelpRequest() Assert.Contains("Get-Date", signatureHelp.Signatures.First().Label); } + + [Fact] + public async Task CanSendDefinitionRequest() + { + string scriptPath = NewTestFile(@" +function CanSendDefinitionRequest { + +} + +CanSendDefinitionRequest +"); + + LocationOrLocationLinks locationOrLocationLinks = + await LanguageClient.SendRequest( + "textDocument/definition", + new DefinitionParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + }, + Position = new Position + { + Line = 5, + Character = 2 + } + }); + + LocationOrLocationLink locationOrLocationLink = + Assert.Single(locationOrLocationLinks); + + Assert.Equal(1, locationOrLocationLink.Location.Range.Start.Line); + Assert.Equal(9, locationOrLocationLink.Location.Range.Start.Character); + Assert.Equal(1, locationOrLocationLink.Location.Range.End.Line); + Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character); + } } } From 56bd5b70e40578a5fda0aa36d60e5e4a064ce5e5 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 22 Aug 2019 20:27:15 -0700 Subject: [PATCH 35/47] Add Plaster messages (#1014) --- .../Hosting/EditorServicesHost.cs | 2 + .../LanguageServer/OmnisharpLanguageServer.cs | 1 + .../Handlers/ITemplateHandlers.cs | 76 +++++++ .../Handlers/TemplateHandlers.cs | 73 ++++++ .../PowerShellContext/TemplateService.cs | 210 ++++++++++++++++++ .../LanguageServerProtocolMessageTests.cs | 22 ++ 6 files changed, 384 insertions(+) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 33c2d9524..2cf1ab05f 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Host; +using Microsoft.PowerShell.EditorServices.Templates; using Serilog; namespace Microsoft.PowerShell.EditorServices.Engine @@ -244,6 +245,7 @@ public void StartLanguageService( GetFullyInitializedPowerShellContext( provider.GetService(), profilePaths)) + .AddSingleton() .AddSingleton() .AddSingleton( (provider) => diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index e0e677eec..612aa0aa5 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -114,6 +114,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs new file mode 100644 index 000000000..9fefae8d1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/getProjectTemplates")] + public interface IGetProjectTemplatesHandler : IJsonRpcRequestHandler { } + + [Serial, Method("powerShell/newProjectFromTemplate")] + public interface INewProjectFromTemplateHandler : IJsonRpcRequestHandler { } + + public class GetProjectTemplatesRequest : IRequest + { + public bool IncludeInstalledModules { get; set; } + } + + public class GetProjectTemplatesResponse + { + public bool NeedsModuleInstall { get; set; } + + public TemplateDetails[] Templates { get; set; } + } + + /// + /// Provides details about a file or project template. + /// + public class TemplateDetails + { + /// + /// Gets or sets the title of the template. + /// + public string Title { get; set; } + + /// + /// Gets or sets the author of the template. + /// + public string Author { get; set; } + + /// + /// Gets or sets the version of the template. + /// + public string Version { get; set; } + + /// + /// Gets or sets the description of the template. + /// + public string Description { get; set; } + + /// + /// Gets or sets the template's comma-delimited string of tags. + /// + public string Tags { get; set; } + + /// + /// Gets or sets the template's folder path. + /// + public string TemplatePath { get; set; } + } + + public class NewProjectFromTemplateRequest : IRequest + { + public string DestinationPath { get; set; } + + public string TemplatePath { get; set; } + } + + public class NewProjectFromTemplateResponse + { + public bool CreationSuccessful { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs new file mode 100644 index 000000000..d02bddcc9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs @@ -0,0 +1,73 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Templates; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class TemplateHandlers : IGetProjectTemplatesHandler, INewProjectFromTemplateHandler + { + private readonly ILogger _logger; + private readonly TemplateService _templateService; + + public TemplateHandlers( + ILoggerFactory factory, + TemplateService templateService) + { + _logger = factory.CreateLogger(); + _templateService = templateService; + } + + public async Task Handle(GetProjectTemplatesRequest request, CancellationToken cancellationToken) + { + bool plasterInstalled = await _templateService.ImportPlasterIfInstalledAsync(); + + if (plasterInstalled) + { + var availableTemplates = + await _templateService.GetAvailableTemplatesAsync( + request.IncludeInstalledModules); + + + return new GetProjectTemplatesResponse + { + Templates = availableTemplates + }; + } + + return new GetProjectTemplatesResponse + { + NeedsModuleInstall = true, + Templates = new TemplateDetails[0] + }; + } + + public async Task Handle(NewProjectFromTemplateRequest request, CancellationToken cancellationToken) + { + bool creationSuccessful; + try + { + await _templateService.CreateFromTemplateAsync(request.TemplatePath, request.DestinationPath); + creationSuccessful = true; + } + catch (Exception e) + { + // We don't really care if this worked or not but we report status. + _logger.LogException("New plaster template failed.", e); + creationSuccessful = false; + } + + return new NewProjectFromTemplateResponse + { + CreationSuccessful = creationSuccessful + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs new file mode 100644 index 000000000..8ac5cfc8e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs @@ -0,0 +1,210 @@ +// +// 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.Utility; +using PowerShellEditorServices.Engine.Services.Handlers; +using System; +using System.Linq; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Templates +{ + /// + /// Provides a service for listing PowerShell project templates and creating + /// new projects from those templates. This service leverages the Plaster + /// module for creating projects from templates. + /// + public class TemplateService + { + #region Private Fields + + private readonly ILogger logger; + private bool isPlasterLoaded; + private bool? isPlasterInstalled; + private readonly PowerShellContextService powerShellContext; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the TemplateService class. + /// + /// The PowerShellContext to use for this service. + /// An ILoggerFactory implementation used for writing log messages. + public TemplateService(PowerShellContextService powerShellContext, ILoggerFactory factory) + { + Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + + this.logger = factory.CreateLogger(); + this.powerShellContext = powerShellContext; + } + + #endregion + + #region Public Methods + + /// + /// Checks if Plaster is installed on the user's machine. + /// + /// A Task that can be awaited until the check is complete. The result will be true if Plaster is installed. + public async Task ImportPlasterIfInstalledAsync() + { + if (!this.isPlasterInstalled.HasValue) + { + PSCommand psCommand = new PSCommand(); + + psCommand + .AddCommand("Get-Module") + .AddParameter("ListAvailable") + .AddParameter("Name", "Plaster"); + + psCommand + .AddCommand("Sort-Object") + .AddParameter("Descending") + .AddParameter("Property", "Version"); + + psCommand + .AddCommand("Select-Object") + .AddParameter("First", 1); + + this.logger.LogTrace("Checking if Plaster is installed..."); + + var getResult = + await this.powerShellContext.ExecuteCommandAsync( + psCommand, false, false); + + PSObject moduleObject = getResult.First(); + this.isPlasterInstalled = moduleObject != null; + string installedQualifier = + this.isPlasterInstalled.Value + ? string.Empty : "not "; + + this.logger.LogTrace($"Plaster is {installedQualifier}installed!"); + + // Attempt to load plaster + if (this.isPlasterInstalled.Value && this.isPlasterLoaded == false) + { + this.logger.LogTrace("Loading Plaster..."); + + psCommand = new PSCommand(); + psCommand + .AddCommand("Import-Module") + .AddParameter("ModuleInfo", (PSModuleInfo)moduleObject.ImmediateBaseObject) + .AddParameter("PassThru"); + + var importResult = + await this.powerShellContext.ExecuteCommandAsync( + psCommand, false, false); + + this.isPlasterLoaded = importResult.Any(); + string loadedQualifier = + this.isPlasterInstalled.Value + ? "was" : "could not be"; + + this.logger.LogTrace($"Plaster {loadedQualifier} loaded successfully!"); + } + } + + return this.isPlasterInstalled.Value; + } + + /// + /// Gets the available file or project templates on the user's + /// machine. + /// + /// + /// If true, searches the user's installed PowerShell modules for + /// included templates. + /// + /// A Task which can be awaited for the TemplateDetails list to be returned. + public async Task GetAvailableTemplatesAsync( + bool includeInstalledModules) + { + if (!this.isPlasterLoaded) + { + throw new InvalidOperationException("Plaster is not loaded, templates cannot be accessed."); + } + + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand("Get-PlasterTemplate"); + + if (includeInstalledModules) + { + psCommand.AddParameter("IncludeModules"); + } + + var templateObjects = + await this.powerShellContext.ExecuteCommandAsync( + psCommand, false, false); + + this.logger.LogTrace($"Found {templateObjects.Count()} Plaster templates"); + + return + templateObjects + .Select(CreateTemplateDetails) + .ToArray(); + } + + /// + /// Creates a new file or project from a specified template and + /// places it in the destination path. This ultimately calls + /// Invoke-Plaster in PowerShell. + /// + /// The folder path containing the template. + /// The folder path where the files will be created. + /// A boolean-returning Task which communicates success or failure. + public async Task CreateFromTemplateAsync( + string templatePath, + string destinationPath) + { + this.logger.LogTrace( + $"Invoking Plaster...\n\n TemplatePath: {templatePath}\n DestinationPath: {destinationPath}"); + + PSCommand command = new PSCommand(); + command.AddCommand("Invoke-Plaster"); + command.AddParameter("TemplatePath", templatePath); + command.AddParameter("DestinationPath", destinationPath); + + var errorString = new System.Text.StringBuilder(); + await this.powerShellContext.ExecuteCommandAsync( + command, + errorString, + new ExecutionOptions + { + WriteOutputToHost = false, + WriteErrorsToHost = true, + InterruptCommandPrompt = true + }); + + // If any errors were written out, creation was not successful + return errorString.Length == 0; + } + + #endregion + + #region Private Methods + + private static TemplateDetails CreateTemplateDetails(PSObject psObject) + { + return new TemplateDetails + { + Title = psObject.Members["Title"].Value as string, + Author = psObject.Members["Author"].Value as string, + Version = psObject.Members["Version"].Value.ToString(), + Description = psObject.Members["Description"].Value as string, + TemplatePath = psObject.Members["TemplatePath"].Value as string, + Tags = + psObject.Members["Tags"].Value is object[] tags + ? string.Join(", ", tags) + : string.Empty + }; + } + + #endregion + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index d66b35f38..49b3ea633 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -728,5 +728,27 @@ await LanguageClient.SendRequest( Assert.Equal(1, locationOrLocationLink.Location.Range.End.Line); Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character); } + + [Fact] + public async Task CanSendGetProjectTemplatesRequest() + { + GetProjectTemplatesResponse getProjectTemplatesResponse = + await LanguageClient.SendRequest( + "powerShell/getProjectTemplates", + new GetProjectTemplatesRequest + { + IncludeInstalledModules = true + }); + + Assert.Collection(getProjectTemplatesResponse.Templates.OrderBy(t => t.Title), + template1 => + { + Assert.Equal("AddPSScriptAnalyzerSettings", template1.Title); + }, + template2 => + { + Assert.Equal("New PowerShell Manifest Module", template2.Title); + }); + } } } From 2b6b2371c0af6107420ad1db2432279a5963f5f4 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 22 Aug 2019 20:38:20 -0700 Subject: [PATCH 36/47] Comment Help and Evaluate (#1015) * Support for Comment Help generator * add evaluate handler --- .../LanguageServer/OmnisharpLanguageServer.cs | 2 + .../Services/Analysis/AnalysisService.cs | 2 +- .../Handlers/EvaluateHandler.cs | 35 +++++++ .../Handlers/GetCommentHelpHandler.cs | 98 +++++++++++++++++++ .../Handlers/IEvaluateHandler.cs | 44 +++++++++ .../Handlers/IGetCommentHelpHandler.cs | 26 +++++ .../Services/Symbols/SymbolsService.cs | 92 +++++++++++++++++ .../LanguageServerProtocolMessageTests.cs | 47 +++++++++ 8 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 612aa0aa5..de2b092f4 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -115,6 +115,8 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index d36a0c987..75db008a6 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -23,7 +23,7 @@ namespace Microsoft.PowerShell.EditorServices /// Provides a high-level service for performing semantic analysis /// of PowerShell scripts. /// - internal class AnalysisService : IDisposable + public class AnalysisService : IDisposable { #region Static fields diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs new file mode 100644 index 000000000..61b3dae51 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class EvaluateHandler : IEvaluateHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public EvaluateHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) + { + await _powerShellContextService.ExecuteScriptStringAsync( + request.Expression, + writeInputToHost: true, + writeOutputToHost: true, + addToHistory: true); + + return new EvaluateResponseBody + { + Result = "", + VariablesReference = 0 + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs new file mode 100644 index 000000000..e00391880 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class GetCommentHelpHandler : IGetCommentHelpHandler + { + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + private readonly AnalysisService _analysisService; + private readonly SymbolsService _symbolsService; + + public GetCommentHelpHandler( + ILoggerFactory factory, + WorkspaceService workspaceService, + AnalysisService analysisService, + SymbolsService symbolsService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _analysisService = analysisService; + _symbolsService = symbolsService; + } + + public async Task Handle(CommentHelpRequestParams request, CancellationToken cancellationToken) + { + var result = new CommentHelpRequestResult(); + + if (!_workspaceService.TryGetFile(request.DocumentUri, out ScriptFile scriptFile)) + { + return result; + } + + int triggerLine = (int) request.TriggerPosition.Line + 1; + + FunctionDefinitionAst functionDefinitionAst = _symbolsService.GetFunctionDefinitionForHelpComment( + scriptFile, + triggerLine, + out string helpLocation); + + if (functionDefinitionAst == null) + { + return result; + } + + IScriptExtent funcExtent = functionDefinitionAst.Extent; + string funcText = funcExtent.Text; + if (helpLocation.Equals("begin")) + { + // check if the previous character is `<` because it invalidates + // the param block the follows it. + IList lines = ScriptFile.GetLinesInternal(funcText); + int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber; + if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<", StringComparison.OrdinalIgnoreCase) > -1) + { + lines[relativeTriggerLine0b] = string.Empty; + } + + funcText = string.Join("\n", lines); + } + + List analysisResults = await _analysisService.GetSemanticMarkersAsync( + funcText, + AnalysisService.GetCommentHelpRuleSettings( + enable: true, + exportedOnly: false, + blockComment: request.BlockComment, + vscodeSnippetCorrection: true, + placement: helpLocation)); + + string helpText = analysisResults?.FirstOrDefault()?.Correction?.Edits[0].Text; + + if (helpText == null) + { + return result; + } + + result.Content = ScriptFile.GetLinesInternal(helpText).ToArray(); + + if (helpLocation != null && + !helpLocation.Equals("before", StringComparison.OrdinalIgnoreCase)) + { + // we need to trim the leading `{` and newline when helpLocation=="begin" + // we also need to trim the leading newline when helpLocation=="end" + result.Content = result.Content.Skip(1).ToArray(); + } + + return result; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs new file mode 100644 index 000000000..1fd053a0f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs @@ -0,0 +1,44 @@ +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("evaluate")] + public interface IEvaluateHandler : IJsonRpcRequestHandler { } + + public class EvaluateRequestArguments : IRequest + { + /// + /// The expression to evaluate. + /// + public string Expression { get; set; } + + /// + /// The context in which the evaluate request is run. Possible + /// values are 'watch' if evaluate is run in a watch or 'repl' + /// if run from the REPL console. + /// + public string Context { get; set; } + + /// + /// Evaluate the expression in the context of this stack frame. + /// If not specified, the top most frame is used. + /// + public int FrameId { get; set; } + } + + public class EvaluateResponseBody + { + /// + /// The evaluation result. + /// + public string Result { get; set; } + + /// + /// If variablesReference is > 0, the evaluate result is + /// structured and its children can be retrieved by passing + /// variablesReference to the VariablesRequest + /// + public int VariablesReference { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs new file mode 100644 index 000000000..85c2f00ad --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + [Serial, Method("powerShell/getCommentHelp")] + public interface IGetCommentHelpHandler : IJsonRpcRequestHandler { } + + public class CommentHelpRequestResult + { + public string[] Content { get; set; } + } + + public class CommentHelpRequestParams : IRequest + { + public string DocumentUri { get; set; } + public Position TriggerPosition { get; set; } + public bool BlockComment { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 3a5ef3b07..216a885d1 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -505,5 +505,97 @@ private ScriptFile[] GetBuiltinCommandScriptFiles( return scriptFiles.ToArray(); } + + /// + /// Finds a function definition that follows or contains the given line number. + /// + /// Open script file. + /// The 1 based line on which to look for function definition. + /// + /// If found, returns the function definition, otherwise, returns null. + public FunctionDefinitionAst GetFunctionDefinitionForHelpComment( + ScriptFile scriptFile, + int lineNumber, + out string helpLocation) + { + // check if the next line contains a function definition + FunctionDefinitionAst funcDefnAst = GetFunctionDefinitionAtLine(scriptFile, lineNumber + 1); + if (funcDefnAst != null) + { + helpLocation = "before"; + return funcDefnAst; + } + + // find all the script definitions that contain the line `lineNumber` + IEnumerable foundAsts = scriptFile.ScriptAst.FindAll( + ast => + { + if (!(ast is FunctionDefinitionAst fdAst)) + { + return false; + } + + return fdAst.Body.Extent.StartLineNumber < lineNumber && + fdAst.Body.Extent.EndLineNumber > lineNumber; + }, + true); + + if (foundAsts == null || !foundAsts.Any()) + { + helpLocation = null; + return null; + } + + // of all the function definitions found, return the innermost function + // definition that contains `lineNumber` + foreach (FunctionDefinitionAst foundAst in foundAsts.Cast()) + { + if (funcDefnAst == null) + { + funcDefnAst = foundAst; + continue; + } + + if (funcDefnAst.Extent.StartOffset >= foundAst.Extent.StartOffset + && funcDefnAst.Extent.EndOffset <= foundAst.Extent.EndOffset) + { + funcDefnAst = foundAst; + } + } + + // TODO use tokens to check for non empty character instead of just checking for line offset + if (funcDefnAst.Body.Extent.StartLineNumber == lineNumber - 1) + { + helpLocation = "begin"; + return funcDefnAst; + } + + if (funcDefnAst.Body.Extent.EndLineNumber == lineNumber + 1) + { + helpLocation = "end"; + return funcDefnAst; + } + + // If we didn't find a function definition, then return null + helpLocation = null; + return null; + } + + /// + /// Gets the function defined on a given line. + /// + /// Open script file. + /// The 1 based line on which to look for function definition. + /// If found, returns the function definition on the given line. Otherwise, returns null. + public FunctionDefinitionAst GetFunctionDefinitionAtLine( + ScriptFile scriptFile, + int lineNumber) + { + Ast functionDefinitionAst = scriptFile.ScriptAst.Find( + ast => ast is FunctionDefinitionAst && ast.Extent.StartLineNumber == lineNumber, + true); + + return functionDefinitionAst as FunctionDefinitionAst; + } } } diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 49b3ea633..2b71ba03f 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -750,5 +751,51 @@ await LanguageClient.SendRequest( Assert.Equal("New PowerShell Manifest Module", template2.Title); }); } + + [Fact] + public async Task CanSendGetCommentHelpRequest() + { + string scriptPath = NewTestFile(@" +function CanSendGetCommentHelpRequest { + param( + [string] + $myParam + ) +} +"); + + CommentHelpRequestResult commentHelpRequestResult = + await LanguageClient.SendRequest( + "powerShell/getCommentHelp", + new CommentHelpRequestParams + { + DocumentUri = new Uri(scriptPath).ToString(), + BlockComment = false, + TriggerPosition = new Position + { + Line = 0, + Character = 0 + } + }); + + Assert.NotEmpty(commentHelpRequestResult.Content); + Assert.Contains("myParam", commentHelpRequestResult.Content[7]); + } + + [Fact] + public async Task CanSendEvaluateRequest() + { + EvaluateResponseBody evaluateResponseBody = + await LanguageClient.SendRequest( + "evaluate", + new EvaluateRequestArguments + { + Expression = "Get-ChildItem" + }); + + // These always gets returned so this test really just makes sure we get _any_ response. + Assert.Equal("", evaluateResponseBody.Result); + Assert.Equal(0, evaluateResponseBody.VariablesReference); + } } } From a2a986eeeb6294147a5905fae78a9303ecf5c53a Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 27 Aug 2019 07:21:10 -0700 Subject: [PATCH 37/47] Last LSP messages (#1016) * support CommandExporer commands and powerShell/runspaceChanged * expand alias --- .../LanguageServer/OmnisharpLanguageServer.cs | 3 + .../Handlers/ExpandAliasHandler.cs | 82 +++++++++++++++++++ .../Handlers/GetCommandHandler.cs | 81 ++++++++++++++++++ .../Handlers/GetVersionHandler.cs | 5 +- .../Handlers/IGetVersionHandler.cs | 32 +++++++- .../Handlers/ShowHelpHandler.cs | 81 ++++++++++++++++++ .../PowerShellContextService.cs | 35 ++++++++ .../LanguageServerProtocolMessageTests.cs | 30 ++++++- 8 files changed, 342 insertions(+), 7 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index de2b092f4..4a08bec2d 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -117,6 +117,9 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs new file mode 100644 index 000000000..bab5d3ee1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/expandAlias")] + public interface IExpandAliasHandler : IJsonRpcRequestHandler { } + + public class ExpandAliasParams : IRequest + { + public string Text { get; set; } + } + + public class ExpandAliasResult + { + public string Text { get; set; } + } + + public class ExpandAliasHandler : IExpandAliasHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public ExpandAliasHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task Handle(ExpandAliasParams request, CancellationToken cancellationToken) + { + const string script = @" +function __Expand-Alias { + + param($targetScript) + + [ref]$errors=$null + + $tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) | + Sort-Object Start -Descending + + foreach ($token in $tokens) { + $definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition + + if($definition) { + $lhs=$targetScript.Substring(0, $token.Start) + $rhs=$targetScript.Substring($token.Start + $token.Length) + + $targetScript=$lhs + $definition + $rhs + } + } + + $targetScript +}"; + + // TODO: Refactor to not rerun the function definition every time. + var psCommand = new PSCommand(); + psCommand + .AddScript(script) + .AddStatement() + .AddCommand("__Expand-Alias") + .AddArgument(request.Text); + var result = await _powerShellContextService.ExecuteCommandAsync(psCommand); + + return new ExpandAliasResult + { + Text = result.First() + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs new file mode 100644 index 000000000..aee02fe1b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/getCommand")] + public interface IGetCommandHandler : IJsonRpcRequestHandler> { } + + public class GetCommandParams : IRequest> { } + + /// + /// Describes the message to get the details for a single PowerShell Command + /// from the current session + /// + public class PSCommandMessage + { + public string Name { get; set; } + public string ModuleName { get; set; } + public string DefaultParameterSet { get; set; } + public Dictionary Parameters { get; set; } + public System.Collections.ObjectModel.ReadOnlyCollection ParameterSets { get; set; } + } + + public class GetCommandHandler : IGetCommandHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public GetCommandHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task> Handle(GetCommandParams request, CancellationToken cancellationToken) + { + PSCommand psCommand = new PSCommand(); + + // Executes the following: + // Get-Command -CommandType Function,Cmdlet,ExternalScript | Select-Object -Property Name,ModuleName | Sort-Object -Property Name + psCommand + .AddCommand("Microsoft.PowerShell.Core\\Get-Command") + .AddParameter("CommandType", new[] { "Function", "Cmdlet", "ExternalScript" }) + .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") + .AddParameter("Property", new[] { "Name", "ModuleName" }) + .AddCommand("Microsoft.PowerShell.Utility\\Sort-Object") + .AddParameter("Property", "Name"); + + IEnumerable result = await _powerShellContextService.ExecuteCommandAsync(psCommand); + + var commandList = new List(); + if (result != null) + { + foreach (dynamic command in result) + { + commandList.Add(new PSCommandMessage + { + Name = command.Name, + ModuleName = command.ModuleName, + Parameters = command.Parameters, + ParameterSets = command.ParameterSets, + DefaultParameterSet = command.DefaultParameterSet + }); + } + } + + return commandList; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs index 724c921d4..4b57ecd39 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -15,7 +15,7 @@ public GetVersionHandler(ILoggerFactory factory) _logger = factory.CreateLogger(); } - public Task Handle(GetVersionParams request, CancellationToken cancellationToken) + public Task Handle(GetVersionParams request, CancellationToken cancellationToken) { var architecture = PowerShellProcessArchitecture.Unknown; // This should be changed to using a .NET call sometime in the future... but it's just for logging purposes. @@ -32,7 +32,8 @@ public Task Handle(GetVersionParams request, Cancellat } } - return Task.FromResult(new PowerShellVersionDetails { + return Task.FromResult(new PowerShellVersion + { Version = VersionUtils.PSVersion.ToString(), Edition = VersionUtils.PSEdition, DisplayVersion = VersionUtils.PSVersion.ToString(2), diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs index c4570d8b8..253e34daa 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs @@ -1,17 +1,43 @@ +using Microsoft.PowerShell.EditorServices.Session; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; namespace PowerShellEditorServices.Engine.Services.Handlers { [Serial, Method("powerShell/getVersion")] - public interface IGetVersionHandler : IJsonRpcRequestHandler { } + public interface IGetVersionHandler : IJsonRpcRequestHandler { } - public class GetVersionParams : IRequest { } + public class GetVersionParams : IRequest { } - public class PowerShellVersionDetails { + public class PowerShellVersion + { public string Version { get; set; } public string DisplayVersion { get; set; } public string Edition { get; set; } public string Architecture { get; set; } + + public PowerShellVersion() + { + } + + public PowerShellVersion(PowerShellVersionDetails versionDetails) + { + this.Version = versionDetails.VersionString; + this.DisplayVersion = $"{versionDetails.Version.Major}.{versionDetails.Version.Minor}"; + this.Edition = versionDetails.Edition; + + switch (versionDetails.Architecture) + { + case PowerShellProcessArchitecture.X64: + this.Architecture = "x64"; + break; + case PowerShellProcessArchitecture.X86: + this.Architecture = "x86"; + break; + default: + this.Architecture = "Architecture Unknown"; + break; + } + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs new file mode 100644 index 000000000..aecf58083 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("powerShell/showHelp")] + public interface IShowHelpHandler : IJsonRpcNotificationHandler { } + + public class ShowHelpParams : IRequest + { + public string Text { get; set; } + } + + public class ShowHelpHandler : IShowHelpHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public ShowHelpHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task Handle(ShowHelpParams request, CancellationToken cancellationToken) + { + const string CheckHelpScript = @" + [CmdletBinding()] + param ( + [String]$CommandName + ) + try { + $command = Microsoft.PowerShell.Core\Get-Command $CommandName -ErrorAction Stop + } catch [System.Management.Automation.CommandNotFoundException] { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + try { + $helpUri = [Microsoft.PowerShell.Commands.GetHelpCodeMethods]::GetHelpUri($command) + + $oldSslVersion = [System.Net.ServicePointManager]::SecurityProtocol + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + + # HEAD means we don't need the content itself back, just the response header + $status = (Microsoft.PowerShell.Utility\Invoke-WebRequest -Method Head -Uri $helpUri -TimeoutSec 5 -ErrorAction Stop).StatusCode + if ($status -lt 400) { + $null = Microsoft.PowerShell.Core\Get-Help $CommandName -Online + return + } + } catch { + # Ignore - we want to drop out to Get-Help -Full + } finally { + [System.Net.ServicePointManager]::SecurityProtocol = $oldSslVersion + } + + return Microsoft.PowerShell.Core\Get-Help $CommandName -Full + "; + + string helpParams = request.Text; + if (string.IsNullOrEmpty(helpParams)) { helpParams = "Get-Help"; } + + PSCommand checkHelpPSCommand = new PSCommand() + .AddScript(CheckHelpScript, useLocalScope: true) + .AddArgument(helpParams); + + // TODO: Rather than print the help in the console, we should send the string back + // to VSCode to display in a help pop-up (or similar) + await _powerShellContextService.ExecuteCommandAsync(checkHelpPSCommand, sendOutputToHost: true); + return Unit.Value; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs index 6d7dc6462..b4dfeea3e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -20,6 +20,7 @@ using Microsoft.PowerShell.EditorServices.Engine; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; +using PowerShellEditorServices.Engine.Services.Handlers; namespace Microsoft.PowerShell.EditorServices { @@ -155,6 +156,7 @@ public PowerShellContextService( this.logger = logger; this.isPSReadLineEnabled = isPSReadLineEnabled; + RunspaceChanged += PowerShellContext_RunspaceChangedAsync; ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; } @@ -1726,6 +1728,39 @@ private void OnExecutionStatusChanged( hadErrors)); } + private void PowerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) + { + _languageServer.SendNotification( + "powerShell/runspaceChanged", + new MinifiedRunspaceDetails(e.NewRunspace)); + } + + + // TODO: Refactor this, RunspaceDetails, PowerShellVersion, and PowerShellVersionDetails + // It's crazy that this is 4 different types. + // P.S. MinifiedRunspaceDetails use to be called RunspaceDetails... as in, there were 2 DIFFERENT + // RunspaceDetails types in this codebase but I've changed it to be minified since the type is + // slightly simpler than the other RunspaceDetails. + public class MinifiedRunspaceDetails + { + public PowerShellVersion PowerShellVersion { get; set; } + + public RunspaceLocation RunspaceType { get; set; } + + public string ConnectionString { get; set; } + + public MinifiedRunspaceDetails() + { + } + + public MinifiedRunspaceDetails(RunspaceDetails eventArgs) + { + this.PowerShellVersion = new PowerShellVersion(eventArgs.PowerShellVersion); + this.RunspaceType = eventArgs.Location; + this.ConnectionString = eventArgs.ConnectionString; + } + } + /// /// Event hook on the PowerShell context to listen for changes in script execution status /// diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 2b71ba03f..6597f494e 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -102,8 +102,8 @@ private async Task WaitForDiagnostics() [Fact] public async Task CanSendPowerShellGetVersionRequest() { - PowerShellVersionDetails details - = await LanguageClient.SendRequest("powerShell/getVersion", new GetVersionParams()); + PowerShellVersion details + = await LanguageClient.SendRequest("powerShell/getVersion", new GetVersionParams()); if(PwshExe == "powershell") { @@ -797,5 +797,31 @@ await LanguageClient.SendRequest( Assert.Equal("", evaluateResponseBody.Result); Assert.Equal(0, evaluateResponseBody.VariablesReference); } + + [Fact] + public async Task CanSendGetCommandRequest() + { + List pSCommandMessages = + await LanguageClient.SendRequest>("powerShell/getCommand", new GetCommandParams()); + + Assert.NotEmpty(pSCommandMessages); + // There should be at least 20 commands or so. + Assert.True(pSCommandMessages.Count > 20); + } + + [Fact] + public async Task CanSendExpandAliasRequest() + { + ExpandAliasResult expandAliasResult = + await LanguageClient.SendRequest( + "powerShell/expandAlias", + new ExpandAliasParams + { + Text = "gci" + } + ); + + Assert.Equal("Get-ChildItem", expandAliasResult.Text); + } } } From 0e2f7b9770908aa836594d03aa12387ef38aa669 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 27 Aug 2019 17:38:22 -0700 Subject: [PATCH 38/47] refactor server setup (#1018) --- .../Hosting/EditorServicesHost.cs | 137 ++--------- .../Hosting/{LogLevel.cs => PsesLogLevel.cs} | 0 .../Interface/ILanguageServer.cs | 11 - .../Interface/ILanguageServerBuilder.cs | 17 -- .../LanguageServer/OmnisharpLanguageServer.cs | 229 ------------------ .../OmnisharpLanguageServerBuilder.cs | 40 --- .../Logging/LoggerExtensions.cs | 1 - .../Server/NamedPipePsesLanguageServer.cs | 151 ++++++++++++ .../Server/PsesLanguageServer.cs | 215 ++++++++++++++++ .../Server/StdioPsesLanguageServer.cs | 41 ++++ 10 files changed, 433 insertions(+), 409 deletions(-) rename src/PowerShellEditorServices.Engine/Hosting/{LogLevel.cs => PsesLogLevel.cs} (100%) delete mode 100644 src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs delete mode 100644 src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs delete mode 100644 src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs delete mode 100644 src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs create mode 100644 src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs create mode 100644 src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs create mode 100644 src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 2cf1ab05f..161d298fc 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -6,20 +6,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Host; -using Microsoft.PowerShell.EditorServices.Templates; +using Microsoft.PowerShell.EditorServices.Engine.Server; using Serilog; namespace Microsoft.PowerShell.EditorServices.Engine @@ -62,8 +57,6 @@ public class EditorServicesHost { #region Private Fields - private readonly IServiceCollection _serviceCollection; - private readonly HostDetails _hostDetails; private readonly PSHost _internalHost; @@ -74,7 +67,7 @@ public class EditorServicesHost private readonly string[] _additionalModules; - private ILanguageServer _languageServer; + private PsesLanguageServer _languageServer; private Microsoft.Extensions.Logging.ILogger _logger; @@ -139,16 +132,15 @@ public EditorServicesHost( Validate.IsNotNull(nameof(hostDetails), hostDetails); Validate.IsNotNull(nameof(internalHost), internalHost); - _serviceCollection = new ServiceCollection(); _hostDetails = hostDetails; //this._hostDetails = hostDetails; - this._enableConsoleRepl = enableConsoleRepl; + _enableConsoleRepl = enableConsoleRepl; //this.bundledModulesPath = bundledModulesPath; - this._additionalModules = additionalModules ?? Array.Empty(); - this._featureFlags = new HashSet(featureFlags ?? Array.Empty()); + _additionalModules = additionalModules ?? Array.Empty(); + _featureFlags = new HashSet(featureFlags ?? Array.Empty()); //this.serverCompletedTask = new TaskCompletionSource(); - this._internalHost = internalHost; + _internalHost = internalHost; #if DEBUG if (waitForDebugger) @@ -236,59 +228,33 @@ public void StartLanguageService( _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - _serviceCollection - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton( - (provider) => - GetFullyInitializedPowerShellContext( - provider.GetService(), - profilePaths)) - .AddSingleton() - .AddSingleton() - .AddSingleton( - (provider) => - { - var extensionService = new ExtensionService( - provider.GetService(), - provider.GetService()); - extensionService.InitializeAsync( - serviceProvider: provider, - editorOperations: provider.GetService()) - .Wait(); - return extensionService; - }) - .AddSingleton( - (provider) => - { - return AnalysisService.Create( - provider.GetService(), - provider.GetService(), - _factory.CreateLogger()); - }); + switch (config.TransportType) { case EditorServiceTransportType.NamedPipe: - _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) - { - NamedPipeName = config.InOutPipeName ?? config.InPipeName, - OutNamedPipeName = config.OutPipeName, - LoggerFactory = _factory, - MinimumLogLevel = LogLevel.Trace, - } - .BuildLanguageServer(); + _languageServer = new NamedPipePsesLanguageServer( + _factory, + LogLevel.Trace, + _enableConsoleRepl, + _featureFlags, + _hostDetails, + _additionalModules, + _internalHost, + profilePaths, + config.InOutPipeName ?? config.InPipeName, + config.OutPipeName); break; case EditorServiceTransportType.Stdio: - _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) - { - Stdio = true, - LoggerFactory = _factory, - MinimumLogLevel = LogLevel.Trace, - } - .BuildLanguageServer(); + _languageServer = new StdioPsesLanguageServer( + _factory, + LogLevel.Trace, + _featureFlags, + _hostDetails, + _additionalModules, + _internalHost, + profilePaths); break; } @@ -302,57 +268,6 @@ public void StartLanguageService( config.TransportType, config.Endpoint)); } - 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, - languageServer, - _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); - - EditorServicesPSHostUserInterface hostUserInterface = - _enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) - : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - _hostDetails, - hostUserInterface, - logger); - - Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - powerShellContext.ImportCommandsModuleAsync( - Path.Combine( - Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), - @"..\Commands")); - - // TODO: This can be moved to the point after the $psEditor object - // gets initialized when that is done earlier than LanguageServer.Initialize - foreach (string module in this._additionalModules) - { - var command = - new PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", module); - - powerShellContext.ExecuteCommandAsync( - command, - sendOutputToHost: false, - sendErrorToHost: true); - } - - return powerShellContext; - } - /// /// Starts the debug service with the specified config. /// diff --git a/src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs b/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Hosting/LogLevel.cs rename to src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs deleted file mode 100644 index 07605468c..000000000 --- a/src/PowerShellEditorServices.Engine/Interface/ILanguageServer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public interface ILanguageServer - { - Task StartAsync(); - - Task WaitForShutdown(); - } -} diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs deleted file mode 100644 index 0b5801b36..000000000 --- a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public interface ILanguageServerBuilder - { - string NamedPipeName { get; set; } - - string OutNamedPipeName { get; set; } - - ILoggerFactory LoggerFactory { get; set; } - - LogLevel MinimumLogLevel { get; set; } - - ILanguageServer BuildLanguageServer(); - } -} diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs deleted file mode 100644 index 4a08bec2d..000000000 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ /dev/null @@ -1,229 +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.IO; -using System.IO.Pipes; -using System.Reflection; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading.Tasks; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.TextDocument; -using OmniSharp.Extensions.LanguageServer.Server; -using OS = OmniSharp.Extensions.LanguageServer.Server; -using PowerShellEditorServices.Engine.Services.Handlers; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public class OmnisharpLanguageServer : ILanguageServer - { - public class Configuration - { - public bool Stdio { get; set; } - - public string NamedPipeName { get; set; } - - public string OutNamedPipeName { get; set; } - - public ILoggerFactory LoggerFactory { get; set; } - - public LogLevel MinimumLogLevel { get; set; } - - public IServiceCollection Services { get; set; } - } - - // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. - private const int CurrentUserOnly = 0x20000000; - - // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, - // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. - // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ - private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = - typeof(NamedPipeServerStream).GetConstructor(new [] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); - - private OS.ILanguageServer _languageServer; - - private TaskCompletionSource _serverStart; - - private readonly Configuration _configuration; - - public OmnisharpLanguageServer( - Configuration configuration) - { - _configuration = configuration; - _serverStart = new TaskCompletionSource(); - } - - public async Task StartAsync() - { - _languageServer = await OS.LanguageServer.From(options => { - - options.AddDefaultLoggingProvider(); - options.LoggerFactory = _configuration.LoggerFactory; - ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); - - if (_configuration.Stdio) - { - options.WithInput(System.Console.OpenStandardInput()); - options.WithOutput(System.Console.OpenStandardOutput()); - } - else - { - NamedPipeServerStream namedPipe = CreateNamedPipe( - _configuration.NamedPipeName, - _configuration.OutNamedPipeName, - out NamedPipeServerStream outNamedPipe); - - logger.LogInformation("Waiting for connection"); - namedPipe.WaitForConnection(); - if (outNamedPipe != null) - { - outNamedPipe.WaitForConnection(); - } - - logger.LogInformation("Connected"); - - options.Input = namedPipe; - options.Output = outNamedPipe ?? namedPipe; - } - - options.MinimumLogLevel = _configuration.MinimumLogLevel; - options.Services = _configuration.Services; - logger.LogInformation("Adding handlers"); - - options - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .OnInitialize( - async (languageServer, request) => - { - var serviceProvider = languageServer.Services; - var workspaceService = serviceProvider.GetService(); - - // Grab the workspace path from the parameters - workspaceService.WorkspacePath = request.RootPath; - - // Set the working directory of the PowerShell session to the workspace path - if (workspaceService.WorkspacePath != null - && Directory.Exists(workspaceService.WorkspacePath)) - { - await serviceProvider.GetService().SetWorkingDirectoryAsync( - workspaceService.WorkspacePath, - isPathAlreadyEscaped: false); - } - }); - - logger.LogInformation("Handlers added"); - }); - - _serverStart.SetResult(true); - } - - public async Task WaitForShutdown() - { - await _serverStart.Task; - await _languageServer.WaitForExit; - } - - private static NamedPipeServerStream CreateNamedPipe( - string inOutPipeName, - string outPipeName, - out NamedPipeServerStream outPipe) - { - // .NET Core implementation is simplest so try that first - if (VersionUtils.IsNetCore) - { - outPipe = outPipeName == null - ? null - : new NamedPipeServerStream( - pipeName: outPipeName, - direction: PipeDirection.Out, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: (PipeOptions)CurrentUserOnly); - - return new NamedPipeServerStream( - pipeName: inOutPipeName, - direction: PipeDirection.InOut, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); - } - - // Now deal with Windows PowerShell - // We need to use reflection to get a nice constructor - - var pipeSecurity = new PipeSecurity(); - - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - WindowsPrincipal principal = new WindowsPrincipal(identity); - - if (principal.IsInRole(WindowsBuiltInRole.Administrator)) - { - // Allow the Administrators group full access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), - PipeAccessRights.FullControl, AccessControlType.Allow)); - } - else - { - // Allow the current user read/write access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - WindowsIdentity.GetCurrent().User, - PipeAccessRights.ReadWrite, AccessControlType.Allow)); - } - - outPipe = outPipeName == null - ? null - : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - outPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - - return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - inOutPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - } - } -} diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs deleted file mode 100644 index d283b99a8..000000000 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public class OmnisharpLanguageServerBuilder : ILanguageServerBuilder - { - public OmnisharpLanguageServerBuilder(IServiceCollection serviceCollection) - { - Services = serviceCollection; - } - - public bool Stdio { get; set; } - - public string NamedPipeName { get; set; } - - public string OutNamedPipeName { get; set; } - - public ILoggerFactory LoggerFactory { get; set; } = new LoggerFactory(); - - public LogLevel MinimumLogLevel { get; set; } = LogLevel.Trace; - - public IServiceCollection Services { get; } - - public ILanguageServer BuildLanguageServer() - { - var config = new OmnisharpLanguageServer.Configuration() - { - LoggerFactory = LoggerFactory, - MinimumLogLevel = MinimumLogLevel, - NamedPipeName = NamedPipeName, - OutNamedPipeName = OutNamedPipeName, - Services = Services, - Stdio = Stdio - }; - - return new OmnisharpLanguageServer(config); - } - } -} diff --git a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs index 4faf9629f..9ee76baa2 100644 --- a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs +++ b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.CompilerServices; -using System.Text; using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices diff --git a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs new file mode 100644 index 000000000..c47cd1c8d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Management.Automation.Host; +using System.Reflection; +using System.Security.AccessControl; +using System.Security.Principal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + internal class NamedPipePsesLanguageServer : PsesLanguageServer + { + // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. + private const int CurrentUserOnly = 0x20000000; + + // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, + // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. + // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ + private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = + typeof(NamedPipeServerStream).GetConstructor(new[] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); + + private readonly string _namedPipeName; + private readonly string _outNamedPipeName; + + internal NamedPipePsesLanguageServer( + ILoggerFactory factory, + LogLevel minimumLogLevel, + bool enableConsoleRepl, + HashSet featureFlags, + HostDetails hostDetails, + string[] additionalModules, + PSHost internalHost, + ProfilePaths profilePaths, + string namedPipeName, + string outNamedPipeName) : base( + factory, + minimumLogLevel, + enableConsoleRepl, + featureFlags, + hostDetails, + additionalModules, + internalHost, + profilePaths) + { + _namedPipeName = namedPipeName; + _outNamedPipeName = outNamedPipeName; + } + + protected override (Stream input, Stream output) GetInputOutputStreams() + { + NamedPipeServerStream namedPipe = CreateNamedPipe( + _namedPipeName, + _outNamedPipeName, + out NamedPipeServerStream outNamedPipe); + + var logger = _loggerFactory.CreateLogger("NamedPipeConnection"); + + logger.LogInformation("Waiting for connection"); + namedPipe.WaitForConnection(); + if (outNamedPipe != null) + { + outNamedPipe.WaitForConnection(); + } + + logger.LogInformation("Connected"); + + return (namedPipe, outNamedPipe ?? namedPipe); + } + + private static NamedPipeServerStream CreateNamedPipe( + string inOutPipeName, + string outPipeName, + out NamedPipeServerStream outPipe) + { + // .NET Core implementation is simplest so try that first + if (VersionUtils.IsNetCore) + { + outPipe = outPipeName == null + ? null + : new NamedPipeServerStream( + pipeName: outPipeName, + direction: PipeDirection.Out, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: (PipeOptions)CurrentUserOnly); + + return new NamedPipeServerStream( + pipeName: inOutPipeName, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); + } + + // Now deal with Windows PowerShell + // We need to use reflection to get a nice constructor + + var pipeSecurity = new PipeSecurity(); + + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + // Allow the Administrators group full access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); + } + else + { + // Allow the current user read/write access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + WindowsIdentity.GetCurrent().User, + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + } + + outPipe = outPipeName == null + ? null + : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + outPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + + return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + inOutPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs new file mode 100644 index 000000000..819ce9aa7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs @@ -0,0 +1,215 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Host; +using Microsoft.PowerShell.EditorServices.Templates; +using Microsoft.PowerShell.EditorServices.TextDocument; +using OmniSharp.Extensions.LanguageServer.Server; +using PowerShellEditorServices.Engine.Services.Handlers; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + internal abstract class PsesLanguageServer + { + protected readonly ILoggerFactory _loggerFactory; + private readonly LogLevel _minimumLogLevel; + private readonly bool _enableConsoleRepl; + private readonly HashSet _featureFlags; + private readonly HostDetails _hostDetails; + private readonly string[] _additionalModules; + private readonly PSHost _internalHost; + private readonly ProfilePaths _profilePaths; + private readonly TaskCompletionSource _serverStart; + + private ILanguageServer _languageServer; + + internal PsesLanguageServer( + ILoggerFactory factory, + LogLevel minimumLogLevel, + bool enableConsoleRepl, + HashSet featureFlags, + HostDetails hostDetails, + string[] additionalModules, + PSHost internalHost, + ProfilePaths profilePaths) + { + _loggerFactory = factory; + _minimumLogLevel = minimumLogLevel; + _enableConsoleRepl = enableConsoleRepl; + _featureFlags = featureFlags; + _hostDetails = hostDetails; + _additionalModules = additionalModules; + _internalHost = internalHost; + _profilePaths = profilePaths; + _serverStart = new TaskCompletionSource(); + } + + public async Task StartAsync() + { + _languageServer = await LanguageServer.From(options => + { + options.AddDefaultLoggingProvider(); + options.LoggerFactory = _loggerFactory; + ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); + options.Services = new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => + GetFullyInitializedPowerShellContext( + provider.GetService(), + _profilePaths)) + .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => + { + var extensionService = new ExtensionService( + provider.GetService(), + provider.GetService()); + extensionService.InitializeAsync( + serviceProvider: provider, + editorOperations: provider.GetService()) + .Wait(); + return extensionService; + }) + .AddSingleton( + (provider) => + { + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + options.LoggerFactory.CreateLogger()); + }); + + (Stream input, Stream output) = GetInputOutputStreams(); + + options + .WithInput(input) + .WithOutput(output); + + options.MinimumLogLevel = _minimumLogLevel; + + logger.LogInformation("Adding handlers"); + + options + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .OnInitialize( + async (languageServer, request) => + { + var serviceProvider = languageServer.Services; + var workspaceService = serviceProvider.GetService(); + + // Grab the workspace path from the parameters + workspaceService.WorkspacePath = request.RootPath; + + // Set the working directory of the PowerShell session to the workspace path + if (workspaceService.WorkspacePath != null + && Directory.Exists(workspaceService.WorkspacePath)) + { + await serviceProvider.GetService().SetWorkingDirectoryAsync( + workspaceService.WorkspacePath, + isPathAlreadyEscaped: false); + } + }); + + logger.LogInformation("Handlers added"); + }); + } + + public async Task WaitForShutdown() + { + await _serverStart.Task; + await _languageServer.WaitForExit; + } + + private PowerShellContextService GetFullyInitializedPowerShellContext( + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + ProfilePaths profilePaths) + { + var logger = _loggerFactory.CreateLogger(); + + // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise + // issues arise when redirecting stdio. + var powerShellContext = new PowerShellContextService( + logger, + languageServer, + _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); + + EditorServicesPSHostUserInterface hostUserInterface = + _enableConsoleRepl + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) + : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + _hostDetails, + hostUserInterface, + logger); + + Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + powerShellContext.ImportCommandsModuleAsync( + Path.Combine( + Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), + @"..\Commands")); + + // TODO: This can be moved to the point after the $psEditor object + // gets initialized when that is done earlier than LanguageServer.Initialize + foreach (string module in this._additionalModules) + { + var command = + new PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("Name", module); + + powerShellContext.ExecuteCommandAsync( + command, + sendOutputToHost: false, + sendErrorToHost: true); + } + + return powerShellContext; + } + + protected abstract (Stream input, Stream output) GetInputOutputStreams(); + } +} diff --git a/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs new file mode 100644 index 000000000..462dcd30f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.IO; +using System.Management.Automation.Host; +using Microsoft.Extensions.Logging; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + internal class StdioPsesLanguageServer : PsesLanguageServer + { + internal StdioPsesLanguageServer( + ILoggerFactory factory, + LogLevel minimumLogLevel, + HashSet featureFlags, + HostDetails hostDetails, + string[] additionalModules, + PSHost internalHost, + ProfilePaths profilePaths) : base( + factory, + minimumLogLevel, + // Stdio server can't support an integrated console so we pass in false. + false, + featureFlags, + hostDetails, + additionalModules, + internalHost, + profilePaths) + { + + } + + protected override (Stream input, Stream output) GetInputOutputStreams() + { + return (System.Console.OpenStandardInput(), System.Console.OpenStandardOutput()); + } + } +} From 5346056870d22f2180c56b89fb07f507dacb33e9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 16 Sep 2019 09:06:30 -0700 Subject: [PATCH 39/47] rename namespaces (#1019) --- .../PowerShellEditorServices.psm1 | 22 +- .../BuildInfo.cs | 7 +- .../Hosting/EditorServicesHost.cs | 3 +- .../Hosting/HostDetails.cs | 2 +- .../Hosting/ProfilePaths.cs | 4 +- .../Hosting/PsesLogLevel.cs | 7 +- .../LanguageServerSettings.cs | 381 ------------------ .../Logging/LoggerExtensions.cs | 7 +- .../Server/NamedPipePsesLanguageServer.cs | 2 + .../Server/PsesLanguageServer.cs | 9 +- .../Server/StdioPsesLanguageServer.cs | 1 + .../Services/Analysis/AnalysisService.cs | 3 +- .../Services/CodeLens/CodeLensData.cs | 2 +- .../Services/CodeLens/ICodeLensProvider.cs | 5 +- .../Services/CodeLens/ICodeLenses.cs | 3 +- .../CodeLens/PesterCodeLensProvider.cs | 8 +- .../CodeLens/ReferencesCodeLensProvider.cs | 12 +- .../Console/ChoiceDetails.cs | 2 +- .../Console/ChoicePromptHandler.cs | 2 +- .../Console/CollectionFieldDetails.cs | 2 +- .../Console/ConsoleChoicePromptHandler.cs | 2 +- .../Console/ConsoleInputPromptHandler.cs | 2 +- .../PowerShellContext/Console/ConsoleProxy.cs | 2 +- .../Console/ConsoleReadLine.cs | 2 +- .../Console/CredentialFieldDetails.cs | 2 +- .../PowerShellContext/Console/FieldDetails.cs | 2 +- .../Console/IConsoleOperations.cs | 2 +- .../Console/InputPromptHandler.cs | 2 +- .../Console/PromptHandler.cs | 2 +- .../Console/TerminalChoicePromptHandler.cs | 2 +- .../Console/TerminalInputPromptHandler.cs | 2 +- .../Console/UnixConsoleOperations.cs | 2 +- .../Console/WindowsConsoleOperations.cs | 2 +- .../EditorOperationsService.cs | 7 +- .../PowerShellContext/ExtensionService.cs | 4 +- .../Extensions/EditorCommand.cs | 2 +- .../Extensions/EditorCommandAttribute.cs | 2 +- .../Extensions/EditorContext.cs | 3 +- .../Extensions/EditorObject.cs | 2 +- .../Extensions/EditorRequests.cs | 2 +- .../Extensions/EditorWindow.cs | 2 +- .../Extensions/EditorWorkspace.cs | 2 +- .../Extensions/FileContext.cs | 3 +- .../Extensions/IEditorOperations.cs | 3 +- .../Handlers/EvaluateHandler.cs | 10 +- .../Handlers/ExpandAliasHandler.cs | 4 +- .../Handlers/GetCommandHandler.cs | 4 +- .../Handlers/GetCommentHelpHandler.cs | 13 +- .../Handlers/GetVersionHandler.cs | 9 +- .../Handlers/IEvaluateHandler.cs | 9 +- .../Handlers/IGetCommentHelpHandler.cs | 2 +- .../Handlers/IGetPSHostProcessesHandler.cs | 7 +- .../Handlers/IGetRunspaceHandler.cs | 7 +- .../Handlers/IGetVersionHandler.cs | 9 +- .../IInvokeExtensionCommandHandler.cs | 9 +- .../Handlers/ITemplateHandlers.cs | 2 +- .../Handlers/InvokeExtensionCommandHandler.cs | 12 +- .../PSHostProcessAndRunspaceHandlers.cs | 12 +- .../Handlers/ShowHelpHandler.cs | 4 +- .../Handlers/TemplateHandlers.cs | 6 +- .../PowerShellContextService.cs | 9 +- .../Capabilities/DscBreakpointCapability.cs | 2 +- .../Session/ExecutionOptions.cs | 2 +- .../Session/ExecutionStatus.cs | 2 +- .../ExecutionStatusChangedEventArgs.cs | 2 +- .../Session/ExecutionTarget.cs | 2 +- .../Session/Host/EditorServicesPSHost.cs | 5 +- .../Host/EditorServicesPSHostUserInterface.cs | 5 +- .../Session/Host/IHostInput.cs | 2 +- .../Session/Host/IHostOutput.cs | 2 +- .../Session/Host/PromptEvents.cs | 2 +- .../Session/Host/PromptHandlers.cs | 4 +- .../Host/ProtocolPSHostUserInterface.cs | 3 +- .../Host/SimplePSHostRawUserInterface.cs | 2 +- .../Host/TerminalPSHostRawUserInterface.cs | 3 +- .../Host/TerminalPSHostUserInterface.cs | 13 +- .../Session/IPromptContext.cs | 2 +- .../Session/IRunspaceCapability.cs | 2 +- .../Session/IVersionSpecificOperations.cs | 2 +- .../Session/InvocationEventQueue.cs | 2 +- .../Session/LegacyReadLineContext.cs | 3 +- .../PowerShellContext/Session/OutputType.cs | 2 +- .../Session/OutputWrittenEventArgs.cs | 2 +- .../Session/PSReadLinePromptContext.cs | 7 +- .../Session/PSReadLineProxy.cs | 2 +- .../Session/PipelineExecutionRequest.cs | 2 +- .../Session/PowerShell5Operations.cs | 2 +- .../Session/PowerShellContextState.cs | 2 +- .../Session/PowerShellExecutionResult.cs | 2 +- .../Session/PowerShellVersionDetails.cs | 2 +- .../Session/ProgressDetails.cs | 2 +- .../PowerShellContext/Session/PromptNest.cs | 2 +- .../Session/PromptNestFrame.cs | 2 +- .../Session/PromptNestFrameType.cs | 2 +- .../Session/RemoteFileManager.cs | 5 +- .../Session/RunspaceChangedEventArgs.cs | 4 +- .../Session/RunspaceDetails.cs | 3 +- .../Session/RunspaceHandle.cs | 2 +- .../Session/SessionDetails.cs | 3 +- .../Session/SessionStateChangedEventArgs.cs | 2 +- .../Session/ThreadController.cs | 2 +- .../PowerShellContext/TemplateService.cs | 5 +- .../Utilities/CommandHelpers.cs | 3 +- .../Symbols/IDocumentSymbolProvider.cs | 3 +- .../Services/Symbols/IDocumentSymbols.cs | 3 +- .../Symbols/ParameterSetSignatures.cs | 6 +- .../Symbols/PesterDocumentSymbolProvider.cs | 3 +- .../Symbols/PsdDocumentSymbolProvider.cs | 3 +- .../Symbols/ScriptDocumentSymbolProvider.cs | 3 +- .../Services/Symbols/ScriptExtent.cs | 2 +- .../Services/Symbols/SymbolDetails.cs | 4 +- .../Services/Symbols/SymbolReference.cs | 4 +- .../Services/Symbols/SymbolType.cs | 2 +- .../Services/Symbols/SymbolsService.cs | 9 +- .../Services/Symbols/Vistors/AstOperations.cs | 3 +- .../Symbols/Vistors/FindCommandVisitor.cs | 2 +- .../Symbols/Vistors/FindDeclarationVisitor.cs | 2 +- .../Symbols/Vistors/FindDotSourcedVisitor.cs | 4 +- .../Symbols/Vistors/FindReferencesVisitor.cs | 2 +- .../Symbols/Vistors/FindSymbolVisitor.cs | 2 +- .../Symbols/Vistors/FindSymbolsVisitor.cs | 2 +- .../Symbols/Vistors/FindSymbolsVisitor2.cs | 2 +- .../Services/TextDocument/BufferPosition.cs | 2 +- .../Services/TextDocument/BufferRange.cs | 2 +- .../TextDocument/CompletionResults.cs | 2 +- .../Services/TextDocument/FileChange.cs | 2 +- .../Services/TextDocument/FilePosition.cs | 2 +- .../Services/TextDocument/FoldingReference.cs | 2 +- .../Handlers/CodeActionHandler.cs | 15 +- .../TextDocument/Handlers/CodeLensHandlers.cs | 15 +- .../Handlers/CompletionHandler.cs | 8 +- .../Handlers/DefinitionHandler.cs | 9 +- .../Handlers/DocumentHighlightHandler.cs | 9 +- .../Handlers/DocumentSymbolHandler.cs | 17 +- .../Handlers/FoldingRangeHandler.cs | 10 +- .../Handlers/FormattingHandlers.cs | 11 +- .../TextDocument/Handlers/HoverHandler.cs | 13 +- .../Handlers/ReferencesHandler.cs | 16 +- .../Handlers/SignatureHelpHandler.cs | 6 +- .../Handlers/TextDocumentHandler.cs | 11 +- .../Services/TextDocument/ScriptFile.cs | 5 +- .../Services/TextDocument/ScriptFileMarker.cs | 3 +- .../Services/TextDocument/ScriptRegion.cs | 3 +- .../Services/TextDocument/TokenOperations.cs | 3 +- .../Workspace/ConfigurationService.cs | 4 +- .../Handlers/ConfigurationHandler.cs | 9 +- .../Handlers/WorkspaceSymbolsHandler.cs | 14 +- .../Workspace/LanguageServerSettings.cs | 3 +- .../Workspace/WorkspaceFileSystemWrapper.cs | 3 +- .../Services/Workspace/WorkspaceService.cs | 5 +- .../IScriptExtentExtensions.cs | 2 +- .../Utility/PathUtils.cs | 7 +- .../Utility/Validate.cs | 7 +- .../Utility/VersionUtils.cs | 7 +- .../LanguageServerProtocolMessageTests.cs | 3 +- 155 files changed, 437 insertions(+), 646 deletions(-) delete mode 100644 src/PowerShellEditorServices.Engine/LanguageServerSettings.cs rename src/PowerShellEditorServices.Engine/{Services/CodeLens => Utility}/IScriptExtentExtensions.cs (93%) diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index 431e208b9..79a5dbd23 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -91,13 +91,13 @@ function Start-EditorServicesHost { $editorServicesHost = $null $hostDetails = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.HostDetails @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.HostDetails @( $HostName, $HostProfileId, (Microsoft.PowerShell.Utility\New-Object System.Version @($HostVersion))) $editorServicesHost = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.EditorServicesHost @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServicesHost @( $hostDetails, $BundledModulesPath, $EnableConsoleRepl.IsPresent, @@ -108,7 +108,7 @@ function Start-EditorServicesHost { # Build the profile paths using the root paths of the current $profile variable $profilePaths = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.ProfilePaths @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.ProfilePaths @( $hostDetails.ProfileId, [System.IO.Path]::GetDirectoryName($profile.AllUsersAllHosts), [System.IO.Path]::GetDirectoryName($profile.CurrentUserAllHosts)) @@ -116,32 +116,32 @@ function Start-EditorServicesHost { $editorServicesHost.StartLogging($LogPath, $LogLevel); $languageServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportConfig + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportConfig $debugServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportConfig + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportConfig switch ($PSCmdlet.ParameterSetName) { "Stdio" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::Stdio - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::Stdio + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::Stdio + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::Stdio break } "NamedPipe" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe $languageServiceConfig.InOutPipeName = "$LanguageServiceNamedPipe" if ($DebugServiceNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe $debugServiceConfig.InOutPipeName = "$DebugServiceNamedPipe" } break } "NamedPipeSimplex" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe $languageServiceConfig.InPipeName = $LanguageServiceInNamedPipe $languageServiceConfig.OutPipeName = $LanguageServiceOutNamedPipe if ($DebugServiceInNamedPipe -and $DebugServiceOutNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe $debugServiceConfig.InPipeName = $DebugServiceInNamedPipe $debugServiceConfig.OutPipeName = $DebugServiceOutNamedPipe } diff --git a/src/PowerShellEditorServices.Engine/BuildInfo.cs b/src/PowerShellEditorServices.Engine/BuildInfo.cs index f808390e5..47c020fbf 100644 --- a/src/PowerShellEditorServices.Engine/BuildInfo.cs +++ b/src/PowerShellEditorServices.Engine/BuildInfo.cs @@ -1,9 +1,14 @@ +// +// 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.Engine { public static class BuildInfo { public const string BuildVersion = ""; public const string BuildOrigin = ""; - public static readonly System.DateTime? BuildTime = null; + public static readonly System.DateTime? BuildTime; } } diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 161d298fc..83f61a729 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -15,9 +15,10 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Engine.Server; +using Microsoft.PowerShell.EditorServices.Utility; using Serilog; -namespace Microsoft.PowerShell.EditorServices.Engine +namespace Microsoft.PowerShell.EditorServices.Engine.Hosting { public enum EditorServicesHostStatus { diff --git a/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs b/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs index febaaf7c8..ffd829087 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine +namespace Microsoft.PowerShell.EditorServices.Engine.Hosting { /// /// Contains details about the current host application (most diff --git a/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs b/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs index 29bab2b56..4f5e17dce 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs @@ -3,13 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine +namespace Microsoft.PowerShell.EditorServices.Engine.Hosting { /// /// Provides profile path resolution behavior relative to the name diff --git a/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs b/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs index dfd50ffaf..4438fdd02 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs @@ -1,6 +1,11 @@ +// +// 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; -namespace Microsoft.PowerShell.EditorServices.Engine +namespace Microsoft.PowerShell.EditorServices.Engine.Hosting { public enum PsesLogLevel { diff --git a/src/PowerShellEditorServices.Engine/LanguageServerSettings.cs b/src/PowerShellEditorServices.Engine/LanguageServerSettings.cs deleted file mode 100644 index 26eb0e9a5..000000000 --- a/src/PowerShellEditorServices.Engine/LanguageServerSettings.cs +++ /dev/null @@ -1,381 +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.Collections; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Security; -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Engine -{ - public class LanguageServerSettings - { - public bool EnableProfileLoading { get; set; } - - public ScriptAnalysisSettings ScriptAnalysis { get; set; } - - public CodeFormattingSettings CodeFormatting { get; set; } - - public CodeFoldingSettings CodeFolding { get; set; } - - public LanguageServerSettings() - { - this.ScriptAnalysis = new ScriptAnalysisSettings(); - this.CodeFormatting = new CodeFormattingSettings(); - this.CodeFolding = new CodeFoldingSettings(); - } - - public void Update( - LanguageServerSettings settings, - string workspaceRootPath, - ILogger logger) - { - if (settings != null) - { - this.EnableProfileLoading = settings.EnableProfileLoading; - this.ScriptAnalysis.Update( - settings.ScriptAnalysis, - workspaceRootPath, - logger); - this.CodeFormatting = new CodeFormattingSettings(settings.CodeFormatting); - this.CodeFolding.Update(settings.CodeFolding, logger); - } - } - } - - public class ScriptAnalysisSettings - { - public bool? Enable { get; set; } - - public string SettingsPath { get; set; } - - public ScriptAnalysisSettings() - { - this.Enable = true; - } - - public void Update( - ScriptAnalysisSettings settings, - string workspaceRootPath, - ILogger logger) - { - if (settings != null) - { - this.Enable = settings.Enable; - - string settingsPath = settings.SettingsPath; - - try - { - if (string.IsNullOrWhiteSpace(settingsPath)) - { - settingsPath = null; - } - else if (!Path.IsPathRooted(settingsPath)) - { - if (string.IsNullOrEmpty(workspaceRootPath)) - { - // The workspace root path could be an empty string - // when the user has opened a PowerShell script file - // without opening an entire folder (workspace) first. - // In this case we should just log an error and let - // the specified settings path go through even though - // it will fail to load. - logger.LogError( - "Could not resolve Script Analyzer settings path due to null or empty workspaceRootPath."); - } - else - { - settingsPath = Path.GetFullPath(Path.Combine(workspaceRootPath, settingsPath)); - } - } - - this.SettingsPath = settingsPath; - logger.LogDebug($"Using Script Analyzer settings path - '{settingsPath ?? ""}'."); - } - catch (Exception ex) when ( - ex is NotSupportedException || - ex is PathTooLongException || - ex is SecurityException) - { - // Invalid chars in path like ${env:HOME} can cause Path.GetFullPath() to throw, catch such errors here - logger.LogException( - $"Invalid Script Analyzer settings path - '{settingsPath}'.", - ex); - - this.SettingsPath = null; - } - } - } - } - - /// - /// Code formatting presets. - /// See https://en.wikipedia.org/wiki/Indent_style for details on indent and brace styles. - /// - public enum CodeFormattingPreset - { - /// - /// Use the formatting settings as-is. - /// - Custom, - - /// - /// Configure the formatting settings to resemble the Allman indent/brace style. - /// - Allman, - - /// - /// Configure the formatting settings to resemble the one true brace style variant of K&R indent/brace style. - /// - OTBS, - - /// - /// Configure the formatting settings to resemble the Stroustrup brace style variant of K&R indent/brace style. - /// - Stroustrup - } - - /// - /// Multi-line pipeline style settings. - /// - public enum PipelineIndentationStyle - { - /// - /// After the indentation level only once after the first pipeline and keep this level for the following pipelines. - /// - IncreaseIndentationForFirstPipeline, - - /// - /// After every pipeline, keep increasing the indentation. - /// - IncreaseIndentationAfterEveryPipeline, - - /// - /// Do not increase indentation level at all after pipeline. - /// - NoIndentation - } - - public class CodeFormattingSettings - { - /// - /// Default constructor. - /// > - public CodeFormattingSettings() - { - - } - - /// - /// Copy constructor. - /// - /// An instance of type CodeFormattingSettings. - public CodeFormattingSettings(CodeFormattingSettings codeFormattingSettings) - { - if (codeFormattingSettings == null) - { - throw new ArgumentNullException(nameof(codeFormattingSettings)); - } - - foreach (var prop in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - prop.SetValue(this, prop.GetValue(codeFormattingSettings)); - } - } - - public CodeFormattingPreset Preset { get; set; } - public bool OpenBraceOnSameLine { get; set; } - public bool NewLineAfterOpenBrace { get; set; } - public bool NewLineAfterCloseBrace { get; set; } - public PipelineIndentationStyle PipelineIndentationStyle { get; set; } - public bool WhitespaceBeforeOpenBrace { get; set; } - public bool WhitespaceBeforeOpenParen { get; set; } - public bool WhitespaceAroundOperator { get; set; } - public bool WhitespaceAfterSeparator { get; set; } - public bool WhitespaceInsideBrace { get; set; } - public bool WhitespaceAroundPipe { get; set; } - public bool IgnoreOneLineBlock { get; set; } - public bool AlignPropertyValuePairs { get; set; } - public bool UseCorrectCasing { get; set; } - - - /// - /// Get the settings hashtable that will be consumed by PSScriptAnalyzer. - /// - /// The tab size in the number spaces. - /// If true, insert spaces otherwise insert tabs for indentation. - /// - public Hashtable GetPSSASettingsHashtable( - int tabSize, - bool insertSpaces) - { - var settings = GetCustomPSSASettingsHashtable(tabSize, insertSpaces); - var ruleSettings = (Hashtable)(settings["Rules"]); - var closeBraceSettings = (Hashtable)ruleSettings["PSPlaceCloseBrace"]; - var openBraceSettings = (Hashtable)ruleSettings["PSPlaceOpenBrace"]; - switch(Preset) - { - case CodeFormattingPreset.Allman: - openBraceSettings["OnSameLine"] = false; - openBraceSettings["NewLineAfter"] = true; - closeBraceSettings["NewLineAfter"] = true; - break; - - case CodeFormattingPreset.OTBS: - openBraceSettings["OnSameLine"] = true; - openBraceSettings["NewLineAfter"] = true; - closeBraceSettings["NewLineAfter"] = false; - break; - - case CodeFormattingPreset.Stroustrup: - openBraceSettings["OnSameLine"] = true; - openBraceSettings["NewLineAfter"] = true; - closeBraceSettings["NewLineAfter"] = true; - break; - - default: - break; - } - - return settings; - } - - private Hashtable GetCustomPSSASettingsHashtable(int tabSize, bool insertSpaces) - { - return new Hashtable - { - {"IncludeRules", new string[] { - "PSPlaceCloseBrace", - "PSPlaceOpenBrace", - "PSUseConsistentWhitespace", - "PSUseConsistentIndentation", - "PSAlignAssignmentStatement" - }}, - {"Rules", new Hashtable { - {"PSPlaceOpenBrace", new Hashtable { - {"Enable", true}, - {"OnSameLine", OpenBraceOnSameLine}, - {"NewLineAfter", NewLineAfterOpenBrace}, - {"IgnoreOneLineBlock", IgnoreOneLineBlock} - }}, - {"PSPlaceCloseBrace", new Hashtable { - {"Enable", true}, - {"NewLineAfter", NewLineAfterCloseBrace}, - {"IgnoreOneLineBlock", IgnoreOneLineBlock} - }}, - {"PSUseConsistentIndentation", new Hashtable { - {"Enable", true}, - {"IndentationSize", tabSize}, - {"PipelineIndentation", PipelineIndentationStyle }, - {"Kind", insertSpaces ? "space" : "tab"} - }}, - {"PSUseConsistentWhitespace", new Hashtable { - {"Enable", true}, - {"CheckOpenBrace", WhitespaceBeforeOpenBrace}, - {"CheckOpenParen", WhitespaceBeforeOpenParen}, - {"CheckOperator", WhitespaceAroundOperator}, - {"CheckSeparator", WhitespaceAfterSeparator}, - {"CheckInnerBrace", WhitespaceInsideBrace}, - {"CheckPipe", WhitespaceAroundPipe}, - }}, - {"PSAlignAssignmentStatement", new Hashtable { - {"Enable", true}, - {"CheckHashtable", AlignPropertyValuePairs} - }}, - {"PSUseCorrectCasing", new Hashtable { - {"Enable", UseCorrectCasing} - }}, - }} - }; - } - } - - /// - /// Code folding settings - /// - public class CodeFoldingSettings - { - /// - /// Whether the folding is enabled. Default is true as per VSCode - /// - public bool Enable { get; set; } = true; - - /// - /// Whether to show or hide the last line of a folding region. Default is true as per VSCode - /// - public bool ShowLastLine { get; set; } = true; - - /// - /// Update these settings from another settings object - /// - public void Update( - CodeFoldingSettings settings, - ILogger logger) - { - if (settings != null) { - if (this.Enable != settings.Enable) { - this.Enable = settings.Enable; - logger.LogDebug(string.Format("Using Code Folding Enabled - {0}", this.Enable)); - } - if (this.ShowLastLine != settings.ShowLastLine) { - this.ShowLastLine = settings.ShowLastLine; - logger.LogDebug(string.Format("Using Code Folding ShowLastLine - {0}", this.ShowLastLine)); - } - } - } - } - - /// - /// Additional settings from the Language Client that affect Language Server operations but - /// do not exist under the 'powershell' section - /// - public class EditorFileSettings - { - /// - /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the - /// the glob is in effect. - /// - public Dictionary Exclude { get; set; } - } - - /// - /// Additional settings from the Language Client that affect Language Server operations but - /// do not exist under the 'powershell' section - /// - public class EditorSearchSettings - { - /// - /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the - /// the glob is in effect. - /// - public Dictionary Exclude { get; set; } - /// - /// Whether to follow symlinks when searching - /// - public bool FollowSymlinks { get; set; } = true; - } - - public class LanguageServerSettingsWrapper - { - // NOTE: This property is capitalized as 'Powershell' because the - // mode name sent from the client is written as 'powershell' and - // JSON.net is using camelCasing. - public LanguageServerSettings Powershell { get; set; } - - // NOTE: This property is capitalized as 'Files' because the - // mode name sent from the client is written as 'files' and - // JSON.net is using camelCasing. - public EditorFileSettings Files { get; set; } - - // NOTE: This property is capitalized as 'Search' because the - // mode name sent from the client is written as 'search' and - // JSON.net is using camelCasing. - public EditorSearchSettings Search { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs index 9ee76baa2..785a72d83 100644 --- a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs +++ b/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs @@ -1,8 +1,13 @@ +// +// 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.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Logging { internal static class LoggerExtensions { diff --git a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs index c47cd1c8d..08086d5d8 100644 --- a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs @@ -11,6 +11,8 @@ using System.Security.AccessControl; using System.Security.Principal; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Hosting; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Engine.Server { diff --git a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs index 819ce9aa7..f0ce7f920 100644 --- a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs @@ -12,12 +12,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Host; -using Microsoft.PowerShell.EditorServices.Templates; -using Microsoft.PowerShell.EditorServices.TextDocument; +using Microsoft.PowerShell.EditorServices.Engine.Handlers; +using Microsoft.PowerShell.EditorServices.Engine.Hosting; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using OmniSharp.Extensions.LanguageServer.Server; -using PowerShellEditorServices.Engine.Services.Handlers; namespace Microsoft.PowerShell.EditorServices.Engine.Server { diff --git a/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs index 462dcd30f..0da4d961e 100644 --- a/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs @@ -7,6 +7,7 @@ using System.IO; using System.Management.Automation.Host; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Hosting; namespace Microsoft.PowerShell.EditorServices.Engine.Server { diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index 75db008a6..d36e497d2 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -16,8 +16,9 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; using System.Collections.Concurrent; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services { /// /// Provides a high-level service for performing semantic analysis diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs index 15fff728d..8768e2555 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.CodeLenses +namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses { /// /// Represents data expected back in an LSP CodeLens response. diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs index fab101580..06921c67c 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs @@ -3,11 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System.Threading; -using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.CodeLenses +namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses { /// /// Specifies the contract for a Code Lens provider. diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs index 6bca0c9be..bafd13bbc 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs @@ -4,9 +4,10 @@ // using System.Collections.Generic; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.CodeLenses +namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses { /// /// Specifies the contract for an implementation of diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs index 3d7d85556..d2b7daaf4 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs @@ -4,13 +4,13 @@ // using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Engine.Utility; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.CodeLenses +namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses { internal class PesterCodeLensProvider : ICodeLensProvider { diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs index 5e1c16581..2c1862d59 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -5,16 +5,16 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using PowerShellEditorServices.Engine.Utility; -namespace Microsoft.PowerShell.EditorServices.CodeLenses +namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses { /// /// Provides the "reference" code lens by extracting document symbols. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs index d8121b6c1..ece5bd4ae 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs @@ -6,7 +6,7 @@ using System; using System.Management.Automation.Host; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Contains the details about a choice that should be displayed diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs index b4524789f..9f9557920 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Indicates the style of prompt to be displayed. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs index 80ae62b5f..e9a6fb425 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs @@ -6,7 +6,7 @@ using System; using System.Collections; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Contains the details of an colleciton input field shown diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs index e5ed4472c..976ff4533 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs @@ -6,7 +6,7 @@ using System.Linq; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a standard implementation of ChoicePromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs index 6e804f86e..be274122c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs @@ -6,7 +6,7 @@ using System; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a standard implementation of InputPromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs index b9312ca7c..00c2e13e6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides asynchronous implementations of the API's as well as diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs index 466e10764..9f5648309 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { using System; using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs index 4b4452f2b..edcbc2fd6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs @@ -7,7 +7,7 @@ using System.Management.Automation; using System.Security; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Contains the details of a PSCredential field shown diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs index 9fc80252e..88274be04 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs @@ -11,7 +11,7 @@ using System.Management.Automation.Host; using System.Reflection; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Contains the details of an input field shown from an diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs index b3fb58561..c124c5cf4 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs @@ -7,7 +7,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides platform specific console utilities. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs index 2b5c9bd23..442266a92 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a base implementation for IPromptHandler classes diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs index 0de8a91b5..1bcdec99e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs @@ -6,7 +6,7 @@ using System; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Defines an abstract base class for prompt handler implementations. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs index 174c15a11..9cc303bf4 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a standard implementation of ChoicePromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs index 4d95bc92c..245a036c2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a standard implementation of InputPromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs index e51547a10..9d715561f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs @@ -9,7 +9,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using UnixConsoleEcho; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class UnixConsoleOperations : IConsoleOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs index 493e66930..c1118b584 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Console +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class WindowsConsoleOperations : IConsoleOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs index 6d651acde..5d42a56d6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs @@ -3,13 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Engine.Handlers; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Services.Handlers; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services { internal class EditorOperationsService : IEditorOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs index 8ecc19300..983d8354a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs @@ -3,14 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System; using System.Collections.Generic; using System.Management.Automation; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services { /// /// Provides a high-level service which enables PowerShell scripts diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs index 8a7b80cef..193878f2b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs @@ -5,7 +5,7 @@ using System.Management.Automation; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides details about a command that has been registered diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs index 71b96d300..c43becca3 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides an attribute that can be used to target PowerShell diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs index 83e94a7a5..c2a1d0a95 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs @@ -6,8 +6,9 @@ using System; using System.Linq; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides context for the host editor at the time of creation. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs index 44a72a6c8..a9643104f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs @@ -6,7 +6,7 @@ using System; using System.Reflection; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides the entry point of the extensibility API, inserted into diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs index 38e5c80f8..a4c293ced 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs @@ -5,7 +5,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { public class ExtensionCommandAddedNotification { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs index 8d241559a..d69649510 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a PowerShell-facing API which allows scripts to diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs index 4eb22de4a..32670d74b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a PowerShell-facing API which allows scripts to diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs index ace8f5271..6ed9a64f2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs @@ -6,8 +6,9 @@ using System; using System.IO; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides context for a file that is open in the editor. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs index 4e3f532fd..2435930cd 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs @@ -4,8 +4,9 @@ // using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Extensions +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides an interface that must be implemented by an editor diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs index 61b3dae51..04a8c01ab 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs @@ -1,10 +1,14 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class EvaluateHandler : IEvaluateHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs index bab5d3ee1..1f9677c46 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs @@ -8,11 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/expandAlias")] public interface IExpandAliasHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs index aee02fe1b..0cffb060a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs @@ -8,11 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/getCommand")] public interface IGetCommandHandler : IJsonRpcRequestHandler> { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs index e00391880..e0f5289c7 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs @@ -1,14 +1,19 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class GetCommentHelpHandler : IGetCommentHelpHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs index 4b57ecd39..03c73b7d4 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -1,10 +1,15 @@ +// +// 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.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Utility; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class GetVersionHandler : IGetVersionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs index 1fd053a0f..90a7e18ee 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs @@ -1,7 +1,12 @@ -using OmniSharp.Extensions.Embedded.MediatR; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("evaluate")] public interface IEvaluateHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs index 85c2f00ad..142d78e8f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs @@ -7,7 +7,7 @@ using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/getCommentHelp")] public interface IGetCommentHelpHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs index f5a366c1d..61b59623e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs @@ -1,7 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/getPSHostProcesses")] public interface IGetPSHostProcessesHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs index dcd94c1e6..039f60563 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs @@ -1,7 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/getRunspace")] public interface IGetRunspaceHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs index 253e34daa..afaa4f4ab 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs @@ -1,8 +1,13 @@ -using Microsoft.PowerShell.EditorServices.Session; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/getVersion")] public interface IGetVersionHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs index e44825c95..465bf1a5b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs @@ -1,8 +1,13 @@ -using OmniSharp.Extensions.Embedded.MediatR; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/invokeExtensionCommand")] public interface IInvokeExtensionCommandHandler : IJsonRpcNotificationHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs index 9fefae8d1..13f2cf4e3 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs @@ -6,7 +6,7 @@ using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/getProjectTemplates")] public interface IGetProjectTemplatesHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs index fee04ff84..51d0b2e6a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs @@ -1,10 +1,16 @@ -using System.Threading; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using OmniSharp.Extensions.Embedded.MediatR; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { internal class InvokeExtensionCommandHandler : IInvokeExtensionCommandHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs index 52b8b3411..5337ffb6f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs @@ -1,14 +1,20 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.Collections.Generic; -using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { + using System.Management.Automation; + public class PSHostProcessAndRunspaceHandlers : IGetPSHostProcessesHandler, IGetRunspaceHandler { private readonly ILogger _logger; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs index aecf58083..348c1893b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs @@ -7,11 +7,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { [Serial, Method("powerShell/showHelp")] public interface IShowHelpHandler : IJsonRpcNotificationHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs index d02bddcc9..59e4e16cb 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs @@ -7,10 +7,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Templates; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class TemplateHandlers : IGetProjectTemplatesHandler, INewProjectFromTemplateHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs index b4dfeea3e..b46eff1e8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -17,14 +17,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine; -using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; -using PowerShellEditorServices.Engine.Services.Handlers; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services { using System.Management.Automation; + using Microsoft.PowerShell.EditorServices.Engine.Handlers; + using Microsoft.PowerShell.EditorServices.Engine.Hosting; + using Microsoft.PowerShell.EditorServices.Engine.Logging; + using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; /// /// Manages the lifetime and usage of a PowerShell session. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs index 03ce2abe9..e9f1b45e0 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs @@ -6,7 +6,7 @@ //using System.Linq; //using System.Threading.Tasks; -//namespace Microsoft.PowerShell.EditorServices.Session.Capabilities +//namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext //{ // using Microsoft.Extensions.Logging; // using Microsoft.PowerShell.EditorServices.Utility; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs index a1071606f..fa2fd0f1e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Defines options for the execution of a command. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs index 233d0499e..daf6e499b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Enumerates the possible execution results that can occur after diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs index cd2dcaaf2..ffb4fb5de 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Contains details about an executed diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs index 70ec3cb6f..79c8a860c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Represents the different API's available for executing commands. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs index 52a94daae..43ab96e41 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs @@ -4,14 +4,13 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine; -using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Engine.Hosting; using System; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides an implementation of the PSHost class for the diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs index e8139beed..8d4007725 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs @@ -12,13 +12,12 @@ using System.Linq; using System.Security; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Console; using System.Threading; -using Microsoft.PowerShell.EditorServices.Session; using System.Globalization; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Logging; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides an implementation of the PSHostUserInterface class diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs index 95da783db..d22da8cf9 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides methods for integrating with the host's input system. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs index 4f36bc54f..53c72b573 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a simplified interface for writing output to a diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs index 72de43587..99bfb6035 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Protocol.Messages +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { public class ShowChoicePromptRequest { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs index 38a619741..b433354fd 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs @@ -4,15 +4,13 @@ // 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 +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs index 3bbec39ae..d3e79f54c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs @@ -4,13 +4,12 @@ // 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 +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs index 3514469e9..4d13abd5f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs @@ -7,7 +7,7 @@ using System.Management.Automation.Host; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides an simple implementation of the PSHostRawUserInterface class. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs index c9e05c7e3..02f11057c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs @@ -4,13 +4,12 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Console; using System; using System.Management.Automation; using System.Management.Automation.Host; using System.Threading; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides an implementation of the PSHostRawUserInterface class diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs index 1ba2a7fc1..b57fcaec7 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs @@ -3,16 +3,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Console; +using System; +using System.Management.Automation.Host; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { - using System; using System.Management.Automation; - using System.Management.Automation.Host; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Extensions.Logging; /// /// Provides an EditorServicesPSHostUserInterface implementation diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs index 157715e7d..d14611cee 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides methods for interacting with implementations of ReadLine. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs index 38d14fb96..a79d15c6b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal interface IRunspaceCapability { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs index c210ff47b..d02c65df5 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs @@ -8,7 +8,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal interface IVersionSpecificOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs index deeec8939..0c41bdc8b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs @@ -12,7 +12,7 @@ using System.Threading; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs index 281b410c2..508ddb9c0 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs @@ -5,9 +5,8 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Console; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class LegacyReadLineContext : IPromptContext { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs index ad67f6891..384d60820 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Enumerates the types of output lines that will be sent diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs index 0e4663094..264576ad6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides details about output that has been written to the diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index 9058579e8..ce3c427f8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -9,13 +9,14 @@ using System.Threading.Tasks; using System; using System.Management.Automation.Runspaces; -using Microsoft.PowerShell.EditorServices.Console; -namespace Microsoft.PowerShell.EditorServices.Session { +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +{ using System.Management.Automation; using Microsoft.Extensions.Logging; - internal class PSReadLinePromptContext : IPromptContext { + internal class PSReadLinePromptContext : IPromptContext + { private const string ReadLineScript = @" [System.Diagnostics.DebuggerHidden()] [System.Diagnostics.DebuggerStepThrough()] diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs index 494a3f9f7..548a0a2c6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs @@ -7,7 +7,7 @@ using System.Reflection; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class PSReadLineProxy { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs index f2d61192c..dc9bb8962 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal interface IPipelineExecutionRequest { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs index a001d84bf..e6764f65f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs @@ -9,7 +9,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { internal class PowerShell5Operations : IVersionSpecificOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs index 6ebdbc947..d5c46dcaa 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Enumerates the possible states for a PowerShellContext. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs index 3c941b5aa..f9261bbf9 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Enumerates the possible execution results that can occur after diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs index d0b8e56e2..682c83228 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs @@ -8,7 +8,7 @@ using System.Collections; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Defines the possible enumeration values for the PowerShell process architecture. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs index d2ec4b1bd..f6afc5829 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs @@ -5,7 +5,7 @@ using System.Management.Automation; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides details about the progress of a particular activity. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs index a39f25f34..7150d46f6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { using System; using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs index cae7dfb8a..05fbbe5fa 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs index b42b42098..4f360edab 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { [Flags] internal enum PromptNestFrameType diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs index 0b24dfa48..1fe3d4086 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs @@ -4,7 +4,8 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; using System.Diagnostics; @@ -15,7 +16,7 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Manages files that are accessed from a remote PowerShell session. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs index 7efaf57d8..f1e558c89 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs @@ -3,7 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Session +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Defines the set of actions that will cause the runspace to be changed. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs index 1188d20a1..94dfee7a1 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs @@ -8,8 +8,9 @@ using System.Management.Automation.Runspaces; using System.Collections.Generic; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Specifies the possible types of a runspace. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs index ab0906fe6..70b643b0c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs @@ -7,7 +7,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides a handle to the runspace that is managed by diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs index 2347ce69a..8da235340 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs @@ -6,8 +6,9 @@ using System; using System.Management.Automation; using System.Collections; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides details about the current PowerShell session. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs index 1d285636e..9cbcf7765 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides details about a change in state of a PowerShellContext. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs index 8720e3fa7..fead192b6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Session +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides the ability to route PowerShell command invocations to a specific thread. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs index 8ac5cfc8e..fff0b60ba 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs @@ -4,14 +4,15 @@ // using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Handlers; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; -using PowerShellEditorServices.Engine.Services.Handlers; using System; using System.Linq; using System.Management.Automation; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Templates +namespace Microsoft.PowerShell.EditorServices.Engine.Services { /// /// Provides a service for listing PowerShell project templates and creating diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs index a73689dd4..4682d0dd4 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs @@ -7,8 +7,9 @@ using System.Linq; using System.Management.Automation; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext { /// /// Provides utility methods for working with PowerShell commands. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs index d971b9b38..604917c0d 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs @@ -4,8 +4,9 @@ // using System.Collections.Generic; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Specifies the contract for a document symbols provider. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs index 42472203e..23c14d850 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs @@ -5,8 +5,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Specifies the contract for an implementation of diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs index 072917e06..3fe325355 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs @@ -6,9 +6,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Management.Automation; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// A class for containing the commandName, the command's @@ -17,6 +17,7 @@ namespace Microsoft.PowerShell.EditorServices public class ParameterSetSignatures { #region Properties + /// /// Gets the name of the command /// @@ -31,6 +32,7 @@ public class ParameterSetSignatures /// Gets the script extent of the command /// public ScriptRegion ScriptRegion { get; internal set; } + #endregion /// diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs index 87a19778e..8d1425db2 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs @@ -7,8 +7,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Provides an IDocumentSymbolProvider implementation for diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs index 695ed2c02..c5cb6099b 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs @@ -7,8 +7,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Provides an IDocumentSymbolProvider implementation for diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs index 2c11747f2..63416c516 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs @@ -7,8 +7,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Provides an IDocumentSymbolProvider implementation for diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs index d695de649..00001f7dd 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs @@ -6,7 +6,7 @@ using System; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Provides a default IScriptExtent implementation diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs index 3604481cc..d8866b024 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs @@ -6,9 +6,9 @@ using System.Diagnostics; using System.Management.Automation; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Provides detailed information for a given symbol. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs index 643ab430b..963ea8b81 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs @@ -3,11 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Diagnostics; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// A class that holds the type, name, script extent, and source line of a symbol diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs index 2dba9a0a0..9344e5ef3 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// A way to define symbols on a higher level diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 216a885d1..1bde8b9ff 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -14,10 +14,13 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Symbols; -using PowerShellEditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services { /// /// Provides a high-level service for performing code completion and diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs index fd54cc7d1..86179305e 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs @@ -13,9 +13,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// Provides common operations for the syntax tree of a parsed script. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs index 254bf6ba3..dcc45eb3b 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// The vistior used to find the commandAst of a specific location in an AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs index c9842c9ef..e159af4c8 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs @@ -6,7 +6,7 @@ using System; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// The visitor used to find the definition of a symbol diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs index de42c7753..6caec1e49 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs @@ -6,9 +6,9 @@ using System; using System.Collections.Generic; using System.Management.Automation.Language; -using PowerShellEditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// The vistor used to find the dont sourced files in an AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs index 2dfb6d86b..5a12dd366 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// The visitor used to find the references of a symbol in a script's AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs index 06bdf8235..41eb8dc59 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs @@ -5,7 +5,7 @@ using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// The visitor used to find the the symbol at a specfic location in the AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs index 9b2dc0dc6..95808be91 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Symbols +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { /// /// The visitor used to find all the symbols (function and class defs) in the AST. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs index 03628ee3e..2aedd1228 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols { // TODO: Restore this when we figure out how to support multiple // PS versions in the new PSES-as-a-module world (issue #276) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs index effdd7660..c7e6bef79 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs @@ -5,7 +5,7 @@ using System.Diagnostics; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Provides details about a position in a file buffer. All diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs index 147eed042..986f3702e 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs @@ -6,7 +6,7 @@ using System; using System.Diagnostics; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Provides details about a range between two positions in diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs index 433d4e5c3..308386d50 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs @@ -10,7 +10,7 @@ using System.Text.RegularExpressions; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Provides the results of a single code completion request. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs index 79f6925ea..f465f0826 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Contains details relating to a content change in an open file. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs index a7c9036c7..5b95905dd 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Provides details and operations for a buffer position in a diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs index 2bfe4874e..e0d5d7ac8 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// A class that holds the information for a foldable region of text in a document diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs index 7afd6f37e..522e3ba1d 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -1,18 +1,21 @@ -using System; -using System.Collections.Concurrent; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; -using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using Newtonsoft.Json.Linq; -using OmniSharp.Extensions.JsonRpc.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Services.Handlers; -namespace Microsoft.PowerShell.EditorServices.TextDocument +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { internal class CodeActionHandler : ICodeActionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs index f83e35d18..46d96cacd 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -1,18 +1,25 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.CodeLenses; +using Microsoft.PowerShell.EditorServices.Engine.CodeLenses; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs index 0f4d81406..da14ce998 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs @@ -9,12 +9,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.TextDocument +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { internal class CompletionHandler : ICompletionHandler, ICompletionResolveHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs index 407408cf9..cd35f8ce1 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs @@ -7,14 +7,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Utility; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class DefinitionHandler : IDefinitionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs index 838d40331..ea1a2e11a 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -4,16 +4,19 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Utility; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.TextDocument +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class DocumentHighlightHandler : IDocumentHighlightHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index 6cbe8ace7..ed354fb04 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -1,4 +1,9 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -6,14 +11,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Utility; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class DocumentSymbolHandler : IDocumentSymbolHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs index 928eb5d03..54e8dfbca 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs @@ -1,13 +1,19 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class FoldingRangeHandler : IFoldingRangeHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs index 4565d7ff8..bc93390c0 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -1,14 +1,17 @@ -using System; -using System.Collections.Generic; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { internal class DocumentFormattingHandler : IDocumentFormattingHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs index 633ef2900..cbc978694 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs @@ -1,13 +1,20 @@ -using System.Collections.Generic; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class HoverHandler : IHoverHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs index d49f7f8d2..c5f73a41b 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -1,15 +1,21 @@ -using System.Collections.Generic; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Utility; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { class ReferencesHandler : IReferencesHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs index 6636da589..102e71410 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -7,12 +7,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class SignatureHelpHandler : ISignatureHelpHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index d7ec17827..52538d2d7 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -1,10 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; @@ -12,7 +17,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { class TextDocumentHandler : ITextDocumentSyncHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs index 57f80c9c3..41563ec25 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs @@ -9,9 +9,10 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Contains the details and contents of an open script file. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs index a6f518c75..0269c2d3d 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs @@ -8,8 +8,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Contains details for a code correction which can be applied from a ScriptFileMarker. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs index d91ec865e..0ffee20b2 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs @@ -3,11 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using OmniSharp.Extensions.LanguageServer.Protocol.Models; using System; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// /// Contains details about a specific region of text in script file. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs index bffd0991a..8a3985573 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs @@ -3,13 +3,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Collections.Generic; using System.Management.Automation.Language; using System.Text.RegularExpressions; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument { /// diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs index 888b18ebe..2351adda1 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs @@ -3,7 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices +using Microsoft.PowerShell.EditorServices.Engine.Services.Configuration; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services { public class ConfigurationService { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs index c22ea8d90..5d2fc49d8 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -1,14 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Configuration; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { internal class ConfigurationHandler : IDidChangeConfigurationHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 5e9c36752..d05a02dea 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -1,17 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using PowerShellEditorServices.Engine.Utility; -namespace PowerShellEditorServices.Engine.Services.Handlers +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers { public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs index 7f2b3dfee..894df1d65 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs @@ -10,8 +10,9 @@ using System.Reflection; using System.Security; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Logging; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Configuration { public class LanguageServerSettings { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs index 116f29b38..6b53b10df 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs @@ -9,8 +9,9 @@ using System.Security; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Logging; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services.Workspace { /// diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs index 32cde6f7b..f8beeba70 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs @@ -12,8 +12,11 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.PowerShell.EditorServices.Engine.Services.Workspace; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Services { /// /// Manages a "workspace" of script files that are open for a particular diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs b/src/PowerShellEditorServices.Engine/Utility/IScriptExtentExtensions.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs rename to src/PowerShellEditorServices.Engine/Utility/IScriptExtentExtensions.cs index 1f8ee1da1..db7148df0 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs +++ b/src/PowerShellEditorServices.Engine/Utility/IScriptExtentExtensions.cs @@ -6,7 +6,7 @@ using System.Management.Automation.Language; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Engine.Utility { internal static class IScriptExtentExtensions { diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index 108c7d05f..13a8cccd0 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -1,8 +1,13 @@ +// +// 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; using System.Runtime.InteropServices; -namespace PowerShellEditorServices.Engine.Utility +namespace Microsoft.PowerShell.EditorServices.Utility { internal class PathUtils { diff --git a/src/PowerShellEditorServices.Engine/Utility/Validate.cs b/src/PowerShellEditorServices.Engine/Utility/Validate.cs index a595bd6c9..20fba09e3 100644 --- a/src/PowerShellEditorServices.Engine/Utility/Validate.cs +++ b/src/PowerShellEditorServices.Engine/Utility/Validate.cs @@ -1,7 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Utility { /// /// Provides common validation methods to simplify method diff --git a/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs b/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs index da99597b4..5406ba1b6 100644 --- a/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs @@ -1,8 +1,13 @@ +// +// 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.Reflection; using System.Runtime.InteropServices; -namespace Microsoft.PowerShell.EditorServices +namespace Microsoft.PowerShell.EditorServices.Utility { /// /// General purpose common utilities to prevent reimplementation. diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 6597f494e..22f37a00b 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -11,11 +11,10 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Engine.Handlers; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using PowerShellEditorServices.Engine.Services.Handlers; using Xunit; using Xunit.Abstractions; using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; From e7543d334b3d2095fba48d71b0e1d215798019fc Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sun, 29 Sep 2019 10:37:49 -0700 Subject: [PATCH 40/47] The entire Debug Adapter moved over... (#1043) * initial non-working dap * working launch but not attach * working attach handler * update namespaces * Disconnect support and handling of JsonRpcServer teardown * Add foundation for debug tests - stdio and fixures * all handlers * remote file manager working * rest of debug adapter * use actual release * Apply suggestions from code review Co-Authored-By: Robert Holt --- PowerShellEditorServices.build.ps1 | 2 + docs/api/index.md | 4 +- docs/guide/extensions.md | 14 +- .../PowerShellEditorServices.VSCode.psm1 | 2 +- .../New-VSCodeHtmlContentView.ps1 | 2 +- .../Commands/Private/BuiltInCommands.ps1 | 16 +- .../Commands/Public/CmdletInterface.ps1 | 2 +- .../Commands/Public/Import-EditorCommand.ps1 | 8 +- module/docs/Import-EditorCommand.md | 6 +- .../Hosting/EditorServicesHost.cs | 193 ++- .../PowerShellEditorServices.Engine.csproj | 4 +- .../Server/NamedPipePsesLanguageServer.cs | 2 +- .../Server/PsesDebugServer.cs | 102 ++ .../Server/PsesLanguageServer.cs | 76 +- .../DebugAdapter/DebugEventHandlerService.cs | 177 +++ .../Services/DebugAdapter/DebugService.cs | 1352 +++++++++++++++++ .../DebugAdapter/DebugStateService.cs | 32 + .../Debugging/BreakpointDetails.cs | 107 ++ .../Debugging/BreakpointDetailsBase.cs | 36 + .../Debugging/CommandBreakpointDetails.cs | 72 + .../Debugging/DebuggerStoppedEventArgs.cs | 117 ++ .../InvalidPowerShellExpressionException.cs | 24 + .../Debugging/StackFrameDetails.cs | 134 ++ .../Debugging/VariableContainerDetails.cs | 100 ++ .../DebugAdapter/Debugging/VariableDetails.cs | 416 +++++ .../Debugging/VariableDetailsBase.cs | 60 + .../DebugAdapter/Debugging/VariableScope.cs | 37 + .../Handlers/BreakpointHandlers.cs | 226 +++ .../Handlers/ConfigurationDoneHandler.cs | 104 ++ .../Handlers/DebugEvaluateHandler.cs | 88 ++ .../Handlers/DebuggerActionHandlers.cs | 123 ++ .../Handlers/DisconnectHandler.cs | 84 + .../Handlers/InitializeHandler.cs | 43 + .../Handlers/LaunchAndAttachHandler.cs | 407 +++++ .../DebugAdapter/Handlers/ScopesHandler.cs | 44 + .../Handlers/SetVariableHandler.cs | 67 + .../DebugAdapter/Handlers/SourceHandler.cs | 21 + .../Handlers/StackTraceHandler.cs | 80 + .../DebugAdapter/Handlers/ThreadsHandler.cs | 29 + .../DebugAdapter/Handlers/VariablesHandler.cs | 58 + .../PowerShellContextService.cs | 58 + ...Manager.cs => RemoteFileManagerService.cs} | 21 +- .../Capabilities/DscBreakpointCapability.cs | 334 ++-- .../Handlers/TextDocumentHandler.cs | 29 +- .../Utility/Extensions.cs | 154 ++ .../Utility/LspDebugUtils.cs | 115 ++ .../LSPTestsFixures.cs | 55 + .../LanguageServerProtocolMessageTests.cs | 4 +- .../PowerShellEditorServices.Test.E2E.csproj | 2 +- .../TestsFixture.cs | 50 +- 50 files changed, 4963 insertions(+), 330 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs rename src/PowerShellEditorServices.Engine/Services/PowerShellContext/{Session/RemoteFileManager.cs => RemoteFileManagerService.cs} (97%) create mode 100644 src/PowerShellEditorServices.Engine/Utility/Extensions.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs create mode 100644 test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 36ade043e..0ac9ff998 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -64,6 +64,8 @@ $script:RequiredBuildAssets = @{ 'publish/OmniSharp.Extensions.JsonRpc.dll', 'publish/OmniSharp.Extensions.LanguageProtocol.dll', 'publish/OmniSharp.Extensions.LanguageServer.dll', + 'publish/OmniSharp.Extensions.DebugAdapter.dll', + 'publish/OmniSharp.Extensions.DebugAdapter.Server.dll', 'publish/runtimes/linux-64/native/libdisablekeyecho.so', 'publish/runtimes/osx-64/native/libdisablekeyecho.dylib', 'publish/Serilog.dll', diff --git a/docs/api/index.md b/docs/api/index.md index a845c1d66..94c6a5d4a 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -20,9 +20,9 @@ the PowerShell debugger. Use the @Microsoft.PowerShell.EditorServices.Console.ConsoleService to provide interactive console support in the user's editor. -Use the @Microsoft.PowerShell.EditorServices.Extensions.ExtensionService to allow +Use the @Microsoft.PowerShell.EditorServices.Engine.Services.ExtensionService to allow the user to extend the host editor with new capabilities using PowerShell code. The core of all the services is the @Microsoft.PowerShell.EditorServices.PowerShellContext class. This class manages a session's runspace and handles script and command -execution no matter what state the runspace is in. \ No newline at end of file +execution no matter what state the runspace is in. diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 6e964416c..66953aa68 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -9,7 +9,7 @@ uses PowerShell Editor Services. ### Introducing `$psEditor` The entry point for the PowerShell Editor Services extensibility model is the `$psEditor` -object of the type @Microsoft.PowerShell.EditorServices.Extensions.EditorObject. For +object of the type @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject. For those familiar with the PowerShell ISE's `$psISE` object, the `$psEditor` object is very similar. The primary difference is that this model has been generalized to work against any editor which leverages PowerShell Editor Services for its PowerShell editing experience. @@ -19,7 +19,7 @@ any editor which leverages PowerShell Editor Services for its PowerShell editing > please file an issue on our GitHub page. This object gives access to all of the high-level services in the current -editing session. For example, the @Microsoft.PowerShell.EditorServices.Extensions.EditorObject.Workspace +editing session. For example, the @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject.Workspace property gives access to the editor's workspace, allowing you to create or open files in the editor. @@ -79,17 +79,17 @@ Register-EditorCommand ` -ScriptBlock { Write-Output "My command's script block was invoked!" } ``` -### The @Microsoft.PowerShell.EditorServices.Extensions.EditorContext parameter +### The @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext parameter Your function, cmdlet, or ScriptBlock can optionally accept a single parameter -of type @Microsoft.PowerShell.EditorServices.Extensions.EditorContext which provides +of type @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext which provides information about the state of the host editor at the time your command was invoked. With this object you can easily perform operations like manipulatin the state of the user's active editor buffer or changing the current selection. The usual convention is that a `$context` parameter is added to your editor command's function. For now it is recommended that you fully specify the -type of the @Microsoft.PowerShell.EditorServices.Extensions.EditorContext object +type of the @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext object so that you get full IntelliSense on your context parameter. Here is an example of using the `$context` parameter: @@ -99,7 +99,7 @@ Register-EditorCommand ` -Name "MyModule.MyEditorCommandWithContext" ` -DisplayName "My command with context usage" ` -ScriptBlock { - param([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context) + param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context) Write-Output "The user's cursor is on line $($context.CursorPosition.Line)!" } ``` @@ -165,4 +165,4 @@ in that editor starts up. > NOTE: In the future we plan to provide an easy way for the user to opt-in > to the automatic loading of any editor command modules that they've installed > from the PowerShell Gallery. If this interests you, please let us know on -> [this GitHub issue](https://github.com/PowerShell/PowerShellEditorServices/issues/215). \ No newline at end of file +> [this GitHub issue](https://github.com/PowerShell/PowerShellEditorServices/issues/215). diff --git a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 index e7b34e076..12b205c98 100644 --- a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 +++ b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 @@ -5,7 +5,7 @@ Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.VSCode.dll" -if ($psEditor -is [Microsoft.PowerShell.EditorServices.Extensions.EditorObject]) { +if ($psEditor -is [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject]) { [Microsoft.PowerShell.EditorServices.VSCode.ComponentRegistration]::Register($psEditor.Components) } else { diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 index 0e9088bd3..d76c69fd4 100644 --- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 +++ b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 @@ -41,7 +41,7 @@ function New-VSCodeHtmlContentView { ) process { - if ($psEditor -is [Microsoft.PowerShell.EditorServices.Extensions.EditorObject]) { + if ($psEditor -is [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject]) { $viewFeature = $psEditor.Components.Get([Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentViews]) $view = $viewFeature.CreateHtmlContentViewAsync($Title).Result diff --git a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 b/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 index 7c73e8358..e808cc16a 100644 --- a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 +++ b/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 @@ -3,7 +3,7 @@ Register-EditorCommand ` -DisplayName 'Open Editor Profile' ` -SuppressOutput ` -ScriptBlock { - param([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context) + param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context) If (!(Test-Path -Path $Profile)) { New-Item -Path $Profile -ItemType File } $psEditor.Workspace.OpenFile($Profile) } @@ -13,18 +13,18 @@ Register-EditorCommand ` -DisplayName 'Open Profile from List (Current User)' ` -SuppressOutput ` -ScriptBlock { - param([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]$context) - - $Current = Split-Path -Path $profile -Leaf + param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context) + + $Current = Split-Path -Path $profile -Leaf $List = @($Current,'Microsoft.VSCode_profile.ps1','Microsoft.PowerShell_profile.ps1','Microsoft.PowerShellISE_profile.ps1','Profile.ps1') | Select-Object -Unique $Choices = [System.Management.Automation.Host.ChoiceDescription[]] @($List) $Selection = $host.ui.PromptForChoice('Please Select a Profile', '(Current User)', $choices,'0') $Name = $List[$Selection] - + $ProfileDir = Split-Path $Profile -Parent $ProfileName = Join-Path -Path $ProfileDir -ChildPath $Name - + If (!(Test-Path -Path $ProfileName)) { New-Item -Path $ProfileName -ItemType File } - + $psEditor.Workspace.OpenFile($ProfileName) - } \ No newline at end of file + } diff --git a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 index 47623296a..efd5b481e 100644 --- a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 +++ b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 @@ -47,7 +47,7 @@ function Register-EditorCommand { $commandArgs += $Function } - $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Extensions.EditorCommand -ArgumentList $commandArgs + $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand -ArgumentList $commandArgs if ($psEditor.RegisterCommand($editorCommand)) { Write-Verbose "Registered new command '$Name'" diff --git a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 b/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 index 9ddec5021..466d9368e 100644 --- a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 +++ b/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 @@ -7,7 +7,7 @@ function Import-EditorCommand { <# .EXTERNALHELP ..\PowerShellEditorServices.Commands-help.xml #> - [OutputType([Microsoft.PowerShell.EditorServices.Extensions.EditorCommand])] + [OutputType([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand])] [CmdletBinding(DefaultParameterSetName='ByCommand')] param( [Parameter(Position=0, @@ -75,7 +75,7 @@ function Import-EditorCommand { $commands = $Command | Get-Command -ErrorAction SilentlyContinue } } - $attributeType = [Microsoft.PowerShell.EditorServices.Extensions.EditorCommandAttribute] + $attributeType = [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommandAttribute] foreach ($aCommand in $commands) { # Get the attribute from our command to get name info. $details = $aCommand.ScriptBlock.Attributes | Where-Object TypeId -eq $attributeType @@ -99,7 +99,7 @@ function Import-EditorCommand { } # Check for a context parameter. $contextParameter = $aCommand.Parameters.Values | - Where-Object ParameterType -eq ([Microsoft.PowerShell.EditorServices.Extensions.EditorContext]) + Where-Object ParameterType -eq ([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]) # If one is found then add a named argument. Otherwise call the command directly. if ($contextParameter) { @@ -109,7 +109,7 @@ function Import-EditorCommand { $scriptBlock = [scriptblock]::Create($aCommand.Name) } - $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Extensions.EditorCommand @( + $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand @( <# commandName: #> $details.Name, <# displayName: #> $details.DisplayName, <# suppressOutput: #> $details.SuppressOutput, diff --git a/module/docs/Import-EditorCommand.md b/module/docs/Import-EditorCommand.md index d84c66d53..b04487595 100644 --- a/module/docs/Import-EditorCommand.md +++ b/module/docs/Import-EditorCommand.md @@ -30,7 +30,7 @@ The Import-EditorCommand function will search the specified module for functions Alternatively, you can specify command info objects (like those from the Get-Command cmdlet) to be processed directly. -To tag a command as an editor command, attach the attribute 'Microsoft.PowerShell.EditorServices.Extensions.EditorCommandAttribute' to the function like you would with 'CmdletBindingAttribute'. The attribute accepts the named parameters 'Name', 'DisplayName', and 'SuppressOutput'. +To tag a command as an editor command, attach the attribute 'Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommandAttribute' to the function like you would with 'CmdletBindingAttribute'. The attribute accepts the named parameters 'Name', 'DisplayName', and 'SuppressOutput'. ## EXAMPLES @@ -55,7 +55,7 @@ Registers all editor commands that contain "Editor" in the name and return all s ```powershell function Invoke-MyEditorCommand { [CmdletBinding()] - [Microsoft.PowerShell.EditorServices.Extensions.EditorCommand(DisplayName='My Command', SuppressOutput)] + [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand(DisplayName='My Command', SuppressOutput)] param() end { ConvertTo-ScriptExtent -Offset 0 | Set-ScriptExtent -Text 'My Command!' @@ -145,7 +145,7 @@ You can pass commands to register as editor commands. ## OUTPUTS -### Microsoft.PowerShell.EditorServices.Extensions.EditorCommand +### Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand If the "PassThru" parameter is specified editor commands that were successfully registered will be returned. This function does not output to the pipeline otherwise. diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 83f61a729..b92d9d487 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -6,15 +6,20 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO.Pipes; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; using System.Reflection; using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Engine.Server; +using Microsoft.PowerShell.EditorServices.Engine.Services; using Microsoft.PowerShell.EditorServices.Utility; using Serilog; @@ -58,6 +63,15 @@ public class EditorServicesHost { #region Private Fields + // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. + private const int CurrentUserOnly = 0x20000000; + + // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, + // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. + // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ + private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = + typeof(NamedPipeServerStream).GetConstructor(new[] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); + private readonly HostDetails _hostDetails; private readonly PSHost _internalHost; @@ -69,6 +83,7 @@ public class EditorServicesHost private readonly string[] _additionalModules; private PsesLanguageServer _languageServer; + private PsesDebugServer _debugServer; private Microsoft.Extensions.Logging.ILogger _logger; @@ -221,16 +236,15 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - while (System.Diagnostics.Debugger.IsAttached) - { - System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); - Thread.Sleep(2000); - } + // Uncomment to debug language service + // while (!System.Diagnostics.Debugger.IsAttached) + // { + // System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); + // Thread.Sleep(2000); + // } _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - - switch (config.TransportType) { case EditorServiceTransportType.NamedPipe: @@ -269,6 +283,8 @@ public void StartLanguageService( config.TransportType, config.Endpoint)); } + + private bool alreadySubscribedDebug; /// /// Starts the debug service with the specified config. /// @@ -280,17 +296,85 @@ public void StartDebugService( ProfilePaths profilePaths, bool useExistingSession) { - /* - this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config); - this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; - this.debugServiceListener.Start(); + //while (System.Diagnostics.Debugger.IsAttached) + //{ + // System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); + // Thread.Sleep(2000); + //} - this.logger.Write( - LogLevel.Normal, - string.Format( - "Debug service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); - */ + _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); + + switch (config.TransportType) + { + case EditorServiceTransportType.NamedPipe: + NamedPipeServerStream inNamedPipe = CreateNamedPipe( + config.InOutPipeName ?? config.InPipeName, + config.OutPipeName, + out NamedPipeServerStream outNamedPipe); + + _debugServer = new PsesDebugServer( + _factory, + inNamedPipe, + outNamedPipe ?? inNamedPipe); + + Task[] tasks = outNamedPipe != null + ? new[] { inNamedPipe.WaitForConnectionAsync(), outNamedPipe.WaitForConnectionAsync() } + : new[] { inNamedPipe.WaitForConnectionAsync() }; + Task.WhenAll(tasks) + .ContinueWith(async task => + { + _logger.LogInformation("Starting debug server"); + await _debugServer.StartAsync(_languageServer.LanguageServer.Services); + _logger.LogInformation( + $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); + }); + + break; + + case EditorServiceTransportType.Stdio: + _debugServer = new PsesDebugServer( + _factory, + Console.OpenStandardInput(), + Console.OpenStandardOutput()); + + Task.Run(async () => + { + _logger.LogInformation("Starting debug server"); + + IServiceProvider serviceProvider = useExistingSession + ? _languageServer.LanguageServer.Services + : new ServiceCollection().AddSingleton( + (provider) => PowerShellContextService.Create( + _factory, + provider.GetService(), + profilePaths, + _featureFlags, + _enableConsoleRepl, + _internalHost, + _hostDetails, + _additionalModules)) + .BuildServiceProvider(); + + await _debugServer.StartAsync(serviceProvider); + _logger.LogInformation( + $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); + }); + break; + + default: + throw new NotSupportedException($"The transport {config.TransportType} is not supported"); + } + + if(!alreadySubscribedDebug) + { + alreadySubscribedDebug = true; + _debugServer.SessionEnded += (sender, eventArgs) => + { + _debugServer.Dispose(); + alreadySubscribedDebug = false; + StartDebugService(config, profilePaths, useExistingSession); + }; + } } /// @@ -350,6 +434,81 @@ private void CurrentDomain_UnhandledException( _logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); } + private static NamedPipeServerStream CreateNamedPipe( + string inOutPipeName, + string outPipeName, + out NamedPipeServerStream outPipe) + { + // .NET Core implementation is simplest so try that first + if (VersionUtils.IsNetCore) + { + outPipe = outPipeName == null + ? null + : new NamedPipeServerStream( + pipeName: outPipeName, + direction: PipeDirection.Out, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: (PipeOptions)CurrentUserOnly); + + return new NamedPipeServerStream( + pipeName: inOutPipeName, + direction: PipeDirection.InOut, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); + } + + // Now deal with Windows PowerShell + // We need to use reflection to get a nice constructor + + var pipeSecurity = new PipeSecurity(); + + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + // Allow the Administrators group full access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); + } + else + { + // Allow the current user read/write access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + WindowsIdentity.GetCurrent().User, + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + } + + outPipe = outPipeName == null + ? null + : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + outPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + + return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( + new object[] { + inOutPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + } + #endregion } } diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index 3c219d986..3a51e5b13 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -21,7 +21,7 @@ - + @@ -29,6 +29,6 @@ + - diff --git a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs index 08086d5d8..4742bbe09 100644 --- a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs @@ -61,7 +61,7 @@ protected override (Stream input, Stream output) GetInputOutputStreams() _outNamedPipeName, out NamedPipeServerStream outNamedPipe); - var logger = _loggerFactory.CreateLogger("NamedPipeConnection"); + var logger = LoggerFactory.CreateLogger("NamedPipeConnection"); logger.LogInformation("Waiting for connection"); namedPipe.WaitForConnection(); diff --git a/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs new file mode 100644 index 000000000..1088cd145 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs @@ -0,0 +1,102 @@ +// +// 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; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Handlers; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Server; + +namespace Microsoft.PowerShell.EditorServices.Engine.Server +{ + public class PsesDebugServer : IDisposable + { + protected readonly ILoggerFactory _loggerFactory; + private readonly Stream _inputStream; + private readonly Stream _outputStream; + + private IJsonRpcServer _jsonRpcServer; + + public PsesDebugServer( + ILoggerFactory factory, + Stream inputStream, + Stream outputStream) + { + _loggerFactory = factory; + _inputStream = inputStream; + _outputStream = outputStream; + } + + public async Task StartAsync(IServiceProvider languageServerServiceProvider) + { + _jsonRpcServer = await JsonRpcServer.From(options => + { + options.Serializer = new DapProtocolSerializer(); + options.Reciever = new DapReciever(); + options.LoggerFactory = _loggerFactory; + ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); + options.Services = new ServiceCollection() + .AddSingleton(languageServerServiceProvider.GetService()) + .AddSingleton(languageServerServiceProvider.GetService()) + .AddSingleton(languageServerServiceProvider.GetService()) + .AddSingleton(this) + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + options + .WithInput(_inputStream) + .WithOutput(_outputStream); + + logger.LogInformation("Adding handlers"); + + options + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler(); + + logger.LogInformation("Handlers added"); + }); + } + + public void Dispose() + { + _jsonRpcServer.Dispose(); + } + + #region Events + + public event EventHandler SessionEnded; + + internal void OnSessionEnded() + { + SessionEnded?.Invoke(this, null); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs index f0ce7f920..7da6fc1e3 100644 --- a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs @@ -22,7 +22,9 @@ namespace Microsoft.PowerShell.EditorServices.Engine.Server { internal abstract class PsesLanguageServer { - protected readonly ILoggerFactory _loggerFactory; + internal ILoggerFactory LoggerFactory { get; private set; } + internal ILanguageServer LanguageServer { get; private set; } + private readonly LogLevel _minimumLogLevel; private readonly bool _enableConsoleRepl; private readonly HashSet _featureFlags; @@ -32,8 +34,6 @@ internal abstract class PsesLanguageServer private readonly ProfilePaths _profilePaths; private readonly TaskCompletionSource _serverStart; - private ILanguageServer _languageServer; - internal PsesLanguageServer( ILoggerFactory factory, LogLevel minimumLogLevel, @@ -44,7 +44,7 @@ internal PsesLanguageServer( PSHost internalHost, ProfilePaths profilePaths) { - _loggerFactory = factory; + LoggerFactory = factory; _minimumLogLevel = minimumLogLevel; _enableConsoleRepl = enableConsoleRepl; _featureFlags = featureFlags; @@ -57,10 +57,10 @@ internal PsesLanguageServer( public async Task StartAsync() { - _languageServer = await LanguageServer.From(options => + LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options => { options.AddDefaultLoggingProvider(); - options.LoggerFactory = _loggerFactory; + options.LoggerFactory = LoggerFactory; ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); options.Services = new ServiceCollection() .AddSingleton() @@ -68,11 +68,18 @@ public async Task StartAsync() .AddSingleton() .AddSingleton( (provider) => - GetFullyInitializedPowerShellContext( + PowerShellContextService.Create( + LoggerFactory, provider.GetService(), - _profilePaths)) + _profilePaths, + _featureFlags, + _enableConsoleRepl, + _internalHost, + _hostDetails, + _additionalModules)) .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton( (provider) => { @@ -155,58 +162,7 @@ await serviceProvider.GetService().SetWorkingDirectory public async Task WaitForShutdown() { await _serverStart.Task; - await _languageServer.WaitForExit; - } - - private PowerShellContextService GetFullyInitializedPowerShellContext( - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, - ProfilePaths profilePaths) - { - var logger = _loggerFactory.CreateLogger(); - - // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise - // issues arise when redirecting stdio. - var powerShellContext = new PowerShellContextService( - logger, - languageServer, - _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); - - EditorServicesPSHostUserInterface hostUserInterface = - _enableConsoleRepl - ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, _internalHost) - : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - _hostDetails, - hostUserInterface, - logger); - - Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - powerShellContext.ImportCommandsModuleAsync( - Path.Combine( - Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), - @"..\Commands")); - - // TODO: This can be moved to the point after the $psEditor object - // gets initialized when that is done earlier than LanguageServer.Initialize - foreach (string module in this._additionalModules) - { - var command = - new PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", module); - - powerShellContext.ExecuteCommandAsync( - command, - sendOutputToHost: false, - sendErrorToHost: true); - } - - return powerShellContext; + await LanguageServer.WaitForExit; } protected abstract (Stream input, Stream output) GetInputOutputStreams(); diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs new file mode 100644 index 000000000..e4410f439 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.JsonRpc; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services +{ + internal class DebugEventHandlerService + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + private readonly DebugService _debugService; + private readonly DebugStateService _debugStateService; + private readonly IJsonRpcServer _jsonRpcServer; + + public DebugEventHandlerService( + ILoggerFactory factory, + PowerShellContextService powerShellContextService, + DebugService debugService, + DebugStateService debugStateService, + IJsonRpcServer jsonRpcServer) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + _debugService = debugService; + _debugStateService = debugStateService; + _jsonRpcServer = jsonRpcServer; + } + + internal void RegisterEventHandlers() + { + _powerShellContextService.RunspaceChanged += PowerShellContext_RunspaceChanged; + _debugService.BreakpointUpdated += DebugService_BreakpointUpdated; + _debugService.DebuggerStopped += DebugService_DebuggerStopped; + _powerShellContextService.DebuggerResumed += PowerShellContext_DebuggerResumed; + } + + internal void UnregisterEventHandlers() + { + _powerShellContextService.RunspaceChanged -= PowerShellContext_RunspaceChanged; + _debugService.BreakpointUpdated -= DebugService_BreakpointUpdated; + _debugService.DebuggerStopped -= DebugService_DebuggerStopped; + _powerShellContextService.DebuggerResumed -= PowerShellContext_DebuggerResumed; + } + + #region Public methods + + internal void TriggerDebuggerStopped(DebuggerStoppedEventArgs e) + { + DebugService_DebuggerStopped(null, e); + } + + #endregion + + #region Event Handlers + + private void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) + { + // Provide the reason for why the debugger has stopped script execution. + // See https://github.com/Microsoft/vscode/issues/3648 + // The reason is displayed in the breakpoints viewlet. Some recommended reasons are: + // "step", "breakpoint", "function breakpoint", "exception" and "pause". + // We don't support exception breakpoints and for "pause", we can't distinguish + // between stepping and the user pressing the pause/break button in the debug toolbar. + string debuggerStoppedReason = "step"; + if (e.OriginalEvent.Breakpoints.Count > 0) + { + debuggerStoppedReason = + e.OriginalEvent.Breakpoints[0] is CommandBreakpoint + ? "function breakpoint" + : "breakpoint"; + } + + _jsonRpcServer.SendNotification(EventNames.Stopped, + new StoppedEvent + { + ThreadId = 1, + Reason = debuggerStoppedReason + }); + } + + private void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEventArgs e) + { + if (_debugStateService.WaitingForAttach && + e.ChangeAction == RunspaceChangeAction.Enter && + e.NewRunspace.Context == RunspaceContext.DebuggedRunspace) + { + // Send the InitializedEvent so that the debugger will continue + // sending configuration requests + _debugStateService.WaitingForAttach = false; + _jsonRpcServer.SendNotification(EventNames.Initialized); + } + else if ( + e.ChangeAction == RunspaceChangeAction.Exit && + _powerShellContextService.IsDebuggerStopped) + { + // Exited the session while the debugger is stopped, + // send a ContinuedEvent so that the client changes the + // UI to appear to be running again + _jsonRpcServer.SendNotification(EventNames.Continued, + new ContinuedEvent + { + ThreadId = 1, + AllThreadsContinued = true + }); + } + } + + private void PowerShellContext_DebuggerResumed(object sender, DebuggerResumeAction e) + { + _jsonRpcServer.SendNotification(EventNames.Continued, + new ContinuedEvent + { + AllThreadsContinued = true, + ThreadId = 1 + }); + } + + private void DebugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) + { + string reason = "changed"; + + if (_debugStateService.SetBreakpointInProgress) + { + // Don't send breakpoint update notifications when setting + // breakpoints on behalf of the client. + return; + } + + switch (e.UpdateType) + { + case BreakpointUpdateType.Set: + reason = "new"; + break; + + case BreakpointUpdateType.Removed: + reason = "removed"; + break; + } + + OmniSharp.Extensions.DebugAdapter.Protocol.Models.Breakpoint breakpoint; + if (e.Breakpoint is LineBreakpoint) + { + breakpoint = LspDebugUtils.CreateBreakpoint(BreakpointDetails.Create(e.Breakpoint)); + } + else if (e.Breakpoint is CommandBreakpoint) + { + _logger.LogTrace("Function breakpoint updated event is not supported yet"); + return; + } + else + { + _logger.LogError($"Unrecognized breakpoint type {e.Breakpoint.GetType().FullName}"); + return; + } + + breakpoint.Verified = e.UpdateType != BreakpointUpdateType.Disabled; + + _jsonRpcServer.SendNotification(EventNames.Breakpoint, + new BreakpointEvent + { + Reason = reason, + Breakpoint = breakpoint + }); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs new file mode 100644 index 000000000..64889ae5c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs @@ -0,0 +1,1352 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services +{ + /// + /// Provides a high-level service for interacting with the + /// PowerShell debugger in the runspace managed by a PowerShellContext. + /// + internal class DebugService + { + #region Fields + + private const string PsesGlobalVariableNamePrefix = "__psEditorServices_"; + private const string TemporaryScriptFileName = "Script Listing.ps1"; + + private readonly ILogger logger; + private readonly PowerShellContextService powerShellContext; + private RemoteFileManagerService remoteFileManager; + + // TODO: This needs to be managed per nested session + private readonly Dictionary> breakpointsPerFile = + new Dictionary>(); + + private int nextVariableId; + private string temporaryScriptListingPath; + private List variables; + private VariableContainerDetails globalScopeVariables; + private VariableContainerDetails scriptScopeVariables; + private StackFrameDetails[] stackFrameDetails; + private readonly PropertyInfo invocationTypeScriptPositionProperty; + + private static int breakpointHitCounter; + + private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + #endregion + + #region Properties + + /// + /// Gets or sets a boolean that indicates whether a debugger client is + /// currently attached to the debugger. + /// + public bool IsClientAttached { get; set; } + + /// + /// Gets a boolean that indicates whether the debugger is currently + /// stopped at a breakpoint. + /// + public bool IsDebuggerStopped => this.powerShellContext.IsDebuggerStopped; + + /// + /// Gets the current DebuggerStoppedEventArgs when the debugger + /// is stopped. + /// + public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the DebugService class and uses + /// the given PowerShellContext for all future operations. + /// + /// + /// The PowerShellContext to use for all debugging operations. + /// + /// An ILogger implementation used for writing log messages. + //public DebugService(PowerShellContextService powerShellContext, ILogger logger) + // : this(powerShellContext, null, logger) + //{ + //} + + /// + /// Initializes a new instance of the DebugService class and uses + /// the given PowerShellContext for all future operations. + /// + /// + /// The PowerShellContext to use for all debugging operations. + /// + //// + //// A RemoteFileManagerService instance to use for accessing files in remote sessions. + //// + /// An ILogger implementation used for writing log messages. + public DebugService( + PowerShellContextService powerShellContext, + RemoteFileManagerService remoteFileManager, + ILoggerFactory factory) + { + Validate.IsNotNull(nameof(powerShellContext), powerShellContext); + + this.logger = factory.CreateLogger(); + this.powerShellContext = powerShellContext; + this.powerShellContext.DebuggerStop += this.OnDebuggerStopAsync; + this.powerShellContext.DebuggerResumed += this.OnDebuggerResumed; + + this.powerShellContext.BreakpointUpdated += this.OnBreakpointUpdated; + + this.remoteFileManager = remoteFileManager; + + this.invocationTypeScriptPositionProperty = + typeof(InvocationInfo) + .GetProperty( + "ScriptPosition", + BindingFlags.NonPublic | BindingFlags.Instance); + } + + #endregion + + #region Public Methods + + /// + /// Sets the list of line breakpoints for the current debugging session. + /// + /// The ScriptFile in which breakpoints will be set. + /// BreakpointDetails for each breakpoint that will be set. + /// If true, causes all existing breakpoints to be cleared before setting new ones. + /// An awaitable Task that will provide details about the breakpoints that were set. + public async Task SetLineBreakpointsAsync( + ScriptFile scriptFile, + BreakpointDetails[] breakpoints, + bool clearExisting = true) + { + var resultBreakpointDetails = new List(); + + var dscBreakpoints = + this.powerShellContext + .CurrentRunspace + .GetCapability(); + + string scriptPath = scriptFile.FilePath; + // Make sure we're using the remote script path + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null) + { + if (!this.remoteFileManager.IsUnderRemoteTempPath(scriptPath)) + { + this.logger.LogTrace( + $"Could not set breakpoints for local path '{scriptPath}' in a remote session."); + + return resultBreakpointDetails.ToArray(); + } + + string mappedPath = + this.remoteFileManager.GetMappedPath( + scriptPath, + this.powerShellContext.CurrentRunspace); + + scriptPath = mappedPath; + } + else if ( + this.temporaryScriptListingPath != null && + this.temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase)) + { + this.logger.LogTrace( + $"Could not set breakpoint on temporary script listing path '{scriptPath}'."); + + return resultBreakpointDetails.ToArray(); + } + + // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to + // quoted and have those wildcard chars escaped. + string escapedScriptPath = + PowerShellContextService.WildcardEscapePath(scriptPath); + + if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath)) + { + if (clearExisting) + { + await this.ClearBreakpointsInFileAsync(scriptFile); + } + + foreach (BreakpointDetails breakpoint in breakpoints) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); + psCommand.AddParameter("Script", escapedScriptPath); + psCommand.AddParameter("Line", breakpoint.LineNumber); + + // Check if the user has specified the column number for the breakpoint. + if (breakpoint.ColumnNumber.HasValue && breakpoint.ColumnNumber.Value > 0) + { + // It bums me out that PowerShell will silently ignore a breakpoint + // where either the line or the column is invalid. I'd rather have an + // error or warning message I could relay back to the client. + psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value); + } + + // Check if this is a "conditional" line breakpoint. + if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || + !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) + { + ScriptBlock actionScriptBlock = + GetBreakpointActionScriptBlock(breakpoint); + + // If there was a problem with the condition string, + // move onto the next breakpoint. + if (actionScriptBlock == null) + { + resultBreakpointDetails.Add(breakpoint); + continue; + } + + psCommand.AddParameter("Action", actionScriptBlock); + } + + IEnumerable configuredBreakpoints = + await this.powerShellContext.ExecuteCommandAsync(psCommand); + + // The order in which the breakpoints are returned is significant to the + // VSCode client and should match the order in which they are passed in. + resultBreakpointDetails.AddRange( + configuredBreakpoints.Select(BreakpointDetails.Create)); + } + } + else + { + resultBreakpointDetails = + await dscBreakpoints.SetLineBreakpointsAsync( + this.powerShellContext, + escapedScriptPath, + breakpoints); + } + + return resultBreakpointDetails.ToArray(); + } + + /// + /// Sets the list of command breakpoints for the current debugging session. + /// + /// CommandBreakpointDetails for each command breakpoint that will be set. + /// If true, causes all existing function breakpoints to be cleared before setting new ones. + /// An awaitable Task that will provide details about the breakpoints that were set. + public async Task SetCommandBreakpointsAsync( + CommandBreakpointDetails[] breakpoints, + bool clearExisting = true) + { + var resultBreakpointDetails = new List(); + + if (clearExisting) + { + await this.ClearCommandBreakpointsAsync(); + } + + if (breakpoints.Length > 0) + { + foreach (CommandBreakpointDetails breakpoint in breakpoints) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); + psCommand.AddParameter("Command", breakpoint.Name); + + // Check if this is a "conditional" command breakpoint. + if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || + !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) + { + ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); + + // If there was a problem with the condition string, + // move onto the next breakpoint. + if (actionScriptBlock == null) + { + resultBreakpointDetails.Add(breakpoint); + continue; + } + + psCommand.AddParameter("Action", actionScriptBlock); + } + + IEnumerable configuredBreakpoints = + await this.powerShellContext.ExecuteCommandAsync(psCommand); + + // The order in which the breakpoints are returned is significant to the + // VSCode client and should match the order in which they are passed in. + resultBreakpointDetails.AddRange( + configuredBreakpoints.Select(CommandBreakpointDetails.Create)); + } + } + + return resultBreakpointDetails.ToArray(); + } + + /// + /// Sends a "continue" action to the debugger when stopped. + /// + public void Continue() + { + this.powerShellContext.ResumeDebugger( + DebuggerResumeAction.Continue); + } + + /// + /// Sends a "step over" action to the debugger when stopped. + /// + public void StepOver() + { + this.powerShellContext.ResumeDebugger( + DebuggerResumeAction.StepOver); + } + + /// + /// Sends a "step in" action to the debugger when stopped. + /// + public void StepIn() + { + this.powerShellContext.ResumeDebugger( + DebuggerResumeAction.StepInto); + } + + /// + /// Sends a "step out" action to the debugger when stopped. + /// + public void StepOut() + { + this.powerShellContext.ResumeDebugger( + DebuggerResumeAction.StepOut); + } + + /// + /// Causes the debugger to break execution wherever it currently + /// is at the time. This is equivalent to clicking "Pause" in a + /// debugger UI. + /// + public void Break() + { + // Break execution in the debugger + this.powerShellContext.BreakExecution(); + } + + /// + /// Aborts execution of the debugger while it is running, even while + /// it is stopped. Equivalent to calling PowerShellContext.AbortExecution. + /// + public void Abort() + { + this.powerShellContext.AbortExecution(shouldAbortDebugSession: true); + } + + /// + /// Gets the list of variables that are children of the scope or variable + /// that is identified by the given referenced ID. + /// + /// + /// An array of VariableDetails instances which describe the requested variables. + public VariableDetailsBase[] GetVariables(int variableReferenceId) + { + VariableDetailsBase[] childVariables; + this.debugInfoHandle.Wait(); + try + { + if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) + { + logger.LogWarning($"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); + return new VariableDetailsBase[0]; + } + + VariableDetailsBase parentVariable = this.variables[variableReferenceId]; + if (parentVariable.IsExpandable) + { + childVariables = parentVariable.GetChildren(this.logger); + foreach (var child in childVariables) + { + // Only add child if it hasn't already been added. + if (child.Id < 0) + { + child.Id = this.nextVariableId++; + this.variables.Add(child); + } + } + } + else + { + childVariables = new VariableDetailsBase[0]; + } + + return childVariables; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + /// + /// Evaluates a variable expression in the context of the stopped + /// debugger. This method decomposes the variable expression to + /// walk the cached variable data for the specified stack frame. + /// + /// The variable expression string to evaluate. + /// The ID of the stack frame in which the expression should be evaluated. + /// A VariableDetailsBase object containing the result. + public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId) + { + // NOTE: From a watch we will get passed expressions that are not naked variables references. + // Probably the right way to do this woudld be to examine the AST of the expr before calling + // this method to make sure it is a VariableReference. But for the most part, non-naked variable + // references are very unlikely to find a matching variable e.g. "$i+5.2" will find no var matching "$i+5". + + // Break up the variable path + string[] variablePathParts = variableExpression.Split('.'); + + VariableDetailsBase resolvedVariable = null; + IEnumerable variableList; + + // Ensure debug info isn't currently being built. + this.debugInfoHandle.Wait(); + try + { + variableList = this.variables; + } + finally + { + this.debugInfoHandle.Release(); + } + + foreach (var variableName in variablePathParts) + { + if (variableList == null) + { + // If there are no children left to search, break out early + return null; + } + + resolvedVariable = + variableList.FirstOrDefault( + v => + string.Equals( + v.Name, + variableName, + StringComparison.CurrentCultureIgnoreCase)); + + if (resolvedVariable != null && + resolvedVariable.IsExpandable) + { + // Continue by searching in this variable's children + variableList = this.GetVariables(resolvedVariable.Id); + } + } + + return resolvedVariable; + } + + /// + /// Sets the specified variable by container variableReferenceId and variable name to the + /// specified new value. If the variable cannot be set or converted to that value this + /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or + /// SessionStateUnauthorizedAccessException. + /// + /// The container (Autos, Local, Script, Global) that holds the variable. + /// The name of the variable prefixed with $. + /// The new string value. This value must not be null. If you want to set the variable to $null + /// pass in the string "$null". + /// The string representation of the value the variable was set to. + public async Task SetVariableAsync(int variableContainerReferenceId, string name, string value) + { + Validate.IsNotNull(nameof(name), name); + Validate.IsNotNull(nameof(value), value); + + this.logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); + + // An empty or whitespace only value is not a valid expression for SetVariable. + if (value.Trim().Length == 0) + { + throw new InvalidPowerShellExpressionException("Expected an expression."); + } + + // Evaluate the expression to get back a PowerShell object from the expression string. + PSCommand psCommand = new PSCommand(); + psCommand.AddScript(value); + var errorMessages = new StringBuilder(); + var results = + await this.powerShellContext.ExecuteCommandAsync( + psCommand, + errorMessages, + false, + false); + + // Check if PowerShell's evaluation of the expression resulted in an error. + object psobject = results.FirstOrDefault(); + if ((psobject == null) && (errorMessages.Length > 0)) + { + throw new InvalidPowerShellExpressionException(errorMessages.ToString()); + } + + // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. + // Ideally we would have a separate means from communicating error records apart from normal output. + if (psobject is ErrorRecord errorRecord) + { + throw new InvalidPowerShellExpressionException(errorRecord.ToString()); + } + + // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. + // Get the variable referenced by variableContainerReferenceId and variable name. + VariableContainerDetails variableContainer = null; + await this.debugInfoHandle.WaitAsync(); + try + { + variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; + } + finally + { + this.debugInfoHandle.Release(); + } + + VariableDetailsBase variable = variableContainer.Children[name]; + // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. + string scope = null; + if (variableContainerReferenceId == this.scriptScopeVariables.Id) + { + scope = "Script"; + } + else if (variableContainerReferenceId == this.globalScopeVariables.Id) + { + scope = "Global"; + } + else + { + // Determine which stackframe's local scope the variable is in. + StackFrameDetails[] stackFrames = await this.GetStackFramesAsync(); + for (int i = 0; i < stackFrames.Length; i++) + { + var stackFrame = stackFrames[i]; + if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) + { + scope = i.ToString(); + break; + } + } + } + + if (scope == null) + { + // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. + throw new Exception("Could not find the scope for this variable."); + } + + // Now that we have the scope, get the associated PSVariable object for the variable to be set. + psCommand.Commands.Clear(); + psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); + psCommand.AddParameter("Name", name.TrimStart('$')); + psCommand.AddParameter("Scope", scope); + + IEnumerable result = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false); + PSVariable psVariable = result.FirstOrDefault(); + if (psVariable == null) + { + throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); + } + + // We have the PSVariable object for the variable the user wants to set and an object to assign to that variable. + // The last step is to determine whether the PSVariable is "strongly typed" which may require a conversion. + // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. + // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - + // ArgumentTransformationAttribute. + var argTypeConverterAttr = + psVariable.Attributes + .OfType() + .FirstOrDefault(a => a.GetType().Name.Equals("ArgumentTypeConverterAttribute")); + + if (argTypeConverterAttr != null) + { + // PSVariable is strongly typed. Need to apply the conversion/transform to the new value. + psCommand.Commands.Clear(); + psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); + psCommand.AddParameter("Name", "ExecutionContext"); + psCommand.AddParameter("ValueOnly"); + + errorMessages.Clear(); + + var getExecContextResults = + await this.powerShellContext.ExecuteCommandAsync( + psCommand, + errorMessages, + sendErrorToHost: false); + + EngineIntrinsics executionContext = getExecContextResults.OfType().FirstOrDefault(); + + var msg = $"Setting variable '{name}' using conversion to value: {psobject ?? ""}"; + this.logger.LogTrace(msg); + + psVariable.Value = argTypeConverterAttr.Transform(executionContext, psobject); + } + else + { + // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. + var msg = $"Setting variable '{name}' directly to value: {psobject ?? ""} - previous type was {psVariable.Value?.GetType().Name ?? ""}"; + this.logger.LogTrace(msg); + psVariable.Value = psobject; + } + + // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. + // This makes the returned string consistent with the strings normally displayed for variables in the debugger. + var tempVariable = new VariableDetails(psVariable); + this.logger.LogTrace($"Set variable '{name}' to: {tempVariable.ValueString ?? ""}"); + return tempVariable.ValueString; + } + + /// + /// Evaluates an expression in the context of the stopped + /// debugger. This method will execute the specified expression + /// PowerShellContext. + /// + /// The expression string to execute. + /// The ID of the stack frame in which the expression should be executed. + /// + /// If true, writes the expression result as host output rather than returning the results. + /// In this case, the return value of this function will be null. + /// A VariableDetails object containing the result. + public async Task EvaluateExpressionAsync( + string expressionString, + int stackFrameId, + bool writeResultAsOutput) + { + var results = + await this.powerShellContext.ExecuteScriptStringAsync( + expressionString, + false, + writeResultAsOutput); + + // Since this method should only be getting invoked in the debugger, + // we can assume that Out-String will be getting used to format results + // of command executions into string output. However, if null is returned + // then return null so that no output gets displayed. + string outputString = + results != null && results.Any() ? + string.Join(Environment.NewLine, results) : + null; + + // If we've written the result as output, don't return a + // VariableDetails instance. + return + writeResultAsOutput ? + null : + new VariableDetails( + expressionString, + outputString); + } + + /// + /// Gets the list of stack frames at the point where the + /// debugger sf stopped. + /// + /// + /// An array of StackFrameDetails instances that contain the stack trace. + /// + public StackFrameDetails[] GetStackFrames() + { + this.debugInfoHandle.Wait(); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal StackFrameDetails[] GetStackFrames(CancellationToken cancellationToken) + { + this.debugInfoHandle.Wait(cancellationToken); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal async Task GetStackFramesAsync() + { + await this.debugInfoHandle.WaitAsync(); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + internal async Task GetStackFramesAsync(CancellationToken cancellationToken) + { + await this.debugInfoHandle.WaitAsync(cancellationToken); + try + { + return this.stackFrameDetails; + } + finally + { + this.debugInfoHandle.Release(); + } + } + + /// + /// Gets the list of variable scopes for the stack frame that + /// is identified by the given ID. + /// + /// The ID of the stack frame at which variable scopes should be retrieved. + /// The list of VariableScope instances which describe the available variable scopes. + public VariableScope[] GetVariableScopes(int stackFrameId) + { + var stackFrames = this.GetStackFrames(); + int localStackFrameVariableId = stackFrames[stackFrameId].LocalVariables.Id; + int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; + + return new VariableScope[] + { + new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName), + new VariableScope(localStackFrameVariableId, VariableContainerDetails.LocalScopeName), + new VariableScope(this.scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName), + new VariableScope(this.globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName), + }; + } + + /// + /// Clears all breakpoints in the current session. + /// + public async Task ClearAllBreakpointsAsync() + { + try + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); + + await this.powerShellContext.ExecuteCommandAsync(psCommand); + } + catch (Exception e) + { + logger.LogException("Caught exception while clearing breakpoints from session", e); + } + } + + #endregion + + #region Private Methods + + private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) + { + // Get the list of breakpoints for this file + if (this.breakpointsPerFile.TryGetValue(scriptFile.Id, out List breakpoints)) + { + if (breakpoints.Count > 0) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); + psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); + + await this.powerShellContext.ExecuteCommandAsync(psCommand); + + // Clear the existing breakpoints list for the file + breakpoints.Clear(); + } + } + } + + private async Task ClearCommandBreakpointsAsync() + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); + psCommand.AddParameter("Type", "Command"); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); + + await this.powerShellContext.ExecuteCommandAsync(psCommand); + } + + private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) + { + await this.debugInfoHandle.WaitAsync(); + try + { + this.nextVariableId = VariableDetailsBase.FirstVariableId; + this.variables = new List + { + + // Create a dummy variable for index 0, should never see this. + new VariableDetails("Dummy", null) + }; + + // Must retrieve global/script variales before stack frame variables + // as we check stack frame variables against globals. + await FetchGlobalAndScriptVariablesAsync(); + await FetchStackFramesAsync(scriptNameOverride); + } + finally + { + this.debugInfoHandle.Release(); + } + } + + private async Task FetchGlobalAndScriptVariablesAsync() + { + // Retrieve globals first as script variable retrieval needs to search globals. + this.globalScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null); + + this.scriptScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null); + } + + private async Task FetchVariableContainerAsync( + string scope, + VariableContainerDetails autoVariables) + { + PSCommand psCommand = new PSCommand(); + psCommand.AddCommand("Get-Variable"); + psCommand.AddParameter("Scope", scope); + + var scopeVariableContainer = + new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); + this.variables.Add(scopeVariableContainer); + + var results = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false); + if (results != null) + { + foreach (PSObject psVariableObject in results) + { + var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ }; + this.variables.Add(variableDetails); + scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); + + if ((autoVariables != null) && AddToAutoVariables(psVariableObject, scope)) + { + autoVariables.Children.Add(variableDetails.Name, variableDetails); + } + } + } + + return scopeVariableContainer; + } + + private bool AddToAutoVariables(PSObject psvariable, string scope) + { + if ((scope == VariableContainerDetails.GlobalScopeName) || + (scope == VariableContainerDetails.ScriptScopeName)) + { + // We don't A) have a good way of distinguishing built-in from user created variables + // and B) globalScopeVariables.Children.ContainsKey() doesn't work for built-in variables + // stored in a child variable container within the globals variable container. + return false; + } + + string variableName = psvariable.Properties["Name"].Value as string; + object variableValue = psvariable.Properties["Value"].Value; + + // Don't put any variables created by PSES in the Auto variable container. + if (variableName.StartsWith(PsesGlobalVariableNamePrefix) || + variableName.Equals("PSDebugContext")) + { + return false; + } + + ScopedItemOptions variableScope = ScopedItemOptions.None; + PSPropertyInfo optionsProperty = psvariable.Properties["Options"]; + if (string.Equals(optionsProperty.TypeNameOfValue, "System.String")) + { + if (!Enum.TryParse( + optionsProperty.Value as string, + out variableScope)) + { + this.logger.LogWarning( + $"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); + } + } + else if (optionsProperty.Value is ScopedItemOptions) + { + variableScope = (ScopedItemOptions)optionsProperty.Value; + } + + // Some local variables, if they exist, should be displayed by default + if (psvariable.TypeNames[0].EndsWith("LocalVariable")) + { + if (variableName.Equals("_")) + { + return true; + } + else if (variableName.Equals("args", StringComparison.OrdinalIgnoreCase)) + { + return variableValue is Array array + && array.Length > 0; + } + + return false; + } + else if (!psvariable.TypeNames[0].EndsWith(nameof(PSVariable))) + { + return false; + } + + var constantAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.Constant; + var readonlyAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.ReadOnly; + + if (((variableScope & constantAllScope) == constantAllScope) || + ((variableScope & readonlyAllScope) == readonlyAllScope)) + { + string prefixedVariableName = VariableDetails.DollarPrefix + variableName; + if (this.globalScopeVariables.Children.ContainsKey(prefixedVariableName)) + { + return false; + } + } + + return true; + } + + private async Task FetchStackFramesAsync(string scriptNameOverride) + { + PSCommand psCommand = new PSCommand(); + + // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame + // objects (or "deserialized" CallStackFrames) when attached to a runspace in another + // process. Without the intermediate variable Get-PSCallStack inexplicably returns + // an array of strings containing the formatted output of the CallStackFrame list. + var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; + psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); + + var results = await this.powerShellContext.ExecuteCommandAsync(psCommand); + + var callStackFrames = results.ToArray(); + + this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; + + for (int i = 0; i < callStackFrames.Length; i++) + { + VariableContainerDetails autoVariables = + new VariableContainerDetails( + this.nextVariableId++, + VariableContainerDetails.AutoVariablesName); + + this.variables.Add(autoVariables); + + VariableContainerDetails localVariables = + await FetchVariableContainerAsync(i.ToString(), autoVariables); + + // When debugging, this is the best way I can find to get what is likely the workspace root. + // This is controlled by the "cwd:" setting in the launch config. + string workspaceRootPath = this.powerShellContext.InitialWorkingDirectory; + + this.stackFrameDetails[i] = + StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath); + + string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath; + if (scriptNameOverride != null && + string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + { + this.stackFrameDetails[i].ScriptPath = scriptNameOverride; + } + else if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null && + !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + { + this.stackFrameDetails[i].ScriptPath = + this.remoteFileManager.GetMappedPath( + stackFrameScriptPath, + this.powerShellContext.CurrentRunspace); + } + } + } + + /// + /// Inspects the condition, putting in the appropriate scriptblock template + /// "if (expression) { break }". If errors are found in the condition, the + /// breakpoint passed in is updated to set Verified to false and an error + /// message is put into the breakpoint.Message property. + /// + /// + /// + private ScriptBlock GetBreakpointActionScriptBlock( + BreakpointDetailsBase breakpoint) + { + try + { + ScriptBlock actionScriptBlock; + int? hitCount = null; + + // If HitCondition specified, parse and verify it. + if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition))) + { + if (Int32.TryParse(breakpoint.HitCondition, out int parsedHitCount)) + { + hitCount = parsedHitCount; + } + else + { + breakpoint.Verified = false; + breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + + "The HitCount must be an integer number."; + return null; + } + } + + // Create an Action scriptblock based on condition and/or hit count passed in. + if (hitCount.HasValue && string.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // In the HitCount only case, this is simple as we can just use the HitCount + // property on the breakpoint object which is represented by $_. + string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; + actionScriptBlock = ScriptBlock.Create(action); + } + else if (!string.IsNullOrWhiteSpace(breakpoint.Condition)) + { + // Must be either condition only OR condition and hit count. + actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); + + // Check for simple, common errors that ScriptBlock parsing will not catch + // e.g. $i == 3 and $i > 3 + if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out string message)) + { + breakpoint.Verified = false; + breakpoint.Message = message; + return null; + } + + // Check for "advanced" condition syntax i.e. if the user has specified + // a "break" or "continue" statement anywhere in their scriptblock, + // pass their scriptblock through to the Action parameter as-is. + Ast breakOrContinueStatementAst = + actionScriptBlock.Ast.Find( + ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); + + // If this isn't advanced syntax then the conditions string should be a simple + // expression that needs to be wrapped in a "if" test that conditionally executes + // a break statement. + if (breakOrContinueStatementAst == null) + { + string wrappedCondition; + + if (hitCount.HasValue) + { + string globalHitCountVarName = + $"$global:{PsesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter++}"; + + wrappedCondition = + $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; + } + else + { + wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; + } + + actionScriptBlock = ScriptBlock.Create(wrappedCondition); + } + } + else + { + // Shouldn't get here unless someone called this with no condition and no hit count. + actionScriptBlock = ScriptBlock.Create("break"); + this.logger.LogWarning("No condition and no hit count specified by caller."); + } + + return actionScriptBlock; + } + catch (ParseException ex) + { + // Failed to create conditional breakpoint likely because the user provided an + // invalid PowerShell expression. Let the user know why. + breakpoint.Verified = false; + breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); + return null; + } + } + + private bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) + { + message = string.Empty; + + // We are only inspecting a few simple scenarios in the EndBlock only. + if (conditionAst is ScriptBlockAst scriptBlockAst && + scriptBlockAst.BeginBlock == null && + scriptBlockAst.ProcessBlock == null && + scriptBlockAst.EndBlock != null && + scriptBlockAst.EndBlock.Statements.Count == 1) + { + StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0]; + string condition = statementAst.Extent.Text; + + if (statementAst is AssignmentStatementAst) + { + message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='."); + return false; + } + + if (statementAst is PipelineAst pipelineAst + && pipelineAst.PipelineElements.Count == 1 + && pipelineAst.PipelineElements[0].Redirections.Count > 0) + { + message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'."); + return false; + } + } + + return true; + } + + private string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) + { + string[] messageLines = parseException.Message.Split('\n'); + + // Skip first line - it is a location indicator "At line:1 char: 4" + for (int i = 1; i < messageLines.Length; i++) + { + string line = messageLines[i]; + if (line.StartsWith("+")) + { + continue; + } + + if (!string.IsNullOrWhiteSpace(line)) + { + // Note '==' and '>" do not generate parse errors + if (line.Contains("'!='")) + { + line += " Use operator '-ne' instead of '!='."; + } + else if (line.Contains("'<'") && condition.Contains("<=")) + { + line += " Use operator '-le' instead of '<='."; + } + else if (line.Contains("'<'")) + { + line += " Use operator '-lt' instead of '<'."; + } + else if (condition.Contains(">=")) + { + line += " Use operator '-ge' instead of '>='."; + } + + return FormatInvalidBreakpointConditionMessage(condition, line); + } + } + + // If the message format isn't in a form we expect, just return the whole message. + return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); + } + + private string FormatInvalidBreakpointConditionMessage(string condition, string message) + { + return $"'{condition}' is not a valid PowerShell expression. {message}"; + } + + private string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength) + { + string scriptLine = scriptLineObj.ToString(); + + if (!string.IsNullOrWhiteSpace(scriptLine)) + { + if (prefixLength == 0) + { + // The prefix is a padded integer ending with ':', an asterisk '*' + // if this is the current line, and one character of padding + prefixLength = scriptLine.IndexOf(':') + 2; + } + + return scriptLine.Substring(prefixLength); + } + + return null; + } + + #endregion + + #region Events + + /// + /// Raised when the debugger stops execution at a breakpoint or when paused. + /// + public event EventHandler DebuggerStopped; + + private async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) + { + bool noScriptName = false; + string localScriptPath = e.InvocationInfo.ScriptName; + + // If there's no ScriptName, get the "list" of the current source + if (this.remoteFileManager != null && string.IsNullOrEmpty(localScriptPath)) + { + // Get the current script listing and create the buffer + PSCommand command = new PSCommand(); + command.AddScript($"list 1 {int.MaxValue}"); + + IEnumerable scriptListingLines = + await this.powerShellContext.ExecuteCommandAsync( + command, false, false); + + if (scriptListingLines != null) + { + int linePrefixLength = 0; + + string scriptListing = + string.Join( + Environment.NewLine, + scriptListingLines + .Select(o => this.TrimScriptListingLine(o, ref linePrefixLength)) + .Where(s => s != null)); + + this.temporaryScriptListingPath = + this.remoteFileManager.CreateTemporaryFile( + $"[{this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", + scriptListing, + this.powerShellContext.CurrentRunspace); + + localScriptPath = + this.temporaryScriptListingPath + ?? StackFrameDetails.NoFileScriptPath; + + noScriptName = localScriptPath != null; + } + else + { + this.logger.LogWarning($"Could not load script context"); + } + } + + // Get call stack and variables. + await this.FetchStackFramesAndVariablesAsync( + noScriptName ? localScriptPath : null); + + // If this is a remote connection and the debugger stopped at a line + // in a script file, get the file contents + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null && + !noScriptName) + { + localScriptPath = + await this.remoteFileManager.FetchRemoteFileAsync( + e.InvocationInfo.ScriptName, + this.powerShellContext.CurrentRunspace); + } + + if (this.stackFrameDetails.Length > 0) + { + // Augment the top stack frame with details from the stop event + + if (this.invocationTypeScriptPositionProperty + .GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) + { + this.stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; + this.stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; + this.stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; + this.stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; + } + } + + this.CurrentDebuggerStoppedEventArgs = + new DebuggerStoppedEventArgs( + e, + this.powerShellContext.CurrentRunspace, + localScriptPath); + + // Notify the host that the debugger is stopped + this.DebuggerStopped?.Invoke( + sender, + this.CurrentDebuggerStoppedEventArgs); + } + + private void OnDebuggerResumed(object sender, DebuggerResumeAction e) + { + this.CurrentDebuggerStoppedEventArgs = null; + } + + /// + /// Raised when a breakpoint is added/removed/updated in the debugger. + /// + public event EventHandler BreakpointUpdated; + + private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) + { + // This event callback also gets called when a CommandBreakpoint is modified. + // Only execute the following code for LineBreakpoint so we can keep track + // of which line breakpoints exist per script file. We use this later when + // we need to clear all breakpoints in a script file. We do not need to do + // this for CommandBreakpoint, as those span all script files. + if (e.Breakpoint is LineBreakpoint lineBreakpoint) + { + string scriptPath = lineBreakpoint.Script; + if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && + this.remoteFileManager != null) + { + string mappedPath = + this.remoteFileManager.GetMappedPath( + scriptPath, + this.powerShellContext.CurrentRunspace); + + if (mappedPath == null) + { + this.logger.LogError( + $"Could not map remote path '{scriptPath}' to a local path."); + + return; + } + + scriptPath = mappedPath; + } + + // Normalize the script filename for proper indexing + string normalizedScriptName = scriptPath.ToLower(); + + // Get the list of breakpoints for this file + if (!this.breakpointsPerFile.TryGetValue(normalizedScriptName, out List breakpoints)) + { + breakpoints = new List(); + this.breakpointsPerFile.Add( + normalizedScriptName, + breakpoints); + } + + // Add or remove the breakpoint based on the update type + if (e.UpdateType == BreakpointUpdateType.Set) + { + breakpoints.Add(e.Breakpoint); + } + else if (e.UpdateType == BreakpointUpdateType.Removed) + { + breakpoints.Remove(e.Breakpoint); + } + else + { + // TODO: Do I need to switch out instances for updated breakpoints? + } + } + + this.BreakpointUpdated?.Invoke(sender, e); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs new file mode 100644 index 000000000..fd2ec00cb --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs @@ -0,0 +1,32 @@ +// +// 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.Engine.Services +{ + internal class DebugStateService + { + internal bool NoDebug { get; set; } + + internal string Arguments { get; set; } + + internal bool IsRemoteAttach { get; set; } + + internal bool IsAttachSession { get; set; } + + internal bool WaitingForAttach { get; set; } + + internal string ScriptToLaunch { get; set; } + + internal bool OwnsEditorSession { get; set; } + + internal bool ExecutionCompleted { get; set; } + + internal bool IsInteractiveDebugSession { get; set; } + + internal bool SetBreakpointInProgress { get; set; } + + internal bool IsUsingTempIntegratedConsole { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs new file mode 100644 index 000000000..fb479dfb7 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs @@ -0,0 +1,107 @@ +// +// 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.Management.Automation; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Provides details about a breakpoint that is set in the + /// PowerShell debugger. + /// + public class BreakpointDetails : BreakpointDetailsBase + { + /// + /// Gets the unique ID of the breakpoint. + /// + /// + public int Id { get; private set; } + + /// + /// Gets the source where the breakpoint is located. Used only for debug purposes. + /// + public string Source { get; private set; } + + /// + /// Gets the line number at which the breakpoint is set. + /// + public int LineNumber { get; private set; } + + /// + /// Gets the column number at which the breakpoint is set. If null, the default of 1 is used. + /// + public int? ColumnNumber { get; private set; } + + private BreakpointDetails() + { + } + + /// + /// Creates an instance of the BreakpointDetails class from the individual + /// pieces of breakpoint information provided by the client. + /// + /// + /// + /// + /// + /// + /// + public static BreakpointDetails Create( + string source, + int line, + int? column = null, + string condition = null, + string hitCondition = null) + { + Validate.IsNotNull("source", source); + + return new BreakpointDetails + { + Verified = true, + Source = source, + LineNumber = line, + ColumnNumber = column, + Condition = condition, + HitCondition = hitCondition + }; + } + + /// + /// Creates an instance of the BreakpointDetails class from a + /// PowerShell Breakpoint object. + /// + /// The Breakpoint instance from which details will be taken. + /// A new instance of the BreakpointDetails class. + public static BreakpointDetails Create(Breakpoint breakpoint) + { + Validate.IsNotNull("breakpoint", breakpoint); + + if (!(breakpoint is LineBreakpoint lineBreakpoint)) + { + throw new ArgumentException( + "Unexpected breakpoint type: " + breakpoint.GetType().Name); + } + + var breakpointDetails = new BreakpointDetails + { + Id = breakpoint.Id, + Verified = true, + Source = lineBreakpoint.Script, + LineNumber = lineBreakpoint.Line, + ColumnNumber = lineBreakpoint.Column, + Condition = lineBreakpoint.Action?.ToString() + }; + + if (lineBreakpoint.Column > 0) + { + breakpointDetails.ColumnNumber = lineBreakpoint.Column; + } + + return breakpointDetails; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs new file mode 100644 index 000000000..3393bd007 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs @@ -0,0 +1,36 @@ +// +// 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.Engine.Services.DebugAdapter +{ + /// + /// Provides details about a breakpoint that is set in the + /// PowerShell debugger. + /// + public abstract class BreakpointDetailsBase + { + /// + /// Gets or sets a boolean indicator that if true, breakpoint could be set + /// (but not necessarily at the desired location). + /// + public bool Verified { get; set; } + + /// + /// Gets or set an optional message about the state of the breakpoint. This is shown to the user + /// and can be used to explain why a breakpoint could not be verified. + /// + public string Message { get; set; } + + /// + /// Gets the breakpoint condition string. + /// + public string Condition { get; protected set; } + + /// + /// Gets the breakpoint hit condition string. + /// + public string HitCondition { get; protected set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs new file mode 100644 index 000000000..d181f3c92 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs @@ -0,0 +1,72 @@ +// +// 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.Management.Automation; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Provides details about a command breakpoint that is set in the PowerShell debugger. + /// + public class CommandBreakpointDetails : BreakpointDetailsBase + { + /// + /// Gets the name of the command on which the command breakpoint has been set. + /// + public string Name { get; private set; } + + private CommandBreakpointDetails() + { + } + + /// + /// Creates an instance of the class from the individual + /// pieces of breakpoint information provided by the client. + /// + /// The name of the command to break on. + /// Condition string that would be applied to the breakpoint Action parameter. + /// Hit condition string that would be applied to the breakpoint Action parameter. + /// + public static CommandBreakpointDetails Create( + string name, + string condition = null, + string hitCondition = null) + { + Validate.IsNotNull(nameof(name), name); + + return new CommandBreakpointDetails { + Name = name, + Condition = condition + }; + } + + /// + /// Creates an instance of the class from a + /// PowerShell CommandBreakpoint object. + /// + /// The Breakpoint instance from which details will be taken. + /// A new instance of the BreakpointDetails class. + public static CommandBreakpointDetails Create(Breakpoint breakpoint) + { + Validate.IsNotNull("breakpoint", breakpoint); + + if (!(breakpoint is CommandBreakpoint commandBreakpoint)) + { + throw new ArgumentException( + "Unexpected breakpoint type: " + breakpoint.GetType().Name); + } + + var breakpointDetails = new CommandBreakpointDetails { + Verified = true, + Name = commandBreakpoint.Command, + Condition = commandBreakpoint.Action?.ToString() + }; + + return breakpointDetails; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs new file mode 100644 index 000000000..9b478afb0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs @@ -0,0 +1,117 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Provides event arguments for the DebugService.DebuggerStopped event. + /// + public class DebuggerStoppedEventArgs + { + #region Properties + + /// + /// Gets the path of the script where the debugger has stopped execution. + /// If 'IsRemoteSession' returns true, this path will be a local filesystem + /// path containing the contents of the script that is executing remotely. + /// + public string ScriptPath { get; private set; } + + /// + /// Returns true if the breakpoint was raised from a remote debugging session. + /// + public bool IsRemoteSession + { + get { return this.RunspaceDetails.Location == RunspaceLocation.Remote; } + } + + /// + /// Gets the original script path if 'IsRemoteSession' returns true. + /// + public string RemoteScriptPath { get; private set; } + + /// + /// Gets the RunspaceDetails for the current runspace. + /// + public RunspaceDetails RunspaceDetails { get; private set; } + + /// + /// Gets the line number at which the debugger stopped execution. + /// + public int LineNumber + { + get + { + return this.OriginalEvent.InvocationInfo.ScriptLineNumber; + } + } + + /// + /// Gets the column number at which the debugger stopped execution. + /// + public int ColumnNumber + { + get + { + return this.OriginalEvent.InvocationInfo.OffsetInLine; + } + } + + /// + /// Gets the original DebuggerStopEventArgs from the PowerShell engine. + /// + public DebuggerStopEventArgs OriginalEvent { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the DebuggerStoppedEventArgs class. + /// + /// The original DebuggerStopEventArgs instance from which this instance is based. + /// The RunspaceDetails of the runspace which raised this event. + public DebuggerStoppedEventArgs( + DebuggerStopEventArgs originalEvent, + RunspaceDetails runspaceDetails) + : this(originalEvent, runspaceDetails, null) + { + } + + /// + /// Creates a new instance of the DebuggerStoppedEventArgs class. + /// + /// The original DebuggerStopEventArgs instance from which this instance is based. + /// The RunspaceDetails of the runspace which raised this event. + /// The local path of the remote script being debugged. + public DebuggerStoppedEventArgs( + DebuggerStopEventArgs originalEvent, + RunspaceDetails runspaceDetails, + string localScriptPath) + { + Validate.IsNotNull(nameof(originalEvent), originalEvent); + Validate.IsNotNull(nameof(runspaceDetails), runspaceDetails); + + if (!string.IsNullOrEmpty(localScriptPath)) + { + this.ScriptPath = localScriptPath; + this.RemoteScriptPath = originalEvent.InvocationInfo.ScriptName; + } + else + { + this.ScriptPath = originalEvent.InvocationInfo.ScriptName; + } + + this.OriginalEvent = originalEvent; + this.RunspaceDetails = runspaceDetails; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs new file mode 100644 index 000000000..a708778f9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Represents the exception that is thrown when an invalid expression is provided to the DebugService's SetVariable method. + /// + public class InvalidPowerShellExpressionException : Exception + { + /// + /// Initializes a new instance of the SetVariableExpressionException class. + /// + /// Message indicating why the expression is invalid. + public InvalidPowerShellExpressionException(string message) + : base(message) + { + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs new file mode 100644 index 000000000..c58f53623 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs @@ -0,0 +1,134 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Contains details pertaining to a single stack frame in + /// the current debugging session. + /// + public class StackFrameDetails + { + #region Fields + + /// + /// A constant string used in the ScriptPath field to represent a + /// stack frame with no associated script file. + /// + public const string NoFileScriptPath = ""; + + #endregion + + #region Properties + + /// + /// Gets the path to the script where the stack frame occurred. + /// + public string ScriptPath { get; internal set; } + + /// + /// Gets the name of the function where the stack frame occurred. + /// + public string FunctionName { get; private set; } + + /// + /// Gets the start line number of the script where the stack frame occurred. + /// + public int StartLineNumber { get; internal set; } + + /// + /// Gets the line number of the script where the stack frame occurred. + /// + public int? EndLineNumber { get; internal set; } + + /// + /// Gets the start column number of the line where the stack frame occurred. + /// + public int StartColumnNumber { get; internal set; } + + /// + /// Gets the end column number of the line where the stack frame occurred. + /// + public int? EndColumnNumber { get; internal set; } + + /// + /// Gets a boolean value indicating whether or not the stack frame is executing + /// in script external to the current workspace root. + /// + public bool IsExternalCode { get; internal set; } + + /// + /// Gets or sets the VariableContainerDetails that contains the auto variables. + /// + public VariableContainerDetails AutoVariables { get; private set; } + + /// + /// Gets or sets the VariableContainerDetails that contains the local variables. + /// + public VariableContainerDetails LocalVariables { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the StackFrameDetails class from a + /// CallStackFrame instance provided by the PowerShell engine. + /// + /// + /// A PSObject representing the CallStackFrame instance from which details will be obtained. + /// + /// + /// A variable container with all the filtered, auto variables for this stack frame. + /// + /// + /// A variable container with all the local variables for this stack frame. + /// + /// + /// Specifies the path to the root of an open workspace, if one is open. This path is used to + /// determine whether individua stack frames are external to the workspace. + /// + /// A new instance of the StackFrameDetails class. + static internal StackFrameDetails Create( + PSObject callStackFrameObject, + VariableContainerDetails autoVariables, + VariableContainerDetails localVariables, + string workspaceRootPath = null) + { + string moduleId = string.Empty; + var isExternal = false; + + var invocationInfo = callStackFrameObject.Properties["InvocationInfo"]?.Value as InvocationInfo; + string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath; + int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0); + + // TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add + // settings to control this feature. + //if (workspaceRootPath != null && + // invocationInfo != null && + // !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase)) + //{ + // isExternal = true; + //} + + return new StackFrameDetails + { + ScriptPath = scriptPath, + FunctionName = callStackFrameObject.Properties["FunctionName"].Value as string, + StartLineNumber = startLineNumber, + EndLineNumber = startLineNumber, // End line number isn't given in PowerShell stack frames + StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames + EndColumnNumber = 0, + AutoVariables = autoVariables, + LocalVariables = localVariables, + IsExternalCode = isExternal + }; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs new file mode 100644 index 000000000..28d2df551 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Container for variables that is not itself a variable per se. However given how + /// VSCode uses an integer variable reference id for every node under the "Variables" tool + /// window, it is useful to treat containers, typically scope containers, as a variable. + /// Note that these containers are not necessarily always a scope container. Consider a + /// container such as "Auto" or "My". These aren't scope related but serve as just another + /// way to organize variables into a useful UI structure. + /// + [DebuggerDisplay("Name = {Name}, Id = {Id}, Count = {Children.Count}")] + public class VariableContainerDetails : VariableDetailsBase + { + /// + /// Provides a constant for the name of the Global scope. + /// + public const string AutoVariablesName = "Auto"; + + /// + /// Provides a constant for the name of the Global scope. + /// + public const string GlobalScopeName = "Global"; + + /// + /// Provides a constant for the name of the Local scope. + /// + public const string LocalScopeName = "Local"; + + /// + /// Provides a constant for the name of the Script scope. + /// + public const string ScriptScopeName = "Script"; + + private readonly Dictionary children; + + /// + /// Instantiates an instance of VariableScopeDetails. + /// + /// The variable reference id for this scope. + /// The name of the variable scope. + public VariableContainerDetails(int id, string name) + { + Validate.IsNotNull(name, "name"); + + this.Id = id; + this.Name = name; + this.IsExpandable = true; + this.ValueString = " "; // An empty string isn't enough due to a temporary bug in VS Code. + + this.children = new Dictionary(); + } + + /// + /// Gets the collection of child variables. + /// + public IDictionary Children + { + get { return this.children; } + } + + /// + /// Returns the details of the variable container's children. If empty, returns an empty array. + /// + /// + public override VariableDetailsBase[] GetChildren(ILogger logger) + { + var variablesArray = new VariableDetailsBase[this.children.Count]; + this.children.Values.CopyTo(variablesArray, 0); + return variablesArray; + } + + /// + /// Determines whether this variable container contains the specified variable by its referenceId. + /// + /// The variableReferenceId to search for. + /// Returns true if this variable container directly contains the specified variableReferenceId, false otherwise. + public bool ContainsVariable(int variableReferenceId) + { + foreach (VariableDetailsBase value in this.children.Values) + { + if (value.Id == variableReferenceId) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs new file mode 100644 index 000000000..9d3e375b0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs @@ -0,0 +1,416 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management.Automation; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Contains details pertaining to a variable in the current + /// debugging session. + /// + [DebuggerDisplay("Name = {Name}, Id = {Id}, Value = {ValueString}")] + public class VariableDetails : VariableDetailsBase + { + #region Fields + + /// + /// Provides a constant for the dollar sign variable prefix string. + /// + public const string DollarPrefix = "$"; + + private object valueObject; + private VariableDetails[] cachedChildren; + + #endregion + + #region Constructors + + /// + /// Initializes an instance of the VariableDetails class from + /// the details contained in a PSVariable instance. + /// + /// + /// The PSVariable instance from which variable details will be obtained. + /// + public VariableDetails(PSVariable psVariable) + : this(DollarPrefix + psVariable.Name, psVariable.Value) + { + } + + /// + /// Initializes an instance of the VariableDetails class from + /// the name and value pair stored inside of a PSObject which + /// represents a PSVariable. + /// + /// + /// The PSObject which represents a PSVariable. + /// + public VariableDetails(PSObject psVariableObject) + : this( + DollarPrefix + psVariableObject.Properties["Name"].Value as string, + psVariableObject.Properties["Value"].Value) + { + } + + /// + /// Initializes an instance of the VariableDetails class from + /// the details contained in a PSPropertyInfo instance. + /// + /// + /// The PSPropertyInfo instance from which variable details will be obtained. + /// + public VariableDetails(PSPropertyInfo psProperty) + : this(psProperty.Name, psProperty.Value) + { + } + + /// + /// Initializes an instance of the VariableDetails class from + /// a given name/value pair. + /// + /// The variable's name. + /// The variable's value. + public VariableDetails(string name, object value) + { + this.valueObject = value; + + this.Id = -1; // Not been assigned a variable reference id yet + this.Name = name; + this.IsExpandable = GetIsExpandable(value); + + string typeName; + this.ValueString = GetValueStringAndType(value, this.IsExpandable, out typeName); + this.Type = typeName; + } + + #endregion + + #region Public Methods + + /// + /// If this variable instance is expandable, this method returns the + /// details of its children. Otherwise it returns an empty array. + /// + /// + public override VariableDetailsBase[] GetChildren(ILogger logger) + { + VariableDetails[] childVariables = null; + + if (this.IsExpandable) + { + if (this.cachedChildren == null) + { + this.cachedChildren = GetChildren(this.valueObject, logger); + } + + return this.cachedChildren; + } + else + { + childVariables = new VariableDetails[0]; + } + + return childVariables; + } + + #endregion + + #region Private Methods + + private static bool GetIsExpandable(object valueObject) + { + if (valueObject == null) + { + return false; + } + + // If a PSObject, unwrap it + var psobject = valueObject as PSObject; + if (psobject != null) + { + valueObject = psobject.BaseObject; + } + + Type valueType = + valueObject != null ? + valueObject.GetType() : + null; + + TypeInfo valueTypeInfo = valueType.GetTypeInfo(); + + return + valueObject != null && + !valueTypeInfo.IsPrimitive && + !valueTypeInfo.IsEnum && // Enums don't have any properties + !(valueObject is string) && // Strings get treated as IEnumerables + !(valueObject is decimal) && + !(valueObject is UnableToRetrievePropertyMessage); + } + + private static string GetValueStringAndType(object value, bool isExpandable, out string typeName) + { + string valueString = null; + typeName = null; + + if (value == null) + { + // Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural. + return "$null"; + } + + Type objType = value.GetType(); + typeName = $"[{objType.FullName}]"; + + if (value is bool) + { + // Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural. + valueString = (bool) value ? "$true" : "$false"; + } + else if (isExpandable) + { + + // Get the "value" for an expandable object. + if (value is DictionaryEntry) + { + // For DictionaryEntry - display the key/value as the value. + var entry = (DictionaryEntry)value; + valueString = + string.Format( + "[{0}, {1}]", + entry.Key, + GetValueStringAndType(entry.Value, GetIsExpandable(entry.Value), out typeName)); + } + else + { + string valueToString = value.SafeToString(); + if (valueToString.Equals(objType.ToString())) + { + // If the ToString() matches the type name, then display the type + // name in PowerShell format. + string shortTypeName = objType.Name; + + // For arrays and ICollection, display the number of contained items. + if (value is Array) + { + var arr = value as Array; + if (arr.Rank == 1) + { + shortTypeName = InsertDimensionSize(shortTypeName, arr.Length); + } + } + else if (value is ICollection) + { + var collection = (ICollection)value; + shortTypeName = InsertDimensionSize(shortTypeName, collection.Count); + } + + valueString = $"[{shortTypeName}]"; + } + else + { + valueString = valueToString; + } + } + } + else + { + // Value is a scalar (not expandable). If it's a string, display it directly otherwise use SafeToString() + if (value is string) + { + valueString = "\"" + value + "\""; + } + else + { + valueString = value.SafeToString(); + } + } + + return valueString; + } + + private static string InsertDimensionSize(string value, int dimensionSize) + { + string result = value; + + int indexLastRBracket = value.LastIndexOf("]"); + if (indexLastRBracket > 0) + { + result = + value.Substring(0, indexLastRBracket) + + dimensionSize + + value.Substring(indexLastRBracket); + } + else + { + // Types like ArrayList don't use [] in type name so + // display value like so - [ArrayList: 5] + result = value + ": " + dimensionSize; + } + + return result; + } + + private VariableDetails[] GetChildren(object obj, ILogger logger) + { + List childVariables = new List(); + + if (obj == null) + { + return childVariables.ToArray(); + } + + try + { + PSObject psObject = obj as PSObject; + + if ((psObject != null) && + (psObject.TypeNames[0] == typeof(PSCustomObject).ToString())) + { + // PowerShell PSCustomObject's properties are completely defined by the ETS type system. + childVariables.AddRange( + psObject + .Properties + .Select(p => new VariableDetails(p))); + } + else + { + // If a PSObject other than a PSCustomObject, unwrap it. + if (psObject != null) + { + // First add the PSObject's ETS propeties + childVariables.AddRange( + psObject + .Properties + .Where(p => p.MemberType == PSMemberTypes.NoteProperty) + .Select(p => new VariableDetails(p))); + + obj = psObject.BaseObject; + } + + IDictionary dictionary = obj as IDictionary; + IEnumerable enumerable = obj as IEnumerable; + + // We're in the realm of regular, unwrapped .NET objects + if (dictionary != null) + { + // Buckle up kids, this is a bit weird. We could not use the LINQ + // operator OfType. Even though R# will squiggle the + // "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT! + // The reason is that LINQ extension methods work with objects of type + // IEnumerable. Objects of type Dictionary<,>, respond to iteration via + // IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic + // dictionaries like HashTable return DictionaryEntry objects. + // It turns out that iteration via C#'s foreach loop, operates on the variable's + // type which in this case is IDictionary. IDictionary was designed to always + // return DictionaryEntry objects upon iteration and the Dictionary<,> implementation + // honors that when the object is reintepreted as an IDictionary object. + // FYI, a test case for this is to open $PSBoundParameters when debugging a + // function that defines parameters and has been passed parameters. + // If you open the $PSBoundParameters variable node in this scenario and see nothing, + // this code is broken. + int i = 0; + foreach (DictionaryEntry entry in dictionary) + { + childVariables.Add( + new VariableDetails( + "[" + i++ + "]", + entry)); + } + } + else if (enumerable != null && !(obj is string)) + { + int i = 0; + foreach (var item in enumerable) + { + childVariables.Add( + new VariableDetails( + "[" + i++ + "]", + item)); + } + } + + AddDotNetProperties(obj, childVariables); + } + } + catch (GetValueInvocationException ex) + { + // This exception occurs when accessing the value of a + // variable causes a script to be executed. Right now + // we aren't loading children on the pipeline thread so + // this causes an exception to be raised. In this case, + // just return an empty list of children. + logger.LogWarning($"Failed to get properties of variable {this.Name}, value invocation was attempted: {ex.Message}"); + } + + return childVariables.ToArray(); + } + + private static void AddDotNetProperties(object obj, List childVariables) + { + Type objectType = obj.GetType(); + var properties = + objectType.GetProperties( + BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + // Don't display indexer properties, it causes an exception anyway. + if (property.GetIndexParameters().Length > 0) + { + continue; + } + + try + { + childVariables.Add( + new VariableDetails( + property.Name, + property.GetValue(obj))); + } + catch (Exception ex) + { + // Some properties can throw exceptions, add the property + // name and info about the error. + if (ex is TargetInvocationException) + { + ex = ex.InnerException; + } + + childVariables.Add( + new VariableDetails( + property.Name, + new UnableToRetrievePropertyMessage( + "Error retrieving property - " + ex.GetType().Name))); + } + } + } + + #endregion + + private struct UnableToRetrievePropertyMessage + { + public UnableToRetrievePropertyMessage(string message) + { + this.Message = message; + } + + public string Message { get; } + + public override string ToString() + { + return "<" + Message + ">"; + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs new file mode 100644 index 000000000..0eb8c32ab --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs @@ -0,0 +1,60 @@ +// +// 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.Utility; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +{ + /// + /// Defines the common details between a variable and a variable container such as a scope + /// in the current debugging session. + /// + public abstract class VariableDetailsBase + { + /// + /// Provides a constant that is used as the starting variable ID for all. + /// Avoid 0 as it indicates a variable node with no children. + /// variables. + /// + public const int FirstVariableId = 1; + + /// + /// Gets the numeric ID of the variable which can be used to refer + /// to it in future requests. + /// + public int Id { get; set; } + + /// + /// Gets the variable's name. + /// + public string Name { get; protected set; } + + /// + /// Gets the string representation of the variable's value. + /// If the variable is an expandable object, this string + /// will be empty. + /// + public string ValueString { get; protected set; } + + /// + /// Gets the type of the variable's value. + /// + public string Type { get; protected set; } + + /// + /// Returns true if the variable's value is expandable, meaning + /// that it has child properties or its contents can be enumerated. + /// + public bool IsExpandable { get; protected set; } + + /// + /// If this variable instance is expandable, this method returns the + /// details of its children. Otherwise it returns an empty array. + /// + /// + public abstract VariableDetailsBase[] GetChildren(ILogger logger); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs new file mode 100644 index 000000000..411388951 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs @@ -0,0 +1,37 @@ +// +// 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.Engine.Services.DebugAdapter +{ + /// + /// Contains details pertaining to a variable scope in the current + /// debugging session. + /// + public class VariableScope + { + /// + /// Gets a numeric ID that can be used in future operations + /// relating to this scope. + /// + public int Id { get; private set; } + + /// + /// Gets a name that describes the variable scope. + /// + public string Name { get; private set; } + + /// + /// Initializes a new instance of the VariableScope class with + /// the given ID and name. + /// + /// The variable scope's ID. + /// The variable scope's name. + public VariableScope(int id, string name) + { + this.Id = id; + this.Name = name; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs new file mode 100644 index 000000000..8b0d0962c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -0,0 +1,226 @@ +// +// 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; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class SetFunctionBreakpointsHandler : ISetFunctionBreakpointsHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + private readonly DebugStateService _debugStateService; + + public SetFunctionBreakpointsHandler( + ILoggerFactory loggerFactory, + DebugService debugService, + DebugStateService debugStateService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + _debugStateService = debugStateService; + } + + public async Task Handle(SetFunctionBreakpointsArguments request, CancellationToken cancellationToken) + { + CommandBreakpointDetails[] breakpointDetails = request.Breakpoints + .Select((funcBreakpoint) => CommandBreakpointDetails.Create( + funcBreakpoint.Name, + funcBreakpoint.Condition, + funcBreakpoint.HitCondition)) + .ToArray(); + + // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. + CommandBreakpointDetails[] updatedBreakpointDetails = breakpointDetails; + if (!_debugStateService.NoDebug) + { + _debugStateService.SetBreakpointInProgress = true; + + try + { + updatedBreakpointDetails = + await _debugService.SetCommandBreakpointsAsync( + breakpointDetails); + } + catch (Exception e) + { + // Log whatever the error is + _logger.LogException($"Caught error while setting command breakpoints", e); + } + finally + { + _debugStateService.SetBreakpointInProgress = false; + } + } + + return new SetFunctionBreakpointsResponse + { + Breakpoints = updatedBreakpointDetails + .Select(LspDebugUtils.CreateBreakpoint) + .ToArray() + }; + } + } + + internal class SetExceptionBreakpointsHandler : ISetExceptionBreakpointsHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + private readonly DebugStateService _debugStateService; + + public SetExceptionBreakpointsHandler( + ILoggerFactory loggerFactory, + DebugService debugService, + DebugStateService debugStateService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + _debugStateService = debugStateService; + } + + public Task Handle(SetExceptionBreakpointsArguments request, CancellationToken cancellationToken) + { + // TODO: When support for exception breakpoints (unhandled and/or first chance) + // are added to the PowerShell engine, wire up the VSCode exception + // breakpoints here using the pattern below to prevent bug regressions. + //if (!noDebug) + //{ + // setBreakpointInProgress = true; + + // try + // { + // // Set exception breakpoints in DebugService + // } + // catch (Exception e) + // { + // // Log whatever the error is + // Logger.WriteException($"Caught error while setting exception breakpoints", e); + // } + // finally + // { + // setBreakpointInProgress = false; + // } + //} + + return Task.FromResult(new SetExceptionBreakpointsResponse()); + } + } + + internal class SetBreakpointsHandler : ISetBreakpointsHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + private readonly DebugStateService _debugStateService; + private readonly WorkspaceService _workspaceService; + + public SetBreakpointsHandler( + ILoggerFactory loggerFactory, + DebugService debugService, + DebugStateService debugStateService, + WorkspaceService workspaceService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + _debugStateService = debugStateService; + _workspaceService = workspaceService; + } + + public async Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = null; + + // When you set a breakpoint in the right pane of a Git diff window on a PS1 file, + // the Source.Path comes through as Untitled-X. That's why we check for IsUntitledPath. + if (!ScriptFile.IsUntitledPath(request.Source.Path) && + !_workspaceService.TryGetFile( + request.Source.Path, + out scriptFile)) + { + string message = _debugStateService.NoDebug ? string.Empty : "Source file could not be accessed, breakpoint not set."; + var srcBreakpoints = request.Breakpoints + .Select(srcBkpt => LspDebugUtils.CreateBreakpoint( + srcBkpt, request.Source.Path, message, verified: _debugStateService.NoDebug)); + + // Return non-verified breakpoint message. + return new SetBreakpointsResponse + { + Breakpoints = new Container(srcBreakpoints) + }; + } + + // Verify source file is a PowerShell script file. + string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); + if (string.IsNullOrEmpty(fileExtension) || ((fileExtension != ".ps1") && (fileExtension != ".psm1"))) + { + _logger.LogWarning( + $"Attempted to set breakpoints on a non-PowerShell file: {request.Source.Path}"); + + string message = _debugStateService.NoDebug ? string.Empty : "Source is not a PowerShell script, breakpoint not set."; + + var srcBreakpoints = request.Breakpoints + .Select(srcBkpt => LspDebugUtils.CreateBreakpoint( + srcBkpt, request.Source.Path, message, verified: _debugStateService.NoDebug)); + + // Return non-verified breakpoint message. + return new SetBreakpointsResponse + { + Breakpoints = new Container(srcBreakpoints) + }; + } + + // At this point, the source file has been verified as a PowerShell script. + BreakpointDetails[] breakpointDetails = request.Breakpoints + .Select((srcBreakpoint) => BreakpointDetails.Create( + scriptFile.FilePath, + (int)srcBreakpoint.Line, + (int?)srcBreakpoint.Column, + srcBreakpoint.Condition, + srcBreakpoint.HitCondition)) + .ToArray(); + + // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. + BreakpointDetails[] updatedBreakpointDetails = breakpointDetails; + if (!_debugStateService.NoDebug) + { + _debugStateService.SetBreakpointInProgress = true; + + try + { + updatedBreakpointDetails = + await _debugService.SetLineBreakpointsAsync( + scriptFile, + breakpointDetails); + } + catch (Exception e) + { + // Log whatever the error is + _logger.LogException($"Caught error while setting breakpoints in SetBreakpoints handler for file {scriptFile?.FilePath}", e); + } + finally + { + _debugStateService.SetBreakpointInProgress = false; + } + } + + return new SetBreakpointsResponse + { + Breakpoints = new Container(updatedBreakpointDetails + .Select(LspDebugUtils.CreateBreakpoint)) + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs new file mode 100644 index 000000000..354d723f4 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -0,0 +1,104 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class ConfigurationDoneHandler : IConfigurationDoneHandler + { + private readonly ILogger _logger; + private readonly IJsonRpcServer _jsonRpcServer; + private readonly DebugService _debugService; + private readonly DebugStateService _debugStateService; + private readonly DebugEventHandlerService _debugEventHandlerService; + private readonly PowerShellContextService _powerShellContextService; + private readonly WorkspaceService _workspaceService; + + public ConfigurationDoneHandler( + ILoggerFactory loggerFactory, + IJsonRpcServer jsonRpcServer, + DebugService debugService, + DebugStateService debugStateService, + DebugEventHandlerService debugEventHandlerService, + PowerShellContextService powerShellContextService, + WorkspaceService workspaceService) + { + _logger = loggerFactory.CreateLogger(); + _jsonRpcServer = jsonRpcServer; + _debugService = debugService; + _debugStateService = debugStateService; + _debugEventHandlerService = debugEventHandlerService; + _powerShellContextService = powerShellContextService; + _workspaceService = workspaceService; + } + + public Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken) + { + _debugService.IsClientAttached = true; + + if (!string.IsNullOrEmpty(_debugStateService.ScriptToLaunch)) + { + if (_powerShellContextService.SessionState == PowerShellContextState.Ready) + { + // Configuration is done, launch the script + var nonAwaitedTask = LaunchScriptAsync(_debugStateService.ScriptToLaunch) + .ConfigureAwait(continueOnCapturedContext: false); + } + else + { + _logger.LogTrace("configurationDone request called after script was already launched, skipping it."); + } + } + + if (_debugStateService.IsInteractiveDebugSession) + { + if (_debugStateService.OwnsEditorSession) + { + // If this is a debug-only session, we need to start + // the command loop manually + // TODO: Bring this back + //_editorSession.HostInput.StartCommandLoop(); + } + + if (_debugService.IsDebuggerStopped) + { + // If this is an interactive session and there's a pending breakpoint, + // send that information along to the debugger client + _debugEventHandlerService.TriggerDebuggerStopped(_debugService.CurrentDebuggerStoppedEventArgs); + } + } + + return Task.FromResult(new ConfigurationDoneResponse()); + } + + private async Task LaunchScriptAsync(string scriptToLaunch) + { + // Is this an untitled script? + if (ScriptFile.IsUntitledPath(scriptToLaunch)) + { + ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch); + + await _powerShellContextService + .ExecuteScriptStringAsync(untitledScript.Contents, true, true); + } + else + { + await _powerShellContextService + .ExecuteScriptWithArgsAsync(scriptToLaunch, _debugStateService.Arguments, writeInputToHost: true); + } + + _jsonRpcServer.SendNotification(EventNames.Terminated); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs new file mode 100644 index 000000000..a35133eb9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs @@ -0,0 +1,88 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class DebugEvaluateHandler : IEvaluateHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + private readonly DebugService _debugService; + + public DebugEvaluateHandler( + ILoggerFactory factory, + PowerShellContextService powerShellContextService, + DebugService debugService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + _debugService = debugService; + } + + public async Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) + { + string valueString = ""; + int variableId = 0; + + bool isFromRepl = + string.Equals( + request.Context, + "repl", + StringComparison.CurrentCultureIgnoreCase); + + if (isFromRepl) + { + var notAwaited = + _powerShellContextService + .ExecuteScriptStringAsync(request.Expression, false, true) + .ConfigureAwait(false); + } + else + { + VariableDetailsBase result = null; + + // VS Code might send this request after the debugger + // has been resumed, return an empty result in this case. + if (_powerShellContextService.IsDebuggerStopped) + { + // First check to see if the watch expression refers to a naked variable reference. + result = + _debugService.GetVariableFromExpression(request.Expression, request.FrameId); + + // If the expression is not a naked variable reference, then evaluate the expression. + if (result == null) + { + result = + await _debugService.EvaluateExpressionAsync( + request.Expression, + request.FrameId, + isFromRepl); + } + } + + if (result != null) + { + valueString = result.ValueString; + variableId = + result.IsExpandable ? + result.Id : 0; + } + } + + return new EvaluateResponseBody + { + Result = valueString, + VariablesReference = variableId + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs new file mode 100644 index 000000000..2ae5c932c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs @@ -0,0 +1,123 @@ +// +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class ContinueHandler : IContinueHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public ContinueHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(ContinueArguments request, CancellationToken cancellationToken) + { + _debugService.Continue(); + return Task.FromResult(new ContinueResponse()); + } + } + + internal class NextHandler : INextHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public NextHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(NextArguments request, CancellationToken cancellationToken) + { + _debugService.StepOver(); + return Task.FromResult(new NextResponse()); + } + } + + internal class PauseHandler : IPauseHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public PauseHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(PauseArguments request, CancellationToken cancellationToken) + { + try + { + _debugService.Break(); + return Task.FromResult(new PauseResponse()); + } + catch(NotSupportedException e) + { + throw new RpcErrorException(0, e.Message); + } + } + } + + internal class StepInHandler : IStepInHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public StepInHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(StepInArguments request, CancellationToken cancellationToken) + { + _debugService.StepIn(); + return Task.FromResult(new StepInResponse()); + } + } + + internal class StepOutHandler : IStepOutHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public StepOutHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(StepOutArguments request, CancellationToken cancellationToken) + { + _debugService.StepOut(); + return Task.FromResult(new StepOutResponse()); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs new file mode 100644 index 000000000..7c5711e3f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs @@ -0,0 +1,84 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Server; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class DisconnectHandler : IDisconnectHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + private readonly DebugService _debugService; + private readonly DebugStateService _debugStateService; + private readonly DebugEventHandlerService _debugEventHandlerService; + private readonly PsesDebugServer _psesDebugServer; + + public DisconnectHandler( + ILoggerFactory factory, + PsesDebugServer psesDebugServer, + PowerShellContextService powerShellContextService, + DebugService debugService, + DebugStateService debugStateService, + DebugEventHandlerService debugEventHandlerService) + { + _logger = factory.CreateLogger(); + _psesDebugServer = psesDebugServer; + _powerShellContextService = powerShellContextService; + _debugService = debugService; + _debugStateService = debugStateService; + _debugEventHandlerService = debugEventHandlerService; + } + + public async Task Handle(DisconnectArguments request, CancellationToken cancellationToken) + { + _debugEventHandlerService.UnregisterEventHandlers(); + if (_debugStateService.ExecutionCompleted == false) + { + _debugStateService.ExecutionCompleted = true; + _powerShellContextService.AbortExecution(shouldAbortDebugSession: true); + + if (_debugStateService.IsInteractiveDebugSession && _debugStateService.IsAttachSession) + { + // Pop the sessions + if (_powerShellContextService.CurrentRunspace.Context == RunspaceContext.EnteredProcess) + { + try + { + await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSHostProcess"); + + if (_debugStateService.IsRemoteAttach && + _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + { + await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSSession"); + } + } + catch (Exception e) + { + _logger.LogException("Caught exception while popping attached process after debugging", e); + } + } + } + + _debugService.IsClientAttached = false; + } + + _logger.LogInformation("Debug adapter is shutting down..."); + + // Trigger the clean up of the debugger. + Task.Run(_psesDebugServer.OnSessionEnded); + + return new DisconnectResponse(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs new file mode 100644 index 000000000..0876fdff1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class InitializeHandler : IInitializeHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public InitializeHandler( + ILoggerFactory factory, + DebugService debugService) + { + _logger = factory.CreateLogger(); + _debugService = debugService; + } + + public async Task Handle(InitializeRequestArguments request, CancellationToken cancellationToken) + { + // Clear any existing breakpoints before proceeding + await _debugService.ClearAllBreakpointsAsync(); + + // Now send the Initialize response to continue setup + return new InitializeResponse + { + SupportsConfigurationDoneRequest = true, + SupportsFunctionBreakpoints = true, + SupportsConditionalBreakpoints = true, + SupportsHitConditionalBreakpoints = true, + SupportsSetVariable = true + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs new file mode 100644 index 000000000..77e62588a --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -0,0 +1,407 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + [Serial, Method("launch")] + interface IPsesLaunchHandler : IJsonRpcRequestHandler { } + + [Serial, Method("attach")] + interface IPsesAttachHandler : IJsonRpcRequestHandler { } + + public class PsesLaunchRequestArguments : IRequest + { + /// + /// Gets or sets the absolute path to the script to debug. + /// + public string Script { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the script should be + /// run with (false) or without (true) debugging support. + /// + public bool NoDebug { get; set; } + + /// + /// Gets or sets a boolean value that determines whether to automatically stop + /// target after launch. If not specified, target does not stop. + /// + public bool StopOnEntry { get; set; } + + /// + /// Gets or sets optional arguments passed to the debuggee. + /// + public string[] Args { get; set; } + + /// + /// Gets or sets the working directory of the launched debuggee (specified as an absolute path). + /// If omitted the debuggee is lauched in its own directory. + /// + public string Cwd { get; set; } + + /// + /// Gets or sets a boolean value that determines whether to create a temporary + /// integrated console for the debug session. Default is false. + /// + public bool CreateTemporaryIntegratedConsole { get; set; } + + /// + /// Gets or sets the absolute path to the runtime executable to be used. + /// Default is the runtime executable on the PATH. + /// + public string RuntimeExecutable { get; set; } + + /// + /// Gets or sets the optional arguments passed to the runtime executable. + /// + public string[] RuntimeArgs { get; set; } + + /// + /// Gets or sets optional environment variables to pass to the debuggee. The string valued + /// properties of the 'environmentVariables' are used as key/value pairs. + /// + public Dictionary Env { get; set; } + } + + public class PsesAttachRequestArguments : IRequest + { + public string ComputerName { get; set; } + + public string ProcessId { get; set; } + + public string RunspaceId { get; set; } + + public string RunspaceName { get; set; } + + public string CustomPipeName { get; set; } + } + + internal class LaunchHandler : IPsesLaunchHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + private readonly PowerShellContextService _powerShellContextService; + private readonly DebugStateService _debugStateService; + private readonly DebugEventHandlerService _debugEventHandlerService; + private readonly IJsonRpcServer _jsonRpcServer; + private readonly RemoteFileManagerService _remoteFileManagerService; + + public LaunchHandler( + ILoggerFactory factory, + IJsonRpcServer jsonRpcServer, + DebugService debugService, + PowerShellContextService powerShellContextService, + DebugStateService debugStateService, + DebugEventHandlerService debugEventHandlerService, + RemoteFileManagerService remoteFileManagerService) + { + _logger = factory.CreateLogger(); + _jsonRpcServer = jsonRpcServer; + _debugService = debugService; + _powerShellContextService = powerShellContextService; + _debugStateService = debugStateService; + _debugEventHandlerService = debugEventHandlerService; + _remoteFileManagerService = remoteFileManagerService; + } + + public async Task Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken) + { + _debugEventHandlerService.RegisterEventHandlers(); + + // Determine whether or not the working directory should be set in the PowerShellContext. + if ((_powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Local) && + !_debugService.IsDebuggerStopped) + { + // Get the working directory that was passed via the debug config + // (either via launch.json or generated via no-config debug). + string workingDir = request.Cwd; + + // Assuming we have a non-empty/null working dir, unescape the path and verify + // the path exists and is a directory. + if (!string.IsNullOrEmpty(workingDir)) + { + try + { + if ((File.GetAttributes(workingDir) & FileAttributes.Directory) != FileAttributes.Directory) + { + workingDir = Path.GetDirectoryName(workingDir); + } + } + catch (Exception ex) + { + workingDir = null; + _logger.LogError( + $"The specified 'cwd' path is invalid: '{request.Cwd}'. Error: {ex.Message}"); + } + } + + // If we have no working dir by this point and we are running in a temp console, + // pick some reasonable default. + if (string.IsNullOrEmpty(workingDir) && request.CreateTemporaryIntegratedConsole) + { + workingDir = Environment.CurrentDirectory; + } + + // At this point, we will either have a working dir that should be set to cwd in + // the PowerShellContext or the user has requested (via an empty/null cwd) that + // the working dir should not be changed. + if (!string.IsNullOrEmpty(workingDir)) + { + await _powerShellContextService.SetWorkingDirectoryAsync(workingDir, isPathAlreadyEscaped: false); + } + + _logger.LogTrace($"Working dir " + (string.IsNullOrEmpty(workingDir) ? "not set." : $"set to '{workingDir}'")); + } + + // Prepare arguments to the script - if specified + string arguments = null; + if ((request.Args != null) && (request.Args.Length > 0)) + { + arguments = string.Join(" ", request.Args); + _logger.LogTrace("Script arguments are: " + arguments); + } + + // Store the launch parameters so that they can be used later + _debugStateService.NoDebug = request.NoDebug; + _debugStateService.ScriptToLaunch = request.Script; + _debugStateService.Arguments = arguments; + _debugStateService.IsUsingTempIntegratedConsole = request.CreateTemporaryIntegratedConsole; + + // TODO: Bring this back + // If the current session is remote, map the script path to the remote + // machine if necessary + if (_debugStateService.ScriptToLaunch != null && + _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + { + _debugStateService.ScriptToLaunch = + _remoteFileManagerService.GetMappedPath( + _debugStateService.ScriptToLaunch, + _powerShellContextService.CurrentRunspace); + } + + // If no script is being launched, mark this as an interactive + // debugging session + _debugStateService.IsInteractiveDebugSession = string.IsNullOrEmpty(_debugStateService.ScriptToLaunch); + + // Send the InitializedEvent so that the debugger will continue + // sending configuration requests + _jsonRpcServer.SendNotification(EventNames.Initialized); + + return Unit.Value; + } + } + + internal class AttachHandler : IPsesAttachHandler + { + private static readonly Version s_minVersionForCustomPipeName = new Version(6, 2); + + private readonly ILogger _logger; + private readonly DebugService _debugService; + private readonly PowerShellContextService _powerShellContextService; + private readonly DebugStateService _debugStateService; + private readonly DebugEventHandlerService _debugEventHandlerService; + private readonly IJsonRpcServer _jsonRpcServer; + + public AttachHandler( + ILoggerFactory factory, + IJsonRpcServer jsonRpcServer, + DebugService debugService, + PowerShellContextService powerShellContextService, + DebugStateService debugStateService, + DebugEventHandlerService debugEventHandlerService) + { + _logger = factory.CreateLogger(); + _jsonRpcServer = jsonRpcServer; + _debugService = debugService; + _powerShellContextService = powerShellContextService; + _debugStateService = debugStateService; + _debugEventHandlerService = debugEventHandlerService; + } + + public async Task Handle(PsesAttachRequestArguments request, CancellationToken cancellationToken) + { + _debugStateService.IsAttachSession = true; + + _debugEventHandlerService.RegisterEventHandlers(); + + bool processIdIsSet = !string.IsNullOrEmpty(request.ProcessId) && request.ProcessId != "undefined"; + bool customPipeNameIsSet = !string.IsNullOrEmpty(request.CustomPipeName) && request.CustomPipeName != "undefined"; + + PowerShellVersionDetails runspaceVersion = + _powerShellContextService.CurrentRunspace.PowerShellVersion; + + // If there are no host processes to attach to or the user cancels selection, we get a null for the process id. + // This is not an error, just a request to stop the original "attach to" request. + // Testing against "undefined" is a HACK because I don't know how to make "Cancel" on quick pick loading + // to cancel on the VSCode side without sending an attachRequest with processId set to "undefined". + if (!processIdIsSet && !customPipeNameIsSet) + { + _logger.LogInformation( + $"Attach request aborted, received {request.ProcessId} for processId."); + + throw new RpcErrorException(0, "User aborted attach to PowerShell host process."); + } + + StringBuilder errorMessages = new StringBuilder(); + + if (request.ComputerName != null) + { + if (runspaceVersion.Version.Major < 4) + { + throw new RpcErrorException(0, $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version})."); + } + else if (_powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + { + throw new RpcErrorException(0, $"Cannot attach to a process in a remote session when already in a remote session."); + } + + await _powerShellContextService.ExecuteScriptStringAsync( + $"Enter-PSSession -ComputerName \"{request.ComputerName}\"", + errorMessages); + + if (errorMessages.Length > 0) + { + throw new RpcErrorException(0, $"Could not establish remote session to computer '{request.ComputerName}'"); + } + + _debugStateService.IsRemoteAttach = true; + } + + if (processIdIsSet && int.TryParse(request.ProcessId, out int processId) && (processId > 0)) + { + if (runspaceVersion.Version.Major < 5) + { + throw new RpcErrorException(0, $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version})."); + } + + await _powerShellContextService.ExecuteScriptStringAsync( + $"Enter-PSHostProcess -Id {processId}", + errorMessages); + + if (errorMessages.Length > 0) + { + throw new RpcErrorException(0, $"Could not attach to process '{processId}'"); + } + } + else if (customPipeNameIsSet) + { + if (runspaceVersion.Version < s_minVersionForCustomPipeName) + { + throw new RpcErrorException(0, $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version})."); + } + + await _powerShellContextService.ExecuteScriptStringAsync( + $"Enter-PSHostProcess -CustomPipeName {request.CustomPipeName}", + errorMessages); + + if (errorMessages.Length > 0) + { + throw new RpcErrorException(0, $"Could not attach to process with CustomPipeName: '{request.CustomPipeName}'"); + } + } + else if (request.ProcessId != "current") + { + _logger.LogError( + $"Attach request failed, '{request.ProcessId}' is an invalid value for the processId."); + + throw new RpcErrorException(0, "A positive integer must be specified for the processId field."); + } + + // Clear any existing breakpoints before proceeding + await _debugService.ClearAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); + + // Execute the Debug-Runspace command but don't await it because it + // will block the debug adapter initialization process. The + // InitializedEvent will be sent as soon as the RunspaceChanged + // event gets fired with the attached runspace. + + string debugRunspaceCmd; + if (request.RunspaceName != null) + { + debugRunspaceCmd = $"\nDebug-Runspace -Name '{request.RunspaceName}'"; + } + else if (request.RunspaceId != null) + { + if (!int.TryParse(request.RunspaceId, out int runspaceId) || runspaceId <= 0) + { + _logger.LogError( + $"Attach request failed, '{request.RunspaceId}' is an invalid value for the processId."); + + throw new RpcErrorException(0, "A positive integer must be specified for the RunspaceId field."); + } + + debugRunspaceCmd = $"\nDebug-Runspace -Id {runspaceId}"; + } + else + { + debugRunspaceCmd = "\nDebug-Runspace -Id 1"; + } + + _debugStateService.WaitingForAttach = true; + Task nonAwaitedTask = _powerShellContextService + .ExecuteScriptStringAsync(debugRunspaceCmd) + .ContinueWith(OnExecutionCompletedAsync); + + return Unit.Value; + } + + private async Task OnExecutionCompletedAsync(Task executeTask) + { + try + { + await executeTask; + } + catch (Exception e) + { + _logger.LogError( + "Exception occurred while awaiting debug launch task.\n\n" + e.ToString()); + } + + _logger.LogTrace("Execution completed, terminating..."); + + //_debugStateService.ExecutionCompleted = true; + + //_debugEventHandlerService.UnregisterEventHandlers(); + + //if (_debugStateService.IsAttachSession) + //{ + // // Pop the sessions + // if (_powerShellContextService.CurrentRunspace.Context == RunspaceContext.EnteredProcess) + // { + // try + // { + // await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSHostProcess"); + + // if (_debugStateService.IsRemoteAttach && + // _powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) + // { + // await _powerShellContextService.ExecuteScriptStringAsync("Exit-PSSession"); + // } + // } + // catch (Exception e) + // { + // _logger.LogException("Caught exception while popping attached process after debugging", e); + // } + // } + //} + + //_debugService.IsClientAttached = false; + _jsonRpcServer.SendNotification(EventNames.Terminated); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs new file mode 100644 index 000000000..22a33154c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class ScopesHandler : IScopesHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public ScopesHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(ScopesArguments request, CancellationToken cancellationToken) + { + VariableScope[] variableScopes = + _debugService.GetVariableScopes( + (int) request.FrameId); + + return Task.FromResult(new ScopesResponse + { + Scopes = new Container(variableScopes + .Select(LspDebugUtils.CreateScope)) + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs new file mode 100644 index 000000000..542b3f36e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs @@ -0,0 +1,67 @@ +// +// 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.Linq; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class SetVariableHandler : ISetVariableHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public SetVariableHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public async Task Handle(SetVariableArguments request, CancellationToken cancellationToken) + { + try + { + string updatedValue = + await _debugService.SetVariableAsync( + (int) request.VariablesReference, + request.Name, + request.Value); + + return new SetVariableResponse + { + Value = updatedValue + }; + + } + catch (Exception ex) when(ex is ArgumentTransformationMetadataException || + ex is InvalidPowerShellExpressionException || + ex is SessionStateUnauthorizedAccessException) + { + // Catch common, innocuous errors caused by the user supplying a value that can't be converted or the variable is not settable. + _logger.LogTrace($"Failed to set variable: {ex.Message}"); + throw new RpcErrorException(0, ex.Message); + } + catch (Exception ex) + { + _logger.LogError($"Unexpected error setting variable: {ex.Message}"); + string msg = + $"Unexpected error: {ex.GetType().Name} - {ex.Message} Please report this error to the PowerShellEditorServices project on GitHub."; + throw new RpcErrorException(0, msg); + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs new file mode 100644 index 000000000..168682ca0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class SourceHandler : ISourceHandler + { + public Task Handle(SourceArguments request, CancellationToken cancellationToken) + { + // TODO: Implement this message. For now, doesn't seem to + // be a problem that it's missing. + return Task.FromResult(new SourceResponse()); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs new file mode 100644 index 000000000..9b2263b24 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class StackTraceHandler : IStackTraceHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public StackTraceHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(StackTraceArguments request, CancellationToken cancellationToken) + { + StackFrameDetails[] stackFrameDetails = + _debugService.GetStackFrames(); + + // Handle a rare race condition where the adapter requests stack frames before they've + // begun building. + if (stackFrameDetails == null) + { + return Task.FromResult(new StackTraceResponse + { + StackFrames = new StackFrame[0], + TotalFrames = 0 + }); + } + + List newStackFrames = new List(); + + long startFrameIndex = request.StartFrame ?? 0; + long maxFrameCount = stackFrameDetails.Length; + + // If the number of requested levels == 0 (or null), that means get all stack frames + // after the specified startFrame index. Otherwise get all the stack frames. + long requestedFrameCount = (request.Levels ?? 0); + if (requestedFrameCount > 0) + { + maxFrameCount = Math.Min(maxFrameCount, startFrameIndex + requestedFrameCount); + } + + for (long i = startFrameIndex; i < maxFrameCount; i++) + { + // Create the new StackFrame object with an ID that can + // be referenced back to the current list of stack frames + //newStackFrames.Add( + // StackFrame.Create( + // stackFrameDetails[i], + // i)); + newStackFrames.Add( + LspDebugUtils.CreateStackFrame(stackFrameDetails[i], id: i)); + } + + return Task.FromResult(new StackTraceResponse + { + StackFrames = newStackFrames, + TotalFrames = newStackFrames.Count + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs new file mode 100644 index 000000000..06c14c44c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class ThreadsHandler : IThreadsHandler + { + public Task Handle(ThreadsArguments request, CancellationToken cancellationToken) + { + return Task.FromResult(new ThreadsResponse + { + // TODO: What do I do with these? + Threads = new Container( + new OmniSharp.Extensions.DebugAdapter.Protocol.Models.Thread + { + Id = 1, + Name = "Main Thread" + }) + }); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs new file mode 100644 index 000000000..cf8003302 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs @@ -0,0 +1,58 @@ +// +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +{ + internal class VariablesHandler : IVariablesHandler + { + private readonly ILogger _logger; + private readonly DebugService _debugService; + + public VariablesHandler( + ILoggerFactory loggerFactory, + DebugService debugService) + { + _logger = loggerFactory.CreateLogger(); + _debugService = debugService; + } + + public Task Handle(VariablesArguments request, CancellationToken cancellationToken) + { + VariableDetailsBase[] variables = + _debugService.GetVariables( + (int)request.VariablesReference); + + VariablesResponse variablesResponse = null; + + try + { + variablesResponse = new VariablesResponse + { + Variables = + variables + .Select(LspDebugUtils.CreateVariable) + .ToArray() + }; + } + catch (Exception) + { + // TODO: This shouldn't be so broad + } + + return Task.FromResult(variablesResponse); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs index b46eff1e8..3afac2843 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -161,6 +161,64 @@ public PowerShellContextService( ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; } + public static PowerShellContextService Create( + ILoggerFactory factory, + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + ProfilePaths profilePaths, + HashSet featureFlags, + bool enableConsoleRepl, + PSHost internalHost, + HostDetails hostDetails, + string[] additionalModules + ) + { + var logger = factory.CreateLogger(); + + // PSReadLine can only be used when -EnableConsoleRepl is specified otherwise + // issues arise when redirecting stdio. + var powerShellContext = new PowerShellContextService( + logger, + languageServer, + featureFlags.Contains("PSReadLine") && enableConsoleRepl); + + EditorServicesPSHostUserInterface hostUserInterface = + enableConsoleRepl + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, internalHost) + : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + powerShellContext, + hostDetails, + hostUserInterface, + logger); + + Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); + powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + + powerShellContext.ImportCommandsModuleAsync( + Path.Combine( + Path.GetDirectoryName(typeof(PowerShellContextService).GetTypeInfo().Assembly.Location), + @"..\Commands")); + + // TODO: This can be moved to the point after the $psEditor object + // gets initialized when that is done earlier than LanguageServer.Initialize + foreach (string module in additionalModules) + { + var command = + new PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("Name", module); + + powerShellContext.ExecuteCommandAsync( + command, + sendOutputToHost: false, + sendErrorToHost: true); + } + + return powerShellContext; + } + /// /// /// diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/RemoteFileManagerService.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs rename to src/PowerShellEditorServices.Engine/Services/PowerShellContext/RemoteFileManagerService.cs index 1fe3d4086..cc32755cc 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RemoteFileManager.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/RemoteFileManagerService.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -16,13 +17,13 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Engine.Services { /// /// Manages files that are accessed from a remote PowerShell session. /// Also manages the registration and handling of the 'psedit' function. /// - public class RemoteFileManager + internal class RemoteFileManagerService { #region Fields @@ -236,23 +237,23 @@ function New-EditorFile { #region Constructors /// - /// Creates a new instance of the RemoteFileManager class. + /// Creates a new instance of the RemoteFileManagerService class. /// + /// An ILoggerFactory implementation used for writing log messages. /// /// The PowerShellContext to use for file loading operations. /// /// /// The IEditorOperations instance to use for opening/closing files in the editor. /// - /// An ILogger implementation used for writing log messages. - public RemoteFileManager( + public RemoteFileManagerService( + ILoggerFactory factory, PowerShellContextService powerShellContext, - IEditorOperations editorOperations, - ILogger logger) + EditorOperationsService editorOperations) { Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - this.logger = logger; + this.logger = factory.CreateLogger(); this.powerShellContext = powerShellContext; this.powerShellContext.RunspaceChanged += HandleRunspaceChangedAsync; @@ -701,7 +702,7 @@ private void TryDeleteTemporaryPath() private class RemotePathMappings { private RunspaceDetails runspaceDetails; - private RemoteFileManager remoteFileManager; + private RemoteFileManagerService remoteFileManager; private HashSet openedPaths = new HashSet(); private Dictionary pathMappings = new Dictionary(); @@ -712,7 +713,7 @@ public IEnumerable OpenedPaths public RemotePathMappings( RunspaceDetails runspaceDetails, - RemoteFileManager remoteFileManager) + RemoteFileManagerService remoteFileManager) { this.runspaceDetails = runspaceDetails; this.remoteFileManager = remoteFileManager; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs index e9f1b45e0..76a5c4d80 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs @@ -1,166 +1,168 @@ -//// -//// Copyright (c) Microsoft. All rights reserved. -//// Licensed under the MIT license. See LICENSE file in the project root for full license information. -//// - -//using System.Linq; -//using System.Threading.Tasks; - -//namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext -//{ -// using Microsoft.Extensions.Logging; -// using Microsoft.PowerShell.EditorServices.Utility; -// using System; -// using System.Collections.Generic; -// using System.Collections.ObjectModel; -// using System.Management.Automation; - -// internal class DscBreakpointCapability : IRunspaceCapability -// { -// private string[] dscResourceRootPaths = new string[0]; - -// private Dictionary breakpointsPerFile = -// new Dictionary(); - -// public async Task> SetLineBreakpointsAsync( -// PowerShellContextService powerShellContext, -// string scriptPath, -// BreakpointDetails[] breakpoints) -// { -// List resultBreakpointDetails = -// new List(); - -// // We always get the latest array of breakpoint line numbers -// // so store that for future use -// if (breakpoints.Length > 0) -// { -// // Set the breakpoints for this scriptPath -// this.breakpointsPerFile[scriptPath] = -// breakpoints.Select(b => b.LineNumber).ToArray(); -// } -// else -// { -// // No more breakpoints for this scriptPath, remove it -// this.breakpointsPerFile.Remove(scriptPath); -// } - -// string hashtableString = -// string.Join( -// ", ", -// this.breakpointsPerFile -// .Select(file => $"@{{Path=\"{file.Key}\";Line=@({string.Join(",", file.Value)})}}")); - -// // Run Enable-DscDebug as a script because running it as a PSCommand -// // causes an error which states that the Breakpoint parameter has not -// // been passed. -// await powerShellContext.ExecuteScriptStringAsync( -// hashtableString.Length > 0 -// ? $"Enable-DscDebug -Breakpoint {hashtableString}" -// : "Disable-DscDebug", -// false, -// false); - -// // Verify all the breakpoints and return them -// foreach (var breakpoint in breakpoints) -// { -// breakpoint.Verified = true; -// } - -// return breakpoints.ToList(); -// } - -// public bool IsDscResourcePath(string scriptPath) -// { -// return dscResourceRootPaths.Any( -// dscResourceRootPath => -// scriptPath.StartsWith( -// dscResourceRootPath, -// StringComparison.CurrentCultureIgnoreCase)); -// } - -// public static DscBreakpointCapability CheckForCapability( -// RunspaceDetails runspaceDetails, -// PowerShellContextService powerShellContext, -// ILogger logger) -// { -// DscBreakpointCapability capability = null; - -// // DSC support is enabled only for Windows PowerShell. -// if ((runspaceDetails.PowerShellVersion.Version.Major < 6) && -// (runspaceDetails.Context != RunspaceContext.DebuggedRunspace)) -// { -// using (PowerShell powerShell = PowerShell.Create()) -// { -// powerShell.Runspace = runspaceDetails.Runspace; - -// // Attempt to import the updated DSC module -// powerShell.AddCommand("Import-Module"); -// powerShell.AddArgument(@"C:\Program Files\DesiredStateConfiguration\1.0.0.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psd1"); -// powerShell.AddParameter("PassThru"); -// powerShell.AddParameter("ErrorAction", "Ignore"); - -// PSObject moduleInfo = null; - -// try -// { -// moduleInfo = powerShell.Invoke().FirstOrDefault(); -// } -// catch (RuntimeException e) -// { -// logger.LogException("Could not load the DSC module!", e); -// } - -// if (moduleInfo != null) -// { -// logger.LogTrace("Side-by-side DSC module found, gathering DSC resource paths..."); - -// // The module was loaded, add the breakpoint capability -// capability = new DscBreakpointCapability(); -// runspaceDetails.AddCapability(capability); - -// powerShell.Commands.Clear(); -// powerShell.AddScript("Write-Host \"Gathering DSC resource paths, this may take a while...\""); -// powerShell.Invoke(); - -// // Get the list of DSC resource paths -// powerShell.Commands.Clear(); -// powerShell.AddCommand("Get-DscResource"); -// powerShell.AddCommand("Select-Object"); -// powerShell.AddParameter("ExpandProperty", "ParentPath"); - -// Collection resourcePaths = null; - -// try -// { -// resourcePaths = powerShell.Invoke(); -// } -// catch (CmdletInvocationException e) -// { -// logger.LogException("Get-DscResource failed!", e); -// } - -// if (resourcePaths != null) -// { -// capability.dscResourceRootPaths = -// resourcePaths -// .Select(o => (string)o.BaseObject) -// .ToArray(); - -// logger.LogTrace($"DSC resources found: {resourcePaths.Count}"); -// } -// else -// { -// logger.LogTrace($"No DSC resources found."); -// } -// } -// else -// { -// logger.LogTrace($"Side-by-side DSC module was not found."); -// } -// } -// } - -// return capability; -// } -// } -//} +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +{ + using Microsoft.Extensions.Logging; + using Microsoft.PowerShell.EditorServices.Engine.Logging; + using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; + using Microsoft.PowerShell.EditorServices.Utility; + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Management.Automation; + + internal class DscBreakpointCapability : IRunspaceCapability + { + private string[] dscResourceRootPaths = new string[0]; + + private Dictionary breakpointsPerFile = + new Dictionary(); + + public async Task> SetLineBreakpointsAsync( + PowerShellContextService powerShellContext, + string scriptPath, + BreakpointDetails[] breakpoints) + { + List resultBreakpointDetails = + new List(); + + // We always get the latest array of breakpoint line numbers + // so store that for future use + if (breakpoints.Length > 0) + { + // Set the breakpoints for this scriptPath + this.breakpointsPerFile[scriptPath] = + breakpoints.Select(b => b.LineNumber).ToArray(); + } + else + { + // No more breakpoints for this scriptPath, remove it + this.breakpointsPerFile.Remove(scriptPath); + } + + string hashtableString = + string.Join( + ", ", + this.breakpointsPerFile + .Select(file => $"@{{Path=\"{file.Key}\";Line=@({string.Join(",", file.Value)})}}")); + + // Run Enable-DscDebug as a script because running it as a PSCommand + // causes an error which states that the Breakpoint parameter has not + // been passed. + await powerShellContext.ExecuteScriptStringAsync( + hashtableString.Length > 0 + ? $"Enable-DscDebug -Breakpoint {hashtableString}" + : "Disable-DscDebug", + false, + false); + + // Verify all the breakpoints and return them + foreach (var breakpoint in breakpoints) + { + breakpoint.Verified = true; + } + + return breakpoints.ToList(); + } + + public bool IsDscResourcePath(string scriptPath) + { + return dscResourceRootPaths.Any( + dscResourceRootPath => + scriptPath.StartsWith( + dscResourceRootPath, + StringComparison.CurrentCultureIgnoreCase)); + } + + public static DscBreakpointCapability CheckForCapability( + RunspaceDetails runspaceDetails, + PowerShellContextService powerShellContext, + ILogger logger) + { + DscBreakpointCapability capability = null; + + // DSC support is enabled only for Windows PowerShell. + if ((runspaceDetails.PowerShellVersion.Version.Major < 6) && + (runspaceDetails.Context != RunspaceContext.DebuggedRunspace)) + { + using (PowerShell powerShell = PowerShell.Create()) + { + powerShell.Runspace = runspaceDetails.Runspace; + + // Attempt to import the updated DSC module + powerShell.AddCommand("Import-Module"); + powerShell.AddArgument(@"C:\Program Files\DesiredStateConfiguration\1.0.0.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psd1"); + powerShell.AddParameter("PassThru"); + powerShell.AddParameter("ErrorAction", "Ignore"); + + PSObject moduleInfo = null; + + try + { + moduleInfo = powerShell.Invoke().FirstOrDefault(); + } + catch (RuntimeException e) + { + logger.LogException("Could not load the DSC module!", e); + } + + if (moduleInfo != null) + { + logger.LogTrace("Side-by-side DSC module found, gathering DSC resource paths..."); + + // The module was loaded, add the breakpoint capability + capability = new DscBreakpointCapability(); + runspaceDetails.AddCapability(capability); + + powerShell.Commands.Clear(); + powerShell.AddScript("Write-Host \"Gathering DSC resource paths, this may take a while...\""); + powerShell.Invoke(); + + // Get the list of DSC resource paths + powerShell.Commands.Clear(); + powerShell.AddCommand("Get-DscResource"); + powerShell.AddCommand("Select-Object"); + powerShell.AddParameter("ExpandProperty", "ParentPath"); + + Collection resourcePaths = null; + + try + { + resourcePaths = powerShell.Invoke(); + } + catch (CmdletInvocationException e) + { + logger.LogException("Get-DscResource failed!", e); + } + + if (resourcePaths != null) + { + capability.dscResourceRootPaths = + resourcePaths + .Select(o => (string)o.BaseObject) + .ToArray(); + + logger.LogTrace($"DSC resources found: {resourcePaths.Count}"); + } + else + { + logger.LogTrace($"No DSC resources found."); + } + } + else + { + logger.LogTrace($"Side-by-side DSC module was not found."); + } + } + } + + return capability; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs index 52538d2d7..d3b70c43a 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -25,6 +25,7 @@ class TextDocumentHandler : ITextDocumentSyncHandler private readonly ILogger _logger; private readonly AnalysisService _analysisService; private readonly WorkspaceService _workspaceService; + private readonly RemoteFileManagerService _remoteFileManagerService; private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() @@ -37,11 +38,16 @@ class TextDocumentHandler : ITextDocumentSyncHandler public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental; - public TextDocumentHandler(ILoggerFactory factory, AnalysisService analysisService, WorkspaceService workspaceService) + public TextDocumentHandler( + ILoggerFactory factory, + AnalysisService analysisService, + WorkspaceService workspaceService, + RemoteFileManagerService remoteFileManagerService) { _logger = factory.CreateLogger(); _analysisService = analysisService; _workspaceService = workspaceService; + _remoteFileManagerService = remoteFileManagerService; } public Task Handle(DidChangeTextDocumentParams notification, CancellationToken token) @@ -117,21 +123,20 @@ public Task Handle(DidCloseTextDocumentParams notification, CancellationTo return Unit.Task; } - public Task Handle(DidSaveTextDocumentParams notification, CancellationToken token) + public async Task Handle(DidSaveTextDocumentParams notification, CancellationToken token) { ScriptFile savedFile = _workspaceService.GetFile( notification.TextDocument.Uri.ToString()); - // TODO bring back - // if (savedFile != null) - // { - // if (this.editorSession.RemoteFileManager.IsUnderRemoteTempPath(savedFile.FilePath)) - // { - // await this.editorSession.RemoteFileManager.SaveRemoteFileAsync( - // savedFile.FilePath); - // } - // } - return Unit.Task; + + if (savedFile != null) + { + if (_remoteFileManagerService.IsUnderRemoteTempPath(savedFile.FilePath)) + { + await _remoteFileManagerService.SaveRemoteFileAsync(savedFile.FilePath); + } + } + return Unit.Value; } TextDocumentSaveRegistrationOptions IRegistration.GetRegistrationOptions() diff --git a/src/PowerShellEditorServices.Engine/Utility/Extensions.cs b/src/PowerShellEditorServices.Engine/Utility/Extensions.cs new file mode 100644 index 000000000..205332d83 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/Extensions.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + internal static class ObjectExtensions + { + /// + /// Extension to evaluate an object's ToString() method in an exception safe way. This will + /// extension method will not throw. + /// + /// The object on which to call ToString() + /// The ToString() return value or a suitable error message is that throws. + public static string SafeToString(this object obj) + { + string str; + + try + { + str = obj.ToString(); + } + catch (Exception ex) + { + str = $""; + } + + return str; + } + + /// + /// Get the maximum of the elements from the given enumerable. + /// + /// Type of object for which the enumerable is defined. + /// An enumerable object of type T + /// A comparer for ordering elements of type T. The comparer should handle null values. + /// An object of type T. If the enumerable is empty or has all null elements, then the method returns null. + public static T MaxElement(this IEnumerable elements, Func comparer) where T:class + { + if (elements == null) + { + throw new ArgumentNullException(nameof(elements)); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + if (!elements.Any()) + { + return null; + } + + var maxElement = elements.First(); + foreach(var element in elements.Skip(1)) + { + if (element != null && comparer(element, maxElement) > 0) + { + maxElement = element; + } + } + + return maxElement; + } + + /// + /// Get the minimum of the elements from the given enumerable. + /// + /// Type of object for which the enumerable is defined. + /// An enumerable object of type T + /// A comparer for ordering elements of type T. The comparer should handle null values. + /// An object of type T. If the enumerable is empty or has all null elements, then the method returns null. + public static T MinElement(this IEnumerable elements, Func comparer) where T : class + { + return MaxElement(elements, (elementX, elementY) => -1 * comparer(elementX, elementY)); + } + + /// + /// Compare extents with respect to their widths. + /// + /// Width of an extent is defined as the difference between its EndOffset and StartOffest properties. + /// + /// Extent of type IScriptExtent. + /// Extent of type IScriptExtent. + /// 0 if extentX and extentY are equal in width. 1 if width of extent X is greater than that of extent Y. Otherwise, -1. + public static int ExtentWidthComparer(this IScriptExtent extentX, IScriptExtent extentY) + { + + if (extentX == null && extentY == null) + { + return 0; + } + + if (extentX != null && extentY == null) + { + return 1; + } + + if (extentX == null) + { + return -1; + } + + var extentWidthX = extentX.EndOffset - extentX.StartOffset; + var extentWidthY = extentY.EndOffset - extentY.StartOffset; + if (extentWidthX > extentWidthY) + { + return 1; + } + else if (extentWidthX < extentWidthY) + { + return -1; + } + else + { + return 0; + } + } + + /// + /// Check if the given coordinates are wholly contained in the instance's extent. + /// + /// Extent of type IScriptExtent. + /// 1-based line number. + /// 1-based column number + /// True if the coordinates are wholly contained in the instance's extent, otherwise, false. + public static bool Contains(this IScriptExtent scriptExtent, int line, int column) + { + if (scriptExtent.StartLineNumber > line || scriptExtent.EndLineNumber < line) + { + return false; + } + + if (scriptExtent.StartLineNumber == line) + { + return scriptExtent.StartColumnNumber <= column; + } + + if (scriptExtent.EndLineNumber == line) + { + return scriptExtent.EndColumnNumber >= column; + } + + return true; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs b/src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs new file mode 100644 index 000000000..914b7e2da --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs @@ -0,0 +1,115 @@ +using System; +using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + public static class LspDebugUtils + { + public static Breakpoint CreateBreakpoint( + BreakpointDetails breakpointDetails) + { + Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); + + return new Breakpoint + { + Id = breakpointDetails.Id, + Verified = breakpointDetails.Verified, + Message = breakpointDetails.Message, + Source = new Source { Path = breakpointDetails.Source }, + Line = breakpointDetails.LineNumber, + Column = breakpointDetails.ColumnNumber + }; + } + + public static Breakpoint CreateBreakpoint( + CommandBreakpointDetails breakpointDetails) + { + Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); + + return new Breakpoint + { + Verified = breakpointDetails.Verified, + Message = breakpointDetails.Message + }; + } + + public static Breakpoint CreateBreakpoint( + SourceBreakpoint sourceBreakpoint, + string source, + string message, + bool verified = false) + { + Validate.IsNotNull(nameof(sourceBreakpoint), sourceBreakpoint); + Validate.IsNotNull(nameof(source), source); + Validate.IsNotNull(nameof(message), message); + + return new Breakpoint + { + Verified = verified, + Message = message, + Source = new Source { Path = source }, + Line = sourceBreakpoint.Line, + Column = sourceBreakpoint.Column + }; + } + + public static StackFrame CreateStackFrame( + StackFrameDetails stackFrame, + long id) + { + var sourcePresentationHint = + stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal; + + // When debugging an interactive session, the ScriptPath is which is not a valid source file. + // We need to make sure the user can't open the file associated with this stack frame. + // It will generate a VSCode error in this case. + Source source = null; + if (!stackFrame.ScriptPath.Contains("<")) + { + source = new Source + { + Path = stackFrame.ScriptPath, + PresentationHint = sourcePresentationHint + }; + } + + return new StackFrame + { + Id = id, + Name = (source != null) ? stackFrame.FunctionName : "Interactive Session", + Line = (source != null) ? stackFrame.StartLineNumber : 0, + EndLine = stackFrame.EndLineNumber, + Column = (source != null) ? stackFrame.StartColumnNumber : 0, + EndColumn = stackFrame.EndColumnNumber, + Source = source + }; + } + + public static Scope CreateScope(VariableScope scope) + { + return new Scope + { + Name = scope.Name, + VariablesReference = scope.Id, + // Temporary fix for #95 to get debug hover tips to work well at least for the local scope. + Expensive = ((scope.Name != VariableContainerDetails.LocalScopeName) && + (scope.Name != VariableContainerDetails.AutoVariablesName)) + }; + } + + public static Variable CreateVariable(VariableDetailsBase variable) + { + return new Variable + { + Name = variable.Name, + Value = variable.ValueString ?? string.Empty, + Type = variable.Type, + EvaluateName = variable.Name, + VariablesReference = + variable.IsExpandable ? + variable.Id : 0 + }; + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs new file mode 100644 index 000000000..fcf10bee3 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Client; +using OmniSharp.Extensions.LanguageServer.Client.Processes; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace PowerShellEditorServices.Test.E2E +{ + public class LSPTestsFixture : TestsFixture + { + public override bool IsDebugAdapterTests => false; + + public LanguageClient LanguageClient { get; private set; } + public List Diagnostics { get; set; } + + public async override Task CustomInitializeAsync( + ILoggerFactory factory, + StdioServerProcess process) + { + LanguageClient = new LanguageClient(factory, process); + + DirectoryInfo testdir = + Directory.CreateDirectory(Path.Combine(s_binDir, Path.GetRandomFileName())); + + await LanguageClient.Initialize(testdir.FullName); + + // Make sure Script Analysis is enabled because we'll need it in the tests. + LanguageClient.Workspace.DidChangeConfiguration(JObject.Parse(@" +{ + ""PowerShell"": { + ""ScriptAnalysis"": { + ""Enable"": true + } + } +} +")); + + Diagnostics = new List(); + LanguageClient.TextDocument.OnPublishDiagnostics((uri, diagnostics) => + { + Diagnostics.AddRange(diagnostics); + }); + } + + public override async Task DisposeAsync() + { + await LanguageClient.Shutdown(); + await _psesProcess.Stop(); + LanguageClient?.Dispose(); + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 22f37a00b..8654832a6 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -21,7 +21,7 @@ namespace PowerShellEditorServices.Test.E2E { - public class LanguageServerProtocolMessageTests : IClassFixture, IDisposable + public class LanguageServerProtocolMessageTests : IClassFixture, IDisposable { private readonly static string s_binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -33,7 +33,7 @@ public class LanguageServerProtocolMessageTests : IClassFixture, I private readonly string PwshExe; private readonly ITestOutputHelper _output; - public LanguageServerProtocolMessageTests(ITestOutputHelper output, TestsFixture data) + public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixture data) { Diagnostics = new List(); LanguageClient = data.LanguageClient; diff --git a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj index 8b7c085b4..a2b3878f8 100644 --- a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj +++ b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj @@ -10,7 +10,7 @@ - + diff --git a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs index 092a8799a..7b51226f3 100644 --- a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs +++ b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs @@ -13,9 +13,9 @@ namespace PowerShellEditorServices.Test.E2E { - public class TestsFixture : IAsyncLifetime + public abstract class TestsFixture : IAsyncLifetime { - private readonly static string s_binDir = + protected readonly static string s_binDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); private readonly static string s_bundledModulePath = new FileInfo(Path.Combine( @@ -38,11 +38,11 @@ public class TestsFixture : IAsyncLifetime const string s_hostVersion = "1.0.0"; readonly static string[] s_additionalModules = { "PowerShellEditorServices.VSCode" }; - private StdioServerProcess _psesProcess; + protected 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 virtual bool IsDebugAdapterTests { get; set; } public async Task InitializeAsync() { @@ -56,7 +56,8 @@ public async Task InitializeAsync() processStartInfo.ArgumentList.Add("-NoProfile"); processStartInfo.ArgumentList.Add("-EncodedCommand"); - string[] args = { + List args = new List + { Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1"), "-LogPath", s_logPath, "-LogLevel", s_logLevel, @@ -70,6 +71,11 @@ public async Task InitializeAsync() "-Stdio" }; + if (IsDebugAdapterTests) + { + args.Add("-DebugServiceOnly"); + } + string base64Str = Convert.ToBase64String( System.Text.Encoding.Unicode.GetBytes(string.Join(' ', args))); @@ -78,36 +84,16 @@ public async Task InitializeAsync() _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); - - // Make sure Script Analysis is enabled because we'll need it in the tests. - LanguageClient.Workspace.DidChangeConfiguration(JObject.Parse(@" -{ - ""PowerShell"": { - ""ScriptAnalysis"": { - ""Enable"": true + await CustomInitializeAsync(factory, _psesProcess); } - } -} -")); - Diagnostics = new List(); - LanguageClient.TextDocument.OnPublishDiagnostics((uri, diagnostics) => - { - Diagnostics.AddRange(diagnostics); - }); - } - - public async Task DisposeAsync() + public virtual async Task DisposeAsync() { - await LanguageClient.Shutdown(); await _psesProcess.Stop(); - LanguageClient?.Dispose(); } + + public abstract Task CustomInitializeAsync( + ILoggerFactory factory, + StdioServerProcess process); } } From 998fc254ebb845e6596a58598540048a67460192 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 1 Oct 2019 11:49:39 -0700 Subject: [PATCH 41/47] Delete projects we wont be keeping around and get pses.vscode working again (#1046) * delete other folders and tweak build script for BuildInfo * working PowerShellEditorServices.VSCode now a binary module! * some typo * Apply suggestions from code review Co-Authored-By: Patrick Meinecke * address additional comments * don't checkin maml * add error handling * deleted buildinfo and address rob's comments --- .gitignore | 3 +- PowerShellEditorServices.build.ps1 | 19 +- PowerShellEditorServices.sln | 45 - .../PowerShellEditorServices.VSCode.psd1 | 14 +- .../PowerShellEditorServices.VSCode.psm1 | 17 - .../Close-VSCodeHtmlContentView.ps1 | 33 - .../New-VSCodeHtmlContentView.ps1 | 55 - .../Set-VSCodeHtmlContentView.ps1 | 69 - .../Show-VSCodeHtmlContentView.ps1 | 47 - .../Write-VSCodeHtmlContentView.ps1 | 46 - .../docs/Close-VSCodeHtmlContentView.md | 64 + .../docs/New-VSCodeHtmlContentView.md | 92 + .../docs/Set-VSCodeHtmlContentView.md | 123 + .../docs/Show-VSCodeHtmlContentView.md | 92 + .../docs/Write-VSCodeHtmlContentView.md | 87 + .../BuildInfo.cs | 14 - .../Extensions/EditorObject.cs | 5 + src/PowerShellEditorServices.Host/App.config | 6 - .../CodeLens/CodeLensExtensions.cs | 47 - .../CodeLens/CodeLensFeature.cs | 203 -- .../CodeLens/IScriptExtentExtensions.cs | 30 - .../CodeLens/PesterCodeLensProvider.cs | 113 - .../CodeLens/ReferencesCodeLensProvider.cs | 177 -- .../Commands/ClientCommandExtensions.cs | 31 - .../EditorServicesHost.cs | 563 ---- .../PSHost/PromptHandlers.cs | 185 -- .../PSHost/ProtocolPSHostUserInterface.cs | 123 - .../PowerShellEditorServices.Host.csproj | 22 - .../Symbols/DocumentSymbolFeature.cs | 115 - .../Client/DebugAdapterClientBase.cs | 97 - .../Client/LanguageClientBase.cs | 102 - .../Client/LanguageServiceClient.cs | 118 - .../DebugAdapter/AttachRequest.cs | 29 - .../DebugAdapter/Breakpoint.cs | 82 - .../DebugAdapter/BreakpointEvent.cs | 20 - .../DebugAdapter/ConfigurationDoneRequest.cs | 16 - .../DebugAdapter/ContinueRequest.cs | 17 - .../DebugAdapter/ContinuedEvent.cs | 21 - .../DebugAdapter/DisconnectRequest.cs | 17 - .../DebugAdapter/EvaluateRequest.cs | 53 - .../DebugAdapter/ExitedEvent.cs | 22 - .../DebugAdapter/InitializeRequest.cs | 70 - .../DebugAdapter/InitializedEvent.cs | 16 - .../DebugAdapter/LaunchRequest.cs | 73 - .../DebugAdapter/NextRequest.cs | 20 - .../DebugAdapter/OutputEvent.cs | 24 - .../DebugAdapter/PauseRequest.cs | 17 - .../DebugAdapter/Scope.cs | 39 - .../DebugAdapter/ScopesRequest.cs | 29 - .../DebugAdapter/SetBreakpointsRequest.cs | 45 - .../SetExceptionBreakpointsRequest.cs | 31 - .../SetFunctionBreakpointsRequest.cs | 33 - .../DebugAdapter/SetVariableRequest.cs | 36 - .../DebugAdapter/Source.cs | 44 - .../DebugAdapter/SourceRequest.cs | 29 - .../DebugAdapter/StackFrame.cs | 112 - .../DebugAdapter/StackTraceRequest.cs | 55 - .../DebugAdapter/StartedEvent.cs | 16 - .../DebugAdapter/StepInRequest.cs | 17 - .../DebugAdapter/StepOutRequest.cs | 16 - .../DebugAdapter/StoppedEvent.cs | 37 - .../DebugAdapter/TerminatedEvent.cs | 19 - .../DebugAdapter/Thread.cs | 15 - .../DebugAdapter/ThreadsRequest.cs | 22 - .../DebugAdapter/Variable.cs | 43 - .../DebugAdapter/VariablesRequest.cs | 29 - .../LanguageServer/ClientCapabilities.cs | 26 - .../LanguageServer/CodeAction.cs | 47 - .../LanguageServer/CodeLens.cs | 56 - .../LanguageServer/CommentHelpRequest.cs | 28 - .../LanguageServer/Completion.cs | 144 - .../LanguageServer/Configuration.cs | 21 - .../LanguageServer/Definition.cs | 17 - .../LanguageServer/Diagnostics.cs | 81 - .../LanguageServer/DocumentHighlight.cs | 31 - .../DynamicRegistrationCapability.cs | 13 - .../LanguageServer/EditorCommands.cs | 185 -- .../ExecutionStatusChangedEvent.cs | 17 - .../LanguageServer/ExpandAliasRequest.cs | 16 - .../LanguageServer/FindModuleRequest.cs | 24 - .../LanguageServer/Folding.cs | 69 - .../LanguageServer/Formatting.cs | 95 - .../LanguageServer/GetCommandRequest.cs | 34 - .../GetPSHostProcessesRequest.cs | 27 - .../LanguageServer/GetPSSARulesRequest.cs | 16 - .../LanguageServer/GetRunspaceRequest.cs | 25 - .../LanguageServer/Hover.cs | 37 - .../LanguageServer/Initialize.cs | 79 - .../LanguageServer/InitializedNotification.cs | 29 - .../LanguageServer/InstallModuleRequest.cs | 16 - .../PowerShellVersionRequest.cs | 52 - .../LanguageServer/ProjectTemplate.cs | 42 - .../LanguageServer/References.cs | 27 - .../LanguageServer/RunspaceChanged.cs | 37 - .../LanguageServer/ScriptRegionRequest.cs | 57 - .../LanguageServer/ServerCapabilities.cs | 121 - .../LanguageServer/ServerCommand.cs | 28 - .../LanguageServer/SetPSSARulesRequest.cs | 16 - .../LanguageServer/ShowHelpRequest.cs | 18 - .../LanguageServer/Shutdown.cs | 32 - .../LanguageServer/SignatureHelp.cs | 50 - .../LanguageServer/StartDebuggerEvent.cs | 16 - .../LanguageServer/TextDocument.cs | 313 -- .../TextDocumentClientCapabilities.cs | 130 - .../WorkspaceClientCapabilities.cs | 48 - .../LanguageServer/WorkspaceSymbols.cs | 73 - .../MessageProtocol/AbstractMessageType.cs | 35 - .../MessageProtocol/Channel/ChannelBase.cs | 68 - .../Channel/NamedPipeClientChannel.cs | 84 - .../Channel/NamedPipeServerChannel.cs | 57 - .../Channel/NamedPipeServerListener.cs | 205 -- .../Channel/ServerListenerBase.cs | 42 - .../Channel/StdioClientChannel.cs | 123 - .../Channel/StdioServerChannel.cs | 64 - .../Channel/StdioServerListener.cs | 36 - .../MessageProtocol/Constants.cs | 31 - .../MessageProtocol/EventContext.cs | 34 - .../MessageProtocol/EventType.cs | 30 - .../MessageProtocol/IMessageHandlers.cs | 25 - .../MessageProtocol/IMessageSender.cs | 25 - .../MessageProtocol/IMessageSerializer.cs | 30 - .../MessageProtocol/Message.cs | 136 - .../MessageProtocol/MessageDispatcher.cs | 181 -- .../MessageProtocol/MessageParseException.cs | 23 - .../MessageProtocol/MessageProtocolType.cs | 23 - .../MessageProtocol/MessageReader.cs | 284 -- .../MessageProtocol/MessageWriter.cs | 173 -- .../MessageProtocol/ProtocolEndpoint.cs | 407 --- .../MessageProtocol/RequestContext.cs | 47 - .../MessageProtocol/RequestType.cs | 35 - .../MessageProtocol/RequestType0.cs | 22 - .../Serializers/JsonRpcMessageSerializer.cs | 102 - .../Serializers/V8MessageSerializer.cs | 118 - .../Messages/PromptEvents.cs | 58 - .../PowerShellEditorServices.Protocol.csproj | 21 - .../Properties/AssemblyInfo.cs | 8 - .../Server/DebugAdapter.cs | 1159 -------- .../Server/IMessageDispatcher.cs | 17 - .../Server/LanguageServer.cs | 2089 -------------- .../Server/LanguageServerEditorOperations.cs | 224 -- .../Server/LanguageServerSettings.cs | 392 --- .../Server/OutputDebouncer.cs | 102 - .../Cmdlets/VSCodeHtmlContentViewCommands.cs | 238 ++ .../ComponentRegistration.cs | 41 - .../CustomViews/CustomViewBase.cs | 65 +- .../CustomViews/CustomViewFeature.cs | 12 +- .../CustomViews/CustomViewMessages.cs | 20 +- .../CustomViews/HtmlContentView.cs | 62 +- .../CustomViews/HtmlContentViewMessages.cs | 13 +- .../CustomViews/HtmlContentViewsFeature.cs | 11 +- .../CustomViews/ICustomView.cs | 2 - .../PowerShellEditorServices.VSCode.csproj | 7 +- .../Analysis/AnalysisOutputWriter.cs | 57 - .../Analysis/AnalysisService.cs | 708 ----- .../CodeLenses/CodeLens.cs | 127 - .../CodeLenses/ICodeLensProvider.cs | 46 - .../CodeLenses/ICodeLenses.cs | 34 - .../Commands/ClientCommand.cs | 46 - .../Components/ComponentRegistry.cs | 84 - .../Components/FeatureComponentBase.cs | 127 - .../Components/IComponentRegistry.cs | 61 - .../IComponentRegistryExtensions.cs | 87 - .../Console/ChoiceDetails.cs | 132 - .../Console/ChoicePromptHandler.cs | 354 --- .../Console/CollectionFieldDetails.cs | 138 - .../Console/ConsoleChoicePromptHandler.cs | 135 - .../Console/ConsoleInputPromptHandler.cs | 105 - .../Console/ConsoleProxy.cs | 196 -- .../Console/ConsoleReadLine.cs | 616 ---- .../Console/CredentialFieldDetails.cs | 122 - .../Console/FieldDetails.cs | 235 -- .../Console/IConsoleOperations.cs | 140 - .../Console/InputPromptHandler.cs | 331 --- .../Console/PromptHandler.cs | 55 - .../Console/TerminalChoicePromptHandler.cs | 63 - .../Console/TerminalInputPromptHandler.cs | 80 - .../Console/UnixConsoleOperations.cs | 297 -- .../Console/WindowsConsoleOperations.cs | 76 - .../Debugging/BreakpointDetails.cs | 108 - .../Debugging/BreakpointDetailsBase.cs | 36 - .../Debugging/CommandBreakpointDetails.cs | 73 - .../Debugging/DebugService.cs | 1359 --------- .../Debugging/DebuggerStoppedEventArgs.cs | 117 - .../InvalidPowerShellExpressionException.cs | 24 - .../Debugging/StackFrameDetails.cs | 135 - .../Debugging/VariableContainerDetails.cs | 99 - .../Debugging/VariableDetails.cs | 415 --- .../Debugging/VariableDetailsBase.cs | 59 - .../Debugging/VariableScope.cs | 37 - .../Extensions/EditorCommand.cs | 89 - .../Extensions/EditorCommandAttribute.cs | 33 - .../Extensions/EditorContext.cs | 117 - .../Extensions/EditorObject.cs | 112 - .../Extensions/EditorWindow.cs | 83 - .../Extensions/EditorWorkspace.cs | 76 - .../Extensions/ExtensionService.cs | 220 -- .../Extensions/FileContext.cs | 281 -- .../Extensions/IEditorOperations.cs | 128 - .../Language/AstOperations.cs | 343 --- .../Language/CommandHelpers.cs | 114 - .../Language/CompletionResults.cs | 340 --- .../Language/FindCommandVisitor.cs | 87 - .../Language/FindDeclarationVisitor.cs | 148 - .../Language/FindDotSourcedVisitor.cs | 90 - .../Language/FindOccurrencesResult.cs | 23 - .../Language/FindReferencesResult.cs | 33 - .../Language/FindReferencesVisitor.cs | 189 -- .../Language/FindSymbolVisitor.cs | 145 - .../Language/FindSymbolsVisitor.cs | 148 - .../Language/FindSymbolsVisitor2.cs | 80 - .../Language/FoldingReference.cs | 112 - .../Language/FullScriptExtent.cs | 179 -- .../Language/FullScriptPosition.cs | 57 - .../Language/GetDefinitionResult.cs | 30 - .../Language/LanguageService.cs | 887 ------ .../Language/ParameterSetSignatures.cs | 150 - .../Language/ScriptExtent.cs | 109 - .../Language/SymbolDetails.cs | 95 - .../Language/SymbolReference.cs | 89 - .../Language/SymbolType.cs | 48 - .../Language/TokenOperations.cs | 215 -- .../PowerShellEditorServices.csproj | 30 - .../Properties/AssemblyInfo.cs | 11 - .../Providers/FeatureProviderBase.cs | 19 - .../Providers/FeatureProviderCollection.cs | 46 - .../Providers/IFeatureProvider.cs | 19 - .../Providers/IFeatureProviderCollection.cs | 22 - .../Capabilities/DscBreakpointCapability.cs | 165 -- .../Session/EditorSession.cs | 192 -- .../Session/ExecutionOptions.cs | 92 - .../Session/ExecutionStatus.cs | 39 - .../ExecutionStatusChangedEventArgs.cs | 52 - .../Session/ExecutionTarget.cs | 28 - .../Session/Host/EditorServicesPSHost.cs | 372 --- .../Host/EditorServicesPSHostUserInterface.cs | 1070 ------- .../Session/Host/IHostInput.cs | 28 - .../Session/Host/IHostOutput.cs | 175 -- .../Host/SimplePSHostRawUserInterface.cs | 230 -- .../Host/TerminalPSHostRawUserInterface.cs | 331 --- .../Host/TerminalPSHostUserInterface.cs | 180 -- .../Session/HostDetails.cs | 92 - .../Session/IPromptContext.cs | 67 - .../Session/IRunspaceCapability.cs | 12 - .../Session/IVersionSpecificOperations.cs | 33 - .../Session/InvocationEventQueue.cs | 263 -- .../Session/LegacyReadLineContext.cs | 56 - .../Session/OutputType.cs | 41 - .../Session/OutputWrittenEventArgs.cs | 65 - .../Session/PSReadLinePromptContext.cs | 203 -- .../Session/PSReadLineProxy.cs | 119 - .../Session/PipelineExecutionRequest.cs | 80 - .../Session/PowerShell5Operations.cs | 107 - .../Session/PowerShellContext.cs | 2532 ----------------- .../Session/PowerShellContextState.cs | 47 - .../Session/PowerShellExecutionResult.cs | 40 - .../Session/PowerShellVersionDetails.cs | 167 -- .../Session/ProfilePaths.cs | 110 - .../Session/ProgressDetails.cs | 33 - .../Session/PromptNest.cs | 564 ---- .../Session/PromptNestFrame.cs | 137 - .../Session/PromptNestFrameType.cs | 21 - .../Session/RemoteFileManager.cs | 789 ----- .../Session/RunspaceChangedEventArgs.cs | 69 - .../Session/RunspaceDetails.cs | 321 --- .../Session/RunspaceHandle.cs | 60 - .../Session/SessionDetails.cs | 64 - .../Session/SessionStateChangedEventArgs.cs | 48 - .../Session/ThreadController.cs | 131 - .../Symbols/IDocumentSymbolProvider.cs | 24 - .../Symbols/IDocumentSymbols.cs | 31 - .../Symbols/PesterDocumentSymbolProvider.cs | 223 -- .../Symbols/PsdDocumentSymbolProvider.cs | 33 - .../Symbols/ScriptDocumentSymbolProvider.cs | 47 - .../Templates/TemplateDetails.cs | 43 - .../Templates/TemplateService.cs | 217 -- .../Utility/AsyncContext.cs | 53 - .../Utility/AsyncContextThread.cs | 86 - .../Utility/AsyncDebouncer.cs | 169 -- .../Utility/AsyncLock.cs | 128 - .../Utility/AsyncQueue.cs | 224 -- .../Utility/AsyncUtils.cs | 25 - .../Utility/ExecutionTimer.cs | 105 - .../Utility/Extensions.cs | 154 - .../Utility/Logging.cs | 288 -- .../Utility/ObjectPool.cs | 29 - .../Utility/PathUtils.cs | 49 - .../Utility/PsesLogger.cs | 221 -- .../Utility/ThreadSynchronizationContext.cs | 100 - src/PowerShellEditorServices/Utility/Utils.cs | 55 - .../Utility/Validate.cs | 143 - .../Workspace/BufferPosition.cs | 111 - .../Workspace/BufferRange.cs | 123 - .../Workspace/FileChange.cs | 45 - .../Workspace/FilePosition.cs | 109 - .../Workspace/ScriptFile.cs | 679 ----- .../Workspace/ScriptFileMarker.cs | 189 -- .../Workspace/ScriptRegion.cs | 112 - .../Workspace/Workspace.cs | 716 ----- .../Workspace/WorkspaceFileSystemWrapper.cs | 381 --- 299 files changed, 805 insertions(+), 38338 deletions(-) delete mode 100644 module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 delete mode 100644 module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Close-VSCodeHtmlContentView.ps1 delete mode 100644 module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 delete mode 100644 module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Set-VSCodeHtmlContentView.ps1 delete mode 100644 module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Show-VSCodeHtmlContentView.ps1 delete mode 100644 module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Write-VSCodeHtmlContentView.ps1 create mode 100644 module/PowerShellEditorServices.VSCode/docs/Close-VSCodeHtmlContentView.md create mode 100644 module/PowerShellEditorServices.VSCode/docs/New-VSCodeHtmlContentView.md create mode 100644 module/PowerShellEditorServices.VSCode/docs/Set-VSCodeHtmlContentView.md create mode 100644 module/PowerShellEditorServices.VSCode/docs/Show-VSCodeHtmlContentView.md create mode 100644 module/PowerShellEditorServices.VSCode/docs/Write-VSCodeHtmlContentView.md delete mode 100644 src/PowerShellEditorServices.Engine/BuildInfo.cs delete mode 100644 src/PowerShellEditorServices.Host/App.config delete mode 100644 src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs delete mode 100644 src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs delete mode 100644 src/PowerShellEditorServices.Host/CodeLens/IScriptExtentExtensions.cs delete mode 100644 src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs delete mode 100644 src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs delete mode 100644 src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs delete mode 100644 src/PowerShellEditorServices.Host/EditorServicesHost.cs delete mode 100644 src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs delete mode 100644 src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs delete mode 100644 src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj delete mode 100644 src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/BreakpointEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ExitedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/InitializedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/OutputEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/Scope.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/Source.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/StackFrame.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/StartedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/StoppedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/Thread.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs delete mode 100644 src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ClientCapabilities.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Configuration.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Diagnostics.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/DynamicRegistrationCapability.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ExecutionStatusChangedEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/FindModuleRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/GetCommandRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/GetPSHostProcessesRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/GetPSSARulesRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/InitializedNotification.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/InstallModuleRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ProjectTemplate.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/References.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ScriptRegionRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/SetPSSARulesRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/StartDebuggerEvent.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/TextDocumentClientCapabilities.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs delete mode 100644 src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/AbstractMessageType.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Constants.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageHandlers.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/MessageParseException.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType0.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs delete mode 100644 src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Messages/PromptEvents.cs delete mode 100644 src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj delete mode 100644 src/PowerShellEditorServices.Protocol/Properties/AssemblyInfo.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs delete mode 100644 src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs create mode 100644 src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs delete mode 100644 src/PowerShellEditorServices.VSCode/ComponentRegistration.cs delete mode 100644 src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs delete mode 100644 src/PowerShellEditorServices/Analysis/AnalysisService.cs delete mode 100644 src/PowerShellEditorServices/CodeLenses/CodeLens.cs delete mode 100644 src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs delete mode 100644 src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs delete mode 100644 src/PowerShellEditorServices/Commands/ClientCommand.cs delete mode 100644 src/PowerShellEditorServices/Components/ComponentRegistry.cs delete mode 100644 src/PowerShellEditorServices/Components/FeatureComponentBase.cs delete mode 100644 src/PowerShellEditorServices/Components/IComponentRegistry.cs delete mode 100644 src/PowerShellEditorServices/Components/IComponentRegistryExtensions.cs delete mode 100644 src/PowerShellEditorServices/Console/ChoiceDetails.cs delete mode 100644 src/PowerShellEditorServices/Console/ChoicePromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/CollectionFieldDetails.cs delete mode 100644 src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/ConsoleProxy.cs delete mode 100644 src/PowerShellEditorServices/Console/ConsoleReadLine.cs delete mode 100644 src/PowerShellEditorServices/Console/CredentialFieldDetails.cs delete mode 100644 src/PowerShellEditorServices/Console/FieldDetails.cs delete mode 100644 src/PowerShellEditorServices/Console/IConsoleOperations.cs delete mode 100644 src/PowerShellEditorServices/Console/InputPromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/PromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs delete mode 100644 src/PowerShellEditorServices/Console/UnixConsoleOperations.cs delete mode 100644 src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs delete mode 100644 src/PowerShellEditorServices/Debugging/BreakpointDetails.cs delete mode 100644 src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs delete mode 100644 src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs delete mode 100644 src/PowerShellEditorServices/Debugging/DebugService.cs delete mode 100644 src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs delete mode 100644 src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs delete mode 100644 src/PowerShellEditorServices/Debugging/StackFrameDetails.cs delete mode 100644 src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs delete mode 100644 src/PowerShellEditorServices/Debugging/VariableDetails.cs delete mode 100644 src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs delete mode 100644 src/PowerShellEditorServices/Debugging/VariableScope.cs delete mode 100644 src/PowerShellEditorServices/Extensions/EditorCommand.cs delete mode 100644 src/PowerShellEditorServices/Extensions/EditorCommandAttribute.cs delete mode 100644 src/PowerShellEditorServices/Extensions/EditorContext.cs delete mode 100644 src/PowerShellEditorServices/Extensions/EditorObject.cs delete mode 100644 src/PowerShellEditorServices/Extensions/EditorWindow.cs delete mode 100644 src/PowerShellEditorServices/Extensions/EditorWorkspace.cs delete mode 100644 src/PowerShellEditorServices/Extensions/ExtensionService.cs delete mode 100644 src/PowerShellEditorServices/Extensions/FileContext.cs delete mode 100644 src/PowerShellEditorServices/Extensions/IEditorOperations.cs delete mode 100644 src/PowerShellEditorServices/Language/AstOperations.cs delete mode 100644 src/PowerShellEditorServices/Language/CommandHelpers.cs delete mode 100644 src/PowerShellEditorServices/Language/CompletionResults.cs delete mode 100644 src/PowerShellEditorServices/Language/FindCommandVisitor.cs delete mode 100644 src/PowerShellEditorServices/Language/FindDeclarationVisitor.cs delete mode 100644 src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs delete mode 100644 src/PowerShellEditorServices/Language/FindOccurrencesResult.cs delete mode 100644 src/PowerShellEditorServices/Language/FindReferencesResult.cs delete mode 100644 src/PowerShellEditorServices/Language/FindReferencesVisitor.cs delete mode 100644 src/PowerShellEditorServices/Language/FindSymbolVisitor.cs delete mode 100644 src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs delete mode 100644 src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs delete mode 100644 src/PowerShellEditorServices/Language/FoldingReference.cs delete mode 100644 src/PowerShellEditorServices/Language/FullScriptExtent.cs delete mode 100644 src/PowerShellEditorServices/Language/FullScriptPosition.cs delete mode 100644 src/PowerShellEditorServices/Language/GetDefinitionResult.cs delete mode 100644 src/PowerShellEditorServices/Language/LanguageService.cs delete mode 100644 src/PowerShellEditorServices/Language/ParameterSetSignatures.cs delete mode 100644 src/PowerShellEditorServices/Language/ScriptExtent.cs delete mode 100644 src/PowerShellEditorServices/Language/SymbolDetails.cs delete mode 100644 src/PowerShellEditorServices/Language/SymbolReference.cs delete mode 100644 src/PowerShellEditorServices/Language/SymbolType.cs delete mode 100644 src/PowerShellEditorServices/Language/TokenOperations.cs delete mode 100644 src/PowerShellEditorServices/PowerShellEditorServices.csproj delete mode 100644 src/PowerShellEditorServices/Properties/AssemblyInfo.cs delete mode 100644 src/PowerShellEditorServices/Providers/FeatureProviderBase.cs delete mode 100644 src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs delete mode 100644 src/PowerShellEditorServices/Providers/IFeatureProvider.cs delete mode 100644 src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs delete mode 100644 src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs delete mode 100644 src/PowerShellEditorServices/Session/EditorSession.cs delete mode 100644 src/PowerShellEditorServices/Session/ExecutionOptions.cs delete mode 100644 src/PowerShellEditorServices/Session/ExecutionStatus.cs delete mode 100644 src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs delete mode 100644 src/PowerShellEditorServices/Session/ExecutionTarget.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/IHostInput.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/IHostOutput.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs delete mode 100644 src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs delete mode 100644 src/PowerShellEditorServices/Session/HostDetails.cs delete mode 100644 src/PowerShellEditorServices/Session/IPromptContext.cs delete mode 100644 src/PowerShellEditorServices/Session/IRunspaceCapability.cs delete mode 100644 src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs delete mode 100644 src/PowerShellEditorServices/Session/InvocationEventQueue.cs delete mode 100644 src/PowerShellEditorServices/Session/LegacyReadLineContext.cs delete mode 100644 src/PowerShellEditorServices/Session/OutputType.cs delete mode 100644 src/PowerShellEditorServices/Session/OutputWrittenEventArgs.cs delete mode 100644 src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs delete mode 100644 src/PowerShellEditorServices/Session/PSReadLineProxy.cs delete mode 100644 src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs delete mode 100644 src/PowerShellEditorServices/Session/PowerShell5Operations.cs delete mode 100644 src/PowerShellEditorServices/Session/PowerShellContext.cs delete mode 100644 src/PowerShellEditorServices/Session/PowerShellContextState.cs delete mode 100644 src/PowerShellEditorServices/Session/PowerShellExecutionResult.cs delete mode 100644 src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs delete mode 100644 src/PowerShellEditorServices/Session/ProfilePaths.cs delete mode 100644 src/PowerShellEditorServices/Session/ProgressDetails.cs delete mode 100644 src/PowerShellEditorServices/Session/PromptNest.cs delete mode 100644 src/PowerShellEditorServices/Session/PromptNestFrame.cs delete mode 100644 src/PowerShellEditorServices/Session/PromptNestFrameType.cs delete mode 100644 src/PowerShellEditorServices/Session/RemoteFileManager.cs delete mode 100644 src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs delete mode 100644 src/PowerShellEditorServices/Session/RunspaceDetails.cs delete mode 100644 src/PowerShellEditorServices/Session/RunspaceHandle.cs delete mode 100644 src/PowerShellEditorServices/Session/SessionDetails.cs delete mode 100644 src/PowerShellEditorServices/Session/SessionStateChangedEventArgs.cs delete mode 100644 src/PowerShellEditorServices/Session/ThreadController.cs delete mode 100644 src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs delete mode 100644 src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs delete mode 100644 src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs delete mode 100644 src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.cs delete mode 100644 src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs delete mode 100644 src/PowerShellEditorServices/Templates/TemplateDetails.cs delete mode 100644 src/PowerShellEditorServices/Templates/TemplateService.cs delete mode 100644 src/PowerShellEditorServices/Utility/AsyncContext.cs delete mode 100644 src/PowerShellEditorServices/Utility/AsyncContextThread.cs delete mode 100644 src/PowerShellEditorServices/Utility/AsyncDebouncer.cs delete mode 100644 src/PowerShellEditorServices/Utility/AsyncLock.cs delete mode 100644 src/PowerShellEditorServices/Utility/AsyncQueue.cs delete mode 100644 src/PowerShellEditorServices/Utility/AsyncUtils.cs delete mode 100644 src/PowerShellEditorServices/Utility/ExecutionTimer.cs delete mode 100644 src/PowerShellEditorServices/Utility/Extensions.cs delete mode 100644 src/PowerShellEditorServices/Utility/Logging.cs delete mode 100644 src/PowerShellEditorServices/Utility/ObjectPool.cs delete mode 100644 src/PowerShellEditorServices/Utility/PathUtils.cs delete mode 100644 src/PowerShellEditorServices/Utility/PsesLogger.cs delete mode 100644 src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs delete mode 100644 src/PowerShellEditorServices/Utility/Utils.cs delete mode 100644 src/PowerShellEditorServices/Utility/Validate.cs delete mode 100644 src/PowerShellEditorServices/Workspace/BufferPosition.cs delete mode 100644 src/PowerShellEditorServices/Workspace/BufferRange.cs delete mode 100644 src/PowerShellEditorServices/Workspace/FileChange.cs delete mode 100644 src/PowerShellEditorServices/Workspace/FilePosition.cs delete mode 100644 src/PowerShellEditorServices/Workspace/ScriptFile.cs delete mode 100644 src/PowerShellEditorServices/Workspace/ScriptFileMarker.cs delete mode 100644 src/PowerShellEditorServices/Workspace/ScriptRegion.cs delete mode 100644 src/PowerShellEditorServices/Workspace/Workspace.cs delete mode 100644 src/PowerShellEditorServices/Workspace/WorkspaceFileSystemWrapper.cs diff --git a/.gitignore b/.gitignore index 3567788f8..be64ce05c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ docs/metadata/ *.zip # Generated build info file -src/PowerShellEditorServices.Host/BuildInfo/BuildInfo.cs +src/PowerShellEditorServices.Engine/Hosting/BuildInfo.cs # quickbuild.exe /VersionGeneratingLogs/ @@ -67,6 +67,7 @@ PowerShellEditorServices.sln.ide/storage.ide # Don't include PlatyPS generated MAML module/PowerShellEditorServices/Commands/en-US/*-help.xml +module/PowerShellEditorServices.VSCode/en-US/*-help.xml # Don't include Third Party Notices in module folder module/PowerShellEditorServices/Third\ Party\ Notices.txt diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 0ac9ff998..9c4f77faa 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -27,7 +27,7 @@ $script:ModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices/bin/" $script:VSCodeModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices.VSCode/bin/" $script:WindowsPowerShellFrameworkTarget = 'net461' $script:NetFrameworkPlatformId = 'win' -$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices.Host", "BuildInfo", "BuildInfo.cs") +$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices.Engine", "Hosting", "BuildInfo.cs") $script:PSCoreModulePath = $null @@ -174,7 +174,7 @@ function Invoke-WithCreateDefaultHook { } } -task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestE2E, PackageNuGet { +task SetupDotNet -Before Clean, Build, TestHost, TestServer, TestProtocol, TestE2E { $dotnetPath = "$PSScriptRoot/.dotnet" $dotnetExePath = if ($script:IsUnix) { "$dotnetPath/dotnet" } else { "$dotnetPath/dotnet.exe" } @@ -253,7 +253,7 @@ task Clean { Get-ChildItem $PSScriptRoot\module\PowerShellEditorServices\Commands\en-US\*-help.xml | Remove-Item -Force -ErrorAction Ignore } -task GetProductVersion -Before PackageNuGet, PackageModule, UploadArtifacts { +task GetProductVersion -Before PackageModule, UploadArtifacts { [xml]$props = Get-Content .\PowerShellEditorServices.Common.props $script:BuildNumber = 9999 @@ -304,7 +304,7 @@ task CreateBuildInfo -Before Build { [string]$buildTime = [datetime]::Now.ToString("s", [System.Globalization.CultureInfo]::InvariantCulture) $buildInfoContents = @" -namespace Microsoft.PowerShell.EditorServices.Host +namespace Microsoft.PowerShell.EditorServices.Engine.Hosting { public static class BuildInfo { @@ -319,9 +319,7 @@ namespace Microsoft.PowerShell.EditorServices.Host } task Build { - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f $script:TargetPlatform } exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Engine\PowerShellEditorServices.Engine.csproj -f $script:TargetPlatform } - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj -f $script:TargetPlatform } exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } } @@ -478,12 +476,7 @@ task RestorePsesModules -After Build { task BuildCmdletHelp { New-ExternalHelp -Path $PSScriptRoot\module\docs -OutputPath $PSScriptRoot\module\PowerShellEditorServices\Commands\en-US -Force -} - -task PackageNuGet { - exec { & $script:dotnetExe pack -c $Configuration --version-suffix $script:VersionSuffix .\src\PowerShellEditorServices\PowerShellEditorServices.csproj $script:TargetFrameworksParam } - exec { & $script:dotnetExe pack -c $Configuration --version-suffix $script:VersionSuffix .\src\PowerShellEditorServices.Protocol\PowerShellEditorServices.Protocol.csproj $script:TargetFrameworksParam } - exec { & $script:dotnetExe pack -c $Configuration --version-suffix $script:VersionSuffix .\src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj $script:TargetFrameworksParam } + New-ExternalHelp -Path $PSScriptRoot\module\PowerShellEditorServices.VSCode\docs -OutputPath $PSScriptRoot\module\PowerShellEditorServices.VSCode\en-US -Force } task PackageModule { @@ -499,4 +492,4 @@ task UploadArtifacts -If ($null -ne $env:TF_BUILD) { } # The default task is to run the entire CI build -task . GetProductVersion, Clean, Build, Test, BuildCmdletHelp, PackageNuGet, PackageModule, UploadArtifacts +task . GetProductVersion, Clean, Build, Test, BuildCmdletHelp, PackageModule, UploadArtifacts diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index 71e28ed0d..91156418d 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -7,10 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F594E7FD-1E7 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{422E561A-8118-4BE7-A54F-9309E4F03AAE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{81E8CBCD-6319-49E7-9662-0475BD0791F4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Host", "src\PowerShellEditorServices.Host\PowerShellEditorServices.Host.csproj", "{B2F6369A-D737-4AFD-8B81-9B094DB07DA7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Test.Host", "test\PowerShellEditorServices.Test.Host\PowerShellEditorServices.Test.Host.csproj", "{3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Test", "test\PowerShellEditorServices.Test\PowerShellEditorServices.Test.csproj", "{8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}" @@ -22,8 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{E231 scripts\AddCopyrightHeaders.ps1 = scripts\AddCopyrightHeaders.ps1 EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Protocol", "src\PowerShellEditorServices.Protocol\PowerShellEditorServices.Protocol.csproj", "{F8A0946A-5D25-4651-8079-B8D5776916FB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Test.Protocol", "test\PowerShellEditorServices.Test.Protocol\PowerShellEditorServices.Test.Protocol.csproj", "{E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.VSCode", "src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj", "{3B38E8DA-8BFF-4264-AF16-47929E6398A3}" @@ -42,30 +36,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|x64.ActiveCfg = Debug|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|x64.Build.0 = Debug|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|x86.ActiveCfg = Debug|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Debug|x86.Build.0 = Debug|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|Any CPU.Build.0 = Release|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|x64.ActiveCfg = Release|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|x64.Build.0 = Release|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|x86.ActiveCfg = Release|Any CPU - {81E8CBCD-6319-49E7-9662-0475BD0791F4}.Release|x86.Build.0 = Release|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|x64.Build.0 = Debug|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Debug|x86.Build.0 = Debug|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|Any CPU.Build.0 = Release|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|x64.ActiveCfg = Release|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|x64.Build.0 = Release|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|x86.ActiveCfg = Release|Any CPU - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7}.Release|x86.Build.0 = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -102,18 +72,6 @@ Global {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x64.Build.0 = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x86.ActiveCfg = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x86.Build.0 = Release|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|x64.Build.0 = Debug|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|x86.ActiveCfg = Debug|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Debug|x86.Build.0 = Debug|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|Any CPU.Build.0 = Release|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|x64.ActiveCfg = Release|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|x64.Build.0 = Release|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|x86.ActiveCfg = Release|Any CPU - {F8A0946A-5D25-4651-8079-B8D5776916FB}.Release|x86.Build.0 = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -167,12 +125,9 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {81E8CBCD-6319-49E7-9662-0475BD0791F4} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} - {B2F6369A-D737-4AFD-8B81-9B094DB07DA7} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} - {F8A0946A-5D25-4651-8079-B8D5776916FB} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {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} diff --git a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psd1 b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psd1 index ddb9aa389..09ff913ff 100644 --- a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psd1 +++ b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psd1 @@ -9,7 +9,7 @@ @{ # Script module or binary module file associated with this manifest. -RootModule = 'PowerShellEditorServices.VSCode.psm1' +RootModule = "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.VSCode.dll" # Version number of this module. ModuleVersion = '0.2.0' @@ -69,14 +69,14 @@ Description = 'Provides added functionality to PowerShell Editor Services for th # NestedModules = @() # 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 = @('New-VSCodeHtmlContentView', - 'Show-VSCodeHtmlContentView', - 'Close-VSCodeHtmlContentView', - 'Set-VSCodeHtmlContentView', - 'Write-VSCodeHtmlContentView') +FunctionsToExport = @() # 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 = @() +CmdletsToExport = @('New-VSCodeHtmlContentView', + 'Show-VSCodeHtmlContentView', + 'Close-VSCodeHtmlContentView', + 'Set-VSCodeHtmlContentView', + 'Write-VSCodeHtmlContentView') # Variables to export from this module VariablesToExport = '*' diff --git a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 b/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 deleted file mode 100644 index 12b205c98..000000000 --- a/module/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.psm1 +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.VSCode.dll" - -if ($psEditor -is [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject]) { - [Microsoft.PowerShell.EditorServices.VSCode.ComponentRegistration]::Register($psEditor.Components) -} -else { - Write-Verbose '$psEditor object not found in the session, components will not be registered.' -} - -Microsoft.PowerShell.Management\Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse | ForEach-Object { - . $PSItem.FullName -} diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Close-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Close-VSCodeHtmlContentView.ps1 deleted file mode 100644 index f8cf0c9c6..000000000 --- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Close-VSCodeHtmlContentView.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -function Close-VSCodeHtmlContentView { - <# - .SYNOPSIS - Closes an HtmlContentView. - - .DESCRIPTION - Closes an HtmlContentView inside of Visual Studio Code if - it is displayed. - - .PARAMETER HtmlContentView - The HtmlContentView to be closed. - - .EXAMPLE - Close-VSCodeHtmlContentView -HtmlContentView $htmlContentView - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Alias("View")] - [ValidateNotNull()] - [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentView] - $HtmlContentView - ) - - process { - $HtmlContentView.Close().Wait(); - } -} diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 deleted file mode 100644 index d76c69fd4..000000000 --- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/New-VSCodeHtmlContentView.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -function New-VSCodeHtmlContentView { - <# - .SYNOPSIS - Creates a custom view in Visual Studio Code which displays HTML content. - - .DESCRIPTION - Creates a custom view in Visual Studio Code which displays HTML content. - - .PARAMETER Title - The title of the view. - - .PARAMETER ShowInColumn - If specified, causes the new view to be displayed in the specified column. - If unspecified, the Show-VSCodeHtmlContentView cmdlet will need to be used - to display the view. - - .EXAMPLE - # Create a new view called "My Custom View" - $htmlContentView = New-VSCodeHtmlContentView -Title "My Custom View" - - .EXAMPLE - # Create a new view and show it in the second view column - $htmlContentView = New-VSCodeHtmlContentView -Title "My Custom View" -ShowInColumn Two - #> - [CmdletBinding()] - [OutputType([Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentView])] - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] - $Title, - - [Parameter(Mandatory = $false)] - [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.ViewColumn] - $ShowInColumn - ) - - process { - if ($psEditor -is [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject]) { - $viewFeature = $psEditor.Components.Get([Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentViews]) - $view = $viewFeature.CreateHtmlContentViewAsync($Title).Result - - if ($ShowInColumn) { - $view.Show($ShowInColumn).Wait(); - } - - return $view - } - } -} diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Set-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Set-VSCodeHtmlContentView.ps1 deleted file mode 100644 index 98abf16a4..000000000 --- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Set-VSCodeHtmlContentView.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -function Set-VSCodeHtmlContentView { - <# - .SYNOPSIS - Sets the content of an HtmlContentView. - - .DESCRIPTION - Sets the content of an HtmlContentView. If an empty string - is passed, it causes the view's content to be cleared. - - .PARAMETER HtmlContentView - The HtmlContentView where content will be set. - - .PARAMETER HtmlBodyContent - The HTML content that will be placed inside the tag - of the view. - - .PARAMETER JavaScriptPaths - An array of paths to JavaScript files that will be loaded - into the view. - - .PARAMETER StyleSheetPaths - An array of paths to stylesheet (CSS) files that will be - loaded into the view. - - .EXAMPLE - # Set the view content with an h1 header - Set-VSCodeHtmlContentView -HtmlContentView $htmlContentView -HtmlBodyContent "

Hello world!

" - - .EXAMPLE - # Clear the view - Set-VSCodeHtmlContentView -View $htmlContentView -Content "" - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Alias("View")] - [ValidateNotNull()] - [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentView] - $HtmlContentView, - - [Parameter(Mandatory = $true)] - [Alias("Content")] - [AllowEmptyString()] - [string] - $HtmlBodyContent, - - [Parameter(Mandatory = $false)] - [string[]] - $JavaScriptPaths, - - [Parameter(Mandatory = $false)] - [string[]] - $StyleSheetPaths - ) - - process { - $htmlContent = New-Object Microsoft.PowerShell.EditorServices.VSCode.CustomViews.HtmlContent - $htmlContent.BodyContent = $HtmlBodyContent - $htmlContent.JavaScriptPaths = $JavaScriptPaths - $htmlContent.StyleSheetPaths = $StyleSheetPaths - - $HtmlContentView.SetContentAsync($htmlContent).Wait(); - } -} diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Show-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Show-VSCodeHtmlContentView.ps1 deleted file mode 100644 index 1be803471..000000000 --- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Show-VSCodeHtmlContentView.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -function Show-VSCodeHtmlContentView { - <# - .SYNOPSIS - Shows an HtmlContentView. - - .DESCRIPTION - Shows an HtmlContentView that has been created and not shown - yet or has previously been closed. - - .PARAMETER HtmlContentView - The HtmlContentView that will be shown. - - .PARAMETER ViewColumn - If specified, causes the new view to be displayed in the specified column. - - .EXAMPLE - # Shows the view in the first editor column - Show-VSCodeHtmlContentView -HtmlContentView $htmlContentView - - .EXAMPLE - # Shows the view in the third editor column - Show-VSCodeHtmlContentView -View $htmlContentView -Column Three - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Alias("View")] - [ValidateNotNull()] - [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentView] - $HtmlContentView, - - [Parameter(Mandatory = $false)] - [Alias("Column")] - [ValidateNotNull()] - [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.ViewColumn] - $ViewColumn = [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.ViewColumn]::One - ) - - process { - $HtmlContentView.Show($ViewColumn).Wait() - } -} diff --git a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Write-VSCodeHtmlContentView.ps1 b/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Write-VSCodeHtmlContentView.ps1 deleted file mode 100644 index c21321ec9..000000000 --- a/module/PowerShellEditorServices.VSCode/Public/HtmlContentView/Write-VSCodeHtmlContentView.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -function Write-VSCodeHtmlContentView { - <# - .SYNOPSIS - Writes an HTML fragment to an HtmlContentView. - - .DESCRIPTION - Writes an HTML fragment to an HtmlContentView. This new fragment - is appended to the existing content, useful in cases where the - output will be appended to an ongoing output stream. - - .PARAMETER HtmlContentView - The HtmlContentView where content will be appended. - - .PARAMETER AppendedHtmlBodyContent - The HTML content that will be appended to the view's element content. - - .EXAMPLE - Write-VSCodeHtmlContentView -HtmlContentView $htmlContentView -AppendedHtmlBodyContent "

Appended content

" - - .EXAMPLE - Write-VSCodeHtmlContentView -View $htmlContentView -Content "

Appended content

" - #> - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Alias("View")] - [ValidateNotNull()] - [Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentView] - $HtmlContentView, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [Alias("Content")] - [ValidateNotNull()] - [string] - $AppendedHtmlBodyContent - ) - - process { - $HtmlContentView.AppendContentAsync($AppendedHtmlBodyContent).Wait(); - } -} diff --git a/module/PowerShellEditorServices.VSCode/docs/Close-VSCodeHtmlContentView.md b/module/PowerShellEditorServices.VSCode/docs/Close-VSCodeHtmlContentView.md new file mode 100644 index 000000000..559d4b9d1 --- /dev/null +++ b/module/PowerShellEditorServices.VSCode/docs/Close-VSCodeHtmlContentView.md @@ -0,0 +1,64 @@ +--- +external help file: Microsoft.PowerShell.EditorServices.VSCode.dll-Help.xml +Module Name: PowerShellEditorServices.VSCode +online version: +schema: 2.0.0 +--- + +# Close-VSCodeHtmlContentView + +## SYNOPSIS + +Closes an HtmlContentView. + +## SYNTAX + +``` +Close-VSCodeHtmlContentView [-HtmlContentView] [] +``` + +## DESCRIPTION + +Closes an HtmlContentView inside of Visual Studio Code if it is displayed. + +## EXAMPLES + +### Example 1 + +```powershell +Close-VSCodeHtmlContentView -HtmlContentView $view +``` + +## PARAMETERS + +### -HtmlContentView + +The HtmlContentView to be closed. + +```yaml +Type: IHtmlContentView +Parameter Sets: (All) +Aliases: View + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + +## NOTES + +## RELATED LINKS diff --git a/module/PowerShellEditorServices.VSCode/docs/New-VSCodeHtmlContentView.md b/module/PowerShellEditorServices.VSCode/docs/New-VSCodeHtmlContentView.md new file mode 100644 index 000000000..ec837ddce --- /dev/null +++ b/module/PowerShellEditorServices.VSCode/docs/New-VSCodeHtmlContentView.md @@ -0,0 +1,92 @@ +--- +external help file: Microsoft.PowerShell.EditorServices.VSCode.dll-Help.xml +Module Name: PowerShellEditorServices.VSCode +online version: +schema: 2.0.0 +--- + +# New-VSCodeHtmlContentView + +## SYNOPSIS + +Creates a custom view in Visual Studio Code which displays HTML content. + +## SYNTAX + +``` +New-VSCodeHtmlContentView [-Title] [[-ShowInColumn] ] [] +``` + +## DESCRIPTION + +Creates a custom view in Visual Studio Code which displays HTML content. + +## EXAMPLES + +### Example 1 + +```powershell +$htmlContentView = New-VSCodeHtmlContentView -Title "My Custom View" +``` + +Create a new view called "My Custom View". + +### Example 2 + +```powershell +$htmlContentView = New-VSCodeHtmlContentView -Title "My Custom View" -ShowInColumn Two +``` + +Create a new view and show it in the second view column. + +## PARAMETERS + +### -ShowInColumn + +If specified, causes the new view to be displayed in the specified column. +If unspecified, the Show-VSCodeHtmlContentView cmdlet will need to be used to display the view. + +```yaml +Type: ViewColumn +Parameter Sets: (All) +Aliases: +Accepted values: One, Two, Three + +Required: False +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Title + +The title of the view. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### Microsoft.PowerShell.EditorServices.VSCode.CustomViews.IHtmlContentView + +## NOTES + +## RELATED LINKS diff --git a/module/PowerShellEditorServices.VSCode/docs/Set-VSCodeHtmlContentView.md b/module/PowerShellEditorServices.VSCode/docs/Set-VSCodeHtmlContentView.md new file mode 100644 index 000000000..830ff42ac --- /dev/null +++ b/module/PowerShellEditorServices.VSCode/docs/Set-VSCodeHtmlContentView.md @@ -0,0 +1,123 @@ +--- +external help file: Microsoft.PowerShell.EditorServices.VSCode.dll-Help.xml +Module Name: PowerShellEditorServices.VSCode +online version: +schema: 2.0.0 +--- + +# Set-VSCodeHtmlContentView + +## SYNOPSIS + +Sets the content of an HtmlContentView. + +## SYNTAX + +``` +Set-VSCodeHtmlContentView [-HtmlContentView] [-HtmlBodyContent] + [[-JavaScriptPaths] ] [[-StyleSheetPaths] ] [] +``` + +## DESCRIPTION + +Sets the content of an HtmlContentView. If an empty string is passed, it causes the view's content to be cleared. + +## EXAMPLES + +### Example 1 + +```powershell +Set-VSCodeHtmlContentView -HtmlContentView $htmlContentView -HtmlBodyContent "

Hello world!

" +``` + +Set the view content with an h1 header. + +### Example 2 + +```powershell +Set-VSCodeHtmlContentView -View $htmlContentView -Content "" +``` + +Clear the view. + +## PARAMETERS + +### -HtmlBodyContent + +The HTML content that will be placed inside the `` tag of the view. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: Content + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -HtmlContentView + +The HtmlContentView where content will be set. + +```yaml +Type: IHtmlContentView +Parameter Sets: (All) +Aliases: View + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -JavaScriptPaths + +An array of paths to JavaScript files that will be loaded into the view. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -StyleSheetPaths + +An array of paths to stylesheet (CSS) files that will be loaded into the view. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + +## NOTES + +## RELATED LINKS diff --git a/module/PowerShellEditorServices.VSCode/docs/Show-VSCodeHtmlContentView.md b/module/PowerShellEditorServices.VSCode/docs/Show-VSCodeHtmlContentView.md new file mode 100644 index 000000000..2659a3ead --- /dev/null +++ b/module/PowerShellEditorServices.VSCode/docs/Show-VSCodeHtmlContentView.md @@ -0,0 +1,92 @@ +--- +external help file: Microsoft.PowerShell.EditorServices.VSCode.dll-Help.xml +Module Name: PowerShellEditorServices.VSCode +online version: +schema: 2.0.0 +--- + +# Show-VSCodeHtmlContentView + +## SYNOPSIS + +Shows an HtmlContentView. + +## SYNTAX + +``` +Show-VSCodeHtmlContentView [-HtmlContentView] [[-ViewColumn] ] + [] +``` + +## DESCRIPTION + +Shows an HtmlContentView that has been created and not shown yet or has previously been closed. + +## EXAMPLES + +### Example 1 + +```powershell +Show-VSCodeHtmlContentView -HtmlContentView $htmlContentView +``` + +Shows the view in the first editor column. + +### Example 2 + +```powershell +Show-VSCodeHtmlContentView -View $htmlContentView -Column Three +``` + +Shows the view in the third editor column. + +## PARAMETERS + +### -HtmlContentView + +The HtmlContentView that will be shown. + +```yaml +Type: IHtmlContentView +Parameter Sets: (All) +Aliases: View + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ViewColumn + +If specified, causes the new view to be displayed in the specified column. + +```yaml +Type: ViewColumn +Parameter Sets: (All) +Aliases: Column +Accepted values: One, Two, Three + +Required: False +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### System.Object + +## NOTES + +## RELATED LINKS diff --git a/module/PowerShellEditorServices.VSCode/docs/Write-VSCodeHtmlContentView.md b/module/PowerShellEditorServices.VSCode/docs/Write-VSCodeHtmlContentView.md new file mode 100644 index 000000000..79c930da4 --- /dev/null +++ b/module/PowerShellEditorServices.VSCode/docs/Write-VSCodeHtmlContentView.md @@ -0,0 +1,87 @@ +--- +external help file: Microsoft.PowerShell.EditorServices.VSCode.dll-Help.xml +Module Name: PowerShellEditorServices.VSCode +online version: +schema: 2.0.0 +--- + +# Write-VSCodeHtmlContentView + +## SYNOPSIS + +Writes an HTML fragment to an HtmlContentView. + +## SYNTAX + +``` +Write-VSCodeHtmlContentView [-HtmlContentView] [-AppendedHtmlBodyContent] + [] +``` + +## DESCRIPTION + +Writes an HTML fragment to an HtmlContentView. This new fragment is appended to the existing content, useful in cases where the output will be appended to an ongoing output stream. + +## EXAMPLES + +### Example 1 + +```powershell +Write-VSCodeHtmlContentView -HtmlContentView $htmlContentView -AppendedHtmlBodyContent "

Appended content

" +``` + +### Example 2 + +```powershell +Write-VSCodeHtmlContentView -View $htmlContentView -Content "

Appended content

" +``` + +## PARAMETERS + +### -AppendedHtmlBodyContent + +The HTML content that will be appended to the view's `` element content. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: Content + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -HtmlContentView + +The HtmlContentView where content will be appended. + +```yaml +Type: IHtmlContentView +Parameter Sets: (All) +Aliases: View + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +## OUTPUTS + +### System.Object + +## NOTES + +## RELATED LINKS diff --git a/src/PowerShellEditorServices.Engine/BuildInfo.cs b/src/PowerShellEditorServices.Engine/BuildInfo.cs deleted file mode 100644 index 47c020fbf..000000000 --- a/src/PowerShellEditorServices.Engine/BuildInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -// -// 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.Engine -{ - public static class BuildInfo - { - public const string BuildVersion = ""; - public const string BuildOrigin = ""; - public static readonly System.DateTime? BuildTime; - } -} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs index a9643104f..3522debb4 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs @@ -42,6 +42,11 @@ public Version EditorServicesVersion /// public EditorWindow Window { get; private set; } + /// + /// Gets the components that are registered. + /// + public IServiceProvider Components => _serviceProvider; + #endregion /// diff --git a/src/PowerShellEditorServices.Host/App.config b/src/PowerShellEditorServices.Host/App.config deleted file mode 100644 index 066f94c5f..000000000 --- a/src/PowerShellEditorServices.Host/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs deleted file mode 100644 index c3be72bfe..000000000 --- a/src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs +++ /dev/null @@ -1,47 +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.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.CodeLenses; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using LanguageServer = Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; - -namespace Microsoft.PowerShell.EditorServices -{ - public static class ICodeLensExtensions - { - public static LanguageServer.CodeLens ToProtocolCodeLens( - this CodeLens codeLens, - JsonSerializer jsonSerializer) - { - return new LanguageServer.CodeLens - { - Range = codeLens.ScriptExtent.ToRange(), - Command = codeLens.Command.ToProtocolCommand(jsonSerializer) - }; - } - - public static LanguageServer.CodeLens ToProtocolCodeLens( - this CodeLens codeLens, - object codeLensData, - JsonSerializer jsonSerializer) - { - LanguageServer.ServerCommand command = null; - - if (codeLens.Command != null) - { - command = codeLens.Command.ToProtocolCommand(jsonSerializer); - } - - return new LanguageServer.CodeLens - { - Range = codeLens.ScriptExtent.ToRange(), - Data = JToken.FromObject(codeLensData, jsonSerializer), - Command = command - }; - } - } -} diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs deleted file mode 100644 index d90d77333..000000000 --- a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs +++ /dev/null @@ -1,203 +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 Microsoft.PowerShell.EditorServices.Components; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using LanguageServer = Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; - -namespace Microsoft.PowerShell.EditorServices.CodeLenses -{ - /// - /// Implements the CodeLens feature for EditorServices. - /// - internal class CodeLensFeature : - FeatureComponentBase, - ICodeLenses - { - - /// - /// Create a new CodeLens instance around a given editor session - /// from the component registry. - /// - /// - /// The component registry to provider other components and to register the CodeLens provider in. - /// - /// The editor session context of the CodeLens provider. - /// A new CodeLens provider for the given editor session. - public static CodeLensFeature Create( - IComponentRegistry components, - EditorSession editorSession) - { - var codeLenses = - new CodeLensFeature( - editorSession, - JsonSerializer.Create(Constants.JsonSerializerSettings), - components.Get()); - - var messageHandlers = components.Get(); - - messageHandlers.SetRequestHandler( - CodeLensRequest.Type, - codeLenses.HandleCodeLensRequestAsync); - - messageHandlers.SetRequestHandler( - CodeLensResolveRequest.Type, - codeLenses.HandleCodeLensResolveRequestAsync); - - codeLenses.Providers.Add( - new ReferencesCodeLensProvider( - editorSession)); - - codeLenses.Providers.Add( - new PesterCodeLensProvider( - editorSession)); - - editorSession.Components.Register(codeLenses); - - return codeLenses; - } - - /// - /// The editor session context to get workspace and language server data from. - /// - private readonly EditorSession _editorSession; - - /// - /// The json serializer instance for CodeLens object translation. - /// - private readonly JsonSerializer _jsonSerializer; - - /// - /// - /// - /// - /// - /// - private CodeLensFeature( - EditorSession editorSession, - JsonSerializer jsonSerializer, - ILogger logger) - : base(logger) - { - _editorSession = editorSession; - _jsonSerializer = jsonSerializer; - } - - /// - /// Get all the CodeLenses for a given script file. - /// - /// The PowerShell script file to get CodeLenses for. - /// All generated CodeLenses for the given script file. - public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) - { - return InvokeProviders(provider => provider.ProvideCodeLenses(scriptFile)) - .SelectMany(codeLens => codeLens) - .ToArray(); - } - - /// - /// Handles a request for CodeLenses from VSCode. - /// - /// Parameters on the CodeLens request that was received. - /// - private async Task HandleCodeLensRequestAsync( - CodeLensRequest codeLensParams, - RequestContext requestContext) - { - ScriptFile scriptFile = _editorSession.Workspace.GetFile( - codeLensParams.TextDocument.Uri); - - CodeLens[] codeLensResults = ProvideCodeLenses(scriptFile); - - var codeLensResponse = new LanguageServer.CodeLens[codeLensResults.Length]; - for (int i = 0; i < codeLensResults.Length; i++) - { - codeLensResponse[i] = codeLensResults[i].ToProtocolCodeLens( - new CodeLensData - { - Uri = codeLensResults[i].File.DocumentUri, - ProviderId = codeLensResults[i].Provider.ProviderId - }, - _jsonSerializer); - } - - await requestContext.SendResultAsync(codeLensResponse); - } - - /// - /// Handle a CodeLens resolve request from VSCode. - /// - /// The CodeLens to be resolved/updated. - /// - private async Task HandleCodeLensResolveRequestAsync( - LanguageServer.CodeLens codeLens, - RequestContext requestContext) - { - if (codeLens.Data != null) - { - // TODO: Catch deserializtion exception on bad object - CodeLensData codeLensData = codeLens.Data.ToObject(); - - ICodeLensProvider originalProvider = - Providers.FirstOrDefault( - provider => provider.ProviderId.Equals(codeLensData.ProviderId)); - - if (originalProvider != null) - { - ScriptFile scriptFile = - _editorSession.Workspace.GetFile( - codeLensData.Uri); - - ScriptRegion region = new ScriptRegion - { - StartLineNumber = codeLens.Range.Start.Line + 1, - StartColumnNumber = codeLens.Range.Start.Character + 1, - EndLineNumber = codeLens.Range.End.Line + 1, - EndColumnNumber = codeLens.Range.End.Character + 1 - }; - - CodeLens originalCodeLens = - new CodeLens( - originalProvider, - scriptFile, - region); - - var resolvedCodeLens = - await originalProvider.ResolveCodeLensAsync( - originalCodeLens, - CancellationToken.None); - - await requestContext.SendResultAsync( - resolvedCodeLens.ToProtocolCodeLens( - _jsonSerializer)); - } - else - { - await requestContext.SendErrorAsync( - $"Could not find provider for the original CodeLens: {codeLensData.ProviderId}"); - } - } - } - - /// - /// Represents data expected back in an LSP CodeLens response. - /// - private class CodeLensData - { - public string Uri { get; set; } - - public string ProviderId {get; set; } - } - } -} diff --git a/src/PowerShellEditorServices.Host/CodeLens/IScriptExtentExtensions.cs b/src/PowerShellEditorServices.Host/CodeLens/IScriptExtentExtensions.cs deleted file mode 100644 index ca8f49d54..000000000 --- a/src/PowerShellEditorServices.Host/CodeLens/IScriptExtentExtensions.cs +++ /dev/null @@ -1,30 +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.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; - -namespace Microsoft.PowerShell.EditorServices -{ - public static class IScriptExtentExtensions - { - public static Range ToRange(this IScriptExtent scriptExtent) - { - return new Range - { - Start = new Position - { - Line = scriptExtent.StartLineNumber - 1, - Character = scriptExtent.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptExtent.EndLineNumber - 1, - Character = scriptExtent.EndColumnNumber - 1 - } - }; - } - } -} diff --git a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs deleted file mode 100644 index cc4a706b0..000000000 --- a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs +++ /dev/null @@ -1,113 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Commands; -using Microsoft.PowerShell.EditorServices.Symbols; - -namespace Microsoft.PowerShell.EditorServices.CodeLenses -{ - internal class PesterCodeLensProvider : FeatureProviderBase, ICodeLensProvider - { - /// - /// The editor session context to provide CodeLenses for. - /// - private EditorSession _editorSession; - - /// - /// The symbol provider to get symbols from to build code lenses with. - /// - private IDocumentSymbolProvider _symbolProvider; - - /// - /// Create a new Pester CodeLens provider for a given editor session. - /// - /// The editor session context for which to provide Pester CodeLenses. - public PesterCodeLensProvider(EditorSession editorSession) - { - _editorSession = editorSession; - _symbolProvider = new PesterDocumentSymbolProvider(); - } - - /// - /// Get the Pester CodeLenses for a given Pester symbol. - /// - /// The Pester symbol to get CodeLenses for. - /// The script file the Pester symbol comes from. - /// All CodeLenses for the given Pester symbol. - private CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile scriptFile) - { - var codeLensResults = new CodeLens[] - { - new CodeLens( - this, - scriptFile, - pesterSymbol.ScriptRegion, - new ClientCommand( - "PowerShell.RunPesterTests", - "Run tests", - new object[] { - scriptFile.DocumentUri, - false /* No debug */, - pesterSymbol.TestName, - pesterSymbol.ScriptRegion?.StartLineNumber })), - - new CodeLens( - this, - scriptFile, - pesterSymbol.ScriptRegion, - new ClientCommand( - "PowerShell.RunPesterTests", - "Debug tests", - new object[] { - scriptFile.DocumentUri, - true /* Run in the debugger */, - pesterSymbol.TestName, - pesterSymbol.ScriptRegion?.StartLineNumber })), - }; - - return codeLensResults; - } - - /// - /// Get all Pester CodeLenses for a given script file. - /// - /// The script file to get Pester CodeLenses for. - /// All Pester CodeLenses for the given script file. - public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) - { - var lenses = new List(); - foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) - { - if (symbol is PesterSymbolReference pesterSymbol) - { - if (pesterSymbol.Command != PesterCommandType.Describe) - { - continue; - } - - lenses.AddRange(GetPesterLens(pesterSymbol, scriptFile)); - } - } - - return lenses.ToArray(); - } - - /// - /// Resolve the CodeLens provision asynchronously -- just wraps the CodeLens argument in a task. - /// - /// The code lens to resolve. - /// - /// The given CodeLens, wrapped in a task. - public Task ResolveCodeLensAsync(CodeLens codeLens, CancellationToken cancellationToken) - { - // This provider has no specific behavior for - // resolving CodeLenses. - return Task.FromResult(codeLens); - } - } -} diff --git a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs deleted file mode 100644 index 69edb6cc3..000000000 --- a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs +++ /dev/null @@ -1,177 +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.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Commands; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Symbols; - -namespace Microsoft.PowerShell.EditorServices.CodeLenses -{ - /// - /// Provides the "reference" code lens by extracting document symbols. - /// - internal class ReferencesCodeLensProvider : FeatureProviderBase, ICodeLensProvider - { - private static readonly Location[] s_emptyLocationArray = new Location[0]; - - /// - /// The editor session code lenses are being provided from. - /// - private EditorSession _editorSession; - - /// - /// The document symbol provider to supply symbols to generate the code lenses. - /// - private IDocumentSymbolProvider _symbolProvider; - - /// - /// Construct a new ReferencesCodeLensProvider for a given EditorSession. - /// - /// - public ReferencesCodeLensProvider(EditorSession editorSession) - { - _editorSession = editorSession; - - // TODO: Pull this from components - _symbolProvider = new ScriptDocumentSymbolProvider( - editorSession.PowerShellContext.LocalPowerShellVersion.Version); - } - - /// - /// Get all reference code lenses for a given script file. - /// - /// The PowerShell script file to get code lenses for. - /// An array of CodeLenses describing all functions in the given script file. - public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) - { - var acc = new List(); - foreach (SymbolReference sym in _symbolProvider.ProvideDocumentSymbols(scriptFile)) - { - if (sym.SymbolType == SymbolType.Function) - { - acc.Add(new CodeLens(this, scriptFile, sym.ScriptRegion)); - } - } - - return acc.ToArray(); - } - - /// - /// Take a codelens and create a new codelens object with updated references. - /// - /// The old code lens to get updated references for. - /// The cancellation token for this request. - /// A new code lens object describing the same data as the old one but with updated references. - public async Task ResolveCodeLensAsync( - CodeLens codeLens, - CancellationToken cancellationToken) - { - ScriptFile[] references = _editorSession.Workspace.ExpandScriptReferences( - codeLens.File); - - SymbolReference foundSymbol = _editorSession.LanguageService.FindFunctionDefinitionAtLocation( - codeLens.File, - codeLens.ScriptExtent.StartLineNumber, - codeLens.ScriptExtent.StartColumnNumber); - - FindReferencesResult referencesResult = await _editorSession.LanguageService.FindReferencesOfSymbolAsync( - foundSymbol, - references, - _editorSession.Workspace); - - Location[] referenceLocations; - if (referencesResult == null) - { - referenceLocations = s_emptyLocationArray; - } - else - { - var acc = new List(); - foreach (SymbolReference foundReference in referencesResult.FoundReferences) - { - if (!NotReferenceDefinition(foundSymbol, foundReference)) - { - continue; - } - - acc.Add(new Location - { - Uri = GetFileUri(foundReference.FilePath), - Range = foundReference.ScriptRegion.ToRange() - }); - } - referenceLocations = acc.ToArray(); - } - - return new CodeLens( - codeLens, - new ClientCommand( - "editor.action.showReferences", - GetReferenceCountHeader(referenceLocations.Length), - new object[] - { - codeLens.File.DocumentUri, - codeLens.ScriptExtent.ToRange().Start, - referenceLocations, - } - )); - } - - /// - /// Check whether a SymbolReference is not a reference to another defined symbol. - /// - /// The symbol definition that may be referenced. - /// The reference symbol to check. - /// True if the reference is not a reference to the definition, false otherwise. - private static bool NotReferenceDefinition( - SymbolReference definition, - SymbolReference reference) - { - return - definition.ScriptRegion.StartLineNumber != reference.ScriptRegion.StartLineNumber - || definition.SymbolType != reference.SymbolType - || !string.Equals(definition.SymbolName, reference.SymbolName, StringComparison.OrdinalIgnoreCase); - } - - /// - /// Get a URI for a given file path. - /// - /// A file path that may be prefixed with URI scheme already. - /// A URI to the file. - private static string GetFileUri(string filePath) - { - // If the file isn't untitled, return a URI-style path - return - !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") - ? Workspace.ConvertPathToDocumentUri(filePath) - : filePath; - } - - /// - /// Get the code lens header for the number of references on a definition, - /// given the number of references. - /// - /// The number of references found for a given definition. - /// The header string for the reference code lens. - private static string GetReferenceCountHeader(int referenceCount) - { - if (referenceCount == 1) - { - return "1 reference"; - } - - var sb = new StringBuilder(14); // "100 references".Length = 14 - sb.Append(referenceCount); - sb.Append(" references"); - return sb.ToString(); - } - } -} diff --git a/src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs b/src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs deleted file mode 100644 index 95594d9b6..000000000 --- a/src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs +++ /dev/null @@ -1,31 +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 Microsoft.PowerShell.EditorServices.Commands; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -using LanguageServer = Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; - -namespace Microsoft.PowerShell.EditorServices -{ - public static class ClientCommandExtensions - { - public static LanguageServer.ServerCommand ToProtocolCommand( - this ClientCommand clientCommand, - JsonSerializer jsonSerializer) - { - return new LanguageServer.ServerCommand - { - Command = clientCommand.Name, - Title = clientCommand.Title, - Arguments = - JArray.FromObject( - clientCommand.Arguments, - jsonSerializer) - }; - } - } -} diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs deleted file mode 100644 index 15ace52f8..000000000 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ /dev/null @@ -1,563 +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 Microsoft.PowerShell.EditorServices.Components; -using Microsoft.PowerShell.EditorServices.CodeLenses; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Protocol.Server; -using Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Symbols; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Management.Automation.Host; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - public enum EditorServicesHostStatus - { - Started, - Failed, - Ended - } - - public enum EditorServiceTransportType - { - NamedPipe, - Stdio - } - - public class EditorServiceTransportConfig - { - public EditorServiceTransportType TransportType { get; set; } - /// - /// Configures the endpoint of the transport. - /// For Stdio it's ignored. - /// For NamedPipe it's the pipe name. - /// - public string InOutPipeName { get; set; } - - public string OutPipeName { get; set; } - - public string InPipeName { get; set; } - - internal string Endpoint => OutPipeName != null && InPipeName != null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}"; - } - - /// - /// Provides a simplified interface for hosting the language and debug services - /// over the named pipe server protocol. - /// - public class EditorServicesHost - { - #region Private Fields - - private readonly PSHost internalHost; - private string[] additionalModules; - private string bundledModulesPath; - private DebugAdapter debugAdapter; - private EditorSession editorSession; - private bool enableConsoleRepl; - private HashSet featureFlags; - private HostDetails hostDetails; - private LanguageServer languageServer; - private ILogger logger; - private ProfilePaths profilePaths; - private TaskCompletionSource serverCompletedTask; - - private IServerListener languageServiceListener; - private IServerListener debugServiceListener; - - #endregion - - #region Properties - - public EditorServicesHostStatus Status { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags) - : this( - hostDetails, - bundledModulesPath, - enableConsoleRepl, - waitForDebugger, - additionalModules, - featureFlags, - GetInternalHostFromDefaultRunspace()) - { - } - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - /// The value of the $Host variable in the original runspace. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags, - PSHost internalHost) - { - Validate.IsNotNull(nameof(hostDetails), hostDetails); - Validate.IsNotNull(nameof(internalHost), internalHost); - - this.hostDetails = hostDetails; - this.enableConsoleRepl = enableConsoleRepl; - this.bundledModulesPath = bundledModulesPath; - this.additionalModules = additionalModules ?? new string[0]; - this.featureFlags = new HashSet(featureFlags ?? new string[0]); - this.serverCompletedTask = new TaskCompletionSource(); - this.internalHost = internalHost; - - while (!System.Diagnostics.Debugger.IsAttached) - { - System.Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id); - System.Threading.Thread.Sleep(2000); - } - -#if DEBUG - if (waitForDebugger) - { - if (System.Diagnostics.Debugger.IsAttached) - { - System.Diagnostics.Debugger.Break(); - } - else - { - System.Diagnostics.Debugger.Launch(); - } - } -#endif - - // Catch unhandled exceptions for logging purposes - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - } - - #endregion - - #region Public Methods - - /// - /// Starts the Logger for the specified file path and log level. - /// - /// The path of the log file to be written. - /// The minimum level of log messages to be written. - public void StartLogging(string logFilePath, LogLevel logLevel) - { - this.logger = Logging.CreateLogger() - .LogLevel(logLevel) - .AddLogFile(logFilePath) - .Build(); - - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); - - string osVersion = RuntimeInformation.OSDescription; - - string osArch = GetOSArchitecture(); - - string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? ""; - - string logHeader = $@" -PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id} - - Host application details: - - Name: {this.hostDetails.Name} - Version: {this.hostDetails.Version} - ProfileId: {this.hostDetails.ProfileId} - Arch: {osArch} - - Operating system details: - - Version: {osVersion} - Arch: {osArch} - - Build information: - - Version: {BuildInfo.BuildVersion} - Origin: {BuildInfo.BuildOrigin} - Date: {buildTime} -"; - - this.logger.Write(LogLevel.Normal, logHeader); - } - - /// - /// Starts the language service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - public void StartLanguageService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths) - { - this.profilePaths = profilePaths; - - this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config); - - this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnectAsync; - this.languageServiceListener.Start(); - - this.logger.Write( - LogLevel.Normal, - string.Format( - "Language service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); - } - - private async void OnLanguageServiceClientConnectAsync( - object sender, - ChannelBase serverChannel) - { - MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); - - ProtocolEndpoint protocolEndpoint = - new ProtocolEndpoint( - serverChannel, - messageDispatcher, - this.logger); - - protocolEndpoint.UnhandledException += ProtocolEndpoint_UnhandledException; - - this.editorSession = - CreateSession( - this.hostDetails, - this.profilePaths, - protocolEndpoint, - messageDispatcher, - this.enableConsoleRepl); - - this.languageServer = - new LanguageServer( - this.editorSession, - messageDispatcher, - protocolEndpoint, - this.serverCompletedTask, - this.logger); - - await this.editorSession.PowerShellContext.ImportCommandsModuleAsync( - Path.Combine( - Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), - @"..\Commands")); - - this.languageServer.Start(); - - // TODO: This can be moved to the point after the $psEditor object - // gets initialized when that is done earlier than LanguageServer.Initialize - foreach (string module in this.additionalModules) - { - var command = - new System.Management.Automation.PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", module); - - await this.editorSession.PowerShellContext.ExecuteCommandAsync( - command, - sendOutputToHost: false, - sendErrorToHost: true); - } - - protocolEndpoint.Start(); - } - - /// - /// Starts the debug service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - /// Determines if we will reuse the session that we have. - public void StartDebugService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths, - bool useExistingSession) - { - this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config); - this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect; - this.debugServiceListener.Start(); - - this.logger.Write( - LogLevel.Normal, - string.Format( - "Debug service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); - } - - private void OnDebugServiceClientConnect(object sender, ChannelBase serverChannel) - { - MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger); - - ProtocolEndpoint protocolEndpoint = - new ProtocolEndpoint( - serverChannel, - messageDispatcher, - this.logger); - - protocolEndpoint.UnhandledException += ProtocolEndpoint_UnhandledException; - - bool ownsEditorSession = this.editorSession == null; - - if (ownsEditorSession) - { - this.editorSession = - this.CreateDebugSession( - this.hostDetails, - profilePaths, - protocolEndpoint, - messageDispatcher, - this.languageServer?.EditorOperations, - this.enableConsoleRepl); - } - - this.debugAdapter = - new DebugAdapter( - this.editorSession, - ownsEditorSession, - messageDispatcher, - protocolEndpoint, - this.logger); - - this.debugAdapter.SessionEnded += - (obj, args) => - { - if (!ownsEditorSession) - { - this.logger.Write( - LogLevel.Normal, - "Previous debug session ended, restarting debug service listener..."); - this.debugServiceListener.Stop(); - this.debugServiceListener.Start(); - } - else if (this.debugAdapter.IsUsingTempIntegratedConsole) - { - this.logger.Write( - LogLevel.Normal, - "Previous temp debug session ended"); - } - else - { - // Exit the host process - this.serverCompletedTask.SetResult(true); - } - }; - - this.debugAdapter.Start(); - protocolEndpoint.Start(); - } - - /// - /// Stops the language or debug services if either were started. - /// - public void StopServices() - { - // TODO: Need a new way to shut down the services - - this.languageServer = null; - - this.debugAdapter = null; - } - - /// - /// Waits for either the language or debug service to shut down. - /// - public void WaitForCompletion() - { - // TODO: We need a way to know when to complete this task! - this.serverCompletedTask.Task.Wait(); - } - - #endregion - - #region Private Methods - - private static PSHost GetInternalHostFromDefaultRunspace() - { - using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - return pwsh.AddScript("$Host").Invoke().First(); - } - } - - private EditorSession CreateSession( - HostDetails hostDetails, - ProfilePaths profilePaths, - IMessageSender messageSender, - IMessageHandlers messageHandlers, - bool enableConsoleRepl) - { - EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext(this.logger, this.featureFlags.Contains("PSReadLine")); - - EditorServicesPSHostUserInterface hostUserInterface = - enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger, this.internalHost) - : new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - hostDetails, - hostUserInterface, - this.logger); - - Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - editorSession.StartSession(powerShellContext, hostUserInterface); - - // TODO: Move component registrations elsewhere! - editorSession.Components.Register(this.logger); - editorSession.Components.Register(messageHandlers); - editorSession.Components.Register(messageSender); - editorSession.Components.Register(powerShellContext); - - CodeLensFeature.Create(editorSession.Components, editorSession); - DocumentSymbolFeature.Create(editorSession.Components, editorSession); - - return editorSession; - } - - private EditorSession CreateDebugSession( - HostDetails hostDetails, - ProfilePaths profilePaths, - IMessageSender messageSender, - IMessageHandlers messageHandlers, - IEditorOperations editorOperations, - bool enableConsoleRepl) - { - EditorSession editorSession = new EditorSession(this.logger); - PowerShellContext powerShellContext = new PowerShellContext( - this.logger, - this.featureFlags.Contains("PSReadLine")); - - EditorServicesPSHostUserInterface hostUserInterface = - enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger, this.internalHost) - : new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger); - - EditorServicesPSHost psHost = - new EditorServicesPSHost( - powerShellContext, - hostDetails, - hostUserInterface, - this.logger); - - Runspace initialRunspace = PowerShellContext.CreateRunspace(psHost); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - - editorSession.StartDebugSession( - powerShellContext, - hostUserInterface, - editorOperations); - - return editorSession; - } - - private void ProtocolEndpoint_UnhandledException(object sender, Exception e) - { - this.logger.Write( - LogLevel.Error, - "PowerShell Editor Services is terminating due to an unhandled exception, see previous logs for details."); - - this.serverCompletedTask.SetException(e); - } - - private void CurrentDomain_UnhandledException( - object sender, - UnhandledExceptionEventArgs e) - { - // Log the exception - this.logger.Write(LogLevel.Error, $"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); - } - - private IServerListener CreateServiceListener(MessageProtocolType protocol, EditorServiceTransportConfig config) - { - switch (config.TransportType) - { - case EditorServiceTransportType.Stdio: - { - return new StdioServerListener(protocol, this.logger); - } - - case EditorServiceTransportType.NamedPipe: - { - if ((config.OutPipeName != null) && (config.InPipeName != null)) - { - this.logger.Write(LogLevel.Verbose, $"Creating NamedPipeServerListener for ${protocol} protocol with two pipes: In: '{config.InPipeName}'. Out: '{config.OutPipeName}'"); - return new NamedPipeServerListener(protocol, config.InPipeName, config.OutPipeName, this.logger); - } - else - { - return new NamedPipeServerListener(protocol, config.InOutPipeName, this.logger); - } - } - - default: - { - throw new NotSupportedException(); - } - } - } - - /// - /// Gets the OSArchitecture for logging. Cannot use System.Runtime.InteropServices.RuntimeInformation.OSArchitecture - /// directly, since this tries to load API set DLLs in win7 and crashes. - /// - /// - private string GetOSArchitecture() - { - // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation - if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version < new Version(6, 2)) - { - if (Environment.Is64BitProcess) - { - return "X64"; - } - - return "X86"; - } - - return RuntimeInformation.OSArchitecture.ToString(); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs b/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs deleted file mode 100644 index d8ec88c4e..000000000 --- a/src/PowerShellEditorServices.Host/PSHost/PromptHandlers.cs +++ /dev/null @@ -1,185 +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 Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Protocol.Messages; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; -using System.Threading.Tasks; -using System.Threading; -using System.Security; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler - { - private IHostInput hostInput; - private IMessageSender messageSender; - private TaskCompletionSource readLineTask; - - public ProtocolChoicePromptHandler( - IMessageSender messageSender, - IHostInput hostInput, - IHostOutput hostOutput, - ILogger logger) - : base(hostOutput, logger) - { - this.hostInput = hostInput; - this.hostOutput = hostOutput; - this.messageSender = messageSender; - } - - protected override void ShowPrompt(PromptStyle promptStyle) - { - base.ShowPrompt(promptStyle); - - messageSender - .SendRequestAsync( - ShowChoicePromptRequest.Type, - new ShowChoicePromptRequest - { - IsMultiChoice = this.IsMultiChoice, - Caption = this.Caption, - Message = this.Message, - Choices = this.Choices, - DefaultChoices = this.DefaultChoices - }, true) - .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.Write( - LogLevel.Error, - "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 IHostInput hostInput; - private IMessageSender messageSender; - private TaskCompletionSource readLineTask; - - public ProtocolInputPromptHandler( - IMessageSender messageSender, - IHostInput hostInput, - IHostOutput hostOutput, - ILogger logger) - : base(hostOutput, logger) - { - this.hostInput = hostInput; - this.hostOutput = hostOutput; - this.messageSender = messageSender; - } - - protected override void ShowFieldPrompt(FieldDetails fieldDetails) - { - base.ShowFieldPrompt(fieldDetails); - - messageSender - .SendRequestAsync( - ShowInputPromptRequest.Type, - new ShowInputPromptRequest - { - Name = fieldDetails.Name, - Label = fieldDetails.Label - }, true) - .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.Write( - LogLevel.Error, - "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.Host/PSHost/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs deleted file mode 100644 index 76b1f7252..000000000 --- a/src/PowerShellEditorServices.Host/PSHost/ProtocolPSHostUserInterface.cs +++ /dev/null @@ -1,123 +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 Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.Server; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface - { - #region Private Fields - - private IMessageSender messageSender; - private OutputDebouncer outputDebouncer; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsoleServicePSHostUserInterface - /// class with the given IConsoleHost implementation. - /// - /// - public ProtocolPSHostUserInterface( - PowerShellContext powerShellContext, - IMessageSender messageSender, - ILogger logger) - : base(powerShellContext, new SimplePSHostRawUserInterface(logger), logger) - { - this.messageSender = messageSender; - this.outputDebouncer = new OutputDebouncer(messageSender); - } - - public void Dispose() - { - // TODO: Need a clear API path for this - - // Make sure remaining output is flushed before exiting - if (this.outputDebouncer != null) - { - this.outputDebouncer.FlushAsync().Wait(); - this.outputDebouncer = null; - } - } - - #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: This should use a synchronous method! - this.outputDebouncer.InvokeAsync( - new OutputWrittenEventArgs( - outputString, - includeNewLine, - outputType, - foregroundColor, - backgroundColor)).Wait(); - } - - /// - /// 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) - { - } - - 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(this.messageSender, this, this, this.Logger); - } - - protected override ChoicePromptHandler OnCreateChoicePromptHandler() - { - return new ProtocolChoicePromptHandler(this.messageSender, this, this, this.Logger); - } - } -} diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj deleted file mode 100644 index 2245a4a22..000000000 --- a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - - PowerShell Editor Services Host Process - Provides a process for hosting the PowerShell Editor Services library exposed by a JSON message protocol. - netstandard2.0 - Microsoft.PowerShell.EditorServices.Host - - - - - - - - - - - diff --git a/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs b/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs deleted file mode 100644 index da3402bde..000000000 --- a/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs +++ /dev/null @@ -1,115 +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 Microsoft.PowerShell.EditorServices.Components; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -using Servers = Microsoft.PowerShell.EditorServices.Protocol.Server; - -namespace Microsoft.PowerShell.EditorServices.Symbols -{ - internal class DocumentSymbolFeature : - FeatureComponentBase, - IDocumentSymbols - { - private EditorSession editorSession; - - public DocumentSymbolFeature( - EditorSession editorSession, - IMessageHandlers messageHandlers, - ILogger logger) - : base(logger) - { - this.editorSession = editorSession; - - messageHandlers.SetRequestHandler( - DocumentSymbolRequest.Type, - this.HandleDocumentSymbolRequestAsync); - } - - public static DocumentSymbolFeature Create( - IComponentRegistry components, - EditorSession editorSession) - { - var documentSymbols = - new DocumentSymbolFeature( - editorSession, - components.Get(), - components.Get()); - - documentSymbols.Providers.Add( - new ScriptDocumentSymbolProvider( - editorSession.PowerShellContext.LocalPowerShellVersion.Version)); - - documentSymbols.Providers.Add( - new PsdDocumentSymbolProvider()); - - documentSymbols.Providers.Add( - new PesterDocumentSymbolProvider()); - - editorSession.Components.Register(documentSymbols); - - return documentSymbols; - } - - public IEnumerable ProvideDocumentSymbols( - ScriptFile scriptFile) - { - return - this.InvokeProviders(p => p.ProvideDocumentSymbols(scriptFile)) - .SelectMany(r => r); - } - - protected async Task HandleDocumentSymbolRequestAsync( - DocumentSymbolParams documentSymbolParams, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - documentSymbolParams.TextDocument.Uri); - - IEnumerable foundSymbols = - this.ProvideDocumentSymbols(scriptFile); - - SymbolInformation[] symbols = null; - - string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - - if (foundSymbols != null) - { - symbols = - foundSymbols - .Select(r => - { - return new SymbolInformation - { - ContainerName = containerName, - Kind = Servers.LanguageServer.GetSymbolKind(r.SymbolType), - Location = new Location - { - Uri = Servers.LanguageServer.GetFileUri(r.FilePath), - Range = Servers.LanguageServer.GetRangeFromScriptRegion(r.ScriptRegion) - }, - Name = Servers.LanguageServer.GetDecoratedSymbolName(r) - }; - }) - .ToArray(); - } - else - { - symbols = new SymbolInformation[0]; - } - - await requestContext.SendResultAsync(symbols); - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs deleted file mode 100644 index b226fd0c4..000000000 --- a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs +++ /dev/null @@ -1,97 +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 Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Utility; -using System.Threading.Tasks; -using System; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Client -{ - public class DebugAdapterClient : IMessageSender, IMessageHandlers - { - private ILogger logger; - private ProtocolEndpoint protocolEndpoint; - private MessageDispatcher messageDispatcher; - - public DebugAdapterClient(ChannelBase clientChannel, ILogger logger) - { - this.logger = logger; - this.messageDispatcher = new MessageDispatcher(logger); - this.protocolEndpoint = new ProtocolEndpoint( - clientChannel, - messageDispatcher, - logger); - } - - public async Task StartAsync() - { - this.protocolEndpoint.Start(); - - // Initialize the debug adapter - await this.SendRequestAsync( - InitializeRequest.Type, - new InitializeRequestArguments - { - LinesStartAt1 = true, - ColumnsStartAt1 = true - }, - true); - } - - public void Stop() - { - this.protocolEndpoint.Stop(); - } - - public async Task LaunchScriptAsync(string scriptFilePath) - { - await this.SendRequestAsync( - LaunchRequest.Type, - new LaunchRequestArguments { - Script = scriptFilePath - }, - true); - - await this.SendRequestAsync( - ConfigurationDoneRequest.Type, - null, - true); - } - - public Task SendEventAsync(NotificationType eventType, TParams eventParams) - { - return ((IMessageSender)protocolEndpoint).SendEventAsync(eventType, eventParams); - } - - public Task SendRequestAsync(RequestType requestType, TParams requestParams, bool waitForResponse) - { - return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType, requestParams, waitForResponse); - } - - public Task SendRequestAsync(RequestType0 requestType0) - { - return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType0); - } - - public void SetRequestHandler(RequestType requestType, Func, Task> requestHandler) - { - ((IMessageHandlers)messageDispatcher).SetRequestHandler(requestType, requestHandler); - } - - public void SetRequestHandler(RequestType0 requestType0, Func, Task> requestHandler) - { - ((IMessageHandlers)messageDispatcher).SetRequestHandler(requestType0, requestHandler); - } - - public void SetEventHandler(NotificationType eventType, Func eventHandler) - { - ((IMessageHandlers)messageDispatcher).SetEventHandler(eventType, eventHandler); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs deleted file mode 100644 index 0ccb3d5b5..000000000 --- a/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs +++ /dev/null @@ -1,102 +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 Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Client -{ - /// - /// Provides a base implementation for language server clients. - /// - public abstract class LanguageClientBase : IMessageHandlers, IMessageSender - { - ILogger logger; - private ProtocolEndpoint protocolEndpoint; - private MessageDispatcher messageDispatcher; - - /// - /// Initializes an instance of the language client using the - /// specified channel for communication. - /// - /// The channel to use for communication with the server. - public LanguageClientBase(ChannelBase clientChannel, ILogger logger) - { - this.logger = logger; - this.messageDispatcher = new MessageDispatcher(logger); - this.protocolEndpoint = new ProtocolEndpoint( - clientChannel, - messageDispatcher, - logger); - } - - public Task Start() - { - this.protocolEndpoint.Start(); - - // Initialize the implementation class - return this.InitializeAsync(); - } - - public async Task StopAsync() - { - await this.OnStopAsync(); - - // First, notify the language server that we're stopping - var response = - await this.SendRequestAsync( - ShutdownRequest.Type); - - await this.SendEventAsync(ExitNotification.Type, new object()); - - this.protocolEndpoint.Stop(); - } - - protected virtual Task OnStopAsync() - { - return Task.FromResult(true); - } - - protected virtual Task InitializeAsync() - { - return Task.FromResult(true); - } - - public Task SendEventAsync(NotificationType eventType, TParams eventParams) - { - return ((IMessageSender)protocolEndpoint).SendEventAsync(eventType, eventParams); - } - - public Task SendRequestAsync(RequestType requestType, TParams requestParams, bool waitForResponse) - { - return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType, requestParams, waitForResponse); - } - - public Task SendRequestAsync(RequestType0 requestType0) - { - return ((IMessageSender)protocolEndpoint).SendRequestAsync(requestType0); - } - - public void SetRequestHandler(RequestType requestType, Func, Task> requestHandler) - { - ((IMessageHandlers)messageDispatcher).SetRequestHandler(requestType, requestHandler); - } - - public void SetRequestHandler(RequestType0 requestType0, Func, Task> requestHandler) - { - ((IMessageHandlers)messageDispatcher).SetRequestHandler(requestType0, requestHandler); - } - - public void SetEventHandler(NotificationType eventType, Func eventHandler) - { - ((IMessageHandlers)messageDispatcher).SetEventHandler(eventType, eventHandler); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs deleted file mode 100644 index e2b1491fd..000000000 --- a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs +++ /dev/null @@ -1,118 +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 Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Client -{ - public class LanguageServiceClient : LanguageClientBase - { - private Dictionary cachedDiagnostics = - new Dictionary(); - - public LanguageServiceClient(ChannelBase clientChannel, ILogger logger) - : base(clientChannel, logger) - { - } - - protected override Task InitializeAsync() - { - // Add handlers for common events - this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEventAsync); - - // Send the 'initialize' request and wait for the response - var initializeParams = new InitializeParams - { - RootPath = "", - Capabilities = new ClientCapabilities() - }; - - return this.SendRequestAsync( - InitializeRequest.Type, - initializeParams, - true); - } - - #region Events - - public event EventHandler DiagnosticsReceived; - - protected void OnDiagnosticsReceived(string filePath) - { - if (this.DiagnosticsReceived != null) - { - this.DiagnosticsReceived(this, filePath); - } - } - - #endregion - - #region Private Methods - - private Task HandlePublishDiagnosticsEventAsync( - PublishDiagnosticsNotification diagnostics, - EventContext eventContext) - { - string normalizedPath = diagnostics.Uri.ToLower(); - - this.cachedDiagnostics[normalizedPath] = - diagnostics.Diagnostics - .Select(GetMarkerFromDiagnostic) - .ToArray(); - - this.OnDiagnosticsReceived(normalizedPath); - - return Task.FromResult(true); - } - - private static ScriptFileMarker GetMarkerFromDiagnostic(Diagnostic diagnostic) - { - DiagnosticSeverity severity = - diagnostic.Severity.GetValueOrDefault( - DiagnosticSeverity.Error); - - return new ScriptFileMarker - { - Level = MapDiagnosticSeverityToLevel(severity), - Message = diagnostic.Message, - ScriptRegion = new ScriptRegion - { - StartLineNumber = diagnostic.Range.Start.Line + 1, - StartColumnNumber = diagnostic.Range.Start.Character + 1, - EndLineNumber = diagnostic.Range.End.Line + 1, - EndColumnNumber = diagnostic.Range.End.Character + 1 - } - }; - } - - private static ScriptFileMarkerLevel MapDiagnosticSeverityToLevel(DiagnosticSeverity severity) - { - switch (severity) - { - case DiagnosticSeverity.Hint: - case DiagnosticSeverity.Information: - return ScriptFileMarkerLevel.Information; - - case DiagnosticSeverity.Warning: - return ScriptFileMarkerLevel.Warning; - - case DiagnosticSeverity.Error: - return ScriptFileMarkerLevel.Error; - - default: - return ScriptFileMarkerLevel.Error; - } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs deleted file mode 100644 index 4c805a2b1..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs +++ /dev/null @@ -1,29 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class AttachRequest - { - public static readonly - RequestType Type = - RequestType.Create("attach"); - } - - public class AttachRequestArguments - { - public string ComputerName { get; set; } - - public string ProcessId { get; set; } - - public string RunspaceId { get; set; } - - public string RunspaceName { get; set; } - - public string CustomPipeName { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs deleted file mode 100644 index 0eeb00d8f..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Breakpoint.cs +++ /dev/null @@ -1,82 +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 Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class Breakpoint - { - public int? Id { get; set; } - - /// - /// Gets an boolean indicator that if true, breakpoint could be set - /// (but not necessarily at the desired location). - /// - public bool Verified { get; set; } - - /// - /// Gets an optional message about the state of the breakpoint. This is shown to the user - /// and can be used to explain why a breakpoint could not be verified. - /// - public string Message { get; set; } - - public Source Source { get; set; } - - public int? Line { get; set; } - - public int? Column { get; set; } - - private Breakpoint() - { - } - - public static Breakpoint Create( - BreakpointDetails breakpointDetails) - { - Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); - - return new Breakpoint - { - Id = breakpointDetails.Id, - Verified = breakpointDetails.Verified, - Message = breakpointDetails.Message, - Source = new Source { Path = breakpointDetails.Source }, - Line = breakpointDetails.LineNumber, - Column = breakpointDetails.ColumnNumber - }; - } - - public static Breakpoint Create( - CommandBreakpointDetails breakpointDetails) - { - Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); - - return new Breakpoint { - Verified = breakpointDetails.Verified, - Message = breakpointDetails.Message - }; - } - - public static Breakpoint Create( - SourceBreakpoint sourceBreakpoint, - string source, - string message, - bool verified = false) - { - Validate.IsNotNull(nameof(sourceBreakpoint), sourceBreakpoint); - Validate.IsNotNull(nameof(source), source); - Validate.IsNotNull(nameof(message), message); - - return new Breakpoint { - Verified = verified, - Message = message, - Source = new Source { Path = source }, - Line = sourceBreakpoint.Line, - Column = sourceBreakpoint.Column - }; - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/BreakpointEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/BreakpointEvent.cs deleted file mode 100644 index a339a70d9..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/BreakpointEvent.cs +++ /dev/null @@ -1,20 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class BreakpointEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("breakpoint"); - - public string Reason { get; set; } - - public Breakpoint Breakpoint { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs deleted file mode 100644 index 11cbc4ded..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ConfigurationDoneRequest.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class ConfigurationDoneRequest - { - public static readonly - RequestType Type = - RequestType.Create("configurationDone"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs deleted file mode 100644 index 3f9dbc59c..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs +++ /dev/null @@ -1,17 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class ContinueRequest - { - public static readonly - RequestType Type = - RequestType.Create("continue"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs deleted file mode 100644 index af442750f..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinuedEvent.cs +++ /dev/null @@ -1,21 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class ContinuedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("continued"); - - public int ThreadId { get; set; } - - public bool AllThreadsContinued { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs deleted file mode 100644 index 2235206cd..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs +++ /dev/null @@ -1,17 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class DisconnectRequest - { - public static readonly - RequestType Type = - RequestType.Create("disconnect"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs deleted file mode 100644 index d45e7d44a..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs +++ /dev/null @@ -1,53 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class EvaluateRequest - { - public static readonly - RequestType Type = - RequestType.Create("evaluate"); - } - - public class EvaluateRequestArguments - { - /// - /// The expression to evaluate. - /// - public string Expression { get; set; } - - /// - /// The context in which the evaluate request is run. Possible - /// values are 'watch' if evaluate is run in a watch or 'repl' - /// if run from the REPL console. - /// - public string Context { get; set; } - - /// - /// Evaluate the expression in the context of this stack frame. - /// If not specified, the top most frame is used. - /// - public int FrameId { get; set; } - } - - public class EvaluateResponseBody - { - /// - /// The evaluation result. - /// - public string Result { get; set; } - - /// - /// If variablesReference is > 0, the evaluate result is - /// structured and its children can be retrieved by passing - /// variablesReference to the VariablesRequest - /// - public int VariablesReference { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ExitedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ExitedEvent.cs deleted file mode 100644 index 9f1a0d6ce..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ExitedEvent.cs +++ /dev/null @@ -1,22 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class ExitedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("exited"); - } - - public class ExitedEventBody - { - public int ExitCode { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs deleted file mode 100644 index 7904759d9..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs +++ /dev/null @@ -1,70 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class InitializeRequest - { - public static readonly - RequestType Type = - RequestType.Create("initialize"); - } - - public class InitializeRequestArguments - { - public string AdapterId { get; set; } - - public bool LinesStartAt1 { get; set; } - - public bool ColumnsStartAt1 { get; set; } - - public string PathFormat { get; set; } - - public bool SourceMaps { get; set; } - - public string GeneratedCodeDirectory { get; set; } - } - - public class InitializeResponseBody - { - /// - /// Gets or sets a boolean value that determines whether the debug adapter - /// supports the configurationDoneRequest. - /// - public bool SupportsConfigurationDoneRequest { get; set; } - - /// - /// Gets or sets a boolean value that determines whether the debug adapter - /// supports functionBreakpoints. - /// - public bool SupportsFunctionBreakpoints { get; set; } - - /// - /// Gets or sets a boolean value that determines whether the debug adapter - /// supports conditionalBreakpoints. - /// - public bool SupportsConditionalBreakpoints { get; set; } - - /// - /// Gets or sets a boolean value that determines whether the debug adapter - /// supports breakpoints that break execution after a specified number of hits. - /// - public bool SupportsHitConditionalBreakpoints { get; set; } - - /// - /// Gets or sets a boolean value that determines whether the debug adapter - /// supports a (side effect free) evaluate request for data hovers. - /// - public bool SupportsEvaluateForHovers { get; set; } - - /// - /// Gets or sets a boolean value that determines whether the debug adapter - /// supports allowing the user to set a variable from the Variables debug windows. - /// - public bool SupportsSetVariable { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializedEvent.cs deleted file mode 100644 index 7253b7b30..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializedEvent.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class InitializedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("initialized"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs deleted file mode 100644 index 1bf5c9ea1..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs +++ /dev/null @@ -1,73 +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.Collections.Generic; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class LaunchRequest - { - public static readonly - RequestType Type = - RequestType.Create("launch"); - } - - public class LaunchRequestArguments - { - /// - /// Gets or sets the absolute path to the script to debug. - /// - public string Script { get; set; } - - /// - /// Gets or sets a boolean value that indicates whether the script should be - /// run with (false) or without (true) debugging support. - /// - public bool NoDebug { get; set; } - - /// - /// Gets or sets a boolean value that determines whether to automatically stop - /// target after launch. If not specified, target does not stop. - /// - public bool StopOnEntry { get; set; } - - /// - /// Gets or sets optional arguments passed to the debuggee. - /// - public string[] Args { get; set; } - - /// - /// Gets or sets the working directory of the launched debuggee (specified as an absolute path). - /// If omitted the debuggee is lauched in its own directory. - /// - public string Cwd { get; set; } - - /// - /// Gets or sets a boolean value that determines whether to create a temporary - /// integrated console for the debug session. Default is false. - /// - public bool CreateTemporaryIntegratedConsole { get; set; } - - /// - /// Gets or sets the absolute path to the runtime executable to be used. - /// Default is the runtime executable on the PATH. - /// - public string RuntimeExecutable { get; set; } - - /// - /// Gets or sets the optional arguments passed to the runtime executable. - /// - public string[] RuntimeArgs { get; set; } - - /// - /// Gets or sets optional environment variables to pass to the debuggee. The string valued - /// properties of the 'environmentVariables' are used as key/value pairs. - /// - public Dictionary Env { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs deleted file mode 100644 index 1a254b96f..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs +++ /dev/null @@ -1,20 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - // /** StepOver request; value of command field is "next". - // he request starts the debuggee to run again for one step. - // penDebug will respond with a StoppedEvent (event type 'step') after running the step. - public class NextRequest - { - public static readonly - RequestType Type = - RequestType.Create("next"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/OutputEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/OutputEvent.cs deleted file mode 100644 index 0044855c4..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/OutputEvent.cs +++ /dev/null @@ -1,24 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class OutputEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("output"); - } - - public class OutputEventBody - { - public string Category { get; set; } - - public string Output { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs deleted file mode 100644 index 6fa67c584..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs +++ /dev/null @@ -1,17 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class PauseRequest - { - public static readonly - RequestType Type = - RequestType.Create("pause"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Scope.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Scope.cs deleted file mode 100644 index 5413869f4..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Scope.cs +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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.DebugAdapter -{ - public class Scope - { - /// - /// Gets or sets the name of the scope (as such 'Arguments', 'Locals') - /// - public string Name { get; set; } - - /// - /// Gets or sets the variables of this scope can be retrieved by passing the - /// value of variablesReference to the VariablesRequest. - /// - public int VariablesReference { get; set; } - - /// - /// Gets or sets a boolean value indicating if number of variables in - /// this scope is large or expensive to retrieve. - /// - public bool Expensive { get; set; } - - public static Scope Create(VariableScope scope) - { - return new Scope { - Name = scope.Name, - VariablesReference = scope.Id, - // Temporary fix for #95 to get debug hover tips to work well at least for the local scope. - Expensive = ((scope.Name != VariableContainerDetails.LocalScopeName) && - (scope.Name != VariableContainerDetails.AutoVariablesName)) - }; - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs deleted file mode 100644 index 4905ffdfe..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs +++ /dev/null @@ -1,29 +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.Diagnostics; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class ScopesRequest - { - public static readonly - RequestType Type = - RequestType.Create("scopes"); - } - - [DebuggerDisplay("FrameId = {FrameId}")] - public class ScopesRequestArguments - { - public int FrameId { get; set; } - } - - public class ScopesResponseBody - { - public Scope[] Scopes { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs deleted file mode 100644 index 82d41ac53..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs +++ /dev/null @@ -1,45 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - /// - /// SetBreakpoints request; value of command field is "setBreakpoints". - /// Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. - /// To clear all breakpoint for a source, specify an empty array. - /// When a breakpoint is hit, a StoppedEvent (event type 'breakpoint') is generated. - /// - public class SetBreakpointsRequest - { - public static readonly - RequestType Type = - RequestType.Create("setBreakpoints"); - } - - public class SetBreakpointsRequestArguments - { - public Source Source { get; set; } - - public SourceBreakpoint[] Breakpoints { get; set; } - } - - public class SourceBreakpoint - { - public int Line { get; set; } - - public int? Column { get; set; } - - public string Condition { get; set; } - - public string HitCondition { get; set; } - } - - public class SetBreakpointsResponseBody - { - public Breakpoint[] Breakpoints { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs deleted file mode 100644 index 6a7313324..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs +++ /dev/null @@ -1,31 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - /// - /// SetExceptionBreakpoints request; value of command field is "setExceptionBreakpoints". - /// Enable that the debuggee stops on exceptions with a StoppedEvent (event type 'exception'). - /// - public class SetExceptionBreakpointsRequest - { - public static readonly - RequestType Type = - RequestType.Create("setExceptionBreakpoints"); - } - - /// - /// Arguments for "setExceptionBreakpoints" request. - /// - public class SetExceptionBreakpointsRequestArguments - { - /// - /// Gets or sets the names of enabled exception breakpoints. - /// - public string[] Filters { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs deleted file mode 100644 index 6fb951553..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetFunctionBreakpointsRequest.cs +++ /dev/null @@ -1,33 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class SetFunctionBreakpointsRequest - { - public static readonly - RequestType Type = - RequestType.Create("setFunctionBreakpoints"); - } - - public class SetFunctionBreakpointsRequestArguments - { - public FunctionBreakpoint[] Breakpoints { get; set; } - } - - public class FunctionBreakpoint - { - /// - /// Gets or sets the name of the function to break on when it is invoked. - /// - public string Name { get; set; } - - public string Condition { get; set; } - - public string HitCondition { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs deleted file mode 100644 index 47e41cb36..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetVariableRequest.cs +++ /dev/null @@ -1,36 +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.Diagnostics; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - /// - /// SetVariable request; value of command field is "setVariable". - /// Request is initiated when user uses the debugger Variables UI to change the value of a variable. - /// - public class SetVariableRequest - { - public static readonly - RequestType Type = - RequestType.Create("setVariable"); - } - - [DebuggerDisplay("VariablesReference = {VariablesReference}")] - public class SetVariableRequestArguments - { - public int VariablesReference { get; set; } - - public string Name { get; set; } - - public string Value { get; set; } - } - - public class SetVariableResponseBody - { - public string Value { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Source.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Source.cs deleted file mode 100644 index 29f21bdf9..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Source.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// 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.DebugAdapter -{ - public class Source - { - public string Name { get; set; } - - public string Path { get; set; } - - public int? SourceReference { get; set; } - - /// - /// Gets an optional hint for how to present the source in the UI. A value of 'deemphasize' - /// can be used to indicate that the source is not available or that it is skipped on stepping. - /// - public string PresentationHint { get; set; } - } - - /// - /// An optional hint for how to present source in the UI. - /// - public enum SourcePresentationHint - { - /// - /// Dispays the source normally. - /// - Normal, - - /// - /// Display the source emphasized. - /// - Emphasize, - - /// - /// Display the source deemphasized. - /// - Deemphasize - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs deleted file mode 100644 index 48a2b0a17..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs +++ /dev/null @@ -1,29 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class SourceRequest - { - public static readonly - RequestType Type = - RequestType.Create("source"); - } - - public class SourceRequestArguments - { - /// - /// Gets or sets the reference to the source. This is the value received in Source.reference. - /// - public int SourceReference { get; set; } - } - - public class SourceResponseBody - { - public string Content { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackFrame.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StackFrame.cs deleted file mode 100644 index 4c8d80d81..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackFrame.cs +++ /dev/null @@ -1,112 +0,0 @@ -// -// 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.DebugAdapter -{ - public class StackFrame - { - /// - /// Gets or sets an identifier for the stack frame. It must be unique across all threads. - /// This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or - /// to restart the execution of a stackframe. */ - /// - public int Id { get; set; } - - /// - /// Gets or sets the name of the stack frame, typically a method name - /// - public string Name { get; set; } - - /// - /// Gets or sets the optional source of the frame. - /// - public Source Source { get; set; } - - /// - /// Gets or sets line within the file of the frame. If source is null or doesn't exist, - /// line is 0 and must be ignored. - /// - public int Line { get; set; } - - /// - /// Gets or sets an optional end line of the range covered by the stack frame. - /// - public int? EndLine { get; set; } - - /// - /// Gets or sets the column within the line. If source is null or doesn't exist, - /// column is 0 and must be ignored. - /// - public int Column { get; set; } - - /// - /// Gets or sets an optional end column of the range covered by the stack frame. - /// - public int? EndColumn { get; set; } - - /// - /// Gets an optional hint for how to present this frame in the UI. A value of 'label' - /// can be used to indicate that the frame is an artificial frame that is used as a - /// visual label or separator. A value of 'subtle' can be used to change the appearance - /// of a frame in a 'subtle' way. - /// - public string PresentationHint { get; private set; } - - public static StackFrame Create( - StackFrameDetails stackFrame, - int id) - { - var sourcePresentationHint = - stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal; - - // When debugging an interactive session, the ScriptPath is which is not a valid source file. - // We need to make sure the user can't open the file associated with this stack frame. - // It will generate a VSCode error in this case. - Source source = null; - if (!stackFrame.ScriptPath.Contains("<")) - { - source = new Source - { - Path = stackFrame.ScriptPath, - PresentationHint = sourcePresentationHint.ToString().ToLower() - }; - } - - return new StackFrame - { - Id = id, - Name = (source != null) ? stackFrame.FunctionName : "Interactive Session", - Line = (source != null) ? stackFrame.StartLineNumber : 0, - EndLine = stackFrame.EndLineNumber, - Column = (source != null) ? stackFrame.StartColumnNumber : 0, - EndColumn = stackFrame.EndColumnNumber, - Source = source - }; - } - } - - /// - /// An optional hint for how to present a stack frame in the UI. - /// - public enum StackFramePresentationHint - { - /// - /// Dispays the stack frame as a normal stack frame. - /// - Normal, - - /// - /// Used to label an entry in the call stack that doesn't actually correspond to a stack frame. - /// This is typically used to label transitions to/from "external" code. - /// - Label, - - /// - /// Displays the stack frame in a subtle way, typically used from loctaions outside of the current project or workspace. - /// - Subtle - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs deleted file mode 100644 index 56a88a950..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs +++ /dev/null @@ -1,55 +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.Diagnostics; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class StackTraceRequest - { - public static readonly - RequestType Type = - RequestType.Create("stackTrace"); - } - - [DebuggerDisplay("ThreadId = {ThreadId}, Levels = {Levels}")] - public class StackTraceRequestArguments - { - /// - /// Gets or sets the ThreadId of this stacktrace. - /// - public int ThreadId { get; set; } - - /// - /// Gets or sets the index of the first frame to return. If omitted frames start at 0. - /// - public int? StartFrame { get; set; } - - /// - /// Gets or sets the maximum number of frames to return. If levels is not specified or 0, all frames are returned. - /// - public int? Levels { get; set; } - - /// - /// Gets or sets the format string that specifies details on how to format the stack frames. - /// - public string Format { get; set; } - } - - public class StackTraceResponseBody - { - /// - /// Gets the frames of the stackframe. If the array has length zero, there are no stackframes available. - /// This means that there is no location information available. - /// - public StackFrame[] StackFrames { get; set; } - - /// - /// Gets the total number of frames available. - /// - public int? TotalFrames { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StartedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StartedEvent.cs deleted file mode 100644 index 5b32d4a84..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StartedEvent.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class StartedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("started"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs deleted file mode 100644 index ec825ebbd..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs +++ /dev/null @@ -1,17 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class StepInRequest - { - public static readonly - RequestType Type = - RequestType.Create("stepIn"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs deleted file mode 100644 index be0a31807..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class StepOutRequest - { - public static readonly - RequestType Type = - RequestType.Create("stepOut"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StoppedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StoppedEvent.cs deleted file mode 100644 index a3c2a7921..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StoppedEvent.cs +++ /dev/null @@ -1,37 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class StoppedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("stopped"); - } - - public class StoppedEventBody - { - /// - /// A value such as "step", "breakpoint", "exception", or "pause" - /// - public string Reason { get; set; } - - /// - /// Gets or sets the current thread ID, if any. - /// - public int? ThreadId { get; set; } - - public Source Source { get; set; } - - /// - /// Gets or sets additional information such as an error message. - /// - public string Text { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs deleted file mode 100644 index 514d0bcae..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/TerminatedEvent.cs +++ /dev/null @@ -1,19 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class TerminatedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("terminated"); - - public bool Restart { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Thread.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Thread.cs deleted file mode 100644 index 35a8a139d..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Thread.cs +++ /dev/null @@ -1,15 +0,0 @@ -// -// 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.DebugAdapter -{ - public class Thread - { - public int Id { get; set; } - - public string Name { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs deleted file mode 100644 index 24432d671..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs +++ /dev/null @@ -1,22 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class ThreadsRequest - { - public static readonly - RequestType Type = - RequestType.Create("threads"); - } - - public class ThreadsResponseBody - { - public Thread[] Threads { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs deleted file mode 100644 index a335b0975..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/Variable.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -// 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.DebugAdapter -{ - public class Variable - { - public string Name { get; set; } - - // /** The variable's value. For structured objects this can be a multi line text, e.g. for a function the body of a function. */ - public string Value { get; set; } - - /// - /// Gets or sets the type of the variable's value. Typically shown in the UI when hovering over the value. - /// - public string Type { get; set; } - - /// - /// Gets or sets the evaluatable name for the variable that will be evaluated by the debugger. - /// - public string EvaluateName { get; set; } - - // /** If variablesReference is > 0, the variable is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ - public int VariablesReference { get; set; } - - public static Variable Create(VariableDetailsBase variable) - { - return new Variable - { - Name = variable.Name, - Value = variable.ValueString ?? string.Empty, - Type = variable.Type, - EvaluateName = variable.Name, - VariablesReference = - variable.IsExpandable ? - variable.Id : 0 - }; - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs deleted file mode 100644 index f387c500f..000000000 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs +++ /dev/null @@ -1,29 +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.Diagnostics; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter -{ - public class VariablesRequest - { - public static readonly - RequestType Type = - RequestType.Create("variables"); - } - - [DebuggerDisplay("VariablesReference = {VariablesReference}")] - public class VariablesRequestArguments - { - public int VariablesReference { get; set; } - } - - public class VariablesResponseBody - { - public Variable[] Variables { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ClientCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ClientCapabilities.cs deleted file mode 100644 index f66982c43..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ClientCapabilities.cs +++ /dev/null @@ -1,26 +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.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Defines a class that describes the capabilities of a language - /// client. At this time no specific capabilities are listed for - /// clients. - /// - public class ClientCapabilities - { - public WorkspaceClientCapabilities Workspace { get; set; } - public TextDocumentClientCapabilities TextDocument { get; set; } - public object Experimental { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs deleted file mode 100644 index a382d8dcf..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/CodeAction.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Newtonsoft.Json.Linq; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class CodeActionRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/codeAction"); - } - - /// - /// Parameters for CodeActionRequest. - /// - public class CodeActionParams - { - /// - /// The document in which the command was invoked. - /// - public TextDocumentIdentifier TextDocument { get; set; } - - /// - /// The range for which the command was invoked. - /// - public Range Range { get; set; } - - /// - /// Context carrying additional information. - /// - public CodeActionContext Context { get; set; } - } - - public class CodeActionContext - { - public Diagnostic[] Diagnostics { get; set; } - } - - public class CodeActionCommand - { - public string Title { get; set; } - - public string Command { get; set; } - - public JArray Arguments { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs deleted file mode 100644 index fd12f284d..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs +++ /dev/null @@ -1,56 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Newtonsoft.Json.Linq; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Code Lens options. - /// - public class CodeLensOptions - { - /// - /// Code lens has a resolve provider as well. - /// - public bool ResolveProvider { get; set; } - } - - public class CodeLens - { - public Range Range { get; set; } - - public ServerCommand Command { get; set; } - - public JToken Data { get; set; } - } - - /// - /// A code lens represents a command that should be shown along with - /// source text, like the number of references, a way to run tests, etc. - /// - /// A code lens is _unresolved_ when no command is associated to it. For performance - /// reasons the creation of a code lens and resolving should be done in two stages. - /// - public class CodeLensRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/codeLens"); - - /// - /// The document to request code lens for. - /// - public TextDocumentIdentifier TextDocument { get; set; } - } - - public class CodeLensResolveRequest - { - public static readonly - RequestType Type = - RequestType.Create("codeLens/resolve"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs deleted file mode 100644 index d4406519f..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs +++ /dev/null @@ -1,28 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - class CommentHelpRequest - { - public static readonly RequestType Type - = RequestType.Create("powerShell/getCommentHelp"); - } - - public class CommentHelpRequestResult - { - public string[] Content { get; set; } - } - - public class CommentHelpRequestParams - { - public string DocumentUri { get; set; } - public Position TriggerPosition { get; set; } - public bool BlockComment { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs deleted file mode 100644 index 2e0cbeeca..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs +++ /dev/null @@ -1,144 +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.Diagnostics; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class CompletionRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/completion"); - } - - public class CompletionResolveRequest - { - public static readonly - RequestType Type = - RequestType.Create("completionItem/resolve"); - } - - /// - /// Completion registration options. - /// - public class CompletionRegistrationOptions : TextDocumentRegistrationOptions - { - // We duplicate the properties of completionOptions class here because - // we cannot derive from two classes. One way to get around this situation - // is to use define CompletionOptions as an interface instead of a class. - public bool? ResolveProvider { get; set; } - - public string[] TriggerCharacters { get; set; } - } - - public enum CompletionItemKind - { - Text = 1, - Method = 2, - Function = 3, - Constructor = 4, - Field = 5, - Variable = 6, - Class = 7, - Interface = 8, - Module = 9, - Property = 10, - Unit = 11, - Value = 12, - Enum = 13, - Keyword = 14, - Snippet = 15, - Color = 16, - File = 17, - Reference = 18, - Folder = 19 - } - - public enum InsertTextFormat - { - PlainText = 1, - Snippet = 2, - } - - [DebuggerDisplay("NewText = {NewText}, Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}")] - public class TextEdit - { - public Range Range { get; set; } - - public string NewText { get; set; } - } - - [DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")] - public class CompletionItem - { - public string Label { get; set; } - - public CompletionItemKind? Kind { get; set; } - - public string Detail { get; set; } - - /// - /// Gets or sets the documentation string for the completion item. - /// - public string Documentation { get; set; } - - public string SortText { get; set; } - - public string FilterText { get; set; } - - public string InsertText { get; set; } - - public InsertTextFormat InsertTextFormat { get; set; } = InsertTextFormat.PlainText; - - public Range Range { get; set; } - - public string[] CommitCharacters { get; set; } - - public TextEdit TextEdit { get; set; } - - public TextEdit[] AdditionalTextEdits { get; set; } - - public CommandType Command { get; set; } - - /// - /// Gets or sets a custom data field that allows the server to mark - /// each completion item with an identifier that will help correlate - /// the item to the previous completion request during a completion - /// resolve request. - /// - public object Data { get; set; } - } - - /// - /// Represents a reference to a command. Provides a title which will be used to - /// represent a command in the UI and, optionally, an array of arguments which - /// will be passed to the command handler function when invoked. - /// - /// The name of the corresponding type in vscode-languageserver-node is Command - /// but since .net does not allow a property name (Command) and its enclosing - /// type name to be the same we change its name to CommandType. - /// - public class CommandType - { - /// - /// Title of the command. - /// - /// - public string Title { get; set; } - - /// - /// The identifier of the actual command handler. - /// - public string Command { get; set; } - - /// - /// Arguments that the command handler should be invoked with. - /// - public object[] Arguments { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Configuration.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Configuration.cs deleted file mode 100644 index a4228ee19..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Configuration.cs +++ /dev/null @@ -1,21 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class DidChangeConfigurationNotification - { - public static readonly - NotificationType, object> Type = - NotificationType, object>.Create("workspace/didChangeConfiguration"); - } - - public class DidChangeConfigurationParams - { - public TConfig Settings { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs deleted file mode 100644 index cee9a7215..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs +++ /dev/null @@ -1,17 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class DefinitionRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/definition"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Diagnostics.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Diagnostics.cs deleted file mode 100644 index e5e46d45f..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Diagnostics.cs +++ /dev/null @@ -1,81 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class PublishDiagnosticsNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("textDocument/publishDiagnostics"); - - /// - /// Gets or sets the URI for which diagnostic information is reported. - /// - public string Uri { get; set; } - - /// - /// Gets or sets the array of diagnostic information items. - /// - public Diagnostic[] Diagnostics { get; set; } - } - - public enum DiagnosticSeverity - { - /// - /// Indicates that the diagnostic represents an error. - /// - Error = 1, - - /// - /// Indicates that the diagnostic represents a warning. - /// - Warning = 2, - - /// - /// Indicates that the diagnostic represents an informational message. - /// - Information = 3, - - /// - /// Indicates that the diagnostic represents a hint. - /// - Hint = 4 - } - - public class Diagnostic - { - public Range Range { get; set; } - - /// - /// Gets or sets the severity of the diagnostic. If omitted, the - /// client should interpret the severity. - /// - public DiagnosticSeverity? Severity { get; set; } - - /// - /// Gets or sets the diagnostic's code (optional). - /// - public string Code { get; set; } - - /// - /// Gets or sets the diagnostic message. - /// - public string Message { get; set; } - - /// - /// Gets or sets the source of the diagnostic message. - /// - public string Source { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs deleted file mode 100644 index cf8196f18..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs +++ /dev/null @@ -1,31 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public enum DocumentHighlightKind - { - Text = 1, - Read = 2, - Write = 3 - } - - public class DocumentHighlight - { - public Range Range { get; set; } - - public DocumentHighlightKind Kind { get; set; } - } - - public class DocumentHighlightRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/documentHighlight"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/DynamicRegistrationCapability.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/DynamicRegistrationCapability.cs deleted file mode 100644 index 64d2c35ce..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/DynamicRegistrationCapability.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Class to represent if a capability supports dynamic registration. - /// - public class DynamicRegistrationCapability - { - /// - /// Whether the capability supports dynamic registration. - /// - public bool? DynamicRegistration { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs deleted file mode 100644 index a7c385bc6..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/EditorCommands.cs +++ /dev/null @@ -1,185 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class ExtensionCommandAddedNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("powerShell/extensionCommandAdded"); - - public string Name { get; set; } - - public string DisplayName { get; set; } - } - - public class ExtensionCommandUpdatedNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("powerShell/extensionCommandUpdated"); - - public string Name { get; set; } - } - - public class ExtensionCommandRemovedNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("powerShell/extensionCommandRemoved"); - - public string Name { get; set; } - } - - public class ClientEditorContext - { - public string CurrentFileContent { get; set; } - - public string CurrentFileLanguage { get; set; } - - public string CurrentFilePath { get; set; } - - public Position CursorPosition { get; set; } - - public Range SelectionRange { get; set; } - - } - - public class InvokeExtensionCommandRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/invokeExtensionCommand"); - - public string Name { get; set; } - - public ClientEditorContext Context { get; set; } - } - - public class GetEditorContextRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/getEditorContext"); - } - - public enum EditorCommandResponse - { - Unsupported, - OK - } - - public class InsertTextRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/insertText"); - - public string FilePath { get; set; } - - public string InsertText { get; set; } - - public Range InsertRange { get; set; } - } - - public class SetSelectionRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/setSelection"); - - public Range SelectionRange { get; set; } - } - - public class SetCursorPositionRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/setCursorPosition"); - - public Position CursorPosition { get; set; } - } - - public class NewFileRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/newFile"); - } - - public class OpenFileRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/openFile"); - } - - public class OpenFileDetails - { - public string FilePath { get; set; } - - public bool Preview { get; set; } - } - - public class CloseFileRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/closeFile"); - } - - public class SaveFileRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/saveFile"); - } - - public class SaveFileDetails - { - public string FilePath { get; set; } - - public string NewPath { get; set; } - } - - public class ShowInformationMessageRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/showInformationMessage"); - } - - public class ShowWarningMessageRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/showWarningMessage"); - } - - public class ShowErrorMessageRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/showErrorMessage"); - } - - public class SetStatusBarMessageRequest - { - public static readonly - RequestType Type = - RequestType.Create("editor/setStatusBarMessage"); - } - - public class StatusBarMessageDetails - { - public string Message { get; set; } - - public int? Timeout { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ExecutionStatusChangedEvent.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ExecutionStatusChangedEvent.cs deleted file mode 100644 index 5ac6eac6d..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ExecutionStatusChangedEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Defines an event type for PowerShell context execution status changes (e.g. execution has completed) - /// - public class ExecutionStatusChangedEvent - { - /// - /// The notification type for execution status change events in the message protocol - /// - public static readonly - NotificationType Type = - NotificationType.Create("powerShell/executionStatusChanged"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs deleted file mode 100644 index d5c2a9bc9..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class ExpandAliasRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/expandAlias"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/FindModuleRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/FindModuleRequest.cs deleted file mode 100644 index fe14fdb65..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/FindModuleRequest.cs +++ /dev/null @@ -1,24 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class FindModuleRequest - { - public static readonly - RequestType, object, object, object> Type = - RequestType, object, object, object>.Create("powerShell/findModule"); - } - - - public class PSModuleMessage - { - public string Name { get; set; } - public string Description { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs deleted file mode 100644 index f7b1d8f01..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs +++ /dev/null @@ -1,69 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class FoldingRangeRequest - { - /// - /// A request to provide folding ranges in a document. The request's - /// parameter is of type [FoldingRangeParams](#FoldingRangeParams), the - /// response is of type [FoldingRangeList](#FoldingRangeList) or a Thenable - /// that resolves to such. - /// Ref: https://github.com/Microsoft/vscode-languageserver-node/blob/5350bc2ffe8afb17357c1a66fbdd3845fa05adfd/protocol/src/protocol.foldingRange.ts#L112-L120 - /// - public static readonly - RequestType Type = - RequestType.Create("textDocument/foldingRange"); - } - - /// - /// Parameters for a [FoldingRangeRequest](#FoldingRangeRequest). - /// Ref: https://github.com/Microsoft/vscode-languageserver-node/blob/5350bc2ffe8afb17357c1a66fbdd3845fa05adfd/protocol/src/protocol.foldingRange.ts#L102-L110 - /// - public class FoldingRangeParams - { - /// - /// The text document - /// - public TextDocumentIdentifier TextDocument { get; set; } - } - - /// - /// Represents a folding range. - /// Ref: https://github.com/Microsoft/vscode-languageserver-node/blob/5350bc2ffe8afb17357c1a66fbdd3845fa05adfd/protocol/src/protocol.foldingRange.ts#L69-L100 - /// - public class FoldingRange - { - /// - /// The zero-based line number from where the folded range starts. - /// - public int StartLine { get; set; } - - /// - /// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. - /// - public int StartCharacter { get; set; } - - /// - /// The zero-based line number where the folded range ends. - /// - public int EndLine { get; set; } - - /// - /// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. - /// - public int EndCharacter { get; set; } - - /// - /// Describes the kind of the folding range such as `comment' or 'region'. The kind - /// is used to categorize folding ranges and used by commands like 'Fold all comments'. See - /// [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. - /// - public string Kind { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs deleted file mode 100644 index f063ba163..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs +++ /dev/null @@ -1,95 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class DocumentFormattingRequest - { - public static readonly RequestType Type = RequestType.Create("textDocument/formatting"); - } - - public class DocumentRangeFormattingRequest - { - public static readonly RequestType Type = RequestType.Create("textDocument/rangeFormatting"); - - } - - public class DocumentOnTypeFormattingRequest - { - public static readonly RequestType Type = RequestType.Create("textDocument/onTypeFormatting"); - - } - - public class DocumentRangeFormattingParams - { - /// - /// The document to format. - /// - public TextDocumentIdentifier TextDocument { get; set; } - - /// - /// The range to format. - /// - /// - public Range Range { get; set; } - - /// - /// The format options. - /// - public FormattingOptions Options { get; set; } - } - - public class DocumentOnTypeFormattingParams - { - /// - /// The document to format. - /// - public TextDocumentIdentifier TextDocument { get; set; } - - /// - /// The position at which this request was sent. - /// - public Position Position { get; set; } - - /// - /// The character that has been typed. - /// - public string ch { get; set; } - - /// - /// The format options. - /// - public FormattingOptions options { get; set; } - } - - public class DocumentFormattingParams - { - /// - /// The document to format. - /// - public TextDocumentIdentifier TextDocument { get; set; } - - /// - /// The format options. - /// - public FormattingOptions options { get; set; } - } - - public class FormattingOptions - { - /// - /// Size of a tab in spaces. - /// - public int TabSize { get; set; } - - /// - /// Prefer spaces over tabs. - /// - public bool InsertSpaces { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/GetCommandRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/GetCommandRequest.cs deleted file mode 100644 index df847dcbe..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/GetCommandRequest.cs +++ /dev/null @@ -1,34 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System.Collections.Generic; -using System.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Describes the request to get the details for PowerShell Commands from the current session. - /// - public class GetCommandRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getCommand"); - } - - /// - /// Describes the message to get the details for a single PowerShell Command - /// from the current session - /// - public class PSCommandMessage - { - public string Name { get; set; } - public string ModuleName { get; set; } - public string DefaultParameterSet { get; set; } - public Dictionary Parameters { get; set; } - public System.Collections.ObjectModel.ReadOnlyCollection ParameterSets { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/GetPSHostProcessesRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/GetPSHostProcessesRequest.cs deleted file mode 100644 index 3c33a61d3..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/GetPSHostProcessesRequest.cs +++ /dev/null @@ -1,27 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class GetPSHostProcessesRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getPSHostProcesses"); - } - - public class GetPSHostProcessesResponse - { - public string ProcessName { get; set; } - - public int ProcessId { get; set; } - - public string AppDomainName { get; set; } - - public string MainWindowTitle { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/GetPSSARulesRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/GetPSSARulesRequest.cs deleted file mode 100644 index 2199558c5..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/GetPSSARulesRequest.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - class GetPSSARulesRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getPSSARules"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs deleted file mode 100644 index e151aa7f0..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/GetRunspaceRequest.cs +++ /dev/null @@ -1,25 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class GetRunspaceRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getRunspace"); - } - - public class GetRunspaceResponse - { - public int Id { get; set; } - - public string Name { get; set; } - - public string Availability { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs deleted file mode 100644 index 42c853254..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs +++ /dev/null @@ -1,37 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class MarkedString - { - public string Language { get; set; } - - public string Value { get; set; } - } - - public class Hover - { - public MarkedString[] Contents { get; set; } - - public Range Range { get; set; } - } - - public class HoverRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/hover"); - - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs deleted file mode 100644 index 234b02169..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs +++ /dev/null @@ -1,79 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class InitializeRequest - { - public static readonly - RequestType Type = - RequestType.Create("initialize"); - } - - public enum TraceType { - Off, - Messages, - Verbose - } - - public class InitializeParams { - /// - /// The process Id of the parent process that started the server - /// - public int ProcessId { get; set; } - - /// - /// The root path of the workspace. It is null if no folder is open. - /// - /// This property has been deprecated in favor of RootUri. - /// - public string RootPath { get; set; } - - /// - /// The root uri of the workspace. It is null if not folder is open. If both - /// `RootUri` and `RootPath` are non-null, `RootUri` should be used. - /// - public string RootUri { get; set; } - - /// - /// The capabilities provided by the client. - /// - public ClientCapabilities Capabilities { get; set; } - - /// - /// User provided initialization options. - /// - /// This is defined as `any` type on the client side. - /// - public object InitializationOptions { get; set; } - - // TODO We need to verify if the deserializer will map the type defined in the client - // to an enum. - /// - /// The initial trace setting. If omitted trace is disabled. - /// - public TraceType Trace { get; set; } = TraceType.Off; - } - - public class InitializeResult - { - /// - /// Gets or sets the capabilities provided by the language server. - /// - public ServerCapabilities Capabilities { get; set; } - } - - public class InitializeError - { - /// - /// Gets or sets a boolean indicating whether the client should retry - /// sending the Initialize request after showing the error to the user. - /// - public bool Retry { get; set;} - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/InitializedNotification.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/InitializedNotification.cs deleted file mode 100644 index 90836017f..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/InitializedNotification.cs +++ /dev/null @@ -1,29 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// The initialized notification is sent from the client to the server after the client received the result - /// of the initialize request but before the client is sending any other request or notification to the server. - /// The server can use the initialized notification for example to dynamically register capabilities. - /// The initialized notification may only be sent once. - /// - public class InitializedNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("initialized"); - } - - /// - /// Currently, the initialized message has no parameters. - /// - public class InitializedParams - { - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/InstallModuleRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/InstallModuleRequest.cs deleted file mode 100644 index 096d0e461..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/InstallModuleRequest.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - class InstallModuleRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/installModule"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs deleted file mode 100644 index 8f16b0abb..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/PowerShellVersionRequest.cs +++ /dev/null @@ -1,52 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Session; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class PowerShellVersionRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getVersion"); - } - - public class PowerShellVersion - { - public string Version { get; set; } - - public string DisplayVersion { get; set; } - - public string Edition { get; set; } - - public string Architecture { get; set; } - - public PowerShellVersion() - { - } - - public PowerShellVersion(PowerShellVersionDetails versionDetails) - { - this.Version = versionDetails.VersionString; - this.DisplayVersion = $"{versionDetails.Version.Major}.{versionDetails.Version.Minor}"; - this.Edition = versionDetails.Edition; - - switch (versionDetails.Architecture) - { - case PowerShellProcessArchitecture.X64: - this.Architecture = "x64"; - break; - case PowerShellProcessArchitecture.X86: - this.Architecture = "x86"; - break; - default: - this.Architecture = "Architecture Unknown"; - break; - } - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ProjectTemplate.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ProjectTemplate.cs deleted file mode 100644 index 1b7441b28..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ProjectTemplate.cs +++ /dev/null @@ -1,42 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Templates; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class NewProjectFromTemplateRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/newProjectFromTemplate"); - - public string DestinationPath { get; set; } - - public string TemplatePath { get; set; } - } - - public class NewProjectFromTemplateResponse - { - public bool CreationSuccessful { get; set; } - } - - public class GetProjectTemplatesRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getProjectTemplates"); - - public bool IncludeInstalledModules { get; set; } - } - - public class GetProjectTemplatesResponse - { - public bool NeedsModuleInstall { get; set; } - - public TemplateDetails[] Templates { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs deleted file mode 100644 index 6965cb073..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs +++ /dev/null @@ -1,27 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class ReferencesRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/references"); - } - - public class ReferencesParams : TextDocumentPositionParams - { - public ReferencesContext Context { get; set; } - } - - public class ReferencesContext - { - public bool IncludeDeclaration { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs deleted file mode 100644 index 3f2448097..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/RunspaceChanged.cs +++ /dev/null @@ -1,37 +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 Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class RunspaceChangedEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("powerShell/runspaceChanged"); - } - - public class RunspaceDetails - { - public PowerShellVersion PowerShellVersion { get; set; } - - public RunspaceLocation RunspaceType { get; set; } - - public string ConnectionString { get; set; } - - public RunspaceDetails() - { - } - - public RunspaceDetails(Session.RunspaceDetails eventArgs) - { - this.PowerShellVersion = new PowerShellVersion(eventArgs.PowerShellVersion); - this.RunspaceType = eventArgs.Location; - this.ConnectionString = eventArgs.ConnectionString; - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ScriptRegionRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ScriptRegionRequest.cs deleted file mode 100644 index 79b36161e..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ScriptRegionRequest.cs +++ /dev/null @@ -1,57 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Class to encapsulate the request type. - /// - class ScriptRegionRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getScriptRegion"); - } - - /// - /// Class to encapsulate the request parameters. - /// - class ScriptRegionRequestParams - { - /// - /// Path of the file for which the formatting region is requested. - /// - public string FileUri; - - /// - /// Hint character. - /// - public string Character; - - /// - /// 1-based line number of the character. - /// - public int Line; - - /// - /// 1-based column number of the character. - /// - public int Column; - } - - /// - /// Class to encapsulate the result of ScriptRegionRequest. - /// - class ScriptRegionRequestResult - { - /// - /// A region in the script that encapsulates the given character/position which is suitable - /// for formatting - /// - public ScriptRegion scriptRegion; - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs deleted file mode 100644 index e53ca4f6c..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs +++ /dev/null @@ -1,121 +0,0 @@ -// -// 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.LanguageServer -{ - public class ServerCapabilities - { - public TextDocumentSyncKind? TextDocumentSync { get; set; } - - public bool? HoverProvider { get; set; } - - public CompletionOptions CompletionProvider { get; set; } - - public SignatureHelpOptions SignatureHelpProvider { get; set; } - - public bool? DefinitionProvider { get; set; } - - public bool? ReferencesProvider { get; set; } - - public bool? DocumentHighlightProvider { get; set; } - - public bool? DocumentSymbolProvider { get; set; } - - public bool? WorkspaceSymbolProvider { get; set; } - - public bool? CodeActionProvider { get; set; } - - public CodeLensOptions CodeLensProvider { get; set; } - - public bool? DocumentFormattingProvider { get; set; } - - public bool? DocumentRangeFormattingProvider { get; set; } - - public DocumentOnTypeFormattingOptions DocumentOnTypeFormattingProvider { get; set; } - - public bool? RenameProvider { get; set; } - - public DocumentLinkOptions DocumentLinkProvider { get; set; } - - public ExecuteCommandOptions ExecuteCommandProvider { get; set; } - - public object Experimental { get; set; } - - public bool FoldingRangeProvider { get; set; } = false; - } - - /// - /// Execute command options. - /// - public class ExecuteCommandOptions - { - /// - /// The commands to be executed on the server. - /// - public string[] Commands { get; set; } - } - - /// - /// Document link options. - /// - public class DocumentLinkOptions - { - /// - /// Document links have a resolve provider. - /// - public bool? ResolveProvider { get; set; } - } - - /// - /// Options that the server provides for OnTypeFormatting request. - /// - public class DocumentOnTypeFormattingOptions - { - /// - /// A character on which formatting should be triggered. - /// - public string FirstTriggerCharacter { get; set; } - - /// - /// More trigger characters. - /// - public string[] MoreTriggerCharacters { get; set; } - } - - /// - /// Defines the document synchronization strategies that a server may support. - /// - public enum TextDocumentSyncKind - { - /// - /// Indicates that documents should not be synced at all. - /// - None = 0, - - /// - /// Indicates that document changes are always sent with the full content. - /// - Full, - - /// - /// Indicates that document changes are sent as incremental changes after - /// the initial document content has been sent. - /// - Incremental - } - - public class CompletionOptions - { - public bool? ResolveProvider { get; set; } - - public string[] TriggerCharacters { get; set; } - } - - public class SignatureHelpOptions - { - public string[] TriggerCharacters { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs deleted file mode 100644 index b299b5606..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs +++ /dev/null @@ -1,28 +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 Newtonsoft.Json.Linq; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class ServerCommand - { - /// - /// Title of the command, like `save`. - /// - public string Title { get; set; } - - /// - /// The identifier of the actual command handler. - /// - public string Command { get; set; } - - /// - /// Arguments that the command handler should be - /// invoked with. - /// - public JArray Arguments { get; set; } - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/SetPSSARulesRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/SetPSSARulesRequest.cs deleted file mode 100644 index e3298cbd8..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/SetPSSARulesRequest.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - class SetPSSARulesRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/setPSSARules"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs deleted file mode 100644 index 0d73074d2..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ShowHelpRequest.cs +++ /dev/null @@ -1,18 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - - public class ShowHelpRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/showHelp"); - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs deleted file mode 100644 index f5fffce1c..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs +++ /dev/null @@ -1,32 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Defines a message that is sent from the client to request - /// that the server shut down. - /// - public class ShutdownRequest - { - public static readonly - RequestType0 Type = - RequestType0.Create("shutdown"); - } - - /// - /// Defines an event that is sent from the client to notify that - /// the client is exiting and the server should as well. - /// - public class ExitNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("exit"); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs deleted file mode 100644 index e24fa6358..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs +++ /dev/null @@ -1,50 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class SignatureHelpRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/signatureHelp"); - } - - public class SignatureHelpRegistrationOptions : TextDocumentRegistrationOptions - { - // We duplicate the properties of SignatureHelpOptions class here because - // we cannot derive from two classes. One way to get around this situation - // is to use define SignatureHelpOptions as an interface instead of a class. - public string[] TriggerCharacters { get; set; } - } - - public class ParameterInformation - { - public string Label { get; set; } - - public string Documentation { get; set; } - } - - public class SignatureInformation - { - public string Label { get; set; } - - public string Documentation { get; set; } - - public ParameterInformation[] Parameters { get; set; } - } - - public class SignatureHelp - { - public SignatureInformation[] Signatures { get; set; } - - public int? ActiveSignature { get; set; } - - public int? ActiveParameter { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/StartDebuggerEvent.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/StartDebuggerEvent.cs deleted file mode 100644 index 49adef43a..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/StartDebuggerEvent.cs +++ /dev/null @@ -1,16 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class StartDebuggerEvent - { - public static readonly - NotificationType Type = - NotificationType.Create("powerShell/startDebugger"); - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs deleted file mode 100644 index edd66f41a..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs +++ /dev/null @@ -1,313 +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.Diagnostics; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - - /// - /// An item to transfer a text document from the client to the server - /// - [DebuggerDisplay("TextDocumentItem = {Uri}:{LanguageId}:{Version}:{Text}")] - public class TextDocumentItem - { - /// - /// Gets or sets the URI which identifies the path of the - /// text document. - /// - public string Uri { get; set; } - - /// - /// The text document's language identifier. - /// - /// - public string LanguageId { get; set; } - - /// - /// The version number of this document, which will strictly increase after each change, including - /// undo/redo. - /// - /// - public int Version { get; set; } - - /// - /// The content of the opened text document. - /// - /// - public string Text { get; set; } - } - - /// - /// Defines a base parameter class for identifying a text document. - /// - [DebuggerDisplay("TextDocumentIdentifier = {Uri}")] - public class TextDocumentIdentifier - { - /// - /// Gets or sets the URI which identifies the path of the - /// text document. - /// - public string Uri { get; set; } - } - - /// - /// An identifier to denote a specific version of a text document. - /// - public class VersionedTextDocumentIdentifier : TextDocumentIdentifier - { - /// - /// The version number of this document. - /// - public int Version { get; set; } - } - - /// - /// A parameter literal used in requests to pass a text document and a position inside that document. - /// - public class TextDocumentPositionParams - { - /// - /// The text document. - /// - /// - public TextDocumentIdentifier TextDocument { get; set; } - - /// - /// The position inside the text document. - /// - /// - public Position Position { get; set; } - } - - public class DidOpenTextDocumentNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("textDocument/didOpen"); - } - - /// - /// The parameters sent in an open text document notification - /// - public class DidOpenTextDocumentParams - { - /// - /// The document that was opened. - /// - public TextDocumentItem TextDocument { get; set; } - } - - /// - /// General text document registration options. - /// - public class TextDocumentRegistrationOptions { - /// - /// A document selector to identify the scope of the registration. If set to null the document - /// selector provided on the client side will be used. - /// - public DocumentFilter[] DocumentSelector { get; set; } - } - - /// - /// A document filter denotes a document by different properties like the language, the scheme - /// of its resource, or a glob-pattern that is applied to the path. - /// - public class DocumentFilter - { - /// - /// A language id, like `powershell` - /// - public string Language { get; set; } - - /// - /// A Uri, like `file` or `untitled` - /// - public string Scheme { get; set; } - - /// - /// A glob pattern, like `*.{ps1,psd1}` - /// - public string Pattern { get; set; } - } - - public class DidCloseTextDocumentNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("textDocument/didClose"); - } - - /// - /// The parameters sent in a close text document notification. - /// - public class DidCloseTextDocumentParams - { - /// - /// The document that was closed. - /// - public TextDocumentIdentifier TextDocument { get; set; } - } - - public class DidSaveTextDocumentNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("textDocument/didSave"); - } - - /// - /// Save options. - /// - public class SaveOptions { - /// - /// The client is supposed to include the content on save. - /// - public bool? IncludeText { get; set; } - } - - public class TextDocumentSaveRegistrationOptions : TextDocumentRegistrationOptions - { - // We cannot inherit from two base classes (SaveOptions and TextDocumentRegistrationOptions) - // simultaneously, hence we repeat this IncludeText flag here. - /// - /// The client is supposed to include the content on save. - /// - public bool? IncludeText { get; set; } - } - - /// - /// The parameters sent in a save text document notification. - /// - public class DidSaveTextDocumentParams - { - /// - /// The document that was saved. - /// - public VersionedTextDocumentIdentifier TextDocument { get; set; } - - /// - /// Optional content when saved. Depends on the includeText value when the save notification was - /// included. - /// - public string Text { get; set; } - } - - public class DidChangeTextDocumentNotification - { - public static readonly - NotificationType Type = - NotificationType.Create("textDocument/didChange"); - } - - /// - /// Describe options to be used when registered for text document change events. - /// - public class TextDocumentChangeRegistrationOptions : TextDocumentRegistrationOptions - { - /// - /// How documents are synced to the server. - /// - public TextDocumentSyncKind SyncKind { get; set; } - } - - /// - /// The change text document notification's paramters. - /// - public class DidChangeTextDocumentParams - { - /// - /// The document that did change. The version number points to the version after - /// all provided content changes have been applied. - /// - public VersionedTextDocumentIdentifier TextDocument; - - /// - /// Gets or sets the list of changes to the document content. - /// - public TextDocumentChangeEvent[] ContentChanges { get; set; } - } - - public class TextDocumentChangeEvent - { - /// - /// Gets or sets the Range where the document was changed. Will - /// be null if the server's TextDocumentSyncKind is Full. - /// - public Range Range { get; set; } - - /// - /// Gets or sets the length of the Range being replaced in the - /// document. Will be null if the server's TextDocumentSyncKind is - /// Full. - /// - public int? RangeLength { get; set; } - - /// - /// Gets or sets the new text of the document. - /// - public string Text { get; set; } - } - - [DebuggerDisplay("Position = {Line}:{Character}")] - public class Position - { - /// - /// Gets or sets the zero-based line number. - /// - public int Line { get; set; } - - /// - /// Gets or sets the zero-based column number. - /// - public int Character { get; set; } - } - - [DebuggerDisplay("Start = {Start.Line}:{Start.Character}, End = {End.Line}:{End.Character}")] - public class Range - { - /// - /// Gets or sets the starting position of the range. - /// - public Position Start { get; set; } - - /// - /// Gets or sets the ending position of the range. - /// - public Position End { get; set; } - } - - [DebuggerDisplay("Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}, Uri = {Uri}")] - public class Location - { - /// - /// Gets or sets the URI indicating the file in which the location refers. - /// - public string Uri { get; set; } - - /// - /// Gets or sets the Range indicating the range in which location refers. - /// - public Range Range { get; set; } - } - - public enum FileChangeType - { - Created = 1, - - Changed, - - Deleted - } - - public class FileEvent - { - public string Uri { get; set; } - - public FileChangeType Type { get; set; } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocumentClientCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocumentClientCapabilities.cs deleted file mode 100644 index c620ad056..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocumentClientCapabilities.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class TextDocumentClientCapabilities - { - /// - /// Synchronization capabilities the client supports. - /// - public SynchronizationCapabilities Synchronization { get; set; } - - /// - /// Capabilities specific to `textDocument/completion`. - /// - public CompletionCapabilities Completion { get; set; } - - /// - /// Capabilities specific to the `textDocument/hover`. - /// - public DynamicRegistrationCapability Hover { get; set; } - - /// - /// Capabilities specific to the `textDocument/signatureHelp`. - /// - public DynamicRegistrationCapability SignatureHelp { get; set; } - - /// - /// Capabilities specific to the `textDocument/references`. - /// - public DynamicRegistrationCapability References { get; set; } - - /// - /// Capabilities specific to the `textDocument/documentHighlight`. - /// - public DynamicRegistrationCapability DocumentHighlight { get; set; } - - /// - /// Capabilities specific to the `textDocument/documentSymbol`. - /// - public DynamicRegistrationCapability DocumentSymbol { get; set; } - - /// - /// Capabilities specific to the `textDocument/formatting`. - /// - public DynamicRegistrationCapability Formatting { get; set; } - - /// - /// Capabilities specific to the `textDocument/rangeFormatting`. - /// - public DynamicRegistrationCapability RangeFormatting { get; set; } - - /// - /// Capabilities specific to the `textDocument/onTypeFormatting`. - /// - public DynamicRegistrationCapability OnTypeFormatting { get; set; } - - /// - /// Capabilities specific to the `textDocument/definition`. - /// - public DynamicRegistrationCapability Definition { get; set; } - - /// - /// Capabilities specific to the `textDocument/codeAction`. - /// - public DynamicRegistrationCapability CodeAction { get; set; } - - /// - /// Capabilities specific to the `textDocument/codeLens`. - /// - public DynamicRegistrationCapability CodeLens { get; set; } - - /// - /// Capabilities specific to the `textDocument/documentLink`. - /// - public DynamicRegistrationCapability DocumentLink { get; set; } - - /// - /// Capabilities specific to the `textDocument/rename`. - /// - public DynamicRegistrationCapability Rename { get; set; } - } - - /// - /// Class to represent capabilities specific to `textDocument/completion`. - /// - public class CompletionCapabilities : DynamicRegistrationCapability - { - /// - /// The client supports the following `CompletionItem` specific capabilities. - /// - /// - public CompletionItemCapabilities CompletionItem { get; set; } - } - - /// - /// Class to represent capabilities specific to `CompletionItem`. - /// - public class CompletionItemCapabilities - { - /// - /// Client supports snippets as insert text. - /// - /// A snippet can define tab stops and placeholders with `$1`, `$2` - /// and `${3:foo}`. `$0` defines the final tab stop, it defaults to - /// the end of the snippet. Placeholders with equal identifiers are linked, - /// that is typing in one will update others too. - /// - public bool? SnippetSupport { get; set; } - } - - /// - /// Class to represent synchronization capabilities the client supports. - /// - public class SynchronizationCapabilities : DynamicRegistrationCapability - { - /// - /// The client supports sending will save notifications. - /// - public bool? WillSave { get; set; } - - /// - /// The client supports sending a will save request and waits for a response - /// providing text edits which will be applied to the document before it is save. - /// - public bool? WillSaveWaitUntil { get; set; } - - /// - /// The client supports did save notifications. - /// - public bool? DidSave { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs deleted file mode 100644 index e83fdec5a..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceClientCapabilities.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public class WorkspaceClientCapabilities - { - /// - /// The client supports applying batch edits to the workspace by - /// by supporting the request `workspace/applyEdit' - /// /// - public bool? ApplyEdit { get; set; } - - - /// - /// Capabilities specific to `WorkspaceEdit`. - /// - public WorkspaceEditCapabilities WorkspaceEdit { get; set; } - - /// - /// Capabilities specific to the `workspace/didChangeConfiguration` notification. - /// - public DynamicRegistrationCapability DidChangeConfiguration { get; set; } - - /// - /// Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - /// - public DynamicRegistrationCapability DidChangeWatchedFiles { get; set; } - - /// - /// Capabilities specific to the `workspace/symbol` request. - /// - public DynamicRegistrationCapability Symbol { get; set; } - - /// - /// Capabilities specific to the `workspace/executeCommand` request. - /// - public DynamicRegistrationCapability ExecuteCommand { get; set; } - } - - /// - /// Class to represent capabilities specific to `WorkspaceEdit`. - /// - public class WorkspaceEditCapabilities - { - /// - /// The client supports versioned document changes in `WorkspaceEdit` - /// - public bool? DocumentChanges { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs deleted file mode 100644 index 97e990345..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs +++ /dev/null @@ -1,73 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - public enum SymbolKind - { - File = 1, - Module = 2, - Namespace = 3, - Package = 4, - Class = 5, - Method = 6, - Property = 7, - Field = 8, - Constructor = 9, - Enum = 10, - Interface = 11, - Function = 12, - Variable = 13, - Constant = 14, - String = 15, - Number = 16, - Boolean = 17, - Array = 18, - } - - public class SymbolInformation - { - public string Name { get; set; } - - public SymbolKind Kind { get; set; } - - public Location Location { get; set; } - - public string ContainerName { get; set;} - } - - public class DocumentSymbolRequest - { - public static readonly - RequestType Type = - RequestType.Create("textDocument/documentSymbol"); - } - - /// - /// Parameters for a DocumentSymbolRequest - /// - public class DocumentSymbolParams - { - /// - /// The text document. - /// - public TextDocumentIdentifier TextDocument { get; set; } - } - - public class WorkspaceSymbolRequest - { - public static readonly - RequestType Type = - RequestType.Create("workspace/symbol"); - } - - public class WorkspaceSymbolParams - { - public string Query { get; set;} - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/AbstractMessageType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/AbstractMessageType.cs deleted file mode 100644 index 1b2fc37f9..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/AbstractMessageType.cs +++ /dev/null @@ -1,35 +0,0 @@ -// -// 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.MessageProtocol -{ - /// - /// Defines an event type with a particular method name. - /// - /// The parameter type for this event. - public class AbstractMessageType - { - private string _method; - private int _numberOfParams; - - /// - /// Gets the method name for the event type. - /// - public string Method { get { return _method; } } - - /// - /// Gets the number of parameters. - /// - public int NumberOfParams { get; } - - public AbstractMessageType(string method, int numberOfParams) - { - _method = method; - _numberOfParams = numberOfParams; - } - } -} - - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs deleted file mode 100644 index ea4922938..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs +++ /dev/null @@ -1,68 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - /// - /// Defines a base implementation for servers and their clients over a - /// single kind of communication channel. - /// - public abstract class ChannelBase - { - /// - /// Gets the MessageReader for reading messages from the channel. - /// - public MessageReader MessageReader { get; protected set; } - - /// - /// Gets the MessageWriter for writing messages to the channel. - /// - public MessageWriter MessageWriter { get; protected set; } - - /// - /// Starts the channel and initializes the MessageDispatcher. - /// - /// The type of message protocol used by the channel. - public void Start(MessageProtocolType messageProtocolType) - { - IMessageSerializer messageSerializer = null; - if (messageProtocolType == MessageProtocolType.LanguageServer) - { - messageSerializer = new JsonRpcMessageSerializer(); - } - else - { - messageSerializer = new V8MessageSerializer(); - } - - this.Initialize(messageSerializer); - } - - /// - /// Stops the channel. - /// - public void Stop() - { - this.Shutdown(); - } - - /// - /// A method to be implemented by subclasses to handle the - /// actual initialization of the channel and the creation and - /// assignment of the MessageReader and MessageWriter properties. - /// - /// The IMessageSerializer to use for message serialization. - protected abstract void Initialize(IMessageSerializer messageSerializer); - - /// - /// A method to be implemented by subclasses to handle shutdown - /// of the channel once Stop is called. - /// - protected abstract void Shutdown(); - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs deleted file mode 100644 index 1d3bd8a7d..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs +++ /dev/null @@ -1,84 +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 System.Runtime.InteropServices; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - public class NamedPipeClientChannel : ChannelBase - { - private ILogger logger; - private NamedPipeClientStream pipeClient; - - // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. - private const int CurrentUserOnly = 536870912; - - private const string NAMED_PIPE_UNIX_PREFIX = "CoreFxPipe_"; - - public NamedPipeClientChannel( - NamedPipeClientStream pipeClient, - ILogger logger) - { - this.pipeClient = pipeClient; - this.logger = logger; - } - - protected override void Initialize(IMessageSerializer messageSerializer) - { - this.MessageReader = - new MessageReader( - this.pipeClient, - messageSerializer, - this.logger); - - this.MessageWriter = - new MessageWriter( - this.pipeClient, - messageSerializer, - this.logger); - } - - protected override void Shutdown() - { - if (this.pipeClient != null) - { - this.pipeClient.Dispose(); - } - } - - public static async Task ConnectAsync( - string pipeFile, - MessageProtocolType messageProtocolType, - ILogger logger) - { - string pipeName = System.IO.Path.GetFileName(pipeFile); - - var options = PipeOptions.Asynchronous; - // on macOS and Linux, the named pipe name is prefixed by .NET Core - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - pipeName = pipeFile.Split(new [] {NAMED_PIPE_UNIX_PREFIX}, StringSplitOptions.None)[1]; - options |= (PipeOptions)CurrentUserOnly; - } - - var pipeClient = - new NamedPipeClientStream( - ".", - pipeName, - PipeDirection.InOut, - options); - - await pipeClient.ConnectAsync(); - var clientChannel = new NamedPipeClientChannel(pipeClient, logger); - clientChannel.Start(messageProtocolType); - - return clientChannel; - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs deleted file mode 100644 index 870640c49..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs +++ /dev/null @@ -1,57 +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 Microsoft.PowerShell.EditorServices.Utility; -using System.IO.Pipes; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - public class NamedPipeServerChannel : ChannelBase - { - private ILogger logger; - private NamedPipeServerStream inOutPipeServer; - private NamedPipeServerStream outPipeServer; - - public NamedPipeServerChannel( - NamedPipeServerStream inOutPipeServer, - ILogger logger) - { - this.inOutPipeServer = inOutPipeServer; - this.logger = logger; - } - public NamedPipeServerChannel( - NamedPipeServerStream inOutPipeServer, - NamedPipeServerStream outPipeServer, - ILogger logger) - { - this.inOutPipeServer = inOutPipeServer; - this.outPipeServer = outPipeServer; - this.logger = logger; - } - - protected override void Initialize(IMessageSerializer messageSerializer) - { - this.MessageReader = - new MessageReader( - this.inOutPipeServer, - messageSerializer, - this.logger); - - this.MessageWriter = - new MessageWriter( - this.outPipeServer ?? this.inOutPipeServer, - messageSerializer, - this.logger); - } - - protected override void Shutdown() - { - // The server listener will take care of the pipe server - this.inOutPipeServer = null; - this.outPipeServer = null; - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs deleted file mode 100644 index df79c806a..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs +++ /dev/null @@ -1,205 +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 Microsoft.PowerShell.EditorServices.Utility; -using Microsoft.Win32.SafeHandles; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Pipes; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - public class NamedPipeServerListener : ServerListenerBase - { - // This int will be casted to a PipeOptions enum that only exists in .NET Core 2.1 and up which is why it's not available to us in .NET Standard. - private const int CurrentUserOnly = 0x20000000; - - // In .NET Framework, NamedPipeServerStream has a constructor that takes in a PipeSecurity object. We will use reflection to call the constructor, - // since .NET Framework doesn't have the `CurrentUserOnly` PipeOption. - // doc: https://docs.microsoft.com/en-us/dotnet/api/system.io.pipes.namedpipeserverstream.-ctor?view=netframework-4.7.2#System_IO_Pipes_NamedPipeServerStream__ctor_System_String_System_IO_Pipes_PipeDirection_System_Int32_System_IO_Pipes_PipeTransmissionMode_System_IO_Pipes_PipeOptions_System_Int32_System_Int32_System_IO_Pipes_PipeSecurity_ - private static readonly ConstructorInfo s_netFrameworkPipeServerConstructor = - typeof(NamedPipeServerStream).GetConstructor(new [] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); - - private readonly ILogger _logger; - private readonly string _inOutPipeName; - private readonly string _outPipeName; - - private NamedPipeServerStream _inOutPipeServer; - private NamedPipeServerStream _outPipeServer; - - public NamedPipeServerListener( - MessageProtocolType messageProtocolType, - string inOutPipeName, - ILogger logger) - : base(messageProtocolType) - { - _logger = logger; - _inOutPipeName = inOutPipeName; - } - - public NamedPipeServerListener( - MessageProtocolType messageProtocolType, - string inPipeName, - string outPipeName, - ILogger logger) - : base(messageProtocolType) - { - _logger = logger; - _inOutPipeName = inPipeName; - _outPipeName = outPipeName; - } - - public override void Start() - { - try - { - _inOutPipeServer = ConnectNamedPipe(_inOutPipeName, _outPipeName, out _outPipeServer); - ListenForConnection(); - } - catch (IOException e) - { - _logger.Write( - LogLevel.Verbose, - "Named pipe server failed to start due to exception:\r\n\r\n" + e.Message); - - throw e; - } - } - - public override void Stop() - { - if (_inOutPipeServer != null) - { - _logger.Write(LogLevel.Verbose, "Named pipe server shutting down..."); - - _inOutPipeServer.Dispose(); - - _logger.Write(LogLevel.Verbose, "Named pipe server has been disposed."); - } - - if (_outPipeServer != null) - { - _logger.Write(LogLevel.Verbose, $"Named out pipe server {_outPipeServer} shutting down..."); - - _outPipeServer.Dispose(); - - _logger.Write(LogLevel.Verbose, $"Named out pipe server {_outPipeServer} has been disposed."); - } - } - - private static NamedPipeServerStream ConnectNamedPipe( - string inOutPipeName, - string outPipeName, - out NamedPipeServerStream outPipe) - { - // .NET Core implementation is simplest so try that first - if (Utils.IsNetCore) - { - outPipe = outPipeName == null - ? null - : new NamedPipeServerStream( - pipeName: outPipeName, - direction: PipeDirection.Out, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: (PipeOptions)CurrentUserOnly); - - return new NamedPipeServerStream( - pipeName: inOutPipeName, - direction: PipeDirection.InOut, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); - } - - // Now deal with Windows PowerShell - // We need to use reflection to get a nice constructor - - PipeSecurity pipeSecurity = new PipeSecurity(); - - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - WindowsPrincipal principal = new WindowsPrincipal(identity); - - if (principal.IsInRole(WindowsBuiltInRole.Administrator)) - { - // Allow the Administrators group full access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)), - PipeAccessRights.FullControl, AccessControlType.Allow)); - } - else - { - // Allow the current user read/write access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - WindowsIdentity.GetCurrent().User, - PipeAccessRights.ReadWrite, AccessControlType.Allow)); - } - - outPipe = outPipeName == null - ? null - : (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - outPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - - return (NamedPipeServerStream)s_netFrameworkPipeServerConstructor.Invoke( - new object[] { - inOutPipeName, - PipeDirection.InOut, - 1, // maxNumberOfServerInstances - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous, - 1024, // inBufferSize - 1024, // outBufferSize - pipeSecurity - }); - } - - private void ListenForConnection() - { - Task.Factory.StartNew(async () => - { - try - { - var connectionTasks = new List {WaitForConnectionAsync(_inOutPipeServer)}; - if (_outPipeServer != null) - { - connectionTasks.Add(WaitForConnectionAsync(_outPipeServer)); - } - - await Task.WhenAll(connectionTasks); - OnClientConnect(new NamedPipeServerChannel(_inOutPipeServer, _outPipeServer, _logger)); - } - catch (Exception e) - { - _logger.WriteException( - "An unhandled exception occurred while listening for a named pipe client connection", - e); - - throw; - } - }); - } - - private static async Task WaitForConnectionAsync(NamedPipeServerStream pipeServerStream) - { - await pipeServerStream.WaitForConnectionAsync(); - await pipeServerStream.FlushAsync(); - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs deleted file mode 100644 index 433f6aabb..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ServerListenerBase.cs +++ /dev/null @@ -1,42 +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.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - public abstract class ServerListenerBase : IServerListener - where TChannel : ChannelBase - { - private MessageProtocolType messageProtocolType; - - public ServerListenerBase(MessageProtocolType messageProtocolType) - { - this.messageProtocolType = messageProtocolType; - } - - public abstract void Start(); - - public abstract void Stop(); - - public event EventHandler ClientConnect; - - protected void OnClientConnect(TChannel channel) - { - channel.Start(this.messageProtocolType); - this.ClientConnect?.Invoke(this, channel); - } - } - - public interface IServerListener - { - void Start(); - - void Stop(); - - event EventHandler ClientConnect; - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs deleted file mode 100644 index 86fe7ce7a..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs +++ /dev/null @@ -1,123 +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.Diagnostics; -using System.IO; -using System.Text; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - /// - /// Provides a client implementation for the standard I/O channel. - /// Launches the server process and then attaches to its console - /// streams. - /// - public class StdioClientChannel : ChannelBase - { - private string serviceProcessPath; - private string serviceProcessArguments; - - private ILogger logger; - private Stream inputStream; - private Stream outputStream; - private Process serviceProcess; - - /// - /// Gets the process ID of the server process. - /// - public int ProcessId { get; private set; } - - /// - /// Initializes an instance of the StdioClient. - /// - /// The full path to the server process executable. - /// Optional arguments to pass to the service process executable. - public StdioClientChannel( - string serverProcessPath, - ILogger logger, - params string[] serverProcessArguments) - { - this.logger = logger; - this.serviceProcessPath = serverProcessPath; - - if (serverProcessArguments != null) - { - this.serviceProcessArguments = - string.Join( - " ", - serverProcessArguments); - } - } - - protected override void Initialize(IMessageSerializer messageSerializer) - { - this.serviceProcess = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = this.serviceProcessPath, - Arguments = this.serviceProcessArguments, - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - StandardOutputEncoding = Encoding.UTF8, - }, - EnableRaisingEvents = true, - }; - - // Start the process - this.serviceProcess.Start(); - - this.ProcessId = this.serviceProcess.Id; - - // Open the standard input/output streams - this.inputStream = this.serviceProcess.StandardOutput.BaseStream; - this.outputStream = this.serviceProcess.StandardInput.BaseStream; - - // Set up the message reader and writer - this.MessageReader = - new MessageReader( - this.inputStream, - messageSerializer, - this.logger); - - this.MessageWriter = - new MessageWriter( - this.outputStream, - messageSerializer, - this.logger); - } - - protected override void Shutdown() - { - if (this.inputStream != null) - { - this.inputStream.Dispose(); - this.inputStream = null; - } - - if (this.outputStream != null) - { - this.outputStream.Dispose(); - this.outputStream = null; - } - - if (this.MessageReader != null) - { - this.MessageReader = null; - } - - if (this.MessageWriter != null) - { - this.MessageWriter = null; - } - - this.serviceProcess.Kill(); - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs deleted file mode 100644 index 95318d860..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs +++ /dev/null @@ -1,64 +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.IO; -using System.Text; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - /// - /// Provides a server implementation for the standard I/O channel. - /// When started in a process, attaches to the console I/O streams - /// to communicate with the client that launched the process. - /// - public class StdioServerChannel : ChannelBase - { - private ILogger logger; - private Stream inputStream; - private Stream outputStream; - - public StdioServerChannel(ILogger logger) - { - this.logger = logger; - } - - protected override void Initialize(IMessageSerializer messageSerializer) - { - if (System.Console.InputEncoding != Encoding.UTF8) - { - System.Console.InputEncoding = Encoding.UTF8; - } - - if (System.Console.OutputEncoding != Encoding.UTF8) - { - System.Console.OutputEncoding = Encoding.UTF8; - } - - // Open the standard input/output streams - this.inputStream = System.Console.OpenStandardInput(); - this.outputStream = System.Console.OpenStandardOutput(); - - // Set up the reader and writer - this.MessageReader = - new MessageReader( - this.inputStream, - messageSerializer, - this.logger); - - this.MessageWriter = - new MessageWriter( - this.outputStream, - messageSerializer, - this.logger); - } - - protected override void Shutdown() - { - // No default implementation needed, streams will be - // disposed on process shutdown. - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs deleted file mode 100644 index 6b850035d..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerListener.cs +++ /dev/null @@ -1,36 +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.IO; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel -{ - public class StdioServerListener : ServerListenerBase - { - private ILogger logger; - - public StdioServerListener( - MessageProtocolType messageProtocolType, - ILogger logger) - : base(messageProtocolType) - { - this.logger = logger; - } - - public override void Start() - { - // Client is connected immediately because stdio - // will buffer all I/O until we get to it - this.OnClientConnect( - new StdioServerChannel( - this.logger)); - } - - public override void Stop() - { - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Constants.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Constants.cs deleted file mode 100644 index d443dc12d..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Constants.cs +++ /dev/null @@ -1,31 +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 Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public static class Constants - { - public const string ContentLengthFormatString = "Content-Length: {0}\r\n\r\n"; - public static readonly JsonSerializerSettings JsonSerializerSettings; - - static Constants() - { - JsonSerializerSettings = new JsonSerializerSettings(); - - // Camel case all object properties - JsonSerializerSettings.ContractResolver = - new CamelCasePropertyNamesContractResolver(); - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs deleted file mode 100644 index 4da757913..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs +++ /dev/null @@ -1,34 +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 Newtonsoft.Json.Linq; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - /// - /// Provides context for a received event so that handlers - /// can write events back to the channel. - /// - public class EventContext - { - private MessageWriter messageWriter; - - public EventContext(MessageWriter messageWriter) - { - this.messageWriter = messageWriter; - } - - public async Task SendEventAsync( - NotificationType eventType, - TParams eventParams) - { - await this.messageWriter.WriteEventAsync( - eventType, - eventParams); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs deleted file mode 100644 index 58e2f1f59..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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.MessageProtocol -{ - /// - /// Defines an event type with a particular method name. - /// - /// The parameter type for this event. - public class NotificationType : AbstractMessageType - { - private NotificationType(string method) : base(method, 1) - { - - } - - /// - /// Creates an EventType instance with the given parameter type and method name. - /// - /// The method name of the event. - /// A new EventType instance for the defined type. - public static NotificationType Create(string method) - { - return new NotificationType(method); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageHandlers.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageHandlers.cs deleted file mode 100644 index 04e1ba9d6..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageHandlers.cs +++ /dev/null @@ -1,25 +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.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public interface IMessageHandlers - { - void SetRequestHandler( - RequestType requestType, - Func, Task> requestHandler); - - void SetRequestHandler( - RequestType0 requestType0, - Func, Task> requestHandler); - - void SetEventHandler( - NotificationType eventType, - Func eventHandler); - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs deleted file mode 100644 index 804bbe74c..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSender.cs +++ /dev/null @@ -1,25 +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.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public interface IMessageSender - { - Task SendEventAsync( - NotificationType eventType, - TParams eventParams); - - Task SendRequestAsync( - RequestType requestType, - TParams requestParams, - bool waitForResponse); - - Task SendRequestAsync( - RequestType0 requestType0); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs deleted file mode 100644 index eafd8f209..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs +++ /dev/null @@ -1,30 +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 Newtonsoft.Json.Linq; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - /// - /// Defines a common interface for message serializers. - /// - public interface IMessageSerializer - { - /// - /// Serializes a Message to a JObject. - /// - /// The message to be serialized. - /// A JObject which contains the JSON representation of the message. - JObject SerializeMessage(Message message); - - /// - /// Deserializes a JObject to a Messsage. - /// - /// The JObject containing the JSON representation of the message. - /// The Message that was represented by the JObject. - Message DeserializeMessage(JObject messageJson); - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs deleted file mode 100644 index 0d304cc03..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs +++ /dev/null @@ -1,136 +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.Diagnostics; -using Newtonsoft.Json.Linq; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - /// - /// Defines all possible message types. - /// - public enum MessageType - { - Unknown, - Request, - Response, - Event - } - - /// - /// Provides common details for protocol messages of any format. - /// - [DebuggerDisplay("MessageType = {MessageType.ToString()}, Method = {Method}, Id = {Id}")] - public class Message - { - /// - /// Gets or sets the message type. - /// - public MessageType MessageType { get; set; } - - /// - /// Gets or sets the message's sequence ID. - /// - public string Id { get; set; } - - /// - /// Gets or sets the message's method/command name. - /// - public string Method { get; set; } - - /// - /// Gets or sets a JToken containing the contents of the message. - /// - public JToken Contents { get; set; } - - /// - /// Gets or sets a JToken containing error details. - /// - public JToken Error { get; set; } - - /// - /// Creates a message with an Unknown type. - /// - /// A message with Unknown type. - public static Message Unknown() - { - return new Message - { - MessageType = MessageType.Unknown - }; - } - - /// - /// Creates a message with a Request type. - /// - /// The sequence ID of the request. - /// The method name of the request. - /// The contents of the request. - /// A message with a Request type. - public static Message Request(string id, string method, JToken contents) - { - return new Message - { - MessageType = MessageType.Request, - Id = id, - Method = method, - Contents = contents - }; - } - - /// - /// Creates a message with a Response type. - /// - /// The sequence ID of the original request. - /// The method name of the original request. - /// The contents of the response. - /// A message with a Response type. - public static Message Response(string id, string method, JToken contents) - { - return new Message - { - MessageType = MessageType.Response, - Id = id, - Method = method, - Contents = contents - }; - } - - /// - /// Creates a message with a Response type and error details. - /// - /// The sequence ID of the original request. - /// The method name of the original request. - /// The error details of the response. - /// A message with a Response type and error details. - public static Message ResponseError(string id, string method, JToken error) - { - return new Message - { - MessageType = MessageType.Response, - Id = id, - Method = method, - Error = error - }; - } - - /// - /// Creates a message with an Event type. - /// - /// The method name of the event. - /// The contents of the event. - /// A message with an Event type. - public static Message Event(string method, JToken contents) - { - return new Message - { - MessageType = MessageType.Event, - Method = method, - Contents = contents - }; - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs deleted file mode 100644 index ff7ae487e..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ /dev/null @@ -1,181 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public class MessageDispatcher : IMessageHandlers, IMessageDispatcher - { - #region Fields - - private ILogger logger; - - private Dictionary> requestHandlers = - new Dictionary>(); - - private Dictionary> eventHandlers = - new Dictionary>(); - - #endregion - - #region Constructors - - public MessageDispatcher(ILogger logger) - { - this.logger = logger; - } - - #endregion - - #region Public Methods - - public void SetRequestHandler( - RequestType requestType, - Func, Task> requestHandler) - { - bool overrideExisting = true; - - if (overrideExisting) - { - // Remove the existing handler so a new one can be set - this.requestHandlers.Remove(requestType.Method); - } - - this.requestHandlers.Add( - requestType.Method, - (requestMessage, messageWriter) => - { - var requestContext = - new RequestContext( - requestMessage, - messageWriter); - - TParams typedParams = default(TParams); - if (requestMessage.Contents != null) - { - // TODO: Catch parse errors! - typedParams = requestMessage.Contents.ToObject(); - } - - return requestHandler(typedParams, requestContext); - }); - } - - public void SetRequestHandler( - RequestType0 requestType0, - Func, Task> requestHandler) - { - this.SetRequestHandler( - RequestType.ConvertToRequestType(requestType0), - (param1, requestContext) => - { - return requestHandler(requestContext); - }); - } - - public void SetEventHandler( - NotificationType eventType, - Func eventHandler) - { - bool overrideExisting = true; - - if (overrideExisting) - { - // Remove the existing handler so a new one can be set - this.eventHandlers.Remove(eventType.Method); - } - - this.eventHandlers.Add( - eventType.Method, - (eventMessage, messageWriter) => - { - var eventContext = new EventContext(messageWriter); - - TParams typedParams = default(TParams); - if (eventMessage.Contents != null) - { - // TODO: Catch parse errors! - typedParams = eventMessage.Contents.ToObject(); - } - - return eventHandler(typedParams, eventContext); - }); - } - - #endregion - - #region Private Methods - - public async Task DispatchMessageAsync( - Message messageToDispatch, - MessageWriter messageWriter) - { - Task handlerToAwait = null; - - if (messageToDispatch.MessageType == MessageType.Request) - { - Func requestHandler = null; - if (this.requestHandlers.TryGetValue(messageToDispatch.Method, out requestHandler)) - { - handlerToAwait = requestHandler(messageToDispatch, messageWriter); - } - else - { - // TODO: Message not supported error - this.logger.Write(LogLevel.Warning, $"MessageDispatcher: No handler registered for Request type '{messageToDispatch.Method}'"); - } - } - else if (messageToDispatch.MessageType == MessageType.Event) - { - Func eventHandler = null; - if (this.eventHandlers.TryGetValue(messageToDispatch.Method, out eventHandler)) - { - handlerToAwait = eventHandler(messageToDispatch, messageWriter); - } - else - { - // TODO: Message not supported error - this.logger.Write(LogLevel.Warning, $"MessageDispatcher: No handler registered for Event type '{messageToDispatch.Method}'"); - } - } - else - { - // TODO: Return message not supported - this.logger.Write(LogLevel.Warning, $"MessageDispatcher received unknown message type of method '{messageToDispatch.Method}'"); - } - - if (handlerToAwait != null) - { - try - { - await handlerToAwait; - } - catch (TaskCanceledException) - { - // Some tasks may be cancelled due to legitimate - // timeouts so don't let those exceptions go higher. - } - catch (AggregateException e) - { - if (!(e.InnerExceptions[0] is TaskCanceledException)) - { - // Cancelled tasks aren't a problem, so rethrow - // anything that isn't a TaskCanceledException - throw e; - } - } - } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageParseException.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageParseException.cs deleted file mode 100644 index 2155bb14a..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageParseException.cs +++ /dev/null @@ -1,23 +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; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public class MessageParseException : Exception - { - public string OriginalMessageText { get; private set; } - - public MessageParseException( - string originalMessageText, - string errorMessage, - params object[] errorMessageArgs) - : base(string.Format(errorMessage, errorMessageArgs)) - { - this.OriginalMessageText = originalMessageText; - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs deleted file mode 100644 index 7aa3d6c81..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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.MessageProtocol -{ - /// - /// Defines the possible message protocol types. - /// - public enum MessageProtocolType - { - /// - /// Identifies the language server message protocol. - /// - LanguageServer, - - /// - /// Identifies the debug adapter message protocol. - /// - DebugAdapter - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs deleted file mode 100644 index fec0d175b..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs +++ /dev/null @@ -1,284 +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 Microsoft.PowerShell.EditorServices.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public class MessageReader - { - #region Private Fields - - public const int DefaultBufferSize = 8192; - public const double BufferResizeTrigger = 0.25; - - private const int CR = 0x0D; - private const int LF = 0x0A; - private static string[] NewLineDelimiters = new string[] { Environment.NewLine }; - - private ILogger logger; - private Stream inputStream; - private IMessageSerializer messageSerializer; - private Encoding messageEncoding; - - private ReadState readState; - private bool needsMoreData = true; - private int readOffset; - private int bufferEndOffset; - private byte[] messageBuffer = new byte[DefaultBufferSize]; - - private int expectedContentLength; - private Dictionary messageHeaders; - - enum ReadState - { - Headers, - Content - } - - #endregion - - #region Constructors - - public MessageReader( - Stream inputStream, - IMessageSerializer messageSerializer, - ILogger logger, - Encoding messageEncoding = null) - { - Validate.IsNotNull("streamReader", inputStream); - Validate.IsNotNull("messageSerializer", messageSerializer); - - this.logger = logger; - this.inputStream = inputStream; - this.messageSerializer = messageSerializer; - - this.messageEncoding = messageEncoding; - if (messageEncoding == null) - { - this.messageEncoding = Encoding.UTF8; - } - - this.messageBuffer = new byte[DefaultBufferSize]; - } - - #endregion - - #region Public Methods - - public async Task ReadMessageAsync() - { - string messageContent = null; - - // Do we need to read more data or can we process the existing buffer? - while (!this.needsMoreData || await this.ReadNextChunkAsync()) - { - // Clear the flag since we should have what we need now - this.needsMoreData = false; - - // Do we need to look for message headers? - if (this.readState == ReadState.Headers && - !this.TryReadMessageHeaders()) - { - // If we don't have enough data to read headers yet, keep reading - this.needsMoreData = true; - continue; - } - - // Do we need to look for message content? - if (this.readState == ReadState.Content && - !this.TryReadMessageContent(out messageContent)) - { - // If we don't have enough data yet to construct the content, keep reading - this.needsMoreData = true; - continue; - } - - // We've read a message now, break out of the loop - break; - } - - // Get the JObject for the JSON content - JObject messageObject = JObject.Parse(messageContent); - - // Deserialize the message from the parsed JSON message - Message parsedMessage = this.messageSerializer.DeserializeMessage(messageObject); - - // Log message info - initial capacity for StringBuilder varies depending on whether - // the log level is Diagnostic where JsonRpc message payloads are logged and vary in size - // from 1K up to the edited file size. When not logging message payloads, the typical - // request log message size is under 256 chars. - var logStrBld = - new StringBuilder(this.logger.MinimumConfiguredLogLevel == LogLevel.Diagnostic ? 4096 : 256) - .Append("Received ") - .Append(parsedMessage.MessageType) - .Append(" '").Append(parsedMessage.Method).Append("'"); - - if (!string.IsNullOrEmpty(parsedMessage.Id)) - { - logStrBld.Append(" with id ").Append(parsedMessage.Id); - } - - if (this.logger.MinimumConfiguredLogLevel == LogLevel.Diagnostic) - { - // Log the JSON representation of the message payload at the Diagnostic log level - string jsonPayload = messageObject.ToString(Formatting.Indented); - logStrBld.Append(Environment.NewLine).Append(Environment.NewLine).Append(jsonPayload); - } - - this.logger.Write(LogLevel.Verbose, logStrBld.ToString()); - - return parsedMessage; - } - - #endregion - - #region Private Methods - - private async Task ReadNextChunkAsync() - { - // Do we need to resize the buffer? See if less than 1/4 of the space is left. - if (((double)(this.messageBuffer.Length - this.bufferEndOffset) / this.messageBuffer.Length) < 0.25) - { - // Double the size of the buffer - Array.Resize( - ref this.messageBuffer, - this.messageBuffer.Length * 2); - } - - // Read the next chunk into the message buffer - int readLength = - await this.inputStream.ReadAsync( - this.messageBuffer, - this.bufferEndOffset, - this.messageBuffer.Length - this.bufferEndOffset); - - this.bufferEndOffset += readLength; - - if (readLength == 0) - { - // If ReadAsync returns 0 then it means that the stream was - // closed unexpectedly (usually due to the client application - // ending suddenly). For now, just terminate the language - // server immediately. - // TODO: Provide a more graceful shutdown path - throw new EndOfStreamException( - "MessageReader's input stream ended unexpectedly, terminating."); - } - - return true; - } - - private bool TryReadMessageHeaders() - { - int scanOffset = this.readOffset; - - // Scan for the final double-newline that marks the - // end of the header lines - while (scanOffset + 3 < this.bufferEndOffset && - (this.messageBuffer[scanOffset] != CR || - this.messageBuffer[scanOffset + 1] != LF || - this.messageBuffer[scanOffset + 2] != CR || - this.messageBuffer[scanOffset + 3] != LF)) - { - scanOffset++; - } - - // No header or body separator found (e.g CRLFCRLF) - if (scanOffset + 3 >= this.bufferEndOffset) - { - return false; - } - - this.messageHeaders = new Dictionary(); - - var headers = - Encoding.ASCII - .GetString(this.messageBuffer, this.readOffset, scanOffset) - .Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries); - - // Read each header and store it in the dictionary - foreach (var header in headers) - { - int currentLength = header.IndexOf(':'); - if (currentLength == -1) - { - throw new ArgumentException("Message header must separate key and value using :"); - } - - var key = header.Substring(0, currentLength); - var value = header.Substring(currentLength + 1).Trim(); - this.messageHeaders[key] = value; - } - - // Make sure a Content-Length header was present, otherwise it - // is a fatal error - string contentLengthString = null; - if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString)) - { - throw new MessageParseException("", "Fatal error: Content-Length header must be provided."); - } - - // Parse the content length to an integer - if (!int.TryParse(contentLengthString, out this.expectedContentLength)) - { - throw new MessageParseException("", "Fatal error: Content-Length value is not an integer."); - } - - // Skip past the headers plus the newline characters - this.readOffset += scanOffset + 4; - - // Done reading headers, now read content - this.readState = ReadState.Content; - - return true; - } - - private bool TryReadMessageContent(out string messageContent) - { - messageContent = null; - - // Do we have enough bytes to reach the expected length? - if ((this.bufferEndOffset - this.readOffset) < this.expectedContentLength) - { - return false; - } - - // Convert the message contents to a string using the specified encoding - messageContent = - this.messageEncoding.GetString( - this.messageBuffer, - this.readOffset, - this.expectedContentLength); - - // Move the remaining bytes to the front of the buffer for the next message - var remainingByteCount = this.bufferEndOffset - (this.expectedContentLength + this.readOffset); - Buffer.BlockCopy( - this.messageBuffer, - this.expectedContentLength + this.readOffset, - this.messageBuffer, - 0, - remainingByteCount); - - // Reset the offsets for the next read - this.readOffset = 0; - this.bufferEndOffset = remainingByteCount; - - // Done reading content, now look for headers - this.readState = ReadState.Headers; - - return true; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs deleted file mode 100644 index 17c7da384..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs +++ /dev/null @@ -1,173 +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 Microsoft.PowerShell.EditorServices.Utility; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using System; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public class MessageWriter - { - #region Private Fields - - private ILogger logger; - private Stream outputStream; - private IMessageSerializer messageSerializer; - private AsyncLock writeLock = new AsyncLock(); - - private JsonSerializer contentSerializer = - JsonSerializer.Create( - Constants.JsonSerializerSettings); - - #endregion - - #region Constructors - - public MessageWriter( - Stream outputStream, - IMessageSerializer messageSerializer, - ILogger logger) - { - Validate.IsNotNull("streamWriter", outputStream); - Validate.IsNotNull("messageSerializer", messageSerializer); - - this.logger = logger; - this.outputStream = outputStream; - this.messageSerializer = messageSerializer; - } - - #endregion - - #region Public Methods - - // TODO: This method should be made protected or private - - public async Task WriteMessageAsync(Message messageToWrite) - { - Validate.IsNotNull("messageToWrite", messageToWrite); - - // Serialize the message - JObject messageObject = - this.messageSerializer.SerializeMessage( - messageToWrite); - - // Log message info - initial capacity for StringBuilder varies depending on whether - // the log level is Diagnostic where JsonRpc message payloads are logged and vary - // in size from 1K up to 225K chars. When not logging message payloads, the typical - // response log message size is under 256 chars. - var logStrBld = - new StringBuilder(this.logger.MinimumConfiguredLogLevel == LogLevel.Diagnostic ? 4096 : 256) - .Append("Writing ") - .Append(messageToWrite.MessageType) - .Append(" '").Append(messageToWrite.Method).Append("'"); - - if (!string.IsNullOrEmpty(messageToWrite.Id)) - { - logStrBld.Append(" with id ").Append(messageToWrite.Id); - } - - if (this.logger.MinimumConfiguredLogLevel == LogLevel.Diagnostic) - { - // Log the JSON representation of the message payload at the Diagnostic log level - string jsonPayload = - JsonConvert.SerializeObject( - messageObject, - Formatting.Indented, - Constants.JsonSerializerSettings); - - logStrBld.Append(Environment.NewLine).Append(Environment.NewLine).Append(jsonPayload); - } - - this.logger.Write(LogLevel.Verbose, logStrBld.ToString()); - - string serializedMessage = - JsonConvert.SerializeObject( - messageObject, - Constants.JsonSerializerSettings); - - byte[] messageBytes = Encoding.UTF8.GetBytes(serializedMessage); - byte[] headerBytes = - Encoding.ASCII.GetBytes( - string.Format( - Constants.ContentLengthFormatString, - messageBytes.Length)); - - // Make sure only one call is writing at a time. You might be thinking - // "Why not use a normal lock?" We use an AsyncLock here so that the - // message loop doesn't get blocked while waiting for I/O to complete. - using (await this.writeLock.LockAsync()) - { - try - { - // Send the message - await this.outputStream.WriteAsync(headerBytes, 0, headerBytes.Length); - await this.outputStream.WriteAsync(messageBytes, 0, messageBytes.Length); - await this.outputStream.FlushAsync(); - } - catch (Exception e) when ( - e is ObjectDisposedException || - e is IOException) - { - // We catch this exception for when the DebugAdapter disconnects while still processing a message. - logger.WriteHandledException("Tried to write to the output stream but it was already closed & broken.", e); - } - } - } - - public async Task WriteRequestAsync( - RequestType requestType, - TParams requestParams, - int requestId) - { - // Allow null content - JToken contentObject = - requestParams != null ? - JToken.FromObject(requestParams, contentSerializer) : - null; - - await this.WriteMessageAsync( - Message.Request( - requestId.ToString(), - requestType.Method, - contentObject)); - } - - public async Task WriteResponseAsync(TResult resultContent, string method, string requestId) - { - // Allow null content - JToken contentObject = - resultContent != null ? - JToken.FromObject(resultContent, contentSerializer) : - null; - - await this.WriteMessageAsync( - Message.Response( - requestId, - method, - contentObject)); - } - - public async Task WriteEventAsync(NotificationType eventType, TParams eventParams) - { - // Allow null content - JToken contentObject = - eventParams != null ? - JToken.FromObject(eventParams, contentSerializer) : - null; - - await this.WriteMessageAsync( - Message.Event( - eventType.Method, - contentObject)); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs deleted file mode 100644 index ba52940a3..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/ProtocolEndpoint.cs +++ /dev/null @@ -1,407 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - /// - /// Provides behavior for a client or server endpoint that - /// communicates using the specified protocol. - /// - public class ProtocolEndpoint : IMessageSender - { - private enum ProtocolEndpointState - { - NotStarted, - Started, - Shutdown - } - - private int currentMessageId; - private ChannelBase protocolChannel; - private ProtocolEndpointState currentState; - private IMessageDispatcher messageDispatcher; - private AsyncContextThread messageLoopThread; - private TaskCompletionSource endpointExitedTask; - private SynchronizationContext originalSynchronizationContext; - private CancellationTokenSource messageLoopCancellationToken = - new CancellationTokenSource(); - - private Dictionary> pendingRequests = - new Dictionary>(); - - public SynchronizationContext SynchronizationContext { get; private set; } - - private bool InMessageLoopThread - { - get - { - // We're in the same thread as the message loop if the - // current synchronization context equals the one we - // know. - return SynchronizationContext.Current == this.SynchronizationContext; - } - } - - protected ILogger Logger { get; private set; } - - /// - /// Initializes an instance of the protocol server using the - /// specified channel for communication. - /// - /// - /// The channel to use for communication with the connected endpoint. - /// - /// - /// The type of message protocol used by the endpoint. - /// - public ProtocolEndpoint( - ChannelBase protocolChannel, - IMessageDispatcher messageDispatcher, - ILogger logger) - { - this.protocolChannel = protocolChannel; - this.messageDispatcher = messageDispatcher; - this.Logger = logger; - - this.originalSynchronizationContext = SynchronizationContext.Current; - } - - /// - /// Starts the language server client and sends the Initialize method. - /// - public void Start() - { - if (this.currentState == ProtocolEndpointState.NotStarted) - { - // Listen for unhandled exceptions from the message loop - this.UnhandledException += MessageDispatcher_UnhandledException; - - // Start the message loop - this.StartMessageLoop(); - - // Endpoint is now started - this.currentState = ProtocolEndpointState.Started; - } - } - - public void WaitForExit() - { - this.endpointExitedTask = new TaskCompletionSource(); - this.endpointExitedTask.Task.Wait(); - } - - public void Stop() - { - if (this.currentState == ProtocolEndpointState.Started) - { - // Make sure no future calls try to stop the endpoint during shutdown - this.currentState = ProtocolEndpointState.Shutdown; - - // Stop the message loop and channel - this.StopMessageLoop(); - this.protocolChannel.Stop(); - - // Notify anyone waiting for exit - if (this.endpointExitedTask != null) - { - this.endpointExitedTask.SetResult(true); - } - } - } - - #region Message Sending - - public Task SendRequestAsync( - RequestType0 requestType0) - { - return this.SendRequestAsync( - RequestType.ConvertToRequestType(requestType0), - null); - } - - - /// - /// Sends a request to the server - /// - /// - /// - /// - /// - /// - public Task SendRequestAsync( - RequestType requestType, - TParams requestParams) - { - return this.SendRequestAsync(requestType, requestParams, true); - } - - public async Task SendRequestAsync( - RequestType requestType, - TParams requestParams, - bool waitForResponse) - { - // Some requests may still be in the SynchronizationContext queue - // after the server stops so don't act on those requests because - // the protocol channel will already be disposed - if (this.currentState == ProtocolEndpointState.Shutdown) - { - return default(TResult); - } - - this.currentMessageId++; - - TaskCompletionSource responseTask = null; - - if (waitForResponse) - { - responseTask = new TaskCompletionSource(); - this.pendingRequests.Add( - this.currentMessageId.ToString(), - responseTask); - } - - await this.protocolChannel.MessageWriter.WriteRequestAsync( - requestType, - requestParams, - this.currentMessageId); - - if (responseTask != null) - { - var responseMessage = await responseTask.Task; - - return - responseMessage.Contents != null ? - responseMessage.Contents.ToObject() : - default(TResult); - } - else - { - // TODO: Better default value here? - return default(TResult); - } - } - - /// - /// Sends an event to the channel's endpoint. - /// - /// The event parameter type. - /// The type of event being sent. - /// The event parameters being sent. - /// A Task that tracks completion of the send operation. - public Task SendEventAsync( - NotificationType eventType, - TParams eventParams) - { - // Some requests may still be in the SynchronizationContext queue - // after the server stops so don't act on those requests because - // the protocol channel will already be disposed - if (this.currentState == ProtocolEndpointState.Shutdown) - { - return Task.FromResult(true); - } - - // Some events could be raised from a different thread. - // To ensure that messages are written serially, dispatch - // dispatch the SendEvent call to the message loop thread. - - if (!this.InMessageLoopThread) - { - TaskCompletionSource writeTask = new TaskCompletionSource(); - - this.SynchronizationContext.Post( - async (obj) => - { - await this.protocolChannel.MessageWriter.WriteEventAsync( - eventType, - eventParams); - - writeTask.SetResult(true); - }, null); - - return writeTask.Task; - } - else - { - return this.protocolChannel.MessageWriter.WriteEventAsync( - eventType, - eventParams); - } - } - - #endregion - - #region Message Handling - - private void HandleResponse(Message responseMessage) - { - TaskCompletionSource pendingRequestTask = null; - - if (this.pendingRequests.TryGetValue(responseMessage.Id, out pendingRequestTask)) - { - pendingRequestTask.SetResult(responseMessage); - this.pendingRequests.Remove(responseMessage.Id); - } - } - - private void StartMessageLoop() - { - // Start the main message loop thread. The Task is - // not explicitly awaited because it is running on - // an independent background thread. - this.messageLoopThread = new AsyncContextThread("Message Dispatcher"); - this.messageLoopThread - .Run( - () => this.ListenForMessagesAsync(this.messageLoopCancellationToken.Token), - this.Logger) - .ContinueWith(this.OnListenTaskCompleted); - } - - private void StopMessageLoop() - { - // Stop the message loop thread - if (this.messageLoopThread != null) - { - this.messageLoopCancellationToken.Cancel(); - this.messageLoopThread.Stop(); - SynchronizationContext.SetSynchronizationContext(null); - } - } - - #endregion - - #region Events - - public event EventHandler UnhandledException; - - protected void OnUnhandledException(Exception unhandledException) - { - if (this.UnhandledException != null) - { - this.UnhandledException(this, unhandledException); - } - } - - #endregion - - #region Event Handlers - - private void MessageDispatcher_UnhandledException(object sender, Exception e) - { - if (this.endpointExitedTask != null) - { - this.endpointExitedTask.SetException(e); - } - else if (this.originalSynchronizationContext != null) - { - this.originalSynchronizationContext.Post(o => { throw e; }, null); - } - } - - #endregion - - #region Private Methods - - private async Task ListenForMessagesAsync(CancellationToken cancellationToken) - { - this.SynchronizationContext = SynchronizationContext.Current; - - // Run the message loop - bool isRunning = true; - while (isRunning && !cancellationToken.IsCancellationRequested) - { - Message newMessage = null; - - try - { - // Read a message from the channel - newMessage = await this.protocolChannel.MessageReader.ReadMessageAsync(); - } - catch (MessageParseException e) - { - // TODO: Write an error response - - Logger.Write( - LogLevel.Error, - "Could not parse a message that was received:\r\n\r\n" + - e.ToString()); - - // Continue the loop - continue; - } - catch (IOException e) - { - // The stream has ended, end the message loop - Logger.Write( - LogLevel.Error, - string.Format( - "Stream terminated unexpectedly, ending MessageDispatcher loop\r\n\r\nException: {0}\r\n{1}", - e.GetType().Name, - e.Message)); - - break; - } - catch (ObjectDisposedException) - { - Logger.Write( - LogLevel.Verbose, - "MessageReader attempted to read from a disposed stream, ending MessageDispatcher loop"); - - break; - } - catch (Exception e) - { - Logger.WriteException( - "Caught unhandled exception in ProtocolEndpoint message loop", - e); - } - - // The message could be null if there was an error parsing the - // previous message. In this case, do not try to dispatch it. - if (newMessage != null) - { - if (newMessage.MessageType == MessageType.Response) - { - this.HandleResponse(newMessage); - } - else - { - // Process the message - await this.messageDispatcher.DispatchMessageAsync( - newMessage, - this.protocolChannel.MessageWriter); - } - } - } - } - - private void OnListenTaskCompleted(Task listenTask) - { - if (listenTask.IsFaulted) - { - Logger.Write( - LogLevel.Error, - string.Format( - "ProtocolEndpoint message loop terminated due to unhandled exception:\r\n\r\n{0}", - listenTask.Exception.ToString())); - - this.OnUnhandledException(listenTask.Exception); - } - else if (listenTask.IsCompleted || listenTask.IsCanceled) - { - // TODO: Dispose of anything? - } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs deleted file mode 100644 index 94c9264b7..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs +++ /dev/null @@ -1,47 +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 Newtonsoft.Json.Linq; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - public class RequestContext - { - private Message requestMessage; - private MessageWriter messageWriter; - - public RequestContext(Message requestMessage, MessageWriter messageWriter) - { - this.requestMessage = requestMessage; - this.messageWriter = messageWriter; - } - - public async Task SendResultAsync(TResult resultDetails) - { - await this.messageWriter.WriteResponseAsync( - resultDetails, - requestMessage.Method, - requestMessage.Id); - } - - public async Task SendEventAsync(NotificationType eventType, TParams eventParams) - { - await this.messageWriter.WriteEventAsync( - eventType, - eventParams); - } - - public async Task SendErrorAsync(object errorDetails) - { - await this.messageWriter.WriteMessageAsync( - Message.ResponseError( - requestMessage.Id, - requestMessage.Method, - JToken.FromObject(errorDetails))); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs deleted file mode 100644 index d7c5a6e8e..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs +++ /dev/null @@ -1,35 +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.Diagnostics; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - [DebuggerDisplay("RequestType Method = {Method}")] - public class RequestType : AbstractMessageType - { - private RequestType(string method) : base(method, 1) - { - - } - - public static RequestType ConvertToRequestType( - RequestType0 requestType0) - { - return RequestType.Create(requestType0.Method); - } - - public static RequestType Create(string method) - { - if (method == null) - { - throw new System.ArgumentNullException(nameof(method)); - } - - return new RequestType(method); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType0.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType0.cs deleted file mode 100644 index e8fc8da1c..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType0.cs +++ /dev/null @@ -1,22 +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.Diagnostics; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol -{ - [DebuggerDisplay("RequestType0 Method = {Method}")] - public class RequestType0 : AbstractMessageType - { - public RequestType0(string method) : base(method, 0) - { - } - - public static RequestType0 Create(string method) - { - return new RequestType0(method); - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs deleted file mode 100644 index 8f736ab01..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs +++ /dev/null @@ -1,102 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers -{ - /// - /// Serializes messages in the JSON RPC format. Used primarily - /// for language servers. - /// - public class JsonRpcMessageSerializer : IMessageSerializer - { - public JObject SerializeMessage(Message message) - { - JObject messageObject = new JObject(); - - messageObject.Add("jsonrpc", JToken.FromObject("2.0")); - - if (message.MessageType == MessageType.Request) - { - messageObject.Add("id", JToken.FromObject(message.Id)); - messageObject.Add("method", message.Method); - messageObject.Add("params", message.Contents); - } - else if (message.MessageType == MessageType.Event) - { - messageObject.Add("method", message.Method); - messageObject.Add("params", message.Contents); - } - else if (message.MessageType == MessageType.Response) - { - messageObject.Add("id", JToken.FromObject(message.Id)); - - if (message.Error != null) - { - // Write error - messageObject.Add("error", message.Error); - } - else - { - // Write result - messageObject.Add("result", message.Contents); - } - } - - return messageObject; - } - - public Message DeserializeMessage(JObject messageJson) - { - // TODO: Check for jsonrpc version - - JToken token = null; - if (messageJson.TryGetValue("id", out token)) - { - // Message is a Request or Response - string messageId = token.ToString(); - - if (messageJson.TryGetValue("result", out token)) - { - return Message.Response(messageId, null, token); - } - else if (messageJson.TryGetValue("error", out token)) - { - return Message.ResponseError(messageId, null, token); - } - else - { - JToken messageParams = null; - messageJson.TryGetValue("params", out messageParams); - - if (!messageJson.TryGetValue("method", out token)) - { - // TODO: Throw parse error - } - - return Message.Request(messageId, token.ToString(), messageParams); - } - } - else - { - // Messages without an id are events - JToken messageParams = token; - messageJson.TryGetValue("params", out messageParams); - - if (!messageJson.TryGetValue("method", out token)) - { - // TODO: Throw parse error - } - - return Message.Event(token.ToString(), messageParams); - } - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs deleted file mode 100644 index dd21bc367..000000000 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs +++ /dev/null @@ -1,118 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; - -namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers -{ - /// - /// Serializes messages in the V8 format. Used primarily for debug adapters. - /// - public class V8MessageSerializer : IMessageSerializer - { - public JObject SerializeMessage(Message message) - { - JObject messageObject = new JObject(); - - if (message.MessageType == MessageType.Request) - { - messageObject.Add("type", JToken.FromObject("request")); - messageObject.Add("seq", JToken.FromObject(message.Id)); - messageObject.Add("command", message.Method); - messageObject.Add("arguments", message.Contents); - } - else if (message.MessageType == MessageType.Event) - { - messageObject.Add("type", JToken.FromObject("event")); - messageObject.Add("event", message.Method); - messageObject.Add("body", message.Contents); - } - else if (message.MessageType == MessageType.Response) - { - int messageId = 0; - int.TryParse(message.Id, out messageId); - - messageObject.Add("type", JToken.FromObject("response")); - messageObject.Add("request_seq", JToken.FromObject(messageId)); - messageObject.Add("command", message.Method); - - if (message.Error != null) - { - // Write error - messageObject.Add("success", JToken.FromObject(false)); - messageObject.Add("message", message.Error); - } - else - { - // Write result - messageObject.Add("success", JToken.FromObject(true)); - messageObject.Add("body", message.Contents); - } - } - - return messageObject; - } - - public Message DeserializeMessage(JObject messageJson) - { - JToken token = null; - - if (messageJson.TryGetValue("type", out token)) - { - string messageType = token.ToString(); - - if (string.Equals("request", messageType, StringComparison.CurrentCultureIgnoreCase)) - { - return Message.Request( - messageJson.GetValue("seq").ToString(), - messageJson.GetValue("command").ToString(), - messageJson.GetValue("arguments")); - } - else if (string.Equals("response", messageType, StringComparison.CurrentCultureIgnoreCase)) - { - if (messageJson.TryGetValue("success", out token)) - { - // Was the response for a successful request? - if (token.ToObject() == true) - { - return Message.Response( - messageJson.GetValue("request_seq").ToString(), - messageJson.GetValue("command").ToString(), - messageJson.GetValue("body")); - } - else - { - return Message.ResponseError( - messageJson.GetValue("request_seq").ToString(), - messageJson.GetValue("command").ToString(), - messageJson.GetValue("message")); - } - } - else - { - // TODO: Parse error - } - - } - else if (string.Equals("event", messageType, StringComparison.CurrentCultureIgnoreCase)) - { - return Message.Event( - messageJson.GetValue("event").ToString(), - messageJson.GetValue("body")); - } - else - { - return Message.Unknown(); - } - } - - return Message.Unknown(); - } - } -} - diff --git a/src/PowerShellEditorServices.Protocol/Messages/PromptEvents.cs b/src/PowerShellEditorServices.Protocol/Messages/PromptEvents.cs deleted file mode 100644 index 3a04eaf1e..000000000 --- a/src/PowerShellEditorServices.Protocol/Messages/PromptEvents.cs +++ /dev/null @@ -1,58 +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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Messages -{ - public class ShowChoicePromptRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/showChoicePrompt"); - - 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 - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/showInputPrompt"); - - /// - /// 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.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj deleted file mode 100644 index dee2bb843..000000000 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - PowerShell Editor Services Host Protocol Library - Provides message types and client/server APIs for the PowerShell Editor Services JSON protocol. - netstandard2.0 - Microsoft.PowerShell.EditorServices.Protocol - - - - - - - - - - - diff --git a/src/PowerShellEditorServices.Protocol/Properties/AssemblyInfo.cs b/src/PowerShellEditorServices.Protocol/Properties/AssemblyInfo.cs deleted file mode 100644 index 790e19d63..000000000 --- a/src/PowerShellEditorServices.Protocol/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +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.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Test.Protocol")] \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs deleted file mode 100644 index 25ac3c81a..000000000 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ /dev/null @@ -1,1159 +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 Microsoft.PowerShell.EditorServices.Debugging; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Security; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Server -{ - public class DebugAdapter - { - private static readonly Version _minVersionForCustomPipeName = new Version(6, 2); - - private EditorSession _editorSession; - - private bool _noDebug; - private ILogger Logger; - private string _arguments; - private bool _isRemoteAttach; - private bool _isAttachSession; - private bool _waitingForAttach; - private string _scriptToLaunch; - private bool _ownsEditorSession; - private bool _executionCompleted; - private IMessageSender _messageSender; - private IMessageHandlers _messageHandlers; - private bool _isInteractiveDebugSession; - private bool _setBreakpointInProgress; - private RequestContext _disconnectRequestContext = null; - - public DebugAdapter( - EditorSession editorSession, - bool ownsEditorSession, - IMessageHandlers messageHandlers, - IMessageSender messageSender, - ILogger logger) - { - Logger = logger; - _editorSession = editorSession; - _messageSender = messageSender; - _messageHandlers = messageHandlers; - _ownsEditorSession = ownsEditorSession; - } - - /// - /// Gets a boolean that indicates whether the current debug adapter is - /// using a temporary integrated console. - /// - public bool IsUsingTempIntegratedConsole { get; private set; } - - public void Start() - { - // Register all supported message types - _messageHandlers.SetRequestHandler(InitializeRequest.Type, HandleInitializeRequestAsync); - - _messageHandlers.SetRequestHandler(LaunchRequest.Type, HandleLaunchRequestAsync); - _messageHandlers.SetRequestHandler(AttachRequest.Type, HandleAttachRequestAsync); - _messageHandlers.SetRequestHandler(ConfigurationDoneRequest.Type, HandleConfigurationDoneRequestAsync); - _messageHandlers.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequestAsync); - - _messageHandlers.SetRequestHandler(SetBreakpointsRequest.Type, HandleSetBreakpointsRequestAsync); - _messageHandlers.SetRequestHandler(SetExceptionBreakpointsRequest.Type, HandleSetExceptionBreakpointsRequestAsync); - _messageHandlers.SetRequestHandler(SetFunctionBreakpointsRequest.Type, HandleSetFunctionBreakpointsRequestAsync); - - _messageHandlers.SetRequestHandler(ContinueRequest.Type, HandleContinueRequestAsync); - _messageHandlers.SetRequestHandler(NextRequest.Type, HandleNextRequestAsync); - _messageHandlers.SetRequestHandler(StepInRequest.Type, HandleStepInRequestAsync); - _messageHandlers.SetRequestHandler(StepOutRequest.Type, HandleStepOutRequestAsync); - _messageHandlers.SetRequestHandler(PauseRequest.Type, HandlePauseRequestAsync); - - _messageHandlers.SetRequestHandler(ThreadsRequest.Type, HandleThreadsRequestAsync); - _messageHandlers.SetRequestHandler(StackTraceRequest.Type, HandleStackTraceRequestAsync); - _messageHandlers.SetRequestHandler(ScopesRequest.Type, HandleScopesRequestAsync); - _messageHandlers.SetRequestHandler(VariablesRequest.Type, HandleVariablesRequestAsync); - _messageHandlers.SetRequestHandler(SetVariableRequest.Type, HandleSetVariablesRequestAsync); - _messageHandlers.SetRequestHandler(SourceRequest.Type, HandleSourceRequestAsync); - _messageHandlers.SetRequestHandler(EvaluateRequest.Type, HandleEvaluateRequestAsync); - } - - protected Task LaunchScriptAsync(RequestContext requestContext, string scriptToLaunch) - { - // Is this an untitled script? - Task launchTask = null; - - if (ScriptFile.IsUntitledPath(scriptToLaunch)) - { - ScriptFile untitledScript = _editorSession.Workspace.GetFile(scriptToLaunch); - - launchTask = _editorSession.PowerShellContext - .ExecuteScriptStringAsync(untitledScript.Contents, true, true); - } - else - { - launchTask = _editorSession.PowerShellContext - .ExecuteScriptWithArgsAsync(scriptToLaunch, _arguments, writeInputToHost: true); - } - - return launchTask.ContinueWith(OnExecutionCompletedAsync); - } - - private async Task OnExecutionCompletedAsync(Task executeTask) - { - try - { - await executeTask; - } - catch (Exception e) - { - Logger.Write( - LogLevel.Error, - "Exception occurred while awaiting debug launch task.\n\n" + e.ToString()); - } - - Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); - - _executionCompleted = true; - - UnregisterEventHandlers(); - - if (_isAttachSession) - { - // Pop the sessions - if (_editorSession.PowerShellContext.CurrentRunspace.Context == RunspaceContext.EnteredProcess) - { - try - { - await _editorSession.PowerShellContext.ExecuteScriptStringAsync("Exit-PSHostProcess"); - - if (_isRemoteAttach && - _editorSession.PowerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) - { - await _editorSession.PowerShellContext.ExecuteScriptStringAsync("Exit-PSSession"); - } - } - catch (Exception e) - { - Logger.WriteException("Caught exception while popping attached process after debugging", e); - } - } - } - - _editorSession.DebugService.IsClientAttached = false; - - if (_disconnectRequestContext != null) - { - // Respond to the disconnect request and stop the server - await _disconnectRequestContext.SendResultAsync(null); - Stop(); - return; - } - - await _messageSender.SendEventAsync( - TerminatedEvent.Type, - new TerminatedEvent()); - } - - protected void Stop() - { - Logger.Write(LogLevel.Normal, "Debug adapter is shutting down..."); - - if (_editorSession != null) - { - _editorSession.PowerShellContext.RunspaceChanged -= powerShellContext_RunspaceChangedAsync; - _editorSession.DebugService.DebuggerStopped -= DebugService_DebuggerStoppedAsync; - _editorSession.PowerShellContext.DebuggerResumed -= powerShellContext_DebuggerResumedAsync; - - if (_ownsEditorSession) - { - _editorSession.Dispose(); - } - - _editorSession = null; - } - - OnSessionEnded(); - } - - #region Built-in Message Handlers - - private async Task HandleInitializeRequestAsync( - object shutdownParams, - RequestContext requestContext) - { - // Clear any existing breakpoints before proceeding - await ClearSessionBreakpointsAsync(); - - // Now send the Initialize response to continue setup - await requestContext.SendResultAsync( - new InitializeResponseBody { - SupportsConfigurationDoneRequest = true, - SupportsFunctionBreakpoints = true, - SupportsConditionalBreakpoints = true, - SupportsHitConditionalBreakpoints = true, - SupportsSetVariable = true - }); - } - - protected async Task HandleConfigurationDoneRequestAsync( - object args, - RequestContext requestContext) - { - _editorSession.DebugService.IsClientAttached = true; - - if (!string.IsNullOrEmpty(_scriptToLaunch)) - { - if (_editorSession.PowerShellContext.SessionState == PowerShellContextState.Ready) - { - // Configuration is done, launch the script - var nonAwaitedTask = LaunchScriptAsync(requestContext, _scriptToLaunch) - .ConfigureAwait(continueOnCapturedContext: false); - } - else - { - Logger.Write( - LogLevel.Verbose, - "configurationDone request called after script was already launched, skipping it."); - } - } - - await requestContext.SendResultAsync(null); - - if (_isInteractiveDebugSession) - { - if (_ownsEditorSession) - { - // If this is a debug-only session, we need to start - // the command loop manually - _editorSession.HostInput.StartCommandLoop(); - } - - if (_editorSession.DebugService.IsDebuggerStopped) - { - // If this is an interactive session and there's a pending breakpoint, - // send that information along to the debugger client - DebugService_DebuggerStoppedAsync( - this, - _editorSession.DebugService.CurrentDebuggerStoppedEventArgs); - } - } - } - - protected async Task HandleLaunchRequestAsync( - LaunchRequestArguments launchParams, - RequestContext requestContext) - { - RegisterEventHandlers(); - - // Determine whether or not the working directory should be set in the PowerShellContext. - if ((_editorSession.PowerShellContext.CurrentRunspace.Location == RunspaceLocation.Local) && - !_editorSession.DebugService.IsDebuggerStopped) - { - // Get the working directory that was passed via the debug config - // (either via launch.json or generated via no-config debug). - string workingDir = launchParams.Cwd; - - // Assuming we have a non-empty/null working dir, unescape the path and verify - // the path exists and is a directory. - if (!string.IsNullOrEmpty(workingDir)) - { - try - { - if ((File.GetAttributes(workingDir) & FileAttributes.Directory) != FileAttributes.Directory) - { - workingDir = Path.GetDirectoryName(workingDir); - } - } - catch (Exception ex) - { - workingDir = null; - Logger.Write( - LogLevel.Error, - $"The specified 'cwd' path is invalid: '{launchParams.Cwd}'. Error: {ex.Message}"); - } - } - - // If we have no working dir by this point and we are running in a temp console, - // pick some reasonable default. - if (string.IsNullOrEmpty(workingDir) && launchParams.CreateTemporaryIntegratedConsole) - { - workingDir = Environment.CurrentDirectory; - } - - // At this point, we will either have a working dir that should be set to cwd in - // the PowerShellContext or the user has requested (via an empty/null cwd) that - // the working dir should not be changed. - if (!string.IsNullOrEmpty(workingDir)) - { - await _editorSession.PowerShellContext.SetWorkingDirectoryAsync(workingDir, isPathAlreadyEscaped: false); - } - - Logger.Write(LogLevel.Verbose, $"Working dir " + (string.IsNullOrEmpty(workingDir) ? "not set." : $"set to '{workingDir}'")); - } - - // Prepare arguments to the script - if specified - string arguments = null; - if ((launchParams.Args != null) && (launchParams.Args.Length > 0)) - { - arguments = string.Join(" ", launchParams.Args); - Logger.Write(LogLevel.Verbose, "Script arguments are: " + arguments); - } - - // Store the launch parameters so that they can be used later - _noDebug = launchParams.NoDebug; - _scriptToLaunch = launchParams.Script; - _arguments = arguments; - IsUsingTempIntegratedConsole = launchParams.CreateTemporaryIntegratedConsole; - - // If the current session is remote, map the script path to the remote - // machine if necessary - if (_scriptToLaunch != null && - _editorSession.PowerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) - { - _scriptToLaunch = - _editorSession.RemoteFileManager.GetMappedPath( - _scriptToLaunch, - _editorSession.PowerShellContext.CurrentRunspace); - } - - await requestContext.SendResultAsync(null); - - // If no script is being launched, mark this as an interactive - // debugging session - _isInteractiveDebugSession = string.IsNullOrEmpty(_scriptToLaunch); - - // Send the InitializedEvent so that the debugger will continue - // sending configuration requests - await _messageSender.SendEventAsync( - InitializedEvent.Type, - null); - } - - protected async Task HandleAttachRequestAsync( - AttachRequestArguments attachParams, - RequestContext requestContext) - { - _isAttachSession = true; - - RegisterEventHandlers(); - - bool processIdIsSet = !string.IsNullOrEmpty(attachParams.ProcessId) && attachParams.ProcessId != "undefined"; - bool customPipeNameIsSet = !string.IsNullOrEmpty(attachParams.CustomPipeName) && attachParams.CustomPipeName != "undefined"; - - PowerShellVersionDetails runspaceVersion = - _editorSession.PowerShellContext.CurrentRunspace.PowerShellVersion; - - // If there are no host processes to attach to or the user cancels selection, we get a null for the process id. - // This is not an error, just a request to stop the original "attach to" request. - // Testing against "undefined" is a HACK because I don't know how to make "Cancel" on quick pick loading - // to cancel on the VSCode side without sending an attachRequest with processId set to "undefined". - if (!processIdIsSet && !customPipeNameIsSet) - { - Logger.Write( - LogLevel.Normal, - $"Attach request aborted, received {attachParams.ProcessId} for processId."); - - await requestContext.SendErrorAsync( - "User aborted attach to PowerShell host process."); - - return; - } - - StringBuilder errorMessages = new StringBuilder(); - - if (attachParams.ComputerName != null) - { - if (runspaceVersion.Version.Major < 4) - { - await requestContext.SendErrorAsync( - $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version})."); - - return; - } - else if (_editorSession.PowerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) - { - await requestContext.SendErrorAsync( - $"Cannot attach to a process in a remote session when already in a remote session."); - - return; - } - - await _editorSession.PowerShellContext.ExecuteScriptStringAsync( - $"Enter-PSSession -ComputerName \"{attachParams.ComputerName}\"", - errorMessages); - - if (errorMessages.Length > 0) - { - await requestContext.SendErrorAsync( - $"Could not establish remote session to computer '{attachParams.ComputerName}'"); - - return; - } - - _isRemoteAttach = true; - } - - if (processIdIsSet && int.TryParse(attachParams.ProcessId, out int processId) && (processId > 0)) - { - if (runspaceVersion.Version.Major < 5) - { - await requestContext.SendErrorAsync( - $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version})."); - return; - } - - await _editorSession.PowerShellContext.ExecuteScriptStringAsync( - $"Enter-PSHostProcess -Id {processId}", - errorMessages); - - if (errorMessages.Length > 0) - { - await requestContext.SendErrorAsync( - $"Could not attach to process '{processId}'"); - - return; - } - } - else if (customPipeNameIsSet) - { - if (runspaceVersion.Version < _minVersionForCustomPipeName) - { - await requestContext.SendErrorAsync( - $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version})."); - return; - } - - await _editorSession.PowerShellContext.ExecuteScriptStringAsync( - $"Enter-PSHostProcess -CustomPipeName {attachParams.CustomPipeName}", - errorMessages); - - if (errorMessages.Length > 0) - { - await requestContext.SendErrorAsync( - $"Could not attach to process with CustomPipeName: '{attachParams.CustomPipeName}'"); - - return; - } - } - else if (attachParams.ProcessId != "current") - { - Logger.Write( - LogLevel.Error, - $"Attach request failed, '{attachParams.ProcessId}' is an invalid value for the processId."); - - await requestContext.SendErrorAsync( - "A positive integer must be specified for the processId field."); - - return; - } - - // Clear any existing breakpoints before proceeding - await ClearSessionBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false); - - // Execute the Debug-Runspace command but don't await it because it - // will block the debug adapter initialization process. The - // InitializedEvent will be sent as soon as the RunspaceChanged - // event gets fired with the attached runspace. - - string debugRunspaceCmd; - if (attachParams.RunspaceName != null) - { - debugRunspaceCmd = $"\nDebug-Runspace -Name '{attachParams.RunspaceName}'"; - } - else if (attachParams.RunspaceId != null) - { - if (!int.TryParse(attachParams.RunspaceId, out int runspaceId) || runspaceId <= 0) - { - Logger.Write( - LogLevel.Error, - $"Attach request failed, '{attachParams.RunspaceId}' is an invalid value for the processId."); - - await requestContext.SendErrorAsync( - "A positive integer must be specified for the RunspaceId field."); - - return; - } - - debugRunspaceCmd = $"\nDebug-Runspace -Id {runspaceId}"; - } - else - { - debugRunspaceCmd = "\nDebug-Runspace -Id 1"; - } - - _waitingForAttach = true; - Task nonAwaitedTask = _editorSession.PowerShellContext - .ExecuteScriptStringAsync(debugRunspaceCmd) - .ContinueWith(OnExecutionCompletedAsync); - - await requestContext.SendResultAsync(null); - } - - protected async Task HandleDisconnectRequestAsync( - object disconnectParams, - RequestContext requestContext) - { - // In some rare cases, the EditorSession will already be disposed - // so we shouldn't try to abort because PowerShellContext will be null - if (_editorSession != null && _editorSession.PowerShellContext != null) - { - if (_executionCompleted == false) - { - _disconnectRequestContext = requestContext; - _editorSession.PowerShellContext.AbortExecution(shouldAbortDebugSession: true); - - if (_isInteractiveDebugSession) - { - await OnExecutionCompletedAsync(null); - } - } - else - { - UnregisterEventHandlers(); - - await requestContext.SendResultAsync(null); - Stop(); - } - } - } - - protected async Task HandleSetBreakpointsRequestAsync( - SetBreakpointsRequestArguments setBreakpointsParams, - RequestContext requestContext) - { - ScriptFile scriptFile = null; - - // When you set a breakpoint in the right pane of a Git diff window on a PS1 file, - // the Source.Path comes through as Untitled-X. That's why we check for IsUntitledPath. - if (!ScriptFile.IsUntitledPath(setBreakpointsParams.Source.Path) && - !_editorSession.Workspace.TryGetFile( - setBreakpointsParams.Source.Path, - out scriptFile)) - { - string message = _noDebug ? string.Empty : "Source file could not be accessed, breakpoint not set."; - var srcBreakpoints = setBreakpointsParams.Breakpoints - .Select(srcBkpt => Protocol.DebugAdapter.Breakpoint.Create( - srcBkpt, setBreakpointsParams.Source.Path, message, verified: _noDebug)); - - // Return non-verified breakpoint message. - await requestContext.SendResultAsync( - new SetBreakpointsResponseBody { - Breakpoints = srcBreakpoints.ToArray() - }); - - return; - } - - // Verify source file is a PowerShell script file. - string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower(); - if (string.IsNullOrEmpty(fileExtension) || ((fileExtension != ".ps1") && (fileExtension != ".psm1"))) - { - Logger.Write( - LogLevel.Warning, - $"Attempted to set breakpoints on a non-PowerShell file: {setBreakpointsParams.Source.Path}"); - - string message = _noDebug ? string.Empty : "Source is not a PowerShell script, breakpoint not set."; - - var srcBreakpoints = setBreakpointsParams.Breakpoints - .Select(srcBkpt => Protocol.DebugAdapter.Breakpoint.Create( - srcBkpt, setBreakpointsParams.Source.Path, message, verified: _noDebug)); - - // Return non-verified breakpoint message. - await requestContext.SendResultAsync( - new SetBreakpointsResponseBody - { - Breakpoints = srcBreakpoints.ToArray() - }); - - return; - } - - // At this point, the source file has been verified as a PowerShell script. - var breakpointDetails = new BreakpointDetails[setBreakpointsParams.Breakpoints.Length]; - for (int i = 0; i < breakpointDetails.Length; i++) - { - SourceBreakpoint srcBreakpoint = setBreakpointsParams.Breakpoints[i]; - breakpointDetails[i] = BreakpointDetails.Create( - scriptFile.FilePath, - srcBreakpoint.Line, - srcBreakpoint.Column, - srcBreakpoint.Condition, - srcBreakpoint.HitCondition); - } - - // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. - BreakpointDetails[] updatedBreakpointDetails = breakpointDetails; - if (!_noDebug) - { - _setBreakpointInProgress = true; - - try - { - updatedBreakpointDetails = - await _editorSession.DebugService.SetLineBreakpointsAsync( - scriptFile, - breakpointDetails); - } - catch (Exception e) - { - // Log whatever the error is - Logger.WriteException($"Caught error while setting breakpoints in SetBreakpoints handler for file {scriptFile?.FilePath}", e); - } - finally - { - _setBreakpointInProgress = false; - } - } - - await requestContext.SendResultAsync( - new SetBreakpointsResponseBody { - Breakpoints = - updatedBreakpointDetails - .Select(Protocol.DebugAdapter.Breakpoint.Create) - .ToArray() - }); - } - - protected async Task HandleSetFunctionBreakpointsRequestAsync( - SetFunctionBreakpointsRequestArguments setBreakpointsParams, - RequestContext requestContext) - { - var breakpointDetails = new CommandBreakpointDetails[setBreakpointsParams.Breakpoints.Length]; - for (int i = 0; i < breakpointDetails.Length; i++) - { - FunctionBreakpoint funcBreakpoint = setBreakpointsParams.Breakpoints[i]; - breakpointDetails[i] = CommandBreakpointDetails.Create( - funcBreakpoint.Name, - funcBreakpoint.Condition, - funcBreakpoint.HitCondition); - } - - // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. - CommandBreakpointDetails[] updatedBreakpointDetails = breakpointDetails; - if (!_noDebug) - { - _setBreakpointInProgress = true; - - try - { - updatedBreakpointDetails = - await _editorSession.DebugService.SetCommandBreakpointsAsync( - breakpointDetails); - } - catch (Exception e) - { - // Log whatever the error is - Logger.WriteException($"Caught error while setting command breakpoints", e); - } - finally - { - _setBreakpointInProgress = false; - } - } - - await requestContext.SendResultAsync( - new SetBreakpointsResponseBody { - Breakpoints = - updatedBreakpointDetails - .Select(Protocol.DebugAdapter.Breakpoint.Create) - .ToArray() - }); - } - - protected async Task HandleSetExceptionBreakpointsRequestAsync( - SetExceptionBreakpointsRequestArguments setExceptionBreakpointsParams, - RequestContext requestContext) - { - // TODO: When support for exception breakpoints (unhandled and/or first chance) - // are added to the PowerShell engine, wire up the VSCode exception - // breakpoints here using the pattern below to prevent bug regressions. - //if (!noDebug) - //{ - // setBreakpointInProgress = true; - - // try - // { - // // Set exception breakpoints in DebugService - // } - // catch (Exception e) - // { - // // Log whatever the error is - // Logger.WriteException($"Caught error while setting exception breakpoints", e); - // } - // finally - // { - // setBreakpointInProgress = false; - // } - //} - - await requestContext.SendResultAsync(null); - } - - protected async Task HandleContinueRequestAsync( - object continueParams, - RequestContext requestContext) - { - _editorSession.DebugService.Continue(); - - await requestContext.SendResultAsync(null); - } - - protected async Task HandleNextRequestAsync( - object nextParams, - RequestContext requestContext) - { - _editorSession.DebugService.StepOver(); - - await requestContext.SendResultAsync(null); - } - - protected Task HandlePauseRequestAsync( - object pauseParams, - RequestContext requestContext) - { - try - { - _editorSession.DebugService.Break(); - } - catch (NotSupportedException e) - { - return requestContext.SendErrorAsync(e.Message); - } - - // This request is responded to by sending the "stopped" event - return Task.FromResult(true); - } - - protected async Task HandleStepInRequestAsync( - object stepInParams, - RequestContext requestContext) - { - _editorSession.DebugService.StepIn(); - - await requestContext.SendResultAsync(null); - } - - protected async Task HandleStepOutRequestAsync( - object stepOutParams, - RequestContext requestContext) - { - _editorSession.DebugService.StepOut(); - - await requestContext.SendResultAsync(null); - } - - protected async Task HandleThreadsRequestAsync( - object threadsParams, - RequestContext requestContext) - { - await requestContext.SendResultAsync( - new ThreadsResponseBody - { - Threads = new Thread[] - { - // TODO: What do I do with these? - new Thread - { - Id = 1, - Name = "Main Thread" - } - } - }); - } - - protected async Task HandleStackTraceRequestAsync( - StackTraceRequestArguments stackTraceParams, - RequestContext requestContext) - { - StackFrameDetails[] stackFrames = - _editorSession.DebugService.GetStackFrames(); - - // Handle a rare race condition where the adapter requests stack frames before they've - // begun building. - if (stackFrames == null) - { - await requestContext.SendResultAsync( - new StackTraceResponseBody - { - StackFrames = new StackFrame[0], - TotalFrames = 0 - }); - - return; - } - - List newStackFrames = new List(); - - int startFrameIndex = stackTraceParams.StartFrame ?? 0; - int maxFrameCount = stackFrames.Length; - - // If the number of requested levels == 0 (or null), that means get all stack frames - // after the specified startFrame index. Otherwise get all the stack frames. - int requestedFrameCount = (stackTraceParams.Levels ?? 0); - if (requestedFrameCount > 0) - { - maxFrameCount = Math.Min(maxFrameCount, startFrameIndex + requestedFrameCount); - } - - for (int i = startFrameIndex; i < maxFrameCount; i++) - { - // Create the new StackFrame object with an ID that can - // be referenced back to the current list of stack frames - newStackFrames.Add( - StackFrame.Create( - stackFrames[i], - i)); - } - - await requestContext.SendResultAsync( - new StackTraceResponseBody - { - StackFrames = newStackFrames.ToArray(), - TotalFrames = newStackFrames.Count - }); - } - - protected async Task HandleScopesRequestAsync( - ScopesRequestArguments scopesParams, - RequestContext requestContext) - { - VariableScope[] variableScopes = - _editorSession.DebugService.GetVariableScopes( - scopesParams.FrameId); - - await requestContext.SendResultAsync( - new ScopesResponseBody - { - Scopes = - variableScopes - .Select(Scope.Create) - .ToArray() - }); - } - - protected async Task HandleVariablesRequestAsync( - VariablesRequestArguments variablesParams, - RequestContext requestContext) - { - VariableDetailsBase[] variables = - _editorSession.DebugService.GetVariables( - variablesParams.VariablesReference); - - VariablesResponseBody variablesResponse = null; - - try - { - variablesResponse = new VariablesResponseBody - { - Variables = - variables - .Select(Variable.Create) - .ToArray() - }; - } - catch (Exception) - { - // TODO: This shouldn't be so broad - } - - await requestContext.SendResultAsync(variablesResponse); - } - - protected async Task HandleSetVariablesRequestAsync( - SetVariableRequestArguments setVariableParams, - RequestContext requestContext) - { - try - { - string updatedValue = - await _editorSession.DebugService.SetVariableAsync( - setVariableParams.VariablesReference, - setVariableParams.Name, - setVariableParams.Value); - - var setVariableResponse = new SetVariableResponseBody - { - Value = updatedValue - }; - - await requestContext.SendResultAsync(setVariableResponse); - } - catch (Exception ex) when (ex is ArgumentTransformationMetadataException || - ex is InvalidPowerShellExpressionException || - ex is SessionStateUnauthorizedAccessException) - { - // Catch common, innocuous errors caused by the user supplying a value that can't be converted or the variable is not settable. - Logger.Write(LogLevel.Verbose, $"Failed to set variable: {ex.Message}"); - await requestContext.SendErrorAsync(ex.Message); - } - catch (Exception ex) - { - Logger.Write(LogLevel.Error, $"Unexpected error setting variable: {ex.Message}"); - string msg = - $"Unexpected error: {ex.GetType().Name} - {ex.Message} Please report this error to the PowerShellEditorServices project on GitHub."; - await requestContext.SendErrorAsync(msg); - } - } - - protected Task HandleSourceRequestAsync( - SourceRequestArguments sourceParams, - RequestContext requestContext) - { - // TODO: Implement this message. For now, doesn't seem to - // be a problem that it's missing. - - return Task.FromResult(true); - } - - protected async Task HandleEvaluateRequestAsync( - EvaluateRequestArguments evaluateParams, - RequestContext requestContext) - { - string valueString = null; - int variableId = 0; - - bool isFromRepl = - string.Equals( - evaluateParams.Context, - "repl", - StringComparison.CurrentCultureIgnoreCase); - - if (isFromRepl) - { - var notAwaited = - _editorSession - .PowerShellContext - .ExecuteScriptStringAsync(evaluateParams.Expression, false, true) - .ConfigureAwait(false); - } - else - { - VariableDetailsBase result = null; - - // VS Code might send this request after the debugger - // has been resumed, return an empty result in this case. - if (_editorSession.PowerShellContext.IsDebuggerStopped) - { - // First check to see if the watch expression refers to a naked variable reference. - result = - _editorSession.DebugService.GetVariableFromExpression(evaluateParams.Expression, evaluateParams.FrameId); - - // If the expression is not a naked variable reference, then evaluate the expression. - if (result == null) - { - result = - await _editorSession.DebugService.EvaluateExpressionAsync( - evaluateParams.Expression, - evaluateParams.FrameId, - isFromRepl); - } - } - - if (result != null) - { - valueString = result.ValueString; - variableId = - result.IsExpandable ? - result.Id : 0; - } - } - - await requestContext.SendResultAsync( - new EvaluateResponseBody - { - Result = valueString, - VariablesReference = variableId - }); - } - - private async Task WriteUseIntegratedConsoleMessageAsync() - { - await _messageSender.SendEventAsync( - OutputEvent.Type, - new OutputEventBody - { - Output = "\nThe Debug Console is no longer used for PowerShell debugging. Please use the 'PowerShell Integrated Console' to execute commands in the debugger. Run the 'PowerShell: Show Integrated Console' command to open it.", - Category = "stderr" - }); - } - - private void RegisterEventHandlers() - { - _editorSession.PowerShellContext.RunspaceChanged += powerShellContext_RunspaceChangedAsync; - _editorSession.DebugService.BreakpointUpdated += DebugService_BreakpointUpdatedAsync; - _editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStoppedAsync; - _editorSession.PowerShellContext.DebuggerResumed += powerShellContext_DebuggerResumedAsync; - } - - private void UnregisterEventHandlers() - { - _editorSession.PowerShellContext.RunspaceChanged -= powerShellContext_RunspaceChangedAsync; - _editorSession.DebugService.BreakpointUpdated -= DebugService_BreakpointUpdatedAsync; - _editorSession.DebugService.DebuggerStopped -= DebugService_DebuggerStoppedAsync; - _editorSession.PowerShellContext.DebuggerResumed -= powerShellContext_DebuggerResumedAsync; - } - - private async Task ClearSessionBreakpointsAsync() - { - try - { - await _editorSession.DebugService.ClearAllBreakpointsAsync(); - } - catch (Exception e) - { - Logger.WriteException("Caught exception while clearing breakpoints from session", e); - } - } - - #endregion - - #region Event Handlers - - async void DebugService_DebuggerStoppedAsync(object sender, DebuggerStoppedEventArgs e) - { - // Provide the reason for why the debugger has stopped script execution. - // See https://github.com/Microsoft/vscode/issues/3648 - // The reason is displayed in the breakpoints viewlet. Some recommended reasons are: - // "step", "breakpoint", "function breakpoint", "exception" and "pause". - // We don't support exception breakpoints and for "pause", we can't distinguish - // between stepping and the user pressing the pause/break button in the debug toolbar. - string debuggerStoppedReason = "step"; - if (e.OriginalEvent.Breakpoints.Count > 0) - { - debuggerStoppedReason = - e.OriginalEvent.Breakpoints[0] is CommandBreakpoint - ? "function breakpoint" - : "breakpoint"; - } - - await _messageSender.SendEventAsync( - StoppedEvent.Type, - new StoppedEventBody - { - Source = new Source - { - Path = e.ScriptPath, - }, - ThreadId = 1, - Reason = debuggerStoppedReason - }); - } - - async void powerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) - { - if (_waitingForAttach && - e.ChangeAction == RunspaceChangeAction.Enter && - e.NewRunspace.Context == RunspaceContext.DebuggedRunspace) - { - // Send the InitializedEvent so that the debugger will continue - // sending configuration requests - _waitingForAttach = false; - await _messageSender.SendEventAsync(InitializedEvent.Type, null); - } - else if ( - e.ChangeAction == RunspaceChangeAction.Exit && - (_editorSession == null || - _editorSession.PowerShellContext.IsDebuggerStopped)) - { - // Exited the session while the debugger is stopped, - // send a ContinuedEvent so that the client changes the - // UI to appear to be running again - await _messageSender.SendEventAsync( - ContinuedEvent.Type, - new ContinuedEvent - { - ThreadId = 1, - AllThreadsContinued = true - }); - } - } - - private async void powerShellContext_DebuggerResumedAsync(object sender, DebuggerResumeAction e) - { - await _messageSender.SendEventAsync( - ContinuedEvent.Type, - new ContinuedEvent - { - AllThreadsContinued = true, - ThreadId = 1 - }); - } - - private async void DebugService_BreakpointUpdatedAsync(object sender, BreakpointUpdatedEventArgs e) - { - string reason = "changed"; - - if (_setBreakpointInProgress) - { - // Don't send breakpoint update notifications when setting - // breakpoints on behalf of the client. - return; - } - - switch (e.UpdateType) - { - case BreakpointUpdateType.Set: - reason = "new"; - break; - - case BreakpointUpdateType.Removed: - reason = "removed"; - break; - } - - Protocol.DebugAdapter.Breakpoint breakpoint; - if (e.Breakpoint is LineBreakpoint) - { - breakpoint = Protocol.DebugAdapter.Breakpoint.Create(BreakpointDetails.Create(e.Breakpoint)); - } - else if (e.Breakpoint is CommandBreakpoint) - { - //breakpoint = Protocol.DebugAdapter.Breakpoint.Create(CommandBreakpointDetails.Create(e.Breakpoint)); - Logger.Write(LogLevel.Verbose, "Function breakpoint updated event is not supported yet"); - return; - } - else - { - Logger.Write(LogLevel.Error, $"Unrecognized breakpoint type {e.Breakpoint.GetType().FullName}"); - return; - } - - breakpoint.Verified = e.UpdateType != BreakpointUpdateType.Disabled; - - await _messageSender.SendEventAsync( - BreakpointEvent.Type, - new BreakpointEvent - { - Reason = reason, - Breakpoint = breakpoint - }); - } - - #endregion - - #region Events - - public event EventHandler SessionEnded; - - protected virtual void OnSessionEnded() - { - SessionEnded?.Invoke(this, null); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs deleted file mode 100644 index 156acb9b2..000000000 --- a/src/PowerShellEditorServices.Protocol/Server/IMessageDispatcher.cs +++ /dev/null @@ -1,17 +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.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; - -namespace Microsoft.PowerShell.EditorServices.Protocol -{ - public interface IMessageDispatcher - { - Task DispatchMessageAsync( - Message messageToDispatch, - MessageWriter messageWriter); - } -} diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs deleted file mode 100644 index 040579c43..000000000 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ /dev/null @@ -1,2089 +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 Microsoft.PowerShell.EditorServices.Debugging; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; -using Microsoft.PowerShell.EditorServices.Templates; -using Microsoft.PowerShell.EditorServices.Utility; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Management.Automation.Language; -using System.Management.Automation.Runspaces; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; - -using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Server -{ - using System.Management.Automation; - - public class LanguageServer - { - private static CancellationTokenSource s_existingRequestCancellation; - - private static readonly Location[] s_emptyLocationResult = new Location[0]; - - private static readonly CompletionItem[] s_emptyCompletionResult = new CompletionItem[0]; - - private static readonly SignatureInformation[] s_emptySignatureResult = new SignatureInformation[0]; - - private static readonly DocumentHighlight[] s_emptyHighlightResult = new DocumentHighlight[0]; - - private static readonly SymbolInformation[] s_emptySymbolResult = new SymbolInformation[0]; - - private ILogger Logger; - private bool profilesLoaded; - private bool consoleReplStarted; - private EditorSession editorSession; - private IMessageSender messageSender; - private IMessageHandlers messageHandlers; - private LanguageServerEditorOperations editorOperations; - private LanguageServerSettings currentSettings = new LanguageServerSettings(); - - // The outer key is the file's uri, the inner key is a unique id for the diagnostic - private Dictionary> codeActionsPerFile = - new Dictionary>(); - - private TaskCompletionSource serverCompletedTask; - - public IEditorOperations EditorOperations - { - get { return this.editorOperations; } - } - - /// - /// Initializes a new language server that is used for handing language server protocol messages - /// - /// The editor session that handles the PowerShell runspace - /// An object that manages all of the message handlers - /// The message sender - /// A TaskCompletionSource that will be completed to stop the running process - /// The logger. - public LanguageServer( - EditorSession editorSession, - IMessageHandlers messageHandlers, - IMessageSender messageSender, - TaskCompletionSource serverCompletedTask, - ILogger logger) - { - this.Logger = logger; - this.editorSession = editorSession; - this.serverCompletedTask = serverCompletedTask; - // Attach to the underlying PowerShell context to listen for changes in the runspace or execution status - this.editorSession.PowerShellContext.RunspaceChanged += PowerShellContext_RunspaceChangedAsync; - this.editorSession.PowerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; - - // Attach to ExtensionService events - this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAddedAsync; - this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdatedAsync; - this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemovedAsync; - - this.messageSender = messageSender; - this.messageHandlers = messageHandlers; - - // Create the IEditorOperations implementation - this.editorOperations = - new LanguageServerEditorOperations( - this.editorSession, - this.messageSender); - - this.editorSession.StartDebugService(this.editorOperations); - this.editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStoppedAsync; - } - - /// - /// Starts the language server client and sends the Initialize method. - /// - /// A Task that can be awaited for initialization to complete. - public void Start() - { - // Register all supported message types - - this.messageHandlers.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequestAsync); - this.messageHandlers.SetEventHandler(ExitNotification.Type, this.HandleExitNotificationAsync); - - this.messageHandlers.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequestAsync); - this.messageHandlers.SetEventHandler(InitializedNotification.Type, this.HandleInitializedNotificationAsync); - - this.messageHandlers.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotificationAsync); - this.messageHandlers.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotificationAsync); - this.messageHandlers.SetEventHandler(DidSaveTextDocumentNotification.Type, this.HandleDidSaveTextDocumentNotificationAsync); - this.messageHandlers.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotificationAsync); - this.messageHandlers.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotificationAsync); - - this.messageHandlers.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequestAsync); - this.messageHandlers.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequestAsync); - this.messageHandlers.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequestAsync); - this.messageHandlers.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequestAsync); - this.messageHandlers.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequestAsync); - this.messageHandlers.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequestAsync); - this.messageHandlers.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequestAsync); - this.messageHandlers.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequestAsync); - this.messageHandlers.SetRequestHandler(CodeActionRequest.Type, this.HandleCodeActionRequestAsync); - this.messageHandlers.SetRequestHandler(DocumentFormattingRequest.Type, this.HandleDocumentFormattingRequestAsync); - this.messageHandlers.SetRequestHandler( - DocumentRangeFormattingRequest.Type, - this.HandleDocumentRangeFormattingRequestAsync); - this.messageHandlers.SetRequestHandler(FoldingRangeRequest.Type, this.HandleFoldingRangeRequestAsync); - - this.messageHandlers.SetRequestHandler(ShowHelpRequest.Type, this.HandleShowHelpRequestAsync); - - this.messageHandlers.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequestAsync); - this.messageHandlers.SetRequestHandler(GetCommandRequest.Type, this.HandleGetCommandRequestAsync); - - this.messageHandlers.SetRequestHandler(FindModuleRequest.Type, this.HandleFindModuleRequestAsync); - this.messageHandlers.SetRequestHandler(InstallModuleRequest.Type, this.HandleInstallModuleRequestAsync); - - this.messageHandlers.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequestAsync); - - this.messageHandlers.SetRequestHandler(PowerShellVersionRequest.Type, this.HandlePowerShellVersionRequestAsync); - - this.messageHandlers.SetRequestHandler(NewProjectFromTemplateRequest.Type, this.HandleNewProjectFromTemplateRequestAsync); - this.messageHandlers.SetRequestHandler(GetProjectTemplatesRequest.Type, this.HandleGetProjectTemplatesRequestAsync); - - this.messageHandlers.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequestAsync); - - this.messageHandlers.SetRequestHandler(GetPSSARulesRequest.Type, this.HandleGetPSSARulesRequestAsync); - this.messageHandlers.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequestAsync); - - this.messageHandlers.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequestAsync); - - this.messageHandlers.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequestAsync); - this.messageHandlers.SetRequestHandler(CommentHelpRequest.Type, this.HandleCommentHelpRequestAsync); - - this.messageHandlers.SetRequestHandler(GetRunspaceRequest.Type, this.HandleGetRunspaceRequestAsync); - - // Initialize the extension service - // TODO: This should be made awaited once Initialize is async! - this.editorSession.ExtensionService.InitializeAsync( - this.editorOperations, - this.editorSession.Components).Wait(); - } - - protected Task Stop() - { - Logger.Write(LogLevel.Normal, "Language service is shutting down..."); - - // complete the task so that the host knows to shut down - this.serverCompletedTask.SetResult(true); - - return Task.FromResult(true); - } - - #region Built-in Message Handlers - - private async Task HandleShutdownRequestAsync( - RequestContext requestContext) - { - // Allow the implementor to shut down gracefully - - await requestContext.SendResultAsync(new object()); - } - - private async Task HandleExitNotificationAsync( - object exitParams, - EventContext eventContext) - { - // Stop the server channel - await this.Stop(); - } - - private Task HandleInitializedNotificationAsync(InitializedParams initializedParams, - EventContext eventContext) - { - // Can do dynamic registration of capabilities in this notification handler - return Task.FromResult(true); - } - - protected async Task HandleInitializeRequestAsync( - InitializeParams initializeParams, - RequestContext requestContext) - { - // Grab the workspace path from the parameters - editorSession.Workspace.WorkspacePath = initializeParams.RootPath; - - // Set the working directory of the PowerShell session to the workspace path - if (editorSession.Workspace.WorkspacePath != null - && Directory.Exists(editorSession.Workspace.WorkspacePath)) - { - await editorSession.PowerShellContext.SetWorkingDirectoryAsync( - editorSession.Workspace.WorkspacePath, - isPathAlreadyEscaped: false); - } - - await requestContext.SendResultAsync( - new InitializeResult - { - Capabilities = new ServerCapabilities - { - TextDocumentSync = TextDocumentSyncKind.Incremental, - DefinitionProvider = true, - ReferencesProvider = true, - DocumentHighlightProvider = true, - DocumentSymbolProvider = true, - WorkspaceSymbolProvider = true, - HoverProvider = true, - CodeActionProvider = true, - CodeLensProvider = new CodeLensOptions { ResolveProvider = true }, - CompletionProvider = new CompletionOptions - { - ResolveProvider = true, - TriggerCharacters = new string[] { ".", "-", ":", "\\" } - }, - SignatureHelpProvider = new SignatureHelpOptions - { - TriggerCharacters = new string[] { " " } // TODO: Other characters here? - }, - DocumentFormattingProvider = false, - DocumentRangeFormattingProvider = false, - RenameProvider = false, - FoldingRangeProvider = true - } - }); - } - - protected async Task HandleShowHelpRequestAsync( - string helpParams, - RequestContext requestContext) - { - const string CheckHelpScript = @" - [CmdletBinding()] - param ( - [String]$CommandName - ) - try { - $command = Microsoft.PowerShell.Core\Get-Command $CommandName -ErrorAction Stop - } catch [System.Management.Automation.CommandNotFoundException] { - $PSCmdlet.ThrowTerminatingError($PSItem) - } - try { - $helpUri = [Microsoft.PowerShell.Commands.GetHelpCodeMethods]::GetHelpUri($command) - - $oldSslVersion = [System.Net.ServicePointManager]::SecurityProtocol - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - - # HEAD means we don't need the content itself back, just the response header - $status = (Microsoft.PowerShell.Utility\Invoke-WebRequest -Method Head -Uri $helpUri -TimeoutSec 5 -ErrorAction Stop).StatusCode - if ($status -lt 400) { - $null = Microsoft.PowerShell.Core\Get-Help $CommandName -Online - return - } - } catch { - # Ignore - we want to drop out to Get-Help -Full - } finally { - [System.Net.ServicePointManager]::SecurityProtocol = $oldSslVersion - } - - return Microsoft.PowerShell.Core\Get-Help $CommandName -Full - "; - - if (string.IsNullOrEmpty(helpParams)) { helpParams = "Get-Help"; } - - PSCommand checkHelpPSCommand = new PSCommand() - .AddScript(CheckHelpScript, useLocalScope: true) - .AddArgument(helpParams); - - // TODO: Rather than print the help in the console, we should send the string back - // to VSCode to display in a help pop-up (or similar) - await editorSession.PowerShellContext.ExecuteCommandAsync(checkHelpPSCommand, sendOutputToHost: true); - await requestContext.SendResultAsync(null); - } - - private async Task HandleSetPSSARulesRequestAsync( - object param, - RequestContext requestContext) - { - var dynParams = param as dynamic; - if (editorSession.AnalysisService != null && - editorSession.AnalysisService.SettingsPath == null) - { - var activeRules = new List(); - var ruleInfos = dynParams.ruleInfos; - foreach (dynamic ruleInfo in ruleInfos) - { - if ((Boolean)ruleInfo.isEnabled) - { - activeRules.Add((string)ruleInfo.name); - } - } - editorSession.AnalysisService.ActiveRules = activeRules.ToArray(); - } - - var sendresult = requestContext.SendResultAsync(null); - var scripFile = editorSession.Workspace.GetFile((string)dynParams.filepath); - await RunScriptDiagnosticsAsync( - new ScriptFile[] { scripFile }, - editorSession, - this.messageSender.SendEventAsync); - await sendresult; - } - - private async Task HandleGetFormatScriptRegionRequestAsync( - ScriptRegionRequestParams requestParams, - RequestContext requestContext) - { - var scriptFile = this.editorSession.Workspace.GetFile(requestParams.FileUri); - var lineNumber = requestParams.Line; - var columnNumber = requestParams.Column; - ScriptRegion scriptRegion = null; - - switch (requestParams.Character) - { - case "\n": - // find the smallest statement ast that occupies - // the element before \n or \r\n and return the extent. - --lineNumber; // vscode sends the next line when pressed enter - var line = scriptFile.GetLine(lineNumber); - if (!String.IsNullOrEmpty(line)) - { - scriptRegion = this.editorSession.LanguageService.FindSmallestStatementAstRegion( - scriptFile, - lineNumber, - line.Length); - } - break; - - case "}": - scriptRegion = this.editorSession.LanguageService.FindSmallestStatementAstRegion( - scriptFile, - lineNumber, - columnNumber); - break; - - default: - break; - } - - await requestContext.SendResultAsync(new ScriptRegionRequestResult - { - scriptRegion = scriptRegion - }); - } - - private async Task HandleGetPSSARulesRequestAsync( - object param, - RequestContext requestContext) - { - List rules = null; - if (editorSession.AnalysisService != null - && editorSession.AnalysisService.SettingsPath == null) - { - rules = new List(); - var ruleNames = editorSession.AnalysisService.GetPSScriptAnalyzerRules(); - var activeRules = editorSession.AnalysisService.ActiveRules; - foreach (var ruleName in ruleNames) - { - rules.Add(new { name = ruleName, isEnabled = activeRules.Contains(ruleName, StringComparer.OrdinalIgnoreCase) }); - } - } - - await requestContext.SendResultAsync(rules); - } - - private async Task HandleInstallModuleRequestAsync( - string moduleName, - RequestContext requestContext - ) - { - var script = string.Format("Install-Module -Name {0} -Scope CurrentUser", moduleName); - - var executeTask = - editorSession.PowerShellContext.ExecuteScriptStringAsync( - script, - true, - true).ConfigureAwait(false); - - await requestContext.SendResultAsync(null); - } - - private Task HandleInvokeExtensionCommandRequestAsync( - InvokeExtensionCommandRequest commandDetails, - RequestContext requestContext) - { - // We don't await the result of the execution here because we want - // to be able to receive further messages while the editor command - // is executing. This important in cases where the pipeline thread - // gets blocked by something in the script like a prompt to the user. - EditorContext editorContext = - this.editorOperations.ConvertClientEditorContext( - commandDetails.Context); - - Task commandTask = - this.editorSession.ExtensionService.InvokeCommandAsync( - commandDetails.Name, - editorContext); - - commandTask.ContinueWith(t => - { - return requestContext.SendResultAsync(null); - }); - - return Task.FromResult(true); - } - - private Task HandleNewProjectFromTemplateRequestAsync( - NewProjectFromTemplateRequest newProjectArgs, - RequestContext requestContext) - { - // Don't await the Task here so that we don't block the session - this.editorSession.TemplateService - .CreateFromTemplateAsync(newProjectArgs.TemplatePath, newProjectArgs.DestinationPath) - .ContinueWith( - async task => - { - await requestContext.SendResultAsync( - new NewProjectFromTemplateResponse - { - CreationSuccessful = task.Result - }); - }); - - return Task.FromResult(true); - } - - private async Task HandleGetProjectTemplatesRequestAsync( - GetProjectTemplatesRequest requestArgs, - RequestContext requestContext) - { - bool plasterInstalled = await this.editorSession.TemplateService.ImportPlasterIfInstalledAsync(); - - if (plasterInstalled) - { - var availableTemplates = - await this.editorSession.TemplateService.GetAvailableTemplatesAsync( - requestArgs.IncludeInstalledModules); - - await requestContext.SendResultAsync( - new GetProjectTemplatesResponse - { - Templates = availableTemplates - }); - } - else - { - await requestContext.SendResultAsync( - new GetProjectTemplatesResponse - { - NeedsModuleInstall = true, - Templates = new TemplateDetails[0] - }); - } - } - - private async Task HandleExpandAliasRequestAsync( - string content, - RequestContext requestContext) - { - var script = @" -function __Expand-Alias { - - param($targetScript) - - [ref]$errors=$null - - $tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) | - Sort-Object Start -Descending - - foreach ($token in $tokens) { - $definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition - - if($definition) { - $lhs=$targetScript.Substring(0, $token.Start) - $rhs=$targetScript.Substring($token.Start + $token.Length) - - $targetScript=$lhs + $definition + $rhs - } - } - - $targetScript -}"; - var psCommand = new PSCommand(); - psCommand.AddScript(script); - await this.editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); - - psCommand = new PSCommand(); - psCommand.AddCommand("__Expand-Alias").AddArgument(content); - var result = await this.editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); - - await requestContext.SendResultAsync(result.First().ToString()); - } - - private async Task HandleGetCommandRequestAsync( - string param, - RequestContext requestContext) - { - PSCommand psCommand = new PSCommand(); - if (!string.IsNullOrEmpty(param)) - { - psCommand.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddArgument(param); - } - else - { - // Executes the following: - // Get-Command -CommandType Function,Cmdlet,ExternalScript | Select-Object -Property Name,ModuleName | Sort-Object -Property Name - psCommand - .AddCommand("Microsoft.PowerShell.Core\\Get-Command") - .AddParameter("CommandType", new[]{"Function", "Cmdlet", "ExternalScript"}) - .AddCommand("Microsoft.PowerShell.Utility\\Select-Object") - .AddParameter("Property", new[]{"Name", "ModuleName"}) - .AddCommand("Microsoft.PowerShell.Utility\\Sort-Object") - .AddParameter("Property", "Name"); - } - - IEnumerable result = await this.editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); - - var commandList = new List(); - if (result != null) - { - foreach (dynamic command in result) - { - commandList.Add(new PSCommandMessage - { - Name = command.Name, - ModuleName = command.ModuleName, - Parameters = command.Parameters, - ParameterSets = command.ParameterSets, - DefaultParameterSet = command.DefaultParameterSet - }); - } - } - - await requestContext.SendResultAsync(commandList); - } - - private async Task HandleFindModuleRequestAsync( - object param, - RequestContext requestContext) - { - var psCommand = new PSCommand(); - psCommand.AddScript("Find-Module | Select Name, Description"); - - var modules = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); - - var moduleList = new List(); - - if (modules != null) - { - foreach (dynamic m in modules) - { - moduleList.Add(new PSModuleMessage { Name = m.Name, Description = m.Description }); - } - } - - await requestContext.SendResultAsync(moduleList); - } - - protected Task HandleDidOpenTextDocumentNotificationAsync( - DidOpenTextDocumentParams openParams, - EventContext eventContext) - { - ScriptFile openedFile = - editorSession.Workspace.GetFileBuffer( - openParams.TextDocument.Uri, - openParams.TextDocument.Text); - - // TODO: Get all recently edited files in the workspace - this.RunScriptDiagnosticsAsync( - new ScriptFile[] { openedFile }, - editorSession, - eventContext); - - Logger.Write(LogLevel.Verbose, "Finished opening document."); - - return Task.FromResult(true); - } - - protected async Task HandleDidCloseTextDocumentNotificationAsync( - DidCloseTextDocumentParams closeParams, - EventContext eventContext) - { - // Find and close the file in the current session - var fileToClose = editorSession.Workspace.GetFile(closeParams.TextDocument.Uri); - - if (fileToClose != null) - { - editorSession.Workspace.CloseFile(fileToClose); - await ClearMarkersAsync(fileToClose, eventContext); - } - - Logger.Write(LogLevel.Verbose, "Finished closing document."); - } - protected async Task HandleDidSaveTextDocumentNotificationAsync( - DidSaveTextDocumentParams saveParams, - EventContext eventContext) - { - ScriptFile savedFile = - this.editorSession.Workspace.GetFile( - saveParams.TextDocument.Uri); - - if (savedFile != null) - { - if (this.editorSession.RemoteFileManager.IsUnderRemoteTempPath(savedFile.FilePath)) - { - await this.editorSession.RemoteFileManager.SaveRemoteFileAsync( - savedFile.FilePath); - } - } - } - - protected Task HandleDidChangeTextDocumentNotificationAsync( - DidChangeTextDocumentParams textChangeParams, - EventContext eventContext) - { - List changedFiles = new List(); - - // A text change notification can batch multiple change requests - foreach (var textChange in textChangeParams.ContentChanges) - { - ScriptFile changedFile = editorSession.Workspace.GetFile(textChangeParams.TextDocument.Uri); - - changedFile.ApplyChange( - GetFileChangeDetails( - textChange.Range, - textChange.Text)); - - changedFiles.Add(changedFile); - } - - // TODO: Get all recently edited files in the workspace - this.RunScriptDiagnosticsAsync( - changedFiles.ToArray(), - editorSession, - eventContext); - - return Task.FromResult(true); - } - - protected async Task HandleDidChangeConfigurationNotificationAsync( - DidChangeConfigurationParams configChangeParams, - EventContext eventContext) - { - bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; - bool oldScriptAnalysisEnabled = - this.currentSettings.ScriptAnalysis.Enable.HasValue ? this.currentSettings.ScriptAnalysis.Enable.Value : false; - string oldScriptAnalysisSettingsPath = - this.currentSettings.ScriptAnalysis?.SettingsPath; - - this.currentSettings.Update( - configChangeParams.Settings.Powershell, - this.editorSession.Workspace.WorkspacePath, - this.Logger); - - if (!this.profilesLoaded && - this.currentSettings.EnableProfileLoading && - oldLoadProfiles != this.currentSettings.EnableProfileLoading) - { - await this.editorSession.PowerShellContext.LoadHostProfilesAsync(); - this.profilesLoaded = true; - } - - // Wait until after profiles are loaded (or not, if that's the - // case) before starting the interactive console. - if (!this.consoleReplStarted) - { - // Start the interactive terminal - this.editorSession.HostInput.StartCommandLoop(); - this.consoleReplStarted = true; - } - - // If there is a new settings file path, restart the analyzer with the new settigs. - bool settingsPathChanged = false; - string newSettingsPath = this.currentSettings.ScriptAnalysis.SettingsPath; - if (!string.Equals(oldScriptAnalysisSettingsPath, newSettingsPath, StringComparison.OrdinalIgnoreCase)) - { - if (this.editorSession.AnalysisService != null) - { - this.editorSession.AnalysisService.SettingsPath = newSettingsPath; - settingsPathChanged = true; - } - } - - // If script analysis settings have changed we need to clear & possibly update the current diagnostic records. - if ((oldScriptAnalysisEnabled != this.currentSettings.ScriptAnalysis?.Enable) || settingsPathChanged) - { - // If the user just turned off script analysis or changed the settings path, send a diagnostics - // event to clear the analysis markers that they already have. - if (!this.currentSettings.ScriptAnalysis.Enable.Value || settingsPathChanged) - { - foreach (var scriptFile in editorSession.Workspace.GetOpenedFiles()) - { - await ClearMarkersAsync(scriptFile, eventContext); - } - } - - await this.RunScriptDiagnosticsAsync( - this.editorSession.Workspace.GetOpenedFiles(), - this.editorSession, - eventContext); - } - - // Convert the editor file glob patterns into an array for the Workspace - // Both the files.exclude and search.exclude hash tables look like (glob-text, is-enabled): - // "files.exclude" : { - // "Makefile": true, - // "*.html": true, - // "build/*": true - // } - var excludeFilePatterns = new List(); - if (configChangeParams.Settings.Files?.Exclude != null) - { - foreach(KeyValuePair patternEntry in configChangeParams.Settings.Files.Exclude) - { - if (patternEntry.Value) { excludeFilePatterns.Add(patternEntry.Key); } - } - } - if (configChangeParams.Settings.Search?.Exclude != null) - { - foreach(KeyValuePair patternEntry in configChangeParams.Settings.Files.Exclude) - { - if (patternEntry.Value && !excludeFilePatterns.Contains(patternEntry.Key)) { excludeFilePatterns.Add(patternEntry.Key); } - } - } - editorSession.Workspace.ExcludeFilesGlob = excludeFilePatterns; - - // Convert the editor file search options to Workspace properties - if (configChangeParams.Settings.Search?.FollowSymlinks != null) - { - editorSession.Workspace.FollowSymlinks = configChangeParams.Settings.Search.FollowSymlinks; - } - } - - protected async Task HandleDefinitionRequestAsync( - TextDocumentPositionParams textDocumentPosition, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - textDocumentPosition.TextDocument.Uri); - - SymbolReference foundSymbol = - editorSession.LanguageService.FindSymbolAtLocation( - scriptFile, - textDocumentPosition.Position.Line + 1, - textDocumentPosition.Position.Character + 1); - - List definitionLocations = new List(); - - GetDefinitionResult definition = null; - if (foundSymbol != null) - { - definition = - await editorSession.LanguageService.GetDefinitionOfSymbolAsync( - scriptFile, - foundSymbol, - editorSession.Workspace); - - if (definition != null) - { - definitionLocations.Add( - new Location - { - Uri = GetFileUri(definition.FoundDefinition.FilePath), - Range = GetRangeFromScriptRegion(definition.FoundDefinition.ScriptRegion) - }); - } - } - - await requestContext.SendResultAsync(definitionLocations.ToArray()); - } - - protected async Task HandleReferencesRequestAsync( - ReferencesParams referencesParams, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - referencesParams.TextDocument.Uri); - - SymbolReference foundSymbol = - editorSession.LanguageService.FindSymbolAtLocation( - scriptFile, - referencesParams.Position.Line + 1, - referencesParams.Position.Character + 1); - - FindReferencesResult referencesResult = - await editorSession.LanguageService.FindReferencesOfSymbolAsync( - foundSymbol, - editorSession.Workspace.ExpandScriptReferences(scriptFile), - editorSession.Workspace); - - Location[] referenceLocations = s_emptyLocationResult; - - if (referencesResult != null) - { - var locations = new List(); - foreach (SymbolReference foundReference in referencesResult.FoundReferences) - { - locations.Add(new Location - { - Uri = GetFileUri(foundReference.FilePath), - Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) - }); - } - referenceLocations = locations.ToArray(); - } - - await requestContext.SendResultAsync(referenceLocations); - } - - protected async Task HandleCompletionRequestAsync( - TextDocumentPositionParams textDocumentPositionParams, - RequestContext requestContext) - { - int cursorLine = textDocumentPositionParams.Position.Line + 1; - int cursorColumn = textDocumentPositionParams.Position.Character + 1; - - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - textDocumentPositionParams.TextDocument.Uri); - - CompletionResults completionResults = - await editorSession.LanguageService.GetCompletionsInFileAsync( - scriptFile, - cursorLine, - cursorColumn); - - CompletionItem[] completionItems = s_emptyCompletionResult; - - if (completionResults != null) - { - completionItems = new CompletionItem[completionResults.Completions.Length]; - for (int i = 0; i < completionItems.Length; i++) - { - completionItems[i] = CreateCompletionItem(completionResults.Completions[i], completionResults.ReplacedRange, i + 1); - } - } - - await requestContext.SendResultAsync(completionItems); - } - - protected async Task HandleCompletionResolveRequestAsync( - CompletionItem completionItem, - RequestContext requestContext) - { - if (completionItem.Kind == CompletionItemKind.Function) - { - // Get the documentation for the function - CommandInfo commandInfo = - await CommandHelpers.GetCommandInfoAsync( - completionItem.Label, - this.editorSession.PowerShellContext); - - if (commandInfo != null) - { - completionItem.Documentation = - await CommandHelpers.GetCommandSynopsisAsync( - commandInfo, - this.editorSession.PowerShellContext); - } - } - - // Send back the updated CompletionItem - await requestContext.SendResultAsync(completionItem); - } - - protected async Task HandleSignatureHelpRequestAsync( - TextDocumentPositionParams textDocumentPositionParams, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - textDocumentPositionParams.TextDocument.Uri); - - ParameterSetSignatures parameterSets = - await editorSession.LanguageService.FindParameterSetsInFileAsync( - scriptFile, - textDocumentPositionParams.Position.Line + 1, - textDocumentPositionParams.Position.Character + 1); - - SignatureInformation[] signatures = s_emptySignatureResult; - - if (parameterSets != null) - { - signatures = new SignatureInformation[parameterSets.Signatures.Length]; - for (int i = 0; i < signatures.Length; i++) - { - var parameters = new ParameterInformation[parameterSets.Signatures[i].Parameters.Count()]; - int j = 0; - foreach (ParameterInfo param in parameterSets.Signatures[i].Parameters) - { - parameters[j] = CreateParameterInfo(param); - j++; - } - - signatures[i] = new SignatureInformation - { - Label = parameterSets.CommandName + " " + parameterSets.Signatures[i].SignatureText, - Documentation = null, - Parameters = parameters, - }; - } - } - - await requestContext.SendResultAsync( - new SignatureHelp - { - Signatures = signatures, - ActiveParameter = null, - ActiveSignature = 0 - }); - } - - protected async Task HandleDocumentHighlightRequestAsync( - TextDocumentPositionParams textDocumentPositionParams, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - textDocumentPositionParams.TextDocument.Uri); - - FindOccurrencesResult occurrencesResult = - editorSession.LanguageService.FindOccurrencesInFile( - scriptFile, - textDocumentPositionParams.Position.Line + 1, - textDocumentPositionParams.Position.Character + 1); - - DocumentHighlight[] documentHighlights = s_emptyHighlightResult; - - if (occurrencesResult != null) - { - var highlights = new List(); - foreach (SymbolReference foundOccurrence in occurrencesResult.FoundOccurrences) - { - highlights.Add(new DocumentHighlight - { - Kind = DocumentHighlightKind.Write, // TODO: Which symbol types are writable? - Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) - }); - } - documentHighlights = highlights.ToArray(); - } - - await requestContext.SendResultAsync(documentHighlights); - } - - protected async Task HandleHoverRequestAsync( - TextDocumentPositionParams textDocumentPositionParams, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - textDocumentPositionParams.TextDocument.Uri); - - SymbolDetails symbolDetails = - await editorSession - .LanguageService - .FindSymbolDetailsAtLocationAsync( - scriptFile, - textDocumentPositionParams.Position.Line + 1, - textDocumentPositionParams.Position.Character + 1); - - List symbolInfo = new List(); - Range symbolRange = null; - - if (symbolDetails != null) - { - symbolInfo.Add( - new MarkedString - { - Language = "PowerShell", - Value = symbolDetails.DisplayString - }); - - if (!string.IsNullOrEmpty(symbolDetails.Documentation)) - { - symbolInfo.Add( - new MarkedString - { - Language = "markdown", - Value = symbolDetails.Documentation - }); - } - - symbolRange = GetRangeFromScriptRegion(symbolDetails.SymbolReference.ScriptRegion); - } - - await requestContext.SendResultAsync( - new Hover - { - Contents = symbolInfo.ToArray(), - Range = symbolRange - }); - } - - protected async Task HandleDocumentSymbolRequestAsync( - DocumentSymbolParams documentSymbolParams, - RequestContext requestContext) - { - ScriptFile scriptFile = - editorSession.Workspace.GetFile( - documentSymbolParams.TextDocument.Uri); - - FindOccurrencesResult foundSymbols = - editorSession.LanguageService.FindSymbolsInFile( - scriptFile); - - string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - - SymbolInformation[] symbols = s_emptySymbolResult; - if (foundSymbols != null) - { - var symbolAcc = new List(); - foreach (SymbolReference foundOccurrence in foundSymbols.FoundOccurrences) - { - var location = new Location - { - Uri = GetFileUri(foundOccurrence.FilePath), - Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) - }; - - symbolAcc.Add(new SymbolInformation - { - ContainerName = containerName, - Kind = GetSymbolKind(foundOccurrence.SymbolType), - Location = location, - Name = GetDecoratedSymbolName(foundOccurrence) - }); - } - symbols = symbolAcc.ToArray(); - } - - await requestContext.SendResultAsync(symbols); - } - - public static SymbolKind GetSymbolKind(SymbolType symbolType) - { - switch (symbolType) - { - case SymbolType.Configuration: - case SymbolType.Function: - case SymbolType.Workflow: - return SymbolKind.Function; - - default: - return SymbolKind.Variable; - } - } - - public static string GetDecoratedSymbolName(SymbolReference symbolReference) - { - string name = symbolReference.SymbolName; - - if (symbolReference.SymbolType == SymbolType.Configuration || - symbolReference.SymbolType == SymbolType.Function || - symbolReference.SymbolType == SymbolType.Workflow) - { - name += " { }"; - } - - return name; - } - - protected async Task HandleWorkspaceSymbolRequestAsync( - WorkspaceSymbolParams workspaceSymbolParams, - RequestContext requestContext) - { - var symbols = new List(); - - foreach (ScriptFile scriptFile in editorSession.Workspace.GetOpenedFiles()) - { - FindOccurrencesResult foundSymbols = - editorSession.LanguageService.FindSymbolsInFile( - scriptFile); - - // TODO: Need to compute a relative path that is based on common path for all workspace files - string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - - if (foundSymbols != null) - { - foreach (SymbolReference foundOccurrence in foundSymbols.FoundOccurrences) - { - if (!IsQueryMatch(workspaceSymbolParams.Query, foundOccurrence.SymbolName)) - { - continue; - } - - var location = new Location - { - Uri = GetFileUri(foundOccurrence.FilePath), - Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) - }; - - symbols.Add(new SymbolInformation - { - ContainerName = containerName, - Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, - Location = location, - Name = GetDecoratedSymbolName(foundOccurrence) - }); - } - } - } - - await requestContext.SendResultAsync(symbols.ToArray()); - } - - protected async Task HandlePowerShellVersionRequestAsync( - object noParams, - RequestContext requestContext) - { - await requestContext.SendResultAsync( - new PowerShellVersion( - this.editorSession.PowerShellContext.LocalPowerShellVersion)); - } - - protected async Task HandleGetPSHostProcessesRequestAsync( - object noParams, - RequestContext requestContext) - { - var psHostProcesses = new List(); - - if (this.editorSession.PowerShellContext.LocalPowerShellVersion.Version.Major >= 5) - { - int processId = System.Diagnostics.Process.GetCurrentProcess().Id; - var psCommand = new PSCommand(); - psCommand.AddCommand("Get-PSHostProcessInfo"); - psCommand.AddCommand("Where-Object") - .AddParameter("Property", "ProcessId") - .AddParameter("NE") - .AddParameter("Value", processId.ToString()); - - var processes = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand); - if (processes != null) - { - foreach (dynamic p in processes) - { - psHostProcesses.Add( - new GetPSHostProcessesResponse - { - ProcessName = p.ProcessName, - ProcessId = p.ProcessId, - AppDomainName = p.AppDomainName, - MainWindowTitle = p.MainWindowTitle - }); - } - } - } - - await requestContext.SendResultAsync(psHostProcesses.ToArray()); - } - - protected async Task HandleCommentHelpRequestAsync( - CommentHelpRequestParams requestParams, - RequestContext requestContext) - { - var result = new CommentHelpRequestResult(); - - ScriptFile scriptFile; - if (!this.editorSession.Workspace.TryGetFile(requestParams.DocumentUri, out scriptFile)) - { - await requestContext.SendResultAsync(result); - return; - } - - int triggerLine = requestParams.TriggerPosition.Line + 1; - - string helpLocation; - FunctionDefinitionAst functionDefinitionAst = editorSession.LanguageService.GetFunctionDefinitionForHelpComment( - scriptFile, - triggerLine, - out helpLocation); - - if (functionDefinitionAst == null) - { - await requestContext.SendResultAsync(result); - return; - } - - IScriptExtent funcExtent = functionDefinitionAst.Extent; - string funcText = funcExtent.Text; - if (helpLocation.Equals("begin")) - { - // check if the previous character is `<` because it invalidates - // the param block the follows it. - IList lines = ScriptFile.GetLines(funcText); - int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber; - if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<") > -1) - { - lines[relativeTriggerLine0b] = string.Empty; - } - - funcText = string.Join("\n", lines); - } - - List analysisResults = await this.editorSession.AnalysisService.GetSemanticMarkersAsync( - funcText, - AnalysisService.GetCommentHelpRuleSettings( - enable: true, - exportedOnly: false, - blockComment: requestParams.BlockComment, - vscodeSnippetCorrection: true, - placement: helpLocation)); - - string helpText = analysisResults?.FirstOrDefault()?.Correction?.Edits[0].Text; - - if (helpText == null) - { - await requestContext.SendResultAsync(result); - return; - } - - result.Content = ScriptFile.GetLines(helpText).ToArray(); - - if (helpLocation != null && - !helpLocation.Equals("before", StringComparison.OrdinalIgnoreCase)) - { - // we need to trim the leading `{` and newline when helpLocation=="begin" - // we also need to trim the leading newline when helpLocation=="end" - result.Content = result.Content.Skip(1).ToArray(); - } - - await requestContext.SendResultAsync(result); - } - - protected async Task HandleGetRunspaceRequestAsync( - string processId, - RequestContext requestContext) - { - IEnumerable runspaces = null; - - if (this.editorSession.PowerShellContext.LocalPowerShellVersion.Version.Major >= 5) - { - if (processId == null) { - processId = "current"; - } - - // If the processId is a valid int, we need to run Get-Runspace within that process - // otherwise just use the current runspace. - if (int.TryParse(processId, out int pid)) - { - // Create a remote runspace that we will invoke Get-Runspace in. - using(var rs = RunspaceFactory.CreateRunspace(new NamedPipeConnectionInfo(pid))) - using(var ps = PowerShell.Create()) - { - rs.Open(); - ps.Runspace = rs; - // Returns deserialized Runspaces. For simpler code, we use PSObject and rely on dynamic later. - runspaces = ps.AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace").Invoke(); - } - } - else - { - var psCommand = new PSCommand().AddCommand("Microsoft.PowerShell.Utility\\Get-Runspace"); - var sb = new StringBuilder(); - // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. - runspaces = await editorSession.PowerShellContext.ExecuteCommandAsync(psCommand, sb); - } - } - - var runspaceResponses = new List(); - - if (runspaces != null) - { - foreach (dynamic runspace in runspaces) - { - runspaceResponses.Add( - new GetRunspaceResponse - { - Id = runspace.Id, - Name = runspace.Name, - Availability = runspace.RunspaceAvailability.ToString() - }); - } - } - - await requestContext.SendResultAsync(runspaceResponses.ToArray()); - } - - private bool IsQueryMatch(string query, string symbolName) - { - return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; - } - - // https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction - protected async Task HandleCodeActionRequestAsync( - CodeActionParams codeActionParams, - RequestContext requestContext) - { - MarkerCorrection correction = null; - Dictionary markerIndex = null; - List codeActionCommands = new List(); - - // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI. - if (this.codeActionsPerFile.TryGetValue(codeActionParams.TextDocument.Uri, out markerIndex)) - { - foreach (var diagnostic in codeActionParams.Context.Diagnostics) - { - if (string.IsNullOrEmpty(diagnostic.Code)) - { - this.Logger.Write( - LogLevel.Warning, - $"textDocument/codeAction skipping diagnostic with empty Code field: {diagnostic.Source} {diagnostic.Message}"); - - continue; - } - - string diagnosticId = GetUniqueIdFromDiagnostic(diagnostic); - if (markerIndex.TryGetValue(diagnosticId, out correction)) - { - codeActionCommands.Add( - new CodeActionCommand - { - Title = correction.Name, - Command = "PowerShell.ApplyCodeActionEdits", - Arguments = JArray.FromObject(correction.Edits) - }); - } - } - } - - // Add "show documentation" commands last so they appear at the bottom of the client UI. - // These commands do not require code fixes. Sometimes we get a batch of diagnostics - // to create commands for. No need to create multiple show doc commands for the same rule. - var ruleNamesProcessed = new HashSet(); - foreach (var diagnostic in codeActionParams.Context.Diagnostics) - { - if (string.IsNullOrEmpty(diagnostic.Code)) { continue; } - - if (string.Equals(diagnostic.Source, "PSScriptAnalyzer", StringComparison.OrdinalIgnoreCase) && - !ruleNamesProcessed.Contains(diagnostic.Code)) - { - ruleNamesProcessed.Add(diagnostic.Code); - - codeActionCommands.Add( - new CodeActionCommand - { - Title = $"Show documentation for \"{diagnostic.Code}\"", - Command = "PowerShell.ShowCodeActionDocumentation", - Arguments = JArray.FromObject(new[] { diagnostic.Code }) - }); - } - } - - await requestContext.SendResultAsync( - codeActionCommands.ToArray()); - } - - protected async Task HandleDocumentFormattingRequestAsync( - DocumentFormattingParams formattingParams, - RequestContext requestContext) - { - if (this.editorSession.AnalysisService == null) - { - await requestContext.SendErrorAsync("Script analysis is not enabled in this session"); - return; - } - - var result = await FormatAsync( - formattingParams.TextDocument.Uri, - formattingParams.options, - null); - - await requestContext.SendResultAsync(new TextEdit[1] - { - new TextEdit - { - NewText = result.Item1, - Range = result.Item2 - }, - }); - } - - protected async Task HandleDocumentRangeFormattingRequestAsync( - DocumentRangeFormattingParams formattingParams, - RequestContext requestContext) - { - if (this.editorSession.AnalysisService == null) - { - await requestContext.SendErrorAsync("Script analysis is not enabled in this session"); - return; - } - - var result = await FormatAsync( - formattingParams.TextDocument.Uri, - formattingParams.Options, - formattingParams.Range); - - await requestContext.SendResultAsync(new TextEdit[1] - { - new TextEdit - { - NewText = result.Item1, - Range = result.Item2 - }, - }); - } - - protected async Task HandleFoldingRangeRequestAsync( - FoldingRangeParams foldingParams, - RequestContext requestContext) - { - await requestContext.SendResultAsync(Fold(foldingParams.TextDocument.Uri)); - } - - protected Task HandleEvaluateRequestAsync( - DebugAdapterMessages.EvaluateRequestArguments evaluateParams, - RequestContext requestContext) - { - // We don't await the result of the execution here because we want - // to be able to receive further messages while the current script - // is executing. This important in cases where the pipeline thread - // gets blocked by something in the script like a prompt to the user. - var executeTask = - this.editorSession.PowerShellContext.ExecuteScriptStringAsync( - evaluateParams.Expression, - writeInputToHost: true, - writeOutputToHost: true, - addToHistory: true); - - // Return the execution result after the task completes so that the - // caller knows when command execution completed. - executeTask.ContinueWith( - (task) => - { - // Return an empty result since the result value is irrelevant - // for this request in the LanguageServer - return - requestContext.SendResultAsync( - new DebugAdapterMessages.EvaluateResponseBody - { - Result = "", - VariablesReference = 0 - }); - }); - - return Task.FromResult(true); - } - - #endregion - - #region Event Handlers - - private FoldingRange[] Fold(string documentUri) - { - // TODO Should be using dynamic registrations - if (!this.currentSettings.CodeFolding.Enable) { return null; } - - // Avoid crash when using untitled: scheme or any other scheme where the document doesn't - // have a backing file. https://github.com/PowerShell/vscode-powershell/issues/1676 - // Perhaps a better option would be to parse the contents of the document as a string - // as opposed to reading a file but the senario of "no backing file" probably doesn't - // warrant the extra effort. - ScriptFile scriptFile; - if (!editorSession.Workspace.TryGetFile(documentUri, out scriptFile)) { return null; } - - var result = new List(); - - // If we're showing the last line, decrement the Endline of all regions by one. - int endLineOffset = this.currentSettings.CodeFolding.ShowLastLine ? -1 : 0; - - foreach (FoldingReference fold in TokenOperations.FoldableReferences(scriptFile.ScriptTokens).References) - { - result.Add(new FoldingRange { - EndCharacter = fold.EndCharacter, - EndLine = fold.EndLine + endLineOffset, - Kind = fold.Kind, - StartCharacter = fold.StartCharacter, - StartLine = fold.StartLine - }); - } - - return result.ToArray(); - } - - private async Task> FormatAsync( - string documentUri, - FormattingOptions options, - Range range) - { - var scriptFile = editorSession.Workspace.GetFile(documentUri); - var pssaSettings = currentSettings.CodeFormatting.GetPSSASettingsHashtable( - options.TabSize, - options.InsertSpaces); - - // TODO raise an error event in case format returns null; - string formattedScript; - Range editRange; - var rangeList = range == null ? null : new int[] { - range.Start.Line + 1, - range.Start.Character + 1, - range.End.Line + 1, - range.End.Character + 1}; - var extent = scriptFile.ScriptAst.Extent; - - // todo create an extension for converting range to script extent - editRange = new Range - { - Start = new Position - { - Line = extent.StartLineNumber - 1, - Character = extent.StartColumnNumber - 1 - }, - End = new Position - { - Line = extent.EndLineNumber - 1, - Character = extent.EndColumnNumber - 1 - } - }; - - formattedScript = await editorSession.AnalysisService.FormatAsync( - scriptFile.Contents, - pssaSettings, - rangeList); - formattedScript = formattedScript ?? scriptFile.Contents; - return Tuple.Create(formattedScript, editRange); - } - - private async void PowerShellContext_RunspaceChangedAsync(object sender, Session.RunspaceChangedEventArgs e) - { - await this.messageSender.SendEventAsync( - RunspaceChangedEvent.Type, - new Protocol.LanguageServer.RunspaceDetails(e.NewRunspace)); - } - - /// - /// Event hook on the PowerShell context to listen for changes in script execution status - /// - /// the PowerShell context sending the execution event - /// details of the execution status change - private async void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e) - { - await this.messageSender.SendEventAsync( - ExecutionStatusChangedEvent.Type, - e); - } - - private async void ExtensionService_ExtensionAddedAsync(object sender, EditorCommand e) - { - await this.messageSender.SendEventAsync( - ExtensionCommandAddedNotification.Type, - new ExtensionCommandAddedNotification - { - Name = e.Name, - DisplayName = e.DisplayName - }); - } - - private async void ExtensionService_ExtensionUpdatedAsync(object sender, EditorCommand e) - { - await this.messageSender.SendEventAsync( - ExtensionCommandUpdatedNotification.Type, - new ExtensionCommandUpdatedNotification - { - Name = e.Name, - }); - } - - private async void ExtensionService_ExtensionRemovedAsync(object sender, EditorCommand e) - { - await this.messageSender.SendEventAsync( - ExtensionCommandRemovedNotification.Type, - new ExtensionCommandRemovedNotification - { - Name = e.Name, - }); - } - - private async void DebugService_DebuggerStoppedAsync(object sender, DebuggerStoppedEventArgs e) - { - if (!this.editorSession.DebugService.IsClientAttached) - { - await this.messageSender.SendEventAsync( - StartDebuggerEvent.Type, - new StartDebuggerEvent()); - } - } - - #endregion - - #region Helper Methods - - public static string GetFileUri(string filePath) - { - // If the file isn't untitled, return a URI-style path - return - !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") - ? new Uri("file://" + filePath).AbsoluteUri - : filePath; - } - - public static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - { - return new Range - { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1 - } - }; - } - - private static FileChange GetFileChangeDetails(Range changeRange, string insertString) - { - // The protocol's positions are zero-based so add 1 to all offsets - - if (changeRange == null) return new FileChange { InsertString = insertString, IsReload = true }; - - return new FileChange - { - InsertString = insertString, - Line = changeRange.Start.Line + 1, - Offset = changeRange.Start.Character + 1, - EndLine = changeRange.End.Line + 1, - EndOffset = changeRange.End.Character + 1, - IsReload = false - }; - } - - private Task RunScriptDiagnosticsAsync( - ScriptFile[] filesToAnalyze, - EditorSession editorSession, - EventContext eventContext) - { - return RunScriptDiagnosticsAsync(filesToAnalyze, editorSession, this.messageSender.SendEventAsync); - } - - private Task RunScriptDiagnosticsAsync( - ScriptFile[] filesToAnalyze, - EditorSession editorSession, - Func, PublishDiagnosticsNotification, Task> eventSender) - { - // If there's an existing task, attempt to cancel it - try - { - if (s_existingRequestCancellation != null) - { - // Try to cancel the request - s_existingRequestCancellation.Cancel(); - - // If cancellation didn't throw an exception, - // clean up the existing token - s_existingRequestCancellation.Dispose(); - s_existingRequestCancellation = null; - } - } - catch (Exception e) - { - // TODO: Catch a more specific exception! - Logger.Write( - LogLevel.Error, - string.Format( - "Exception while canceling analysis task:\n\n{0}", - e.ToString())); - - TaskCompletionSource cancelTask = new TaskCompletionSource(); - cancelTask.SetCanceled(); - return cancelTask.Task; - } - - // If filesToAnalzye is empty, nothing to do so return early. - if (filesToAnalyze.Length == 0) - { - return Task.FromResult(true); - } - - // Create a fresh cancellation token and then start the task. - // We create this on a different TaskScheduler so that we - // don't block the main message loop thread. - // TODO: Is there a better way to do this? - s_existingRequestCancellation = new CancellationTokenSource(); - Task.Factory.StartNew( - () => - DelayThenInvokeDiagnosticsAsync( - 750, - filesToAnalyze, - this.currentSettings.ScriptAnalysis?.Enable.Value ?? false, - this.codeActionsPerFile, - editorSession, - eventSender, - this.Logger, - s_existingRequestCancellation.Token), - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default); - - return Task.FromResult(true); - } - - private static async Task DelayThenInvokeDiagnosticsAsync( - int delayMilliseconds, - ScriptFile[] filesToAnalyze, - bool isScriptAnalysisEnabled, - Dictionary> correctionIndex, - EditorSession editorSession, - EventContext eventContext, - ILogger Logger, - CancellationToken cancellationToken) - { - await DelayThenInvokeDiagnosticsAsync( - delayMilliseconds, - filesToAnalyze, - isScriptAnalysisEnabled, - correctionIndex, - editorSession, - eventContext.SendEventAsync, - Logger, - cancellationToken); - } - - private static async Task DelayThenInvokeDiagnosticsAsync( - int delayMilliseconds, - ScriptFile[] filesToAnalyze, - bool isScriptAnalysisEnabled, - Dictionary> correctionIndex, - EditorSession editorSession, - Func, PublishDiagnosticsNotification, Task> eventSender, - ILogger Logger, - CancellationToken cancellationToken) - { - // First of all, wait for the desired delay period before - // analyzing the provided list of files - try - { - await Task.Delay(delayMilliseconds, cancellationToken); - } - catch (TaskCanceledException) - { - // If the task is cancelled, exit directly - foreach (var script in filesToAnalyze) - { - await PublishScriptDiagnosticsAsync( - script, - script.DiagnosticMarkers, - correctionIndex, - eventSender); - } - - return; - } - - // If we've made it past the delay period then we don't care - // about the cancellation token anymore. This could happen - // when the user stops typing for long enough that the delay - // period ends but then starts typing while analysis is going - // on. It makes sense to send back the results from the first - // delay period while the second one is ticking away. - - // Get the requested files - foreach (ScriptFile scriptFile in filesToAnalyze) - { - List semanticMarkers = null; - if (isScriptAnalysisEnabled && editorSession.AnalysisService != null) - { - using (Logger.LogExecutionTime($"Script analysis of {scriptFile.FilePath} completed.")) - { - semanticMarkers = await editorSession.AnalysisService.GetSemanticMarkersAsync(scriptFile); - } - } - else - { - // Semantic markers aren't available if the AnalysisService - // isn't available - semanticMarkers = new List(); - } - - scriptFile.DiagnosticMarkers.AddRange(semanticMarkers); - - await PublishScriptDiagnosticsAsync( - scriptFile, - // Concat script analysis errors to any existing parse errors - scriptFile.DiagnosticMarkers, - correctionIndex, - eventSender); - } - } - - private async Task ClearMarkersAsync(ScriptFile scriptFile, EventContext eventContext) - { - // send empty diagnostic markers to clear any markers associated with the given file - await PublishScriptDiagnosticsAsync( - scriptFile, - new List(), - this.codeActionsPerFile, - eventContext); - } - - private static async Task PublishScriptDiagnosticsAsync( - ScriptFile scriptFile, - List markers, - Dictionary> correctionIndex, - EventContext eventContext) - { - await PublishScriptDiagnosticsAsync( - scriptFile, - markers, - correctionIndex, - eventContext.SendEventAsync); - } - - private static async Task PublishScriptDiagnosticsAsync( - ScriptFile scriptFile, - List markers, - Dictionary> correctionIndex, - Func, PublishDiagnosticsNotification, Task> eventSender) - { - List diagnostics = new List(); - - // Hold on to any corrections that may need to be applied later - Dictionary fileCorrections = - new Dictionary(); - - foreach (var marker in markers) - { - // Does the marker contain a correction? - Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); - if (marker.Correction != null) - { - string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); - fileCorrections.Add(diagnosticId, marker.Correction); - } - - diagnostics.Add(markerDiagnostic); - } - - correctionIndex[scriptFile.DocumentUri] = fileCorrections; - - // Always send syntax and semantic errors. We want to - // make sure no out-of-date markers are being displayed. - await eventSender( - PublishDiagnosticsNotification.Type, - new PublishDiagnosticsNotification - { - Uri = scriptFile.DocumentUri, - Diagnostics = diagnostics.ToArray() - }); - } - - // Generate a unique id that is used as a key to look up the associated code action (code fix) when - // we receive and process the textDocument/codeAction message. - private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) - { - Position start = diagnostic.Range.Start; - Position end = diagnostic.Range.End; - - var sb = new StringBuilder(256) - .Append(diagnostic.Source ?? "?") - .Append("_") - .Append(diagnostic.Code ?? "?") - .Append("_") - .Append(diagnostic.Severity?.ToString() ?? "?") - .Append("_") - .Append(start.Line) - .Append(":") - .Append(start.Character) - .Append("-") - .Append(end.Line) - .Append(":") - .Append(end.Character); - - var id = sb.ToString(); - return id; - } - - private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) - { - return new Diagnostic - { - Severity = MapDiagnosticSeverity(scriptFileMarker.Level), - Message = scriptFileMarker.Message, - Code = scriptFileMarker.RuleName, - Source = scriptFileMarker.Source, - Range = new Range - { - Start = new Position - { - Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, - Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 - } - } - }; - } - - private static CompletionItemKind MapCompletionKind(CompletionType completionType) - { - switch (completionType) - { - case CompletionType.Command: - return CompletionItemKind.Function; - - case CompletionType.Property: - return CompletionItemKind.Property; - - case CompletionType.Method: - return CompletionItemKind.Method; - - case CompletionType.Variable: - case CompletionType.ParameterName: - return CompletionItemKind.Variable; - - case CompletionType.File: - return CompletionItemKind.File; - - case CompletionType.Folder: - return CompletionItemKind.Folder; - - default: - return CompletionItemKind.Text; - } - } - - private static CompletionItem CreateCompletionItem( - CompletionDetails completionDetails, - BufferRange completionRange, - int sortIndex) - { - string detailString = null; - string documentationString = null; - string completionText = completionDetails.CompletionText; - InsertTextFormat insertTextFormat = InsertTextFormat.PlainText; - - if ((completionDetails.CompletionType == CompletionType.Variable) || - (completionDetails.CompletionType == CompletionType.ParameterName)) - { - // Look for type encoded in the tooltip for parameters and variables. - // Display PowerShell type names in [] to be consistent with PowerShell syntax - // and now the debugger displays type names. - var matches = Regex.Matches(completionDetails.ToolTipText, @"^(\[.+\])"); - if ((matches.Count > 0) && (matches[0].Groups.Count > 1)) - { - detailString = matches[0].Groups[1].Value; - } - } - else if ((completionDetails.CompletionType == CompletionType.Method) || - (completionDetails.CompletionType == CompletionType.Property)) - { - // We have a raw signature for .NET members, heck let's display it. It's - // better than nothing. - documentationString = completionDetails.ToolTipText; - } - else if (completionDetails.CompletionType == CompletionType.Command) - { - // For Commands, let's extract the resolved command or the path for an exe - // from the ToolTipText - if there is any ToolTipText. - if (completionDetails.ToolTipText != null) - { - // Fix for #240 - notepad++.exe in tooltip text caused regex parser to throw. - string escapedToolTipText = Regex.Escape(completionDetails.ToolTipText); - - // Don't display ToolTipText if it is the same as the ListItemText. - // Reject command syntax ToolTipText - it's too much to display as a detailString. - if (!completionDetails.ListItemText.Equals( - completionDetails.ToolTipText, - StringComparison.OrdinalIgnoreCase) && - !Regex.IsMatch(completionDetails.ToolTipText, - @"^\s*" + escapedToolTipText + @"\s+\[")) - { - detailString = completionDetails.ToolTipText; - } - } - } - else if ((completionDetails.CompletionType == CompletionType.Folder) && - (completionText.EndsWith("\"") || completionText.EndsWith("'"))) - { - // Insert a final "tab stop" as identified by $0 in the snippet provided for completion. - // For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert - // the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'. - // This causes the editing cursor to be placed *before* the final quote after completion, - // which makes subsequent path completions work. See this part of the LSP spec for details: - // https://microsoft.github.io/language-server-protocol/specification#textDocument_completion - - // Since we want to use a "tab stop" we need to escape a few things for Textmate to render properly. - var sb = new StringBuilder(completionDetails.CompletionText) - .Replace(@"\", @"\\") - .Replace(@"}", @"\}") - .Replace(@"$", @"\$"); - completionText = sb.Insert(sb.Length - 1, "$0").ToString(); - insertTextFormat = InsertTextFormat.Snippet; - } - - // Force the client to maintain the sort order in which the - // original completion results were returned. We just need to - // make sure the default order also be the lexicographical order - // which we do by prefixing the ListItemText with a leading 0's - // four digit index. - var sortText = $"{sortIndex:D4}{completionDetails.ListItemText}"; - - return new CompletionItem - { - InsertText = completionText, - InsertTextFormat = insertTextFormat, - Label = completionDetails.ListItemText, - Kind = MapCompletionKind(completionDetails.CompletionType), - Detail = detailString, - Documentation = documentationString, - SortText = sortText, - FilterText = completionDetails.CompletionText, - TextEdit = new TextEdit - { - NewText = completionText, - Range = new Range - { - Start = new Position - { - Line = completionRange.Start.Line - 1, - Character = completionRange.Start.Column - 1 - }, - End = new Position - { - Line = completionRange.End.Line - 1, - Character = completionRange.End.Column - 1 - } - } - } - }; - } - - private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) - { - switch (markerLevel) - { - case ScriptFileMarkerLevel.Error: - return DiagnosticSeverity.Error; - - case ScriptFileMarkerLevel.Warning: - return DiagnosticSeverity.Warning; - - case ScriptFileMarkerLevel.Information: - return DiagnosticSeverity.Information; - - default: - return DiagnosticSeverity.Error; - } - } - - private static ParameterInformation CreateParameterInfo(ParameterInfo parameterInfo) - { - return new ParameterInformation - { - Label = parameterInfo.Name, - Documentation = string.Empty - }; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs deleted file mode 100644 index 5df247bd5..000000000 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerEditorOperations.cs +++ /dev/null @@ -1,224 +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 Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Server -{ - internal class LanguageServerEditorOperations : IEditorOperations - { - private const bool DefaultPreviewSetting = true; - - private EditorSession editorSession; - private IMessageSender messageSender; - - public LanguageServerEditorOperations( - EditorSession editorSession, - IMessageSender messageSender) - { - this.editorSession = editorSession; - this.messageSender = messageSender; - } - - public async Task GetEditorContextAsync() - { - ClientEditorContext clientContext = - await this.messageSender.SendRequestAsync( - GetEditorContextRequest.Type, - new GetEditorContextRequest(), - true); - - return this.ConvertClientEditorContext(clientContext); - } - - public async Task InsertTextAsync(string filePath, string text, BufferRange insertRange) - { - await this.messageSender.SendRequestAsync( - InsertTextRequest.Type, - new InsertTextRequest - { - FilePath = filePath, - InsertText = text, - InsertRange = - new Range - { - Start = new Position - { - Line = insertRange.Start.Line - 1, - Character = insertRange.Start.Column - 1 - }, - End = new Position - { - Line = insertRange.End.Line - 1, - Character = insertRange.End.Column - 1 - } - } - }, false); - - // TODO: Set the last param back to true! - } - - public Task SetSelectionAsync(BufferRange selectionRange) - { - return this.messageSender.SendRequestAsync( - SetSelectionRequest.Type, - new SetSelectionRequest - { - SelectionRange = - new Range - { - Start = new Position - { - Line = selectionRange.Start.Line - 1, - Character = selectionRange.Start.Column - 1 - }, - End = new Position - { - Line = selectionRange.End.Line - 1, - Character = selectionRange.End.Column - 1 - } - } - }, true); - } - - public EditorContext ConvertClientEditorContext( - ClientEditorContext clientContext) - { - ScriptFile scriptFile = this.editorSession.Workspace.CreateScriptFileFromFileBuffer( - clientContext.CurrentFilePath, - clientContext.CurrentFileContent); - - return - new EditorContext( - this, - scriptFile, - new BufferPosition( - clientContext.CursorPosition.Line + 1, - clientContext.CursorPosition.Character + 1), - new BufferRange( - clientContext.SelectionRange.Start.Line + 1, - clientContext.SelectionRange.Start.Character + 1, - clientContext.SelectionRange.End.Line + 1, - clientContext.SelectionRange.End.Character + 1), - clientContext.CurrentFileLanguage); - } - - public Task NewFileAsync() - { - return - this.messageSender.SendRequestAsync( - NewFileRequest.Type, - null, - true); - } - - public Task OpenFileAsync(string filePath) - { - return - this.messageSender.SendRequestAsync( - OpenFileRequest.Type, - new OpenFileDetails - { - FilePath = filePath, - Preview = DefaultPreviewSetting - }, - true); - } - - public Task OpenFileAsync(string filePath, bool preview) - { - return - this.messageSender.SendRequestAsync( - OpenFileRequest.Type, - new OpenFileDetails - { - FilePath = filePath, - Preview = preview - }, - true); - } - - public Task CloseFileAsync(string filePath) - { - return - this.messageSender.SendRequestAsync( - CloseFileRequest.Type, - filePath, - true); - } - - public Task SaveFileAsync(string filePath) - { - return SaveFileAsync(filePath, null); - } - - public Task SaveFileAsync(string currentPath, string newSavePath) - { - return - this.messageSender.SendRequestAsync( - SaveFileRequest.Type, - new SaveFileDetails - { - FilePath = currentPath, - NewPath = newSavePath - }, - true); - } - - public string GetWorkspacePath() - { - return this.editorSession.Workspace.WorkspacePath; - } - - public string GetWorkspaceRelativePath(string filePath) - { - return this.editorSession.Workspace.GetRelativePath(filePath); - } - - public Task ShowInformationMessageAsync(string message) - { - return - this.messageSender.SendRequestAsync( - ShowInformationMessageRequest.Type, - message, - true); - } - - public Task ShowErrorMessageAsync(string message) - { - return - this.messageSender.SendRequestAsync( - ShowErrorMessageRequest.Type, - message, - true); - } - - public Task ShowWarningMessageAsync(string message) - { - return - this.messageSender.SendRequestAsync( - ShowWarningMessageRequest.Type, - message, - true); - } - - public Task SetStatusBarMessageAsync(string message, int? timeout) - { - return - this.messageSender.SendRequestAsync( - SetStatusBarMessageRequest.Type, - new StatusBarMessageDetails - { - Message = message, - Timeout = timeout - }, - true); - } - } -} diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs deleted file mode 100644 index 369c5ee45..000000000 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs +++ /dev/null @@ -1,392 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Security; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Server -{ - public class LanguageServerSettings - { - public bool EnableProfileLoading { get; set; } - - public ScriptAnalysisSettings ScriptAnalysis { get; set; } - - public CodeFormattingSettings CodeFormatting { get; set; } - - public CodeFoldingSettings CodeFolding { get; set; } - - public LanguageServerSettings() - { - this.ScriptAnalysis = new ScriptAnalysisSettings(); - this.CodeFormatting = new CodeFormattingSettings(); - this.CodeFolding = new CodeFoldingSettings(); - } - - public void Update( - LanguageServerSettings settings, - string workspaceRootPath, - ILogger logger) - { - if (settings != null) - { - this.EnableProfileLoading = settings.EnableProfileLoading; - this.ScriptAnalysis.Update( - settings.ScriptAnalysis, - workspaceRootPath, - logger); - this.CodeFormatting = new CodeFormattingSettings(settings.CodeFormatting); - this.CodeFolding.Update(settings.CodeFolding, logger); - } - } - } - - public class ScriptAnalysisSettings - { - public bool? Enable { get; set; } - - public string SettingsPath { get; set; } - - public ScriptAnalysisSettings() - { - this.Enable = true; - } - - public void Update( - ScriptAnalysisSettings settings, - string workspaceRootPath, - ILogger logger) - { - if (settings != null) - { - this.Enable = settings.Enable; - - string settingsPath = settings.SettingsPath; - - try - { - if (string.IsNullOrWhiteSpace(settingsPath)) - { - settingsPath = null; - } - else if (!Path.IsPathRooted(settingsPath)) - { - if (string.IsNullOrEmpty(workspaceRootPath)) - { - // The workspace root path could be an empty string - // when the user has opened a PowerShell script file - // without opening an entire folder (workspace) first. - // In this case we should just log an error and let - // the specified settings path go through even though - // it will fail to load. - logger.Write( - LogLevel.Error, - "Could not resolve Script Analyzer settings path due to null or empty workspaceRootPath."); - } - else - { - settingsPath = Path.GetFullPath(Path.Combine(workspaceRootPath, settingsPath)); - } - } - - this.SettingsPath = settingsPath; - logger.Write(LogLevel.Verbose, $"Using Script Analyzer settings path - '{settingsPath ?? ""}'."); - } - catch (Exception ex) when ( - ex is NotSupportedException || - ex is PathTooLongException || - ex is SecurityException) - { - // Invalid chars in path like ${env:HOME} can cause Path.GetFullPath() to throw, catch such errors here - logger.WriteException( - $"Invalid Script Analyzer settings path - '{settingsPath}'.", - ex); - - this.SettingsPath = null; - } - } - } - } - - /// - /// Code formatting presets. - /// See https://en.wikipedia.org/wiki/Indent_style for details on indent and brace styles. - /// - public enum CodeFormattingPreset - { - /// - /// Use the formatting settings as-is. - /// - Custom, - - /// - /// Configure the formatting settings to resemble the Allman indent/brace style. - /// - Allman, - - /// - /// Configure the formatting settings to resemble the one true brace style variant of K&R indent/brace style. - /// - OTBS, - - /// - /// Configure the formatting settings to resemble the Stroustrup brace style variant of K&R indent/brace style. - /// - Stroustrup - } - - /// - /// Multi-line pipeline style settings. - /// - public enum PipelineIndentationStyle - { - /// - /// After the indentation level only once after the first pipeline and keep this level for the following pipelines. - /// - IncreaseIndentationForFirstPipeline, - - /// - /// After every pipeline, keep increasing the indentation. - /// - IncreaseIndentationAfterEveryPipeline, - - /// - /// Do not increase indentation level at all after pipeline. - /// - NoIndentation - } - - public class CodeFormattingSettings - { - /// - /// Default constructor. - /// > - public CodeFormattingSettings() - { - - } - - /// - /// Copy constructor. - /// - /// An instance of type CodeFormattingSettings. - public CodeFormattingSettings(CodeFormattingSettings codeFormattingSettings) - { - if (codeFormattingSettings == null) - { - throw new ArgumentNullException(nameof(codeFormattingSettings)); - } - - foreach (var prop in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - prop.SetValue(this, prop.GetValue(codeFormattingSettings)); - } - } - - public bool AutoCorrectAliases { get; set; } - public CodeFormattingPreset Preset { get; set; } - public bool OpenBraceOnSameLine { get; set; } - public bool NewLineAfterOpenBrace { get; set; } - public bool NewLineAfterCloseBrace { get; set; } - public PipelineIndentationStyle PipelineIndentationStyle { get; set; } - public bool WhitespaceBeforeOpenBrace { get; set; } - public bool WhitespaceBeforeOpenParen { get; set; } - public bool WhitespaceAroundOperator { get; set; } - public bool WhitespaceAfterSeparator { get; set; } - public bool WhitespaceInsideBrace { get; set; } - public bool WhitespaceAroundPipe { get; set; } - public bool IgnoreOneLineBlock { get; set; } - public bool AlignPropertyValuePairs { get; set; } - public bool UseCorrectCasing { get; set; } - - - /// - /// Get the settings hashtable that will be consumed by PSScriptAnalyzer. - /// - /// The tab size in the number spaces. - /// If true, insert spaces otherwise insert tabs for indentation. - /// - public Hashtable GetPSSASettingsHashtable( - int tabSize, - bool insertSpaces) - { - var settings = GetCustomPSSASettingsHashtable(tabSize, insertSpaces); - var ruleSettings = (Hashtable)(settings["Rules"]); - var closeBraceSettings = (Hashtable)ruleSettings["PSPlaceCloseBrace"]; - var openBraceSettings = (Hashtable)ruleSettings["PSPlaceOpenBrace"]; - switch(Preset) - { - case CodeFormattingPreset.Allman: - openBraceSettings["OnSameLine"] = false; - openBraceSettings["NewLineAfter"] = true; - closeBraceSettings["NewLineAfter"] = true; - break; - - case CodeFormattingPreset.OTBS: - openBraceSettings["OnSameLine"] = true; - openBraceSettings["NewLineAfter"] = true; - closeBraceSettings["NewLineAfter"] = false; - break; - - case CodeFormattingPreset.Stroustrup: - openBraceSettings["OnSameLine"] = true; - openBraceSettings["NewLineAfter"] = true; - closeBraceSettings["NewLineAfter"] = true; - break; - - default: - break; - } - - return settings; - } - - private Hashtable GetCustomPSSASettingsHashtable(int tabSize, bool insertSpaces) - { - var ruleConfigurations = new Hashtable { - {"PSPlaceOpenBrace", new Hashtable { - {"Enable", true}, - {"OnSameLine", OpenBraceOnSameLine}, - {"NewLineAfter", NewLineAfterOpenBrace}, - {"IgnoreOneLineBlock", IgnoreOneLineBlock} - }}, - {"PSPlaceCloseBrace", new Hashtable { - {"Enable", true}, - {"NewLineAfter", NewLineAfterCloseBrace}, - {"IgnoreOneLineBlock", IgnoreOneLineBlock} - }}, - {"PSUseConsistentIndentation", new Hashtable { - {"Enable", true}, - {"IndentationSize", tabSize}, - {"PipelineIndentation", PipelineIndentationStyle }, - {"Kind", insertSpaces ? "space" : "tab"} - }}, - {"PSUseConsistentWhitespace", new Hashtable { - {"Enable", true}, - {"CheckOpenBrace", WhitespaceBeforeOpenBrace}, - {"CheckOpenParen", WhitespaceBeforeOpenParen}, - {"CheckOperator", WhitespaceAroundOperator}, - {"CheckSeparator", WhitespaceAfterSeparator}, - {"CheckInnerBrace", WhitespaceInsideBrace}, - {"CheckPipe", WhitespaceAroundPipe}, - }}, - {"PSAlignAssignmentStatement", new Hashtable { - {"Enable", true}, - {"CheckHashtable", AlignPropertyValuePairs} - }}, - {"PSUseCorrectCasing", new Hashtable { - {"Enable", UseCorrectCasing} - }}, - }; - if (AutoCorrectAliases) - { - ruleConfigurations.Add("PSAvoidUsingCmdletAliases", new Hashtable()); - } - - return new Hashtable - { - {"IncludeRules", new string[] { - "PSPlaceCloseBrace", - "PSPlaceOpenBrace", - "PSUseConsistentWhitespace", - "PSUseConsistentIndentation", - "PSAlignAssignmentStatement", - "PSAvoidUsingCmdletAliases", - }}, - { - "Rules", ruleConfigurations - }, - }; - } - } - - /// - /// Code folding settings - /// - public class CodeFoldingSettings - { - /// - /// Whether the folding is enabled. Default is true as per VSCode - /// - public bool Enable { get; set; } = true; - - /// - /// Whether to show or hide the last line of a folding region. Default is true as per VSCode - /// - public bool ShowLastLine { get; set; } = true; - - /// - /// Update these settings from another settings object - /// - public void Update( - CodeFoldingSettings settings, - ILogger logger) - { - if (settings != null) { - if (this.Enable != settings.Enable) { - this.Enable = settings.Enable; - logger.Write(LogLevel.Verbose, string.Format("Using Code Folding Enabled - {0}", this.Enable)); - } - if (this.ShowLastLine != settings.ShowLastLine) { - this.ShowLastLine = settings.ShowLastLine; - logger.Write(LogLevel.Verbose, string.Format("Using Code Folding ShowLastLine - {0}", this.ShowLastLine)); - } - } - } - } - - /// - /// Additional settings from the Language Client that affect Language Server operations but - /// do not exist under the 'powershell' section - /// - public class EditorFileSettings - { - /// - /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the - /// the glob is in effect. - /// - public Dictionary Exclude { get; set; } - } - - /// - /// Additional settings from the Language Client that affect Language Server operations but - /// do not exist under the 'powershell' section - /// - public class EditorSearchSettings - { - /// - /// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the - /// the glob is in effect. - /// - public Dictionary Exclude { get; set; } - /// - /// Whether to follow symlinks when searching - /// - public bool FollowSymlinks { get; set; } = true; - } - - public class LanguageServerSettingsWrapper - { - // NOTE: This property is capitalized as 'Powershell' because the - // mode name sent from the client is written as 'powershell' and - // JSON.net is using camelCasing. - public LanguageServerSettings Powershell { get; set; } - - // NOTE: This property is capitalized as 'Files' because the - // mode name sent from the client is written as 'files' and - // JSON.net is using camelCasing. - public EditorFileSettings Files { get; set; } - - // NOTE: This property is capitalized as 'Search' because the - // mode name sent from the client is written as 'search' and - // JSON.net is using camelCasing. - public EditorSearchSettings Search { get; set; } - } -} diff --git a/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs b/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs deleted file mode 100644 index 079183a26..000000000 --- a/src/PowerShellEditorServices.Protocol/Server/OutputDebouncer.cs +++ /dev/null @@ -1,102 +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 Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Protocol.Server -{ - /// - /// Throttles output written via OutputEvents by batching all output - /// written within a short time window and writing it all out at once. - /// - public class OutputDebouncer : AsyncDebouncer - { - #region Private Fields - - private IMessageSender messageSender; - private bool currentOutputIsError = false; - private string currentOutputString = null; - - #endregion - - #region Constants - - // Set a really short window for output flushes. This - // gives the appearance of fast output without the crushing - // overhead of sending an OutputEvent for every single line - // written. At this point it seems that around 10-20 lines get - // batched for each flush when Get-Process is called. - public const int OutputFlushInterval = 200; - - #endregion - - #region Constructors - - public OutputDebouncer(IMessageSender messageSender) - : base(OutputFlushInterval, false) - { - this.messageSender = messageSender; - } - - #endregion - - #region Private Methods - - protected override async Task OnInvokeAsync(OutputWrittenEventArgs output) - { - bool outputIsError = output.OutputType == OutputType.Error; - - if (this.currentOutputIsError != outputIsError) - { - if (this.currentOutputString != null) - { - // Flush the output - await this.OnFlushAsync(); - } - - this.currentOutputString = string.Empty; - this.currentOutputIsError = outputIsError; - } - - // Output string could be null if the last output was already flushed - if (this.currentOutputString == null) - { - this.currentOutputString = string.Empty; - } - - // Add to string (and include newline) - this.currentOutputString += - output.OutputText + - (output.IncludeNewLine ? - System.Environment.NewLine : - string.Empty); - } - - protected override async Task OnFlushAsync() - { - // Only flush output if there is some to flush - if (this.currentOutputString != null) - { - // Send an event for the current output - await this.messageSender.SendEventAsync( - OutputEvent.Type, - new OutputEventBody - { - Output = this.currentOutputString, - Category = (this.currentOutputIsError) ? "stderr" : "stdout" - }); - - // Clear the output string for the next batch - this.currentOutputString = null; - } - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs b/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs new file mode 100644 index 000000000..d26162743 --- /dev/null +++ b/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs @@ -0,0 +1,238 @@ +// +// 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.Management.Automation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.VSCode.CustomViews; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace Microsoft.PowerShell.EditorServices.VSCode +{ + /// + [Cmdlet(VerbsCommon.New,"VSCodeHtmlContentView")] + [OutputType(typeof(IHtmlContentView))] + public class NewVSCodeHtmlContentViewCommand : PSCmdlet + { + private HtmlContentViewsFeature _htmlContentViewsFeature; + + private ILogger _logger; + + private ViewColumn? _showInColumn; + + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Title { get; set; } + + /// + [Parameter(Position = 1)] + public ViewColumn ShowInColumn + { + get => _showInColumn.GetValueOrDefault(); + set => _showInColumn = value; + } + + /// + protected override void BeginProcessing() + { + if (_htmlContentViewsFeature == null) + { + if (GetVariableValue("psEditor") is EditorObject psEditor) + { + _logger = psEditor.Components.GetService().CreateLogger("PowerShellEditorServices.VSCode"); + + _htmlContentViewsFeature = new HtmlContentViewsFeature( + psEditor.Components.GetService(), + _logger); + + _logger.LogInformation("PowerShell Editor Services VS Code module loaded."); + } + else + { + ThrowTerminatingError( + new ErrorRecord( + new ItemNotFoundException("Cannot find the '$psEditor' variable."), + "PSEditorNotFound", + ErrorCategory.ObjectNotFound, + targetObject: null)); + return; + } + } + + IHtmlContentView view = _htmlContentViewsFeature.CreateHtmlContentViewAsync(Title) + .GetAwaiter() + .GetResult(); + + if (_showInColumn != null) { + try + { + view.Show(_showInColumn.Value).GetAwaiter().GetResult(); + } + catch (Exception e) + { + WriteError( + new ErrorRecord( + e, + "HtmlContentViewCouldNotShow", + ErrorCategory.OpenError, + targetObject: null)); + + return; + } + } + + WriteObject(view); + } + } + + /// + [Cmdlet(VerbsCommon.Set,"VSCodeHtmlContentView")] + public class SetVSCodeHtmlContentViewCommand : PSCmdlet + { + /// + [Parameter(Mandatory = true, Position = 0)] + [Alias("View")] + [ValidateNotNull] + public IHtmlContentView HtmlContentView { get; set; } + + /// + [Parameter(Mandatory = true, Position = 1)] + [Alias("Content")] + [AllowEmptyString] + public string HtmlBodyContent { get; set; } + + /// + [Parameter(Position = 2)] + public string[] JavaScriptPaths { get; set; } + + /// + [Parameter(Position = 3)] + public string[] StyleSheetPaths { get; set; } + + /// + protected override void BeginProcessing() + { + var htmlContent = new HtmlContent(); + htmlContent.BodyContent = HtmlBodyContent; + htmlContent.JavaScriptPaths = JavaScriptPaths; + htmlContent.StyleSheetPaths = StyleSheetPaths; + try + { + HtmlContentView.SetContentAsync(htmlContent).GetAwaiter().GetResult(); + } + catch (Exception e) + { + WriteError( + new ErrorRecord( + e, + "HtmlContentViewCouldNotSet", + ErrorCategory.WriteError, + targetObject: null)); + } + } + } + + /// + [Cmdlet(VerbsCommon.Close,"VSCodeHtmlContentView")] + public class CloseVSCodeHtmlContentViewCommand : PSCmdlet + { + /// + [Parameter(Mandatory = true, Position = 0)] + [Alias("View")] + [ValidateNotNull] + public IHtmlContentView HtmlContentView { get; set; } + + /// + protected override void BeginProcessing() + { + try + { + HtmlContentView.Close().GetAwaiter().GetResult(); + } + catch (Exception e) + { + WriteError( + new ErrorRecord( + e, + "HtmlContentViewCouldNotClose", + ErrorCategory.CloseError, + targetObject: null)); + } + } + } + + /// + [Cmdlet(VerbsCommon.Show,"VSCodeHtmlContentView")] + public class ShowVSCodeHtmlContentViewCommand : PSCmdlet + { + /// + [Parameter(Mandatory = true, Position = 0)] + [Alias("View")] + [ValidateNotNull] + public IHtmlContentView HtmlContentView { get; set; } + + /// + [Parameter(Position = 1)] + [Alias("Column")] + [ValidateNotNull] + public ViewColumn ViewColumn { get; set; } = ViewColumn.One; + + /// + protected override void BeginProcessing() + { + try + { + HtmlContentView.Show(ViewColumn).GetAwaiter().GetResult(); + } + catch (Exception e) + { + WriteError( + new ErrorRecord( + e, + "HtmlContentViewCouldNotShow", + ErrorCategory.OpenError, + targetObject: null)); + } + } + } + + /// + [Cmdlet(VerbsCommunications.Write,"VSCodeHtmlContentView")] + public class WriteVSCodeHtmlContentViewCommand : PSCmdlet + { + /// + [Parameter(Mandatory = true, Position = 0)] + [Alias("View")] + [ValidateNotNull] + public IHtmlContentView HtmlContentView { get; set; } + + /// + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 1)] + [Alias("Content")] + [ValidateNotNull] + public string AppendedHtmlBodyContent { get; set; } + + /// + protected override void ProcessRecord() + { + try + { + HtmlContentView.AppendContentAsync(AppendedHtmlBodyContent).GetAwaiter().GetResult(); + } + catch (Exception e) + { + WriteError( + new ErrorRecord( + e, + "HtmlContentViewCouldNotWrite", + ErrorCategory.WriteError, + targetObject: null)); + } + } + } +} diff --git a/src/PowerShellEditorServices.VSCode/ComponentRegistration.cs b/src/PowerShellEditorServices.VSCode/ComponentRegistration.cs deleted file mode 100644 index 5ea4422cd..000000000 --- a/src/PowerShellEditorServices.VSCode/ComponentRegistration.cs +++ /dev/null @@ -1,41 +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 Microsoft.PowerShell.EditorServices.Components; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; -using Microsoft.PowerShell.EditorServices.VSCode.CustomViews; - -namespace Microsoft.PowerShell.EditorServices.VSCode -{ - /// - /// Methods for registering components from this module into - /// the editor session. - /// - public static class ComponentRegistration - { - /// - /// Registers the feature components in this module with the - /// host editor. - /// - /// - /// The IComponentRegistry where feature components will be registered. - /// - public static void Register(IComponentRegistry components) - { - ILogger logger = components.Get(); - - components.Register( - new HtmlContentViewsFeature( - components.Get(), - logger)); - - logger.Write( - LogLevel.Normal, - "PowerShell Editor Services VS Code module loaded."); - } - } -} diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs index 1fdb6ddf0..524ea181b 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewBase.cs @@ -5,14 +5,15 @@ using System; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { internal abstract class CustomViewBase : ICustomView { - protected IMessageSender messageSender; + protected ILanguageServer languageServer; + protected ILogger logger; public Guid Id { get; private set; } @@ -24,50 +25,50 @@ internal abstract class CustomViewBase : ICustomView public CustomViewBase( string viewTitle, CustomViewType viewType, - IMessageSender messageSender, + ILanguageServer languageServer, ILogger logger) { this.Id = Guid.NewGuid(); this.Title = viewTitle; this.ViewType = viewType; - this.messageSender = messageSender; + this.languageServer = languageServer; this.logger = logger; } - internal Task CreateAsync() + internal async Task CreateAsync() { - return - this.messageSender.SendRequestAsync( - NewCustomViewRequest.Type, - new NewCustomViewRequest - { - Id = this.Id, - Title = this.Title, - ViewType = this.ViewType, - }, true); + await languageServer.SendRequest( + NewCustomViewRequest.Method, + new NewCustomViewRequest + { + Id = this.Id, + Title = this.Title, + ViewType = this.ViewType, + } + ); } - public Task Show(ViewColumn viewColumn) + public async Task Show(ViewColumn viewColumn) { - return - this.messageSender.SendRequestAsync( - ShowCustomViewRequest.Type, - new ShowCustomViewRequest - { - Id = this.Id, - ViewColumn = viewColumn - }, true); + await languageServer.SendRequest( + ShowCustomViewRequest.Method, + new ShowCustomViewRequest + { + Id = this.Id, + ViewColumn = viewColumn + } + ); } - public Task Close() + public async Task Close() { - return - this.messageSender.SendRequestAsync( - CloseCustomViewRequest.Type, - new CloseCustomViewRequest - { - Id = this.Id, - }, true); + await languageServer.SendRequest( + CloseCustomViewRequest.Method, + new CloseCustomViewRequest + { + Id = this.Id, + } + ); } } } diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewFeature.cs b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewFeature.cs index ac65d02fa..0e6c9a85f 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewFeature.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewFeature.cs @@ -5,25 +5,25 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { internal abstract class CustomViewFeatureBase where TView : ICustomView { - protected IMessageSender messageSender; + protected ILanguageServer languageServer; + protected ILogger logger; private readonly Dictionary viewIndex; public CustomViewFeatureBase( - IMessageSender messageSender, + ILanguageServer languageServer, ILogger logger) { this.viewIndex = new Dictionary(); - this.messageSender = messageSender; + this.languageServer = languageServer; this.logger = logger; } diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewMessages.cs b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewMessages.cs index d87a5e7f7..a2a3cd5e8 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewMessages.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/CustomViewMessages.cs @@ -4,8 +4,6 @@ // using System; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { @@ -15,11 +13,9 @@ namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews public class NewCustomViewRequest { /// - /// The RequestType for this request. + /// The Language Server Protocol 'method'. /// - public static readonly - RequestType Type = - RequestType.Create("powerShell/newCustomView"); + public static string Method => "powerShell/newCustomView"; /// /// Gets or sets the Id of the view. @@ -43,11 +39,9 @@ public static readonly public class ShowCustomViewRequest { /// - /// The RequestType for this request. + /// The Language Server Protocol 'method'. /// - public static readonly - RequestType Type = - RequestType.Create("powerShell/showCustomView"); + public static string Method => "powerShell/showCustomView"; /// /// Gets or sets the Id of the view. @@ -66,11 +60,9 @@ public static readonly public class CloseCustomViewRequest { /// - /// The RequestType for this request. + /// The Language Server Protocol 'method'. /// - public static readonly - RequestType Type = - RequestType.Create("powerShell/closeCustomView"); + public static string Method => "powerShell/closeCustomView"; /// /// Gets or sets the Id of the view. diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs index c7a553707..b615e58fe 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentView.cs @@ -7,8 +7,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { @@ -16,29 +16,29 @@ internal class HtmlContentView : CustomViewBase, IHtmlContentView { public HtmlContentView( string viewTitle, - IMessageSender messageSender, + ILanguageServer languageServer, ILogger logger) : base( viewTitle, CustomViewType.HtmlContent, - messageSender, + languageServer, logger) { } - public Task SetContentAsync(string htmlBodyContent) + public async Task SetContentAsync(string htmlBodyContent) { - return - this.messageSender.SendRequestAsync( - SetHtmlContentViewRequest.Type, - new SetHtmlContentViewRequest - { - Id = this.Id, - HtmlContent = new HtmlContent { BodyContent = htmlBodyContent } - }, true); + await languageServer.SendRequest( + SetHtmlContentViewRequest.Method, + new SetHtmlContentViewRequest + { + Id = this.Id, + HtmlContent = new HtmlContent { BodyContent = htmlBodyContent } + } + ); } - public Task SetContentAsync(HtmlContent htmlContent) + public async Task SetContentAsync(HtmlContent htmlContent) { HtmlContent validatedContent = new HtmlContent() @@ -48,26 +48,26 @@ public Task SetContentAsync(HtmlContent htmlContent) StyleSheetPaths = this.GetUriPaths(htmlContent.StyleSheetPaths) }; - return - this.messageSender.SendRequestAsync( - SetHtmlContentViewRequest.Type, - new SetHtmlContentViewRequest - { - Id = this.Id, - HtmlContent = validatedContent - }, true); + await languageServer.SendRequest( + SetHtmlContentViewRequest.Method, + new SetHtmlContentViewRequest + { + Id = this.Id, + HtmlContent = validatedContent + } + ); } - public Task AppendContentAsync(string appendedHtmlBodyContent) + public async Task AppendContentAsync(string appendedHtmlBodyContent) { - return - this.messageSender.SendRequestAsync( - AppendHtmlContentViewRequest.Type, - new AppendHtmlContentViewRequest - { - Id = this.Id, - AppendedHtmlBodyContent = appendedHtmlBodyContent - }, true); + await languageServer.SendRequest( + AppendHtmlContentViewRequest.Method, + new AppendHtmlContentViewRequest + { + Id = this.Id, + AppendedHtmlBodyContent = appendedHtmlBodyContent + } + ); } private string[] GetUriPaths(string[] filePaths) diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewMessages.cs b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewMessages.cs index 09e91b621..c982ca945 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewMessages.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewMessages.cs @@ -4,7 +4,6 @@ // using System; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { @@ -14,11 +13,9 @@ namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews public class SetHtmlContentViewRequest { /// - /// The RequestType for this request. + /// The Language Server Protocol 'method'. /// - public static readonly - RequestType Type = - RequestType.Create("powerShell/setHtmlViewContent"); + public static string Method => "powerShell/setHtmlViewContent"; /// /// Gets or sets the Id of the view. @@ -37,11 +34,9 @@ public static readonly public class AppendHtmlContentViewRequest { /// - /// The RequestType for this request. + /// The Language Server Protocol 'method'. /// - public static readonly - RequestType Type = - RequestType.Create("powerShell/appendHtmlViewContent"); + public static string Method => "powerShell/appendHtmlViewContent"; /// /// Gets or sets the Id of the view. diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs index a520fa610..e23d1b552 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/HtmlContentViewsFeature.cs @@ -3,19 +3,18 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { internal class HtmlContentViewsFeature : CustomViewFeatureBase, IHtmlContentViews { public HtmlContentViewsFeature( - IMessageSender messageSender, + ILanguageServer languageServer, ILogger logger) - : base(messageSender, logger) + : base(languageServer, logger) { } @@ -24,7 +23,7 @@ public async Task CreateHtmlContentViewAsync(string viewTitle) HtmlContentView htmlView = new HtmlContentView( viewTitle, - this.messageSender, + this.languageServer, this.logger); await htmlView.CreateAsync(); diff --git a/src/PowerShellEditorServices.VSCode/CustomViews/ICustomView.cs b/src/PowerShellEditorServices.VSCode/CustomViews/ICustomView.cs index 5b52781b3..a9561770c 100644 --- a/src/PowerShellEditorServices.VSCode/CustomViews/ICustomView.cs +++ b/src/PowerShellEditorServices.VSCode/CustomViews/ICustomView.cs @@ -5,8 +5,6 @@ using System; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.VSCode.CustomViews { diff --git a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj index c50e535ea..54a39becd 100644 --- a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj +++ b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj @@ -15,9 +15,10 @@ - - - + + + All + diff --git a/src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs b/src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs deleted file mode 100644 index 4cfe4dc7d..000000000 --- a/src/PowerShellEditorServices/Analysis/AnalysisOutputWriter.cs +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -#if ScriptAnalyzerLogger -using Microsoft.Windows.PowerShell.ScriptAnalyzer; -using System; -using System.Management.Automation; -using Microsoft.PowerShell.EditorServices.Console; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an implementation of ScriptAnalyzer's IOutputWriter - /// interface that writes to trace logs. - /// - - internal class AnalysisOutputWriter : IOutputWriter - { - private IConsoleHost consoleHost; - - public AnalysisOutputWriter(IConsoleHost consoleHost) - { - this.consoleHost = consoleHost; - } - - #region IOutputWriter Implementation - - void IOutputWriter.WriteError(ErrorRecord error) - { - this.consoleHost?.WriteOutput(error.ToString(), true, OutputType.Error, ConsoleColor.Red, ConsoleColor.Black); - } - - void IOutputWriter.WriteWarning(string message) - { - this.consoleHost?.WriteOutput(message, true, OutputType.Warning, ConsoleColor.Yellow, ConsoleColor.Black); - } - - void IOutputWriter.WriteVerbose(string message) - { - } - - void IOutputWriter.WriteDebug(string message) - { - } - - void IOutputWriter.ThrowTerminatingError(ErrorRecord record) - { - this.consoleHost?.WriteOutput(record.ToString(), true, OutputType.Error, ConsoleColor.Red, ConsoleColor.Black); - } - - #endregion - } -} - -#endif diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs deleted file mode 100644 index 045a50d31..000000000 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ /dev/null @@ -1,708 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Console; -using System.Management.Automation.Runspaces; -using System.Management.Automation; -using System.Collections.Generic; -using System.Text; -using System.Collections; -using System.IO; -using Microsoft.PowerShell.Commands; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a high-level service for performing semantic analysis - /// of PowerShell scripts. - /// - public class AnalysisService : IDisposable - { - #region Static fields - - /// - /// Defines the list of Script Analyzer rules to include by default if - /// no settings file is specified. - /// - private static readonly string[] s_includedRules = { - "PSUseToExportFieldsInManifest", - "PSMisleadingBacktick", - "PSAvoidUsingCmdletAliases", - "PSUseApprovedVerbs", - "PSAvoidUsingPlainTextForPassword", - "PSReservedCmdletChar", - "PSReservedParams", - "PSShouldProcess", - "PSMissingModuleManifestField", - "PSAvoidDefaultValueSwitchParameter", - "PSUseDeclaredVarsMoreThanAssignments", - "PSPossibleIncorrectComparisonWithNull", - "PSAvoidDefaultValueForMandatoryParameter", - "PSPossibleIncorrectUsageOfRedirectionOperator" - }; - - /// - /// An empty diagnostic result to return when a script fails analysis. - /// - private static readonly PSObject[] s_emptyDiagnosticResult = new PSObject[0]; - - private static readonly string[] s_emptyGetRuleResult = new string[0]; - - /// - /// The indentation to add when the logger lists errors. - /// - private static readonly string s_indentJoin = Environment.NewLine + " "; - - #endregion // Static fields - - #region Private Fields - - /// - /// Maximum number of runspaces we allow to be in use for script analysis. - /// - private const int NumRunspaces = 1; - - /// - /// Name of the PSScriptAnalyzer module, to be used for PowerShell module interactions. - /// - private const string PSSA_MODULE_NAME = "PSScriptAnalyzer"; - - /// - /// Provides logging. - /// - private ILogger _logger; - - /// - /// Runspace pool to generate runspaces for script analysis and handle - /// ansynchronous analysis requests. - /// - private RunspacePool _analysisRunspacePool; - - /// - /// Info object describing the PSScriptAnalyzer module that has been loaded in - /// to provide analysis services. - /// - private PSModuleInfo _pssaModuleInfo; - - #endregion // Private Fields - - #region Properties - - /// - /// Set of PSScriptAnalyzer rules used for analysis. - /// - public string[] ActiveRules { get; set; } - - /// - /// Gets or sets the path to a settings file (.psd1) - /// containing PSScriptAnalyzer settings. - /// - public string SettingsPath { get; set; } - - #endregion - - #region Constructors - - /// - /// Construct a new AnalysisService object. - /// - /// - /// The runspace pool with PSScriptAnalyzer module loaded that will handle - /// analysis tasks. - /// - /// - /// The path to the PSScriptAnalyzer settings file to handle analysis settings. - /// - /// An array of rules to be used for analysis. - /// Maintains logs for the analysis service. - /// - /// Optional module info of the loaded PSScriptAnalyzer module. If not provided, - /// the analysis service will populate it, but it can be given here to save time. - /// - private AnalysisService( - RunspacePool analysisRunspacePool, - string pssaSettingsPath, - IEnumerable activeRules, - ILogger logger, - PSModuleInfo pssaModuleInfo = null) - { - _analysisRunspacePool = analysisRunspacePool; - SettingsPath = pssaSettingsPath; - ActiveRules = activeRules.ToArray(); - _logger = logger; - _pssaModuleInfo = pssaModuleInfo; - } - - #endregion // constructors - - #region Public Methods - - /// - /// Factory method for producing AnalysisService instances. Handles loading of the PSScriptAnalyzer module - /// and runspace pool instantiation before creating the service instance. - /// - /// Path to the PSSA settings file to be used for this service instance. - /// EditorServices logger for logging information. - /// - /// A new analysis service instance with a freshly imported PSScriptAnalyzer module and runspace pool. - /// Returns null if problems occur. This method should never throw. - /// - public static AnalysisService Create(string settingsPath, ILogger logger) - { - try - { - RunspacePool analysisRunspacePool; - PSModuleInfo pssaModuleInfo; - try - { - // Try and load a PSScriptAnalyzer module with the required version - // by looking on the script path. Deep down, this internally runs Get-Module -ListAvailable, - // so we'll use this to check whether such a module exists - analysisRunspacePool = CreatePssaRunspacePool(out pssaModuleInfo); - - } - catch (Exception e) - { - throw new AnalysisServiceLoadException("PSScriptAnalyzer runspace pool could not be created", e); - } - - if (analysisRunspacePool == null) - { - throw new AnalysisServiceLoadException("PSScriptAnalyzer runspace pool failed to be created"); - } - - // Having more than one runspace doesn't block code formatting if one - // runspace is occupied for diagnostics - analysisRunspacePool.SetMaxRunspaces(NumRunspaces); - analysisRunspacePool.ThreadOptions = PSThreadOptions.ReuseThread; - analysisRunspacePool.Open(); - - var analysisService = new AnalysisService( - analysisRunspacePool, - settingsPath, - s_includedRules, - logger, - pssaModuleInfo); - - // Log what features are available in PSSA here - analysisService.LogAvailablePssaFeatures(); - - return analysisService; - } - catch (AnalysisServiceLoadException e) - { - logger.WriteException("PSScriptAnalyzer cannot be imported, AnalysisService will be disabled", e); - return null; - } - catch (Exception e) - { - logger.WriteException("AnalysisService could not be started due to an unexpected exception", e); - return null; - } - } - - /// - /// Get PSScriptAnalyzer settings hashtable for PSProvideCommentHelp rule. - /// - /// Enable the rule. - /// Analyze only exported functions/cmdlets. - /// Use block comment or line comment. - /// Return a vscode snipped correction should be returned. - /// Place comment help at the given location relative to the function definition. - /// A PSScriptAnalyzer settings hashtable. - public static Hashtable GetCommentHelpRuleSettings( - bool enable, - bool exportedOnly, - bool blockComment, - bool vscodeSnippetCorrection, - string placement) - { - var settings = new Dictionary(); - var ruleSettings = new Hashtable(); - ruleSettings.Add("Enable", enable); - ruleSettings.Add("ExportedOnly", exportedOnly); - ruleSettings.Add("BlockComment", blockComment); - ruleSettings.Add("VSCodeSnippetCorrection", vscodeSnippetCorrection); - ruleSettings.Add("Placement", placement); - settings.Add("PSProvideCommentHelp", ruleSettings); - return GetPSSASettingsHashtable(settings); - } - - /// - /// Construct a PSScriptAnalyzer settings hashtable - /// - /// A settings hashtable - /// - public static Hashtable GetPSSASettingsHashtable(IDictionary ruleSettingsMap) - { - var hashtable = new Hashtable(); - var ruleSettingsHashtable = new Hashtable(); - - hashtable["IncludeRules"] = ruleSettingsMap.Keys.ToArray(); - hashtable["Rules"] = ruleSettingsHashtable; - - foreach (var kvp in ruleSettingsMap) - { - ruleSettingsHashtable.Add(kvp.Key, kvp.Value); - } - - return hashtable; - } - - /// - /// Perform semantic analysis on the given ScriptFile and returns - /// an array of ScriptFileMarkers. - /// - /// The ScriptFile which will be analyzed for semantic markers. - /// An array of ScriptFileMarkers containing semantic analysis results. - public async Task> GetSemanticMarkersAsync(ScriptFile file) - { - return await GetSemanticMarkersAsync(file, ActiveRules, SettingsPath); - } - - /// - /// Perform semantic analysis on the given ScriptFile with the given settings. - /// - /// The ScriptFile to be analyzed. - /// ScriptAnalyzer settings - /// - public async Task> GetSemanticMarkersAsync(ScriptFile file, Hashtable settings) - { - return await GetSemanticMarkersAsync(file, null, settings); - } - - /// - /// Perform semantic analysis on the given script with the given settings. - /// - /// The script content to be analyzed. - /// ScriptAnalyzer settings - /// - public async Task> GetSemanticMarkersAsync( - string scriptContent, - Hashtable settings) - { - return await GetSemanticMarkersAsync(scriptContent, null, settings); - } - - /// - /// Returns a list of builtin-in PSScriptAnalyzer rules - /// - public IEnumerable GetPSScriptAnalyzerRules() - { - PowerShellResult getRuleResult = InvokePowerShell("Get-ScriptAnalyzerRule"); - if (getRuleResult == null) - { - _logger.Write(LogLevel.Warning, "Get-ScriptAnalyzerRule returned null result"); - return s_emptyGetRuleResult; - } - - var ruleNames = new List(); - foreach (var rule in getRuleResult.Output) - { - ruleNames.Add((string)rule.Members["RuleName"].Value); - } - - return ruleNames; - } - - /// - /// Format a given script text with default codeformatting settings. - /// - /// Script text to be formatted - /// ScriptAnalyzer settings - /// The range within which formatting should be applied. - /// The formatted script text. - public async Task FormatAsync( - string scriptDefinition, - Hashtable settings, - int[] rangeList) - { - // We cannot use Range type therefore this workaround of using -1 default value. - // Invoke-Formatter throws a ParameterBinderValidationException if the ScriptDefinition is an empty string. - if (string.IsNullOrEmpty(scriptDefinition)) - { - return null; - } - - var argsDict = new Dictionary { - {"ScriptDefinition", scriptDefinition}, - {"Settings", settings} - }; - if (rangeList != null) - { - argsDict.Add("Range", rangeList); - } - - PowerShellResult result = await InvokePowerShellAsync("Invoke-Formatter", argsDict); - - if (result == null) - { - _logger.Write(LogLevel.Error, "Formatter returned null result"); - return null; - } - - if (result.HasErrors) - { - var errorBuilder = new StringBuilder().Append(s_indentJoin); - foreach (ErrorRecord err in result.Errors) - { - errorBuilder.Append(err).Append(s_indentJoin); - } - _logger.Write(LogLevel.Warning, $"Errors found while formatting file: {errorBuilder}"); - return null; - } - - foreach (PSObject resultObj in result.Output) - { - string formatResult = resultObj?.BaseObject as string; - if (formatResult != null) - { - return formatResult; - } - } - - return null; - } - - #endregion // public methods - - #region Private Methods - - private async Task> GetSemanticMarkersAsync( - ScriptFile file, - string[] rules, - TSettings settings) where TSettings : class - { - if (file.IsAnalysisEnabled) - { - return await GetSemanticMarkersAsync( - file.Contents, - rules, - settings); - } - else - { - // Return an empty marker list - return new List(); - } - } - - private async Task> GetSemanticMarkersAsync( - string scriptContent, - string[] rules, - TSettings settings) where TSettings : class - { - if ((typeof(TSettings) == typeof(string) || typeof(TSettings) == typeof(Hashtable)) - && (rules != null || settings != null)) - { - var scriptFileMarkers = await GetDiagnosticRecordsAsync(scriptContent, rules, settings); - return scriptFileMarkers.Select(ScriptFileMarker.FromDiagnosticRecord).ToList(); - } - else - { - // Return an empty marker list - return new List(); - } - } - - /// - /// Log the features available from the PSScriptAnalyzer module that has been imported - /// for use with the AnalysisService. - /// - private void LogAvailablePssaFeatures() - { - // Save ourselves some work here - var featureLogLevel = LogLevel.Verbose; - if (_logger.MinimumConfiguredLogLevel > featureLogLevel) - { - return; - } - - // If we already know the module that was imported, save some work - if (_pssaModuleInfo == null) - { - PowerShellResult getModuleResult = InvokePowerShell( - "Get-Module", - new Dictionary{ {"Name", PSSA_MODULE_NAME} }); - - if (getModuleResult == null) - { - throw new AnalysisServiceLoadException("Get-Module call to find PSScriptAnalyzer module failed"); - } - - _pssaModuleInfo = getModuleResult.Output - .Select(m => m.BaseObject) - .OfType() - .FirstOrDefault(); - } - - if (_pssaModuleInfo == null) - { - throw new AnalysisServiceLoadException("Unable to find loaded PSScriptAnalyzer module for logging"); - } - - var sb = new StringBuilder(); - sb.AppendLine("PSScriptAnalyzer successfully imported:"); - - // Log version - sb.Append(" Version: "); - sb.AppendLine(_pssaModuleInfo.Version.ToString()); - - // Log exported cmdlets - sb.AppendLine(" Exported Cmdlets:"); - foreach (string cmdletName in _pssaModuleInfo.ExportedCmdlets.Keys.OrderBy(name => name)) - { - sb.Append(" "); - sb.AppendLine(cmdletName); - } - - // Log available rules - sb.AppendLine(" Available Rules:"); - foreach (string ruleName in GetPSScriptAnalyzerRules()) - { - sb.Append(" "); - sb.AppendLine(ruleName); - } - - _logger.Write(featureLogLevel, sb.ToString()); - } - - private async Task GetDiagnosticRecordsAsync( - string scriptContent, - string[] rules, - TSettings settings) where TSettings : class - { - var diagnosticRecords = s_emptyDiagnosticResult; - - // When a new, empty file is created there are by definition no issues. - // Furthermore, if you call Invoke-ScriptAnalyzer with an empty ScriptDefinition - // it will generate a ParameterBindingValidationException. - if (string.IsNullOrEmpty(scriptContent)) - { - return diagnosticRecords; - } - - if (typeof(TSettings) == typeof(string) || typeof(TSettings) == typeof(Hashtable)) - { - //Use a settings file if one is provided, otherwise use the default rule list. - string settingParameter; - object settingArgument; - if (settings != null) - { - settingParameter = "Settings"; - settingArgument = settings; - } - else - { - settingParameter = "IncludeRule"; - settingArgument = rules; - } - - PowerShellResult result = await InvokePowerShellAsync( - "Invoke-ScriptAnalyzer", - new Dictionary - { - { "ScriptDefinition", scriptContent }, - { settingParameter, settingArgument }, - // We ignore ParseErrors from PSSA because we already send them when we parse the file. - { "Severity", new [] { ScriptFileMarkerLevel.Error, ScriptFileMarkerLevel.Information, ScriptFileMarkerLevel.Warning }} - }); - - diagnosticRecords = result?.Output; - } - - _logger.Write( - LogLevel.Verbose, - String.Format("Found {0} violations", diagnosticRecords.Count())); - - return diagnosticRecords; - } - - private PowerShellResult InvokePowerShell(string command, IDictionary paramArgMap = null) - { - using (var powerShell = System.Management.Automation.PowerShell.Create()) - { - powerShell.RunspacePool = _analysisRunspacePool; - powerShell.AddCommand(command); - if (paramArgMap != null) - { - foreach (KeyValuePair kvp in paramArgMap) - { - powerShell.AddParameter(kvp.Key, kvp.Value); - } - } - - PowerShellResult result = null; - try - { - PSObject[] output = powerShell.Invoke().ToArray(); - ErrorRecord[] errors = powerShell.Streams.Error.ToArray(); - result = new PowerShellResult(output, errors, powerShell.HadErrors); - } - catch (CommandNotFoundException ex) - { - // This exception is possible if the module path loaded - // is wrong even though PSScriptAnalyzer is available as a module - _logger.Write(LogLevel.Error, ex.Message); - } - catch (CmdletInvocationException ex) - { - // We do not want to crash EditorServices for exceptions caused by cmdlet invocation. - // Two main reasons that cause the exception are: - // * PSCmdlet.WriteOutput being called from another thread than Begin/Process - // * CompositionContainer.ComposeParts complaining that "...Only one batch can be composed at a time" - _logger.Write(LogLevel.Error, ex.Message); - } - - return result; - } - } - - private async Task InvokePowerShellAsync(string command, IDictionary paramArgMap = null) - { - var task = Task.Run(() => - { - return InvokePowerShell(command, paramArgMap); - }); - - return await task; - } - - /// - /// Create a new runspace pool around a PSScriptAnalyzer module for asynchronous script analysis tasks. - /// This looks for the latest version of PSScriptAnalyzer on the path and loads that. - /// - /// A runspace pool with PSScriptAnalyzer loaded for running script analysis tasks. - private static RunspacePool CreatePssaRunspacePool(out PSModuleInfo pssaModuleInfo) - { - using (var ps = System.Management.Automation.PowerShell.Create()) - { - // Run `Get-Module -ListAvailable -Name "PSScriptAnalyzer"` - ps.AddCommand("Get-Module") - .AddParameter("ListAvailable") - .AddParameter("Name", PSSA_MODULE_NAME); - - try - { - // Get the latest version of PSScriptAnalyzer we can find - pssaModuleInfo = ps.Invoke()? - .Select(psObj => psObj.BaseObject) - .OfType() - .OrderByDescending(moduleInfo => moduleInfo.Version) - .FirstOrDefault(); - } - catch (Exception e) - { - throw new AnalysisServiceLoadException("Unable to find PSScriptAnalyzer module on the module path", e); - } - - if (pssaModuleInfo == null) - { - throw new AnalysisServiceLoadException("Unable to find PSScriptAnalyzer module on the module path"); - } - - // Create a base session state with PSScriptAnalyzer loaded - InitialSessionState sessionState; - if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { - sessionState = InitialSessionState.CreateDefault(); - } else { - sessionState = InitialSessionState.CreateDefault2(); - } - sessionState.ImportPSModule(new [] { pssaModuleInfo.ModuleBase }); - - // RunspacePool takes care of queuing commands for us so we do not - // need to worry about executing concurrent commands - return RunspaceFactory.CreateRunspacePool(sessionState); - } - } - - #endregion //private methods - - #region IDisposable Support - - private bool _disposedValue = false; // To detect redundant calls - - /// - /// Dispose of this object. - /// - /// True if the method is called by the Dispose method, false if called by the finalizer. - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _analysisRunspacePool.Dispose(); - _analysisRunspacePool = null; - } - - _disposedValue = true; - } - } - - /// - /// Clean up all internal resources and dispose of the analysis service. - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - } - - #endregion - - /// - /// Wraps the result of an execution of PowerShell to send back through - /// asynchronous calls. - /// - private class PowerShellResult - { - public PowerShellResult( - PSObject[] output, - ErrorRecord[] errors, - bool hasErrors) - { - Output = output; - Errors = errors; - HasErrors = hasErrors; - } - - public PSObject[] Output { get; } - - public ErrorRecord[] Errors { get; } - - public bool HasErrors { get; } - } - } - - /// - /// Class to catch known failure modes for starting the AnalysisService. - /// - public class AnalysisServiceLoadException : Exception - { - /// - /// Instantiate an AnalysisService error based on a simple message. - /// - /// The message to display to the user detailing the error. - public AnalysisServiceLoadException(string message) - : base(message) - { - } - - /// - /// Instantiate an AnalysisService error based on another error that occurred internally. - /// - /// The message to display to the user detailing the error. - /// The inner exception that occurred to trigger this error. - public AnalysisServiceLoadException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/src/PowerShellEditorServices/CodeLenses/CodeLens.cs b/src/PowerShellEditorServices/CodeLenses/CodeLens.cs deleted file mode 100644 index 2fd251595..000000000 --- a/src/PowerShellEditorServices/CodeLenses/CodeLens.cs +++ /dev/null @@ -1,127 +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 Microsoft.PowerShell.EditorServices.Commands; -using Microsoft.PowerShell.EditorServices.Utility; -using System.Collections.Generic; -using System.Management.Automation.Language; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.CodeLenses -{ - /// - /// Defines the data for a "code lens" which is displayed - /// above a symbol in a text document and has an associated - /// command. - /// - public class CodeLens - { - /// - /// Gets the ICodeLensProvider that created this CodeLens. - /// - public ICodeLensProvider Provider { get; private set; } - - /// - /// Gets the ScriptFile for which the CodeLens was created. - /// - public ScriptFile File { get; private set; } - - /// - /// Gets the IScriptExtent for the region which the CodeLens - /// pertains. - /// - public IScriptExtent ScriptExtent { get; private set; } - - /// - /// Gets the command which will be invoked in the editor - /// when the CodeLens is clicked. - /// - public ClientCommand Command { get; private set; } - - /// - /// Creates an instance of the CodeLens class. - /// - /// - /// The ICodeLensProvider which created this CodeLens. - /// - /// - /// The ScriptFile for which the CodeLens was created. - /// - /// - /// The IScriptExtent for the region which the CodeLens - /// pertains. - /// - public CodeLens( - ICodeLensProvider provider, - ScriptFile scriptFile, - IScriptExtent scriptExtent) - : this( - provider, - scriptFile, - scriptExtent, - null) - { - } - - /// - /// Creates an instance of the CodeLens class based on an - /// original CodeLens instance, generally used when resolving - /// the Command for a CodeLens. - /// - /// - /// The original CodeLens upon which this instance is based. - /// - /// - /// The resolved ClientCommand for the original CodeLens. - /// - public CodeLens( - CodeLens originalCodeLens, - ClientCommand resolvedCommand) - { - Validate.IsNotNull(nameof(originalCodeLens), originalCodeLens); - Validate.IsNotNull(nameof(resolvedCommand), resolvedCommand); - - this.Provider = originalCodeLens.Provider; - this.File = originalCodeLens.File; - this.ScriptExtent = originalCodeLens.ScriptExtent; - this.Command = resolvedCommand; - } - - /// - /// Creates an instance of the CodeLens class. - /// - /// - /// The ICodeLensProvider which created this CodeLens. - /// - /// - /// The ScriptFile for which the CodeLens was created. - /// - /// - /// The IScriptExtent for the region which the CodeLens - /// pertains. - /// - /// - /// The ClientCommand to execute when this CodeLens is clicked. - /// If null, this CodeLens will be resolved by the editor when it - /// gets displayed. - /// - public CodeLens( - ICodeLensProvider provider, - ScriptFile scriptFile, - IScriptExtent scriptExtent, - ClientCommand command) - { - Validate.IsNotNull(nameof(provider), provider); - Validate.IsNotNull(nameof(scriptFile), scriptFile); - Validate.IsNotNull(nameof(scriptExtent), scriptExtent); - - this.Provider = provider; - this.File = scriptFile; - this.ScriptExtent = scriptExtent; - this.Command = command; - } - } -} diff --git a/src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs b/src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs deleted file mode 100644 index 71a1c0243..000000000 --- a/src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs +++ /dev/null @@ -1,46 +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 Microsoft.PowerShell.EditorServices.Utility; -using System.Collections.Generic; -using System.Management.Automation.Language; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.CodeLenses -{ - /// - /// Specifies the contract for a Code Lens provider. - /// - public interface ICodeLensProvider : IFeatureProvider - { - /// - /// Provides a collection of CodeLenses for the given - /// document. - /// - /// - /// The document for which CodeLenses should be provided. - /// - /// An array of CodeLenses. - CodeLens[] ProvideCodeLenses(ScriptFile scriptFile); - - /// - /// Resolves a CodeLens that was created without a Command. - /// - /// - /// The CodeLens to resolve. - /// - /// - /// A CancellationToken which can be used to cancel the - /// request. - /// - /// - /// A Task which returns the resolved CodeLens when completed. - /// - Task ResolveCodeLensAsync( - CodeLens codeLens, - CancellationToken cancellationToken); - } -} diff --git a/src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs b/src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs deleted file mode 100644 index f63319c1c..000000000 --- a/src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs +++ /dev/null @@ -1,34 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.CodeLenses -{ - /// - /// Specifies the contract for an implementation of - /// the ICodeLenses component. - /// - public interface ICodeLenses - { - /// - /// Gets the collection of ICodeLensProvider implementations - /// that are registered with this component. - /// - IFeatureProviderCollection Providers { get; } - - /// - /// Provides a collection of CodeLenses for the given - /// document. - /// - /// - /// The document for which CodeLenses should be provided. - /// - /// An array of CodeLenses. - CodeLens[] ProvideCodeLenses(ScriptFile scriptFile); - } -} diff --git a/src/PowerShellEditorServices/Commands/ClientCommand.cs b/src/PowerShellEditorServices/Commands/ClientCommand.cs deleted file mode 100644 index 42ee9a147..000000000 --- a/src/PowerShellEditorServices/Commands/ClientCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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.Commands -{ - /// - /// Provides details for a command which will be executed - /// in the host editor. - /// - public class ClientCommand - { - /// - /// Gets the identifying name of the command. - /// - public string Name { get; private set; } - - /// - /// Gets the display title of the command. - /// - public string Title { get; private set; } - - /// - /// Gets the array of objects which are passed as - /// arguments to the command. - /// - public object[] Arguments { get; private set; } - - /// - /// Creates an instance of the ClientCommand class. - /// - /// The name of the command. - /// The display title of the command. - /// The arguments to be passed to the command. - public ClientCommand( - string commandName, - string commandTitle, - object[] arguments) - { - this.Name = commandName; - this.Title = commandTitle; - this.Arguments = arguments; - } - } -} diff --git a/src/PowerShellEditorServices/Components/ComponentRegistry.cs b/src/PowerShellEditorServices/Components/ComponentRegistry.cs deleted file mode 100644 index 9a1de6d01..000000000 --- a/src/PowerShellEditorServices/Components/ComponentRegistry.cs +++ /dev/null @@ -1,84 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Components -{ - /// - /// Provides a default implementation for the IComponentRegistry - /// interface. - /// - public class ComponentRegistry : IComponentRegistry - { - private Dictionary componentRegistry = - new Dictionary(); - - /// - /// Registers an instance of the specified component type - /// or throws an ArgumentException if an instance has - /// already been registered. - /// - /// - /// The component type that the instance represents. - /// - /// - /// The instance of the component to be registered. - /// - /// - /// The provided component instance for convenience in assignment - /// statements. - /// - public object Register(Type componentType, object componentInstance) - { - this.componentRegistry.Add(componentType, componentInstance); - return componentInstance; - } - - - /// - /// Gets the registered instance of the specified - /// component type or throws a KeyNotFoundException if - /// no instance has been registered. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// The implementation of the specified type. - public object Get(Type componentType) - { - return this.componentRegistry[componentType]; - } - - /// - /// Attempts to retrieve the instance of the specified - /// component type and, if found, stores it in the - /// componentInstance parameter. - /// - /// - /// The out parameter in which the found instance will be stored. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// - /// True if a registered instance was found, false otherwise. - /// - public bool TryGet(Type componentType, out object componentInstance) - { - componentInstance = null; - - if (this.componentRegistry.TryGetValue(componentType, out componentInstance)) - { - return componentInstance != null; - } - - return false; - } - } -} diff --git a/src/PowerShellEditorServices/Components/FeatureComponentBase.cs b/src/PowerShellEditorServices/Components/FeatureComponentBase.cs deleted file mode 100644 index b2eac7539..000000000 --- a/src/PowerShellEditorServices/Components/FeatureComponentBase.cs +++ /dev/null @@ -1,127 +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.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Components -{ - /// - /// Provides common functionality needed to implement a feature - /// component which uses IFeatureProviders to provide further - /// extensibility. - /// - public abstract class FeatureComponentBase - where TProvider : IFeatureProvider - { - /// - /// Gets the collection of IFeatureProviders registered with - /// this feature component. - /// - public IFeatureProviderCollection Providers { get; private set; } - - /// - /// Gets the ILogger implementation to use for writing log - /// messages. - /// - protected ILogger Logger { get; private set; } - - /// - /// Creates an instance of the FeatureComponentBase class with - /// the specified ILogger. - /// - /// The ILogger implementation to use for this instance. - public FeatureComponentBase(ILogger logger) - { - this.Providers = new FeatureProviderCollection(); - this.Logger = logger; - } - - /// - /// Invokes the given function synchronously against all - /// registered providers. - /// - /// The function to be invoked. - /// - /// An IEnumerable containing the results of all providers - /// that were invoked successfully. - /// - protected IEnumerable InvokeProviders( - Func invokeFunc) - { - Stopwatch invokeTimer = new Stopwatch(); - List providerResults = new List(); - - foreach (var provider in this.Providers) - { - try - { - invokeTimer.Restart(); - - providerResults.Add(invokeFunc(provider)); - - invokeTimer.Stop(); - - this.Logger.Write( - LogLevel.Verbose, - $"Invocation of provider '{provider.ProviderId}' completed in {invokeTimer.ElapsedMilliseconds}ms."); - } - catch (Exception e) - { - this.Logger.WriteException( - $"Exception caught while invoking provider {provider.ProviderId}:", - e); - } - } - - return providerResults; - } - - /// - /// Invokes the given function asynchronously against all - /// registered providers. - /// - /// The function to be invoked. - /// - /// A Task that, when completed, returns an IEnumerable containing - /// the results of all providers that were invoked successfully. - /// - protected async Task> InvokeProvidersAsync( - Func> invokeFunc) - { - Stopwatch invokeTimer = new Stopwatch(); - List providerResults = new List(); - - foreach (var provider in this.Providers) - { - try - { - invokeTimer.Restart(); - - providerResults.Add( - await invokeFunc(provider)); - - invokeTimer.Stop(); - - this.Logger.Write( - LogLevel.Verbose, - $"Invocation of provider '{provider.ProviderId}' completed in {invokeTimer.ElapsedMilliseconds}ms."); - } - catch (Exception e) - { - this.Logger.WriteException( - $"Exception caught while invoking provider {provider.ProviderId}:", - e); - } - } - - return providerResults; - } - } -} diff --git a/src/PowerShellEditorServices/Components/IComponentRegistry.cs b/src/PowerShellEditorServices/Components/IComponentRegistry.cs deleted file mode 100644 index 1acd59588..000000000 --- a/src/PowerShellEditorServices/Components/IComponentRegistry.cs +++ /dev/null @@ -1,61 +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; - -namespace Microsoft.PowerShell.EditorServices.Components -{ - /// - /// Specifies the contract for a registry of component interfaces. - /// - public interface IComponentRegistry - { - /// - /// Registers an instance of the specified component type - /// or throws an ArgumentException if an instance has - /// already been registered. - /// - /// - /// The component type that the instance represents. - /// - /// - /// The instance of the component to be registered. - /// - /// - /// The provided component instance for convenience in assignment - /// statements. - /// - object Register( - Type componentType, - object componentInstance); - - /// - /// Gets the registered instance of the specified - /// component type or throws a KeyNotFoundException if - /// no instance has been registered. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// The implementation of the specified type. - object Get(Type componentType); - - /// - /// Attempts to retrieve the instance of the specified - /// component type and, if found, stores it in the - /// componentInstance parameter. - /// - /// - /// The component type for which an instance will be retrieved. - /// - /// - /// The out parameter in which the found instance will be stored. - /// - /// - /// True if a registered instance was found, false otherwise. - /// - bool TryGet(Type componentType, out object componentInstance); - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Components/IComponentRegistryExtensions.cs b/src/PowerShellEditorServices/Components/IComponentRegistryExtensions.cs deleted file mode 100644 index cbbb119c1..000000000 --- a/src/PowerShellEditorServices/Components/IComponentRegistryExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -// -// 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.Components -{ - /// - /// Provides generic helper methods for working with IComponentRegistry - /// methods. - /// - public static class IComponentRegistryExtensions - { - /// - /// Registers an instance of the specified component type - /// or throws an ArgumentException if an instance has - /// already been registered. - /// - /// - /// The IComponentRegistry instance. - /// - /// - /// The instance of the component to be registered. - /// - /// - /// The provided component instance for convenience in assignment - /// statements. - /// - public static TComponent Register( - this IComponentRegistry componentRegistry, - TComponent componentInstance) - where TComponent : class - { - return - (TComponent)componentRegistry.Register( - typeof(TComponent), - componentInstance); - } - - /// - /// Gets the registered instance of the specified - /// component type or throws a KeyNotFoundException if - /// no instance has been registered. - /// - /// - /// The IComponentRegistry instance. - /// - /// The implementation of the specified type. - public static TComponent Get( - this IComponentRegistry componentRegistry) - where TComponent : class - { - return (TComponent)componentRegistry.Get(typeof(TComponent)); - } - - /// - /// Attempts to retrieve the instance of the specified - /// component type and, if found, stores it in the - /// componentInstance parameter. - /// - /// - /// The IComponentRegistry instance. - /// - /// - /// The out parameter in which the found instance will be stored. - /// - /// - /// True if a registered instance was found, false otherwise. - /// - public static bool TryGet( - this IComponentRegistry componentRegistry, - out TComponent componentInstance) - where TComponent : class - { - object componentObject = null; - componentInstance = null; - - if (componentRegistry.TryGet(typeof(TComponent), out componentObject)) - { - componentInstance = componentObject as TComponent; - return componentInstance != null; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Console/ChoiceDetails.cs b/src/PowerShellEditorServices/Console/ChoiceDetails.cs deleted file mode 100644 index d8121b6c1..000000000 --- a/src/PowerShellEditorServices/Console/ChoiceDetails.cs +++ /dev/null @@ -1,132 +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.Management.Automation.Host; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Contains the details about a choice that should be displayed - /// to the user. This class is meant to be serializable to the - /// user's UI. - /// - public class ChoiceDetails - { - #region Private Fields - - private string hotKeyString; - - #endregion - - #region Properties - - /// - /// Gets the label for the choice. - /// - public string Label { get; set; } - - /// - /// Gets the index of the hot key character for the choice. - /// - public int HotKeyIndex { get; set; } - - /// - /// Gets the hot key character. - /// - public char? HotKeyCharacter { get; set; } - - /// - /// Gets the help string that describes the choice. - /// - public string HelpMessage { get; set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ChoiceDetails class with - /// the provided details. - /// - public ChoiceDetails() - { - // Parameterless constructor for deserialization. - } - - /// - /// Creates an instance of the ChoiceDetails class with - /// the provided details. - /// - /// - /// The label of the choice. An ampersand '&' may be inserted - /// before the character that will used as a hot key for the - /// choice. - /// - /// - /// A help message that describes the purpose of the choice. - /// - public ChoiceDetails(string label, string helpMessage) - { - this.HelpMessage = helpMessage; - - this.HotKeyIndex = label.IndexOf('&'); - if (this.HotKeyIndex >= 0) - { - this.Label = label.Remove(this.HotKeyIndex, 1); - - if (this.HotKeyIndex < this.Label.Length) - { - this.hotKeyString = this.Label[this.HotKeyIndex].ToString().ToUpper(); - this.HotKeyCharacter = this.hotKeyString[0]; - } - } - else - { - this.Label = label; - } - } - - /// - /// Creates a new instance of the ChoicePromptDetails class - /// based on a ChoiceDescription from the PowerShell layer. - /// - /// - /// A ChoiceDescription on which this instance will be based. - /// - /// A new ChoicePromptDetails instance. - public static ChoiceDetails Create(ChoiceDescription choiceDescription) - { - return new ChoiceDetails( - choiceDescription.Label, - choiceDescription.HelpMessage); - } - - #endregion - - #region Public Methods - - /// - /// Compares an input string to this choice to determine - /// whether the input string is a match. - /// - /// - /// The input string to compare to the choice. - /// - /// True if the input string is a match for the choice. - public bool MatchesInput(string inputString) - { - // Make sure the input string is trimmed of whitespace - inputString = inputString.Trim(); - - // Is it the hotkey? - return - string.Equals(inputString, this.hotKeyString, StringComparison.CurrentCultureIgnoreCase) || - string.Equals(inputString, this.Label, StringComparison.CurrentCultureIgnoreCase); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs deleted file mode 100644 index 579d5b708..000000000 --- a/src/PowerShellEditorServices/Console/ChoicePromptHandler.cs +++ /dev/null @@ -1,354 +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.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Indicates the style of prompt to be displayed. - /// - public enum PromptStyle - { - /// - /// Indicates that the full prompt should be displayed - /// with all relevant details. - /// - Full, - - /// - /// Indicates that a minimal prompt should be displayed, - /// generally used after the full prompt has already been - /// displayed and the options must be displayed again. - /// - Minimal - } - - /// - /// Provides a base implementation for IPromptHandler classes - /// that present the user a set of options from which a selection - /// should be made. - /// - public abstract class ChoicePromptHandler : PromptHandler - { - #region Private Fields - - private CancellationTokenSource promptCancellationTokenSource = - new CancellationTokenSource(); - private TaskCompletionSource> cancelTask = - new TaskCompletionSource>(); - - #endregion - - /// - /// - /// - /// An ILogger implementation used for writing log messages. - public ChoicePromptHandler(ILogger logger) : base(logger) - { - } - - #region Properties - - /// - /// Returns true if the choice prompt allows multiple selections. - /// - protected bool IsMultiChoice { get; private set; } - - /// - /// Gets the caption (title) string to display with the prompt. - /// - protected string Caption { get; private set; } - - /// - /// Gets the descriptive message to display with the prompt. - /// - protected string Message { get; private set; } - - /// - /// Gets the array of choices from which the user must select. - /// - protected ChoiceDetails[] Choices { get; private set; } - - /// - /// Gets the index of the default choice so that the user - /// interface can make it easy to select this option. - /// - protected int[] DefaultChoices { get; private set; } - - #endregion - - #region Public Methods - - /// - /// Prompts the user to make a choice using the provided details. - /// - /// - /// The caption string which will be displayed to the user. - /// - /// - /// The descriptive message which will be displayed to the user. - /// - /// - /// The list of choices from which the user will select. - /// - /// - /// The default choice to highlight for the user. - /// - /// - /// A CancellationToken that can be used to cancel the prompt. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's choice. - /// - public async Task PromptForChoiceAsync( - string promptCaption, - string promptMessage, - ChoiceDetails[] choices, - int defaultChoice, - CancellationToken cancellationToken) - { - // TODO: Guard against multiple calls - - this.Caption = promptCaption; - this.Message = promptMessage; - this.Choices = choices; - - this.DefaultChoices = - defaultChoice == -1 - ? new int[] { } - : new int[] { defaultChoice }; - - // Cancel the TaskCompletionSource if the caller cancels the task - cancellationToken.Register(this.CancelPrompt, true); - - // Convert the int[] result to int - return await this.WaitForTaskAsync( - this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token) - .ContinueWith( - task => - { - if (task.IsFaulted) - { - throw task.Exception; - } - else if (task.IsCanceled) - { - throw new TaskCanceledException(task); - } - - return this.GetSingleResult(task.Result); - })); - } - - /// - /// Prompts the user to make a choice of one or more options using the - /// provided details. - /// - /// - /// The caption string which will be displayed to the user. - /// - /// - /// The descriptive message which will be displayed to the user. - /// - /// - /// The list of choices from which the user will select. - /// - /// - /// The default choice(s) to highlight for the user. - /// - /// - /// A CancellationToken that can be used to cancel the prompt. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's choices. - /// - public async Task PromptForChoiceAsync( - string promptCaption, - string promptMessage, - ChoiceDetails[] choices, - int[] defaultChoices, - CancellationToken cancellationToken) - { - // TODO: Guard against multiple calls - - this.Caption = promptCaption; - this.Message = promptMessage; - this.Choices = choices; - this.DefaultChoices = defaultChoices; - this.IsMultiChoice = true; - - // Cancel the TaskCompletionSource if the caller cancels the task - cancellationToken.Register(this.CancelPrompt, true); - - return await this.WaitForTaskAsync( - this.StartPromptLoopAsync( - this.promptCancellationTokenSource.Token)); - } - - private async Task WaitForTaskAsync(Task taskToWait) - { - Task finishedTask = - await Task.WhenAny( - this.cancelTask.Task, - taskToWait); - - if (this.cancelTask.Task.IsCanceled) - { - throw new PipelineStoppedException(); - } - - return taskToWait.Result; - } - - private async Task StartPromptLoopAsync( - CancellationToken cancellationToken) - { - int[] choiceIndexes = null; - - // Show the prompt to the user - this.ShowPrompt(PromptStyle.Full); - - while (!cancellationToken.IsCancellationRequested) - { - string responseString = await this.ReadInputStringAsync(cancellationToken); - if (responseString == null) - { - // If the response string is null, the prompt has been cancelled - break; - } - - choiceIndexes = this.HandleResponse(responseString); - - // Return the default choice values if no choices were entered - if (choiceIndexes == null && string.IsNullOrEmpty(responseString)) - { - choiceIndexes = this.DefaultChoices; - } - - // If the user provided no choices, we should prompt again - if (choiceIndexes != null) - { - break; - } - - // The user did not respond with a valid choice, - // show the prompt again to give another chance - this.ShowPrompt(PromptStyle.Minimal); - } - - if (cancellationToken.IsCancellationRequested) - { - // Throw a TaskCanceledException to stop the pipeline - throw new TaskCanceledException(); - } - - return choiceIndexes?.ToArray(); - } - - /// - /// Implements behavior to handle the user's response. - /// - /// The string representing the user's response. - /// - /// True if the prompt is complete, false if the prompt is - /// still waiting for a valid response. - /// - protected virtual int[] HandleResponse(string responseString) - { - List choiceIndexes = new List(); - - // Clean up the response string and split it - var choiceStrings = - responseString.Trim().Split( - new char[] { ',' }, - StringSplitOptions.RemoveEmptyEntries); - - foreach (string choiceString in choiceStrings) - { - for (int i = 0; i < this.Choices.Length; i++) - { - if (this.Choices[i].MatchesInput(choiceString)) - { - choiceIndexes.Add(i); - - // If this is a single-choice prompt, break out after - // the first matched choice - if (!this.IsMultiChoice) - { - break; - } - } - } - } - - if (choiceIndexes.Count == 0) - { - // The user did not respond with a valid choice, - // show the prompt again to give another chance - return null; - } - - return choiceIndexes.ToArray(); - } - - /// - /// Called when the active prompt should be cancelled. - /// - protected override void OnPromptCancelled() - { - // Cancel the prompt task - this.promptCancellationTokenSource.Cancel(); - this.cancelTask.TrySetCanceled(); - } - - #endregion - - #region Abstract Methods - - /// - /// Called when the prompt should be displayed to the user. - /// - /// - /// Indicates the prompt style to use when showing the prompt. - /// - protected abstract void ShowPrompt(PromptStyle promptStyle); - - /// - /// Reads an input string asynchronously from the console. - /// - /// - /// A CancellationToken that can be used to cancel the read. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's input. - /// - protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); - - #endregion - - #region Private Methods - - private int GetSingleResult(int[] choiceArray) - { - return - choiceArray != null - ? choiceArray.DefaultIfEmpty(-1).First() - : -1; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Console/CollectionFieldDetails.cs b/src/PowerShellEditorServices/Console/CollectionFieldDetails.cs deleted file mode 100644 index 80ae62b5f..000000000 --- a/src/PowerShellEditorServices/Console/CollectionFieldDetails.cs +++ /dev/null @@ -1,138 +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.Collections; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Contains the details of an colleciton input field shown - /// from an InputPromptHandler. This class is meant to be - /// serializable to the user's UI. - /// - public class CollectionFieldDetails : FieldDetails - { - #region Private Fields - - private bool isArray; - private bool isEntryComplete; - private string fieldName; - private int currentCollectionIndex; - private ArrayList collectionItems = new ArrayList(); - - #endregion - - #region Constructors - - /// - /// Creates an instance of the CollectionFieldDetails class. - /// - /// The field's name. - /// The field's label. - /// The field's value type. - /// If true, marks the field as mandatory. - /// The field's default value. - public CollectionFieldDetails( - string name, - string label, - Type fieldType, - bool isMandatory, - object defaultValue) - : base(name, label, fieldType, isMandatory, defaultValue) - { - this.fieldName = name; - - this.FieldType = typeof(object); - - if (fieldType.IsArray) - { - this.isArray = true; - this.FieldType = fieldType.GetElementType(); - } - - this.Name = - string.Format( - "{0}[{1}]", - this.fieldName, - this.currentCollectionIndex); - } - - #endregion - - #region Public Methods - - /// - /// Gets the next field to display if this is a complex - /// field, otherwise returns null. - /// - /// - /// A FieldDetails object if there's another field to - /// display or if this field is complete. - /// - public override FieldDetails GetNextField() - { - if (!this.isEntryComplete) - { - // Get the next collection field - this.currentCollectionIndex++; - this.Name = - string.Format( - "{0}[{1}]", - this.fieldName, - this.currentCollectionIndex); - - return this; - } - else - { - return null; - } - } - - /// - /// Sets the field's value. - /// - /// The field's value. - /// - /// True if a value has been supplied by the user, false if the user supplied no value. - /// - public override void SetValue(object fieldValue, bool hasValue) - { - if (hasValue) - { - // Add the item to the collection - this.collectionItems.Add(fieldValue); - } - else - { - this.isEntryComplete = true; - } - } - - /// - /// Gets the field's final value after the prompt is - /// complete. - /// - /// The field's final value. - protected override object OnGetValue() - { - object collection = this.collectionItems; - - // Should the result collection be an array? - if (this.isArray) - { - // Convert the ArrayList to an array - collection = - this.collectionItems.ToArray( - this.FieldType); - } - - return collection; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs deleted file mode 100644 index 74b51c876..000000000 --- a/src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs +++ /dev/null @@ -1,135 +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.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides a standard implementation of ChoicePromptHandler - /// for use in the interactive console (REPL). - /// - public abstract class ConsoleChoicePromptHandler : ChoicePromptHandler - { - #region Private Fields - - /// - /// The IHostOutput instance to use for this prompt. - /// - protected IHostOutput hostOutput; - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ConsoleChoicePromptHandler class. - /// - /// - /// The IHostOutput implementation to use for writing to the - /// console. - /// - /// An ILogger implementation used for writing log messages. - public ConsoleChoicePromptHandler( - IHostOutput hostOutput, - ILogger logger) - : base(logger) - { - this.hostOutput = hostOutput; - } - - #endregion - - /// - /// Called when the prompt should be displayed to the user. - /// - /// - /// Indicates the prompt style to use when showing the prompt. - /// - protected override void ShowPrompt(PromptStyle promptStyle) - { - if (promptStyle == PromptStyle.Full) - { - if (this.Caption != null) - { - this.hostOutput.WriteOutput(this.Caption); - } - - if (this.Message != null) - { - this.hostOutput.WriteOutput(this.Message); - } - } - - foreach (var choice in this.Choices) - { - string hotKeyString = - choice.HotKeyIndex > -1 ? - choice.Label[choice.HotKeyIndex].ToString().ToUpper() : - string.Empty; - - this.hostOutput.WriteOutput( - string.Format( - "[{0}] {1} ", - hotKeyString, - choice.Label), - false); - } - - this.hostOutput.WriteOutput("[?] Help", false); - - var validDefaultChoices = - this.DefaultChoices.Where( - choice => choice > -1 && choice < this.Choices.Length); - - if (validDefaultChoices.Any()) - { - var choiceString = - string.Join( - ", ", - this.DefaultChoices - .Select(choice => this.Choices[choice].Label)); - - this.hostOutput.WriteOutput( - $" (default is \"{choiceString}\"): ", - false); - } - } - - - /// - /// Implements behavior to handle the user's response. - /// - /// The string representing the user's response. - /// - /// True if the prompt is complete, false if the prompt is - /// still waiting for a valid response. - /// - protected override int[] HandleResponse(string responseString) - { - if (responseString.Trim() == "?") - { - // Print help text - foreach (var choice in this.Choices) - { - this.hostOutput.WriteOutput( - string.Format( - "{0} - {1}", - (choice.HotKeyCharacter.HasValue ? - choice.HotKeyCharacter.Value.ToString() : - choice.Label), - choice.HelpMessage)); - } - - return null; - } - - return base.HandleResponse(responseString); - } - } -} diff --git a/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs deleted file mode 100644 index 8124633a7..000000000 --- a/src/PowerShellEditorServices/Console/ConsoleInputPromptHandler.cs +++ /dev/null @@ -1,105 +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.Security; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides a standard implementation of InputPromptHandler - /// for use in the interactive console (REPL). - /// - public abstract class ConsoleInputPromptHandler : InputPromptHandler - { - #region Private Fields - - /// - /// The IHostOutput instance to use for this prompt. - /// - protected IHostOutput hostOutput; - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ConsoleInputPromptHandler class. - /// - /// - /// The IHostOutput implementation to use for writing to the - /// console. - /// - /// An ILogger implementation used for writing log messages. - public ConsoleInputPromptHandler( - IHostOutput hostOutput, - ILogger logger) - : base(logger) - { - this.hostOutput = hostOutput; - } - - #endregion - - #region Public Methods - - /// - /// Called when the prompt caption and message should be - /// displayed to the user. - /// - /// The caption string to be displayed. - /// The message string to be displayed. - protected override void ShowPromptMessage(string caption, string message) - { - if (!string.IsNullOrEmpty(caption)) - { - this.hostOutput.WriteOutput(caption, true); - } - - if (!string.IsNullOrEmpty(message)) - { - this.hostOutput.WriteOutput(message, true); - } - } - - /// - /// Called when a prompt should be displayed for a specific - /// input field. - /// - /// The details of the field to be displayed. - protected override void ShowFieldPrompt(FieldDetails fieldDetails) - { - // For a simple prompt there won't be any field name. - // In this case don't write anything - if (!string.IsNullOrEmpty(fieldDetails.Name)) - { - this.hostOutput.WriteOutput( - fieldDetails.Name + ": ", - false); - } - } - - /// - /// Called when an error should be displayed, such as when the - /// user types in a string with an incorrect format for the - /// current field. - /// - /// - /// The Exception containing the error to be displayed. - /// - protected override void ShowErrorMessage(Exception e) - { - this.hostOutput.WriteOutput( - e.Message, - true, - OutputType.Error); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Console/ConsoleProxy.cs deleted file mode 100644 index c38c81724..000000000 --- a/src/PowerShellEditorServices/Console/ConsoleProxy.cs +++ /dev/null @@ -1,196 +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.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides asynchronous implementations of the API's as well as - /// synchronous implementations that work around platform specific issues. - /// - internal static class ConsoleProxy - { - private static IConsoleOperations s_consoleProxy; - - static ConsoleProxy() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - s_consoleProxy = new WindowsConsoleOperations(); - return; - } - - s_consoleProxy = new UnixConsoleOperations(); - } - - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// An object that describes the constant and Unicode character, if any, - /// that correspond to the pressed console key. The object also - /// describes, in a bitwise combination of values, whether - /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. - /// - public static ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) => - s_consoleProxy.ReadKey(intercept, cancellationToken); - - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// A task that will complete with a result of the key pressed by the user. - /// - public static Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) => - s_consoleProxy.ReadKeyAsync(intercept, cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The horizontal position of the console cursor. - public static int GetCursorLeft() => - s_consoleProxy.GetCursorLeft(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The horizontal position of the console cursor. - public static int GetCursorLeft(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorLeft(cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - public static Task GetCursorLeftAsync() => - s_consoleProxy.GetCursorLeftAsync(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorLeftAsync(cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The vertical position of the console cursor. - public static int GetCursorTop() => - s_consoleProxy.GetCursorTop(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The vertical position of the console cursor. - public static int GetCursorTop(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorTop(cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - public static Task GetCursorTopAsync() => - s_consoleProxy.GetCursorTopAsync(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - public static Task GetCursorTopAsync(CancellationToken cancellationToken) => - s_consoleProxy.GetCursorTopAsync(cancellationToken); - - /// - /// On Unix platforms this method is sent to PSReadLine as a work around for issues - /// with the System.Console implementation for that platform. Functionally it is the - /// same as System.Console.ReadKey, with the exception that it will not lock the - /// standard input stream. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// true to not display the pressed key; otherwise, false. - /// - /// - /// The that can be used to cancel the request. - /// - /// - /// An object that describes the ConsoleKey constant and Unicode character, if any, - /// that correspond to the pressed console key. The ConsoleKeyInfo object also describes, - /// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt, - /// or Ctrl modifier keys was pressed simultaneously with the console key. - /// - internal static ConsoleKeyInfo UnixReadKey(bool intercept, CancellationToken cancellationToken) - { - try - { - return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken); - } - catch (OperationCanceledException) - { - return default; - } - } - } -} diff --git a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs deleted file mode 100644 index af6ca044c..000000000 --- a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs +++ /dev/null @@ -1,616 +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.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - using System; - using System.Management.Automation; - using System.Management.Automation.Language; - using System.Security; - - internal class ConsoleReadLine - { - #region Private Field - private PowerShellContext powerShellContext; - - #endregion - - #region Constructors - - public ConsoleReadLine(PowerShellContext powerShellContext) - { - this.powerShellContext = powerShellContext; - } - - #endregion - - #region Public Methods - - public Task ReadCommandLineAsync(CancellationToken cancellationToken) - { - return this.ReadLineAsync(true, cancellationToken); - } - - public Task ReadSimpleLineAsync(CancellationToken cancellationToken) - { - return this.ReadLineAsync(false, cancellationToken); - } - - public async Task ReadSecureLineAsync(CancellationToken cancellationToken) - { - SecureString secureString = new SecureString(); - - int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); - int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); - int previousInputLength = 0; - - Console.TreatControlCAsInput = true; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken); - - if ((int)keyInfo.Key == 3 || - keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - throw new PipelineStoppedException(); - } - if (keyInfo.Key == ConsoleKey.Enter) - { - // Break to return the completed string - break; - } - if (keyInfo.Key == ConsoleKey.Tab) - { - continue; - } - if (keyInfo.Key == ConsoleKey.Backspace) - { - if (secureString.Length > 0) - { - secureString.RemoveAt(secureString.Length - 1); - } - } - else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) - { - secureString.AppendChar(keyInfo.KeyChar); - } - - // Re-render the secure string characters - int currentInputLength = secureString.Length; - int consoleWidth = Console.WindowWidth; - - if (currentInputLength > previousInputLength) - { - Console.Write('*'); - } - else if (previousInputLength > 0 && currentInputLength < previousInputLength) - { - int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken); - int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); - - // Back up the cursor before clearing the character - col--; - if (col < 0) - { - col = consoleWidth - 1; - row--; - } - - Console.SetCursorPosition(col, row); - Console.Write(' '); - Console.SetCursorPosition(col, row); - } - - previousInputLength = currentInputLength; - } - } - finally - { - Console.TreatControlCAsInput = false; - } - - return secureString; - } - - #endregion - - #region Private Methods - - private static async Task ReadKeyAsync(CancellationToken cancellationToken) - { - return await ConsoleProxy.ReadKeyAsync(intercept: true, cancellationToken); - } - - private async Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken); - } - - /// - /// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine. - /// This method should be used when PSReadLine is disabled, either by user settings or - /// unsupported PowerShell versions. - /// - /// - /// Indicates whether ReadLine should act like a command line. - /// - /// - /// The cancellation token that will be checked prior to completing the returned task. - /// - /// - /// A task object representing the asynchronus operation. The Result property on - /// the task object returns the user input string. - /// - internal async Task InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - string inputBeforeCompletion = null; - string inputAfterCompletion = null; - CommandCompletion currentCompletion = null; - - int historyIndex = -1; - Collection currentHistory = null; - - StringBuilder inputLine = new StringBuilder(); - - int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken); - int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken); - - int initialWindowLeft = Console.WindowLeft; - int initialWindowTop = Console.WindowTop; - - int currentCursorIndex = 0; - - Console.TreatControlCAsInput = true; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken); - - // Do final position calculation after the key has been pressed - // because the window could have been resized before then - int promptStartCol = initialCursorCol; - int promptStartRow = initialCursorRow; - int consoleWidth = Console.WindowWidth; - - if ((int)keyInfo.Key == 3 || - keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - throw new PipelineStoppedException(); - } - else if (keyInfo.Key == ConsoleKey.Tab && isCommandLine) - { - if (currentCompletion == null) - { - inputBeforeCompletion = inputLine.ToString(); - inputAfterCompletion = null; - - // TODO: This logic should be moved to AstOperations or similar! - - if (this.powerShellContext.IsDebuggerStopped) - { - PSCommand command = new PSCommand(); - command.AddCommand("TabExpansion2"); - command.AddParameter("InputScript", inputBeforeCompletion); - command.AddParameter("CursorColumn", currentCursorIndex); - command.AddParameter("Options", null); - - var results = - await this.powerShellContext.ExecuteCommandAsync(command, false, false); - - currentCompletion = results.FirstOrDefault(); - } - else - { - using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandleAsync()) - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - currentCompletion = - CommandCompletion.CompleteInput( - inputBeforeCompletion, - currentCursorIndex, - null, - powerShell); - - if (currentCompletion.CompletionMatches.Count > 0) - { - int replacementEndIndex = - currentCompletion.ReplacementIndex + - currentCompletion.ReplacementLength; - - inputAfterCompletion = - inputLine.ToString( - replacementEndIndex, - inputLine.Length - replacementEndIndex); - } - else - { - currentCompletion = null; - } - } - } - } - - CompletionResult completion = - currentCompletion?.GetNextResult( - !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); - - if (completion != null) - { - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - $"{completion.CompletionText}{inputAfterCompletion}", - currentCursorIndex, - insertIndex: currentCompletion.ReplacementIndex, - replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, - finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); - } - } - else if (keyInfo.Key == ConsoleKey.LeftArrow) - { - currentCompletion = null; - - if (currentCursorIndex > 0) - { - currentCursorIndex = - this.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - currentCursorIndex - 1); - } - } - else if (keyInfo.Key == ConsoleKey.Home) - { - currentCompletion = null; - - currentCursorIndex = - this.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - 0); - } - else if (keyInfo.Key == ConsoleKey.RightArrow) - { - currentCompletion = null; - - if (currentCursorIndex < inputLine.Length) - { - currentCursorIndex = - this.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - currentCursorIndex + 1); - } - } - else if (keyInfo.Key == ConsoleKey.End) - { - currentCompletion = null; - - currentCursorIndex = - this.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - inputLine.Length); - } - else if (keyInfo.Key == ConsoleKey.UpArrow && isCommandLine) - { - currentCompletion = null; - - // TODO: Ctrl+Up should allow navigation in multi-line input - - if (currentHistory == null) - { - historyIndex = -1; - - PSCommand command = new PSCommand(); - command.AddCommand("Get-History"); - - currentHistory = - await this.powerShellContext.ExecuteCommandAsync( - command, - false, - false) as Collection; - - if (currentHistory != null) - { - historyIndex = currentHistory.Count; - } - } - - if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) - { - historyIndex--; - - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - (string)currentHistory[historyIndex].Properties["CommandLine"].Value, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - } - else if (keyInfo.Key == ConsoleKey.DownArrow && isCommandLine) - { - currentCompletion = null; - - // The down arrow shouldn't cause history to be loaded, - // it's only for navigating an active history array - - if (historyIndex > -1 && historyIndex < currentHistory.Count && - currentHistory != null && currentHistory.Count > 0) - { - historyIndex++; - - if (historyIndex < currentHistory.Count) - { - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - (string)currentHistory[historyIndex].Properties["CommandLine"].Value, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - else if (historyIndex == currentHistory.Count) - { - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - } - } - else if (keyInfo.Key == ConsoleKey.Escape) - { - currentCompletion = null; - historyIndex = currentHistory != null ? currentHistory.Count : -1; - - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - else if (keyInfo.Key == ConsoleKey.Backspace) - { - currentCompletion = null; - - if (currentCursorIndex > 0) - { - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: currentCursorIndex - 1, - replaceLength: 1, - finalCursorIndex: currentCursorIndex - 1); - } - } - else if (keyInfo.Key == ConsoleKey.Delete) - { - currentCompletion = null; - - if (currentCursorIndex < inputLine.Length) - { - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - replaceLength: 1, - finalCursorIndex: currentCursorIndex); - } - } - else if (keyInfo.Key == ConsoleKey.Enter) - { - string completedInput = inputLine.ToString(); - currentCompletion = null; - currentHistory = null; - - //if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) - //{ - // // TODO: Start a new line! - // continue; - //} - - Parser.ParseInput( - completedInput, - out Token[] tokens, - out ParseError[] parseErrors); - - //if (parseErrors.Any(e => e.IncompleteInput)) - //{ - // // TODO: Start a new line! - // continue; - //} - - return completedInput; - } - else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) - { - // Normal character input - currentCompletion = null; - - currentCursorIndex = - this.InsertInput( - inputLine, - promptStartCol, - promptStartRow, - keyInfo.KeyChar.ToString(), - currentCursorIndex, - finalCursorIndex: currentCursorIndex + 1); - } - } - } - finally - { - Console.TreatControlCAsInput = false; - } - - return null; - } - - private int CalculateIndexFromCursor( - int promptStartCol, - int promptStartRow, - int consoleWidth) - { - return - ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + - ConsoleProxy.GetCursorLeft() - promptStartCol; - } - - private void CalculateCursorFromIndex( - int promptStartCol, - int promptStartRow, - int consoleWidth, - int inputIndex, - out int cursorCol, - out int cursorRow) - { - cursorCol = promptStartCol + inputIndex; - cursorRow = promptStartRow + cursorCol / consoleWidth; - cursorCol = cursorCol % consoleWidth; - } - - private int InsertInput( - StringBuilder inputLine, - int promptStartCol, - int promptStartRow, - string insertedInput, - int cursorIndex, - int insertIndex = -1, - int replaceLength = 0, - int finalCursorIndex = -1) - { - int consoleWidth = Console.WindowWidth; - int previousInputLength = inputLine.Length; - - if (insertIndex == -1) - { - insertIndex = cursorIndex; - } - - // Move the cursor to the new insertion point - this.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - insertIndex); - - // Edit the input string based on the insertion - if (insertIndex < inputLine.Length) - { - if (replaceLength > 0) - { - inputLine.Remove(insertIndex, replaceLength); - } - - inputLine.Insert(insertIndex, insertedInput); - } - else - { - inputLine.Append(insertedInput); - } - - // Re-render affected section - Console.Write( - inputLine.ToString( - insertIndex, - inputLine.Length - insertIndex)); - - if (inputLine.Length < previousInputLength) - { - Console.Write( - new string( - ' ', - previousInputLength - inputLine.Length)); - } - - // Automatically set the final cursor position to the end - // of the new input string. This is needed if the previous - // input string is longer than the new one and needed to have - // its old contents overwritten. This will position the cursor - // back at the end of the new text - if (finalCursorIndex == -1 && inputLine.Length < previousInputLength) - { - finalCursorIndex = inputLine.Length; - } - - if (finalCursorIndex > -1) - { - // Move the cursor to the final position - return - this.MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - finalCursorIndex); - } - else - { - return inputLine.Length; - } - } - - private int MoveCursorToIndex( - int promptStartCol, - int promptStartRow, - int consoleWidth, - int newCursorIndex) - { - this.CalculateCursorFromIndex( - promptStartCol, - promptStartRow, - consoleWidth, - newCursorIndex, - out int newCursorCol, - out int newCursorRow); - - Console.SetCursorPosition(newCursorCol, newCursorRow); - - return newCursorIndex; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/CredentialFieldDetails.cs b/src/PowerShellEditorServices/Console/CredentialFieldDetails.cs deleted file mode 100644 index 4b4452f2b..000000000 --- a/src/PowerShellEditorServices/Console/CredentialFieldDetails.cs +++ /dev/null @@ -1,122 +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.Management.Automation; -using System.Security; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Contains the details of a PSCredential field shown - /// from an InputPromptHandler. This class is meant to - /// be serializable to the user's UI. - /// - public class CredentialFieldDetails : FieldDetails - { - private string userName; - private SecureString password; - - /// - /// Creates an instance of the CredentialFieldDetails class. - /// - /// The field's name. - /// The field's label. - /// The initial value of the userName field. - public CredentialFieldDetails( - string name, - string label, - string userName) - : this(name, label, typeof(PSCredential), true, null) - { - if (!string.IsNullOrEmpty(userName)) - { - // Call GetNextField to prepare the password field - this.userName = userName; - this.GetNextField(); - } - } - - /// - /// Creates an instance of the CredentialFieldDetails class. - /// - /// The field's name. - /// The field's label. - /// The field's value type. - /// If true, marks the field as mandatory. - /// The field's default value. - public CredentialFieldDetails( - string name, - string label, - Type fieldType, - bool isMandatory, - object defaultValue) - : base(name, label, fieldType, isMandatory, defaultValue) - { - this.Name = "User"; - this.FieldType = typeof(string); - } - - #region Public Methods - - /// - /// Gets the next field to display if this is a complex - /// field, otherwise returns null. - /// - /// - /// A FieldDetails object if there's another field to - /// display or if this field is complete. - /// - public override FieldDetails GetNextField() - { - if (this.password != null) - { - // No more fields to display - return null; - } - else if (this.userName != null) - { - this.Name = $"Password for user {this.userName}"; - this.FieldType = typeof(SecureString); - } - - return this; - } - - /// - /// Sets the field's value. - /// - /// The field's value. - /// - /// True if a value has been supplied by the user, false if the user supplied no value. - /// - public override void SetValue(object fieldValue, bool hasValue) - { - if (hasValue) - { - if (this.userName == null) - { - this.userName = (string)fieldValue; - } - else - { - this.password = (SecureString)fieldValue; - } - } - } - - /// - /// Gets the field's final value after the prompt is - /// complete. - /// - /// The field's final value. - protected override object OnGetValue() - { - return new PSCredential(this.userName, this.password); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/FieldDetails.cs b/src/PowerShellEditorServices/Console/FieldDetails.cs deleted file mode 100644 index ffec536b3..000000000 --- a/src/PowerShellEditorServices/Console/FieldDetails.cs +++ /dev/null @@ -1,235 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Reflection; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Contains the details of an input field shown from an - /// InputPromptHandler. This class is meant to be - /// serializable to the user's UI. - /// - public class FieldDetails - { - #region Private Fields - - private object fieldValue; - - #endregion - - #region Properties - - /// - /// Gets or sets the name of the field. - /// - public string Name { get; set; } - - /// - /// Gets or sets the original name of the field before it was manipulated. - /// - public string OriginalName { get; set; } - - /// - /// Gets or sets the descriptive label for the field. - /// - public string Label { get; set; } - - /// - /// Gets or sets the field's value type. - /// - public Type FieldType { get; set; } - - /// - /// Gets or sets the field's help message. - /// - public string HelpMessage { get; set; } - - /// - /// Gets or sets a boolean that is true if the user - /// must enter a value for the field. - /// - public bool IsMandatory { get; set; } - - /// - /// Gets or sets the default value for the field. - /// - public object DefaultValue { get; set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the FieldDetails class. - /// - /// The field's name. - /// The field's label. - /// The field's value type. - /// If true, marks the field as mandatory. - /// The field's default value. - public FieldDetails( - string name, - string label, - Type fieldType, - bool isMandatory, - object defaultValue) - { - this.OriginalName = name; - this.Name = name; - this.Label = label; - this.FieldType = fieldType; - this.IsMandatory = isMandatory; - this.DefaultValue = defaultValue; - - if (fieldType.GetTypeInfo().IsGenericType) - { - throw new PSArgumentException( - "Generic types are not supported for input fields at this time."); - } - } - - #endregion - - #region Public Methods - - /// - /// Sets the field's value. - /// - /// The field's value. - /// - /// True if a value has been supplied by the user, false if the user supplied no value. - /// - public virtual void SetValue(object fieldValue, bool hasValue) - { - if (hasValue) - { - this.fieldValue = fieldValue; - } - } - - /// - /// Gets the field's final value after the prompt is - /// complete. - /// - /// The field's final value. - public object GetValue(ILogger logger) - { - object fieldValue = this.OnGetValue(); - - if (fieldValue == null) - { - if (!this.IsMandatory) - { - fieldValue = this.DefaultValue; - } - else - { - // This "shoudln't" happen, so log in case it does - logger.Write( - LogLevel.Error, - $"Cannot retrieve value for field {this.Label}"); - } - } - - return fieldValue; - } - - /// - /// Gets the field's final value after the prompt is - /// complete. - /// - /// The field's final value. - protected virtual object OnGetValue() - { - return this.fieldValue; - } - - /// - /// Gets the next field if this field can accept multiple - /// values, like a collection or an object with multiple - /// properties. - /// - /// - /// A new FieldDetails instance if there is a next field - /// or null otherwise. - /// - public virtual FieldDetails GetNextField() - { - return null; - } - - #endregion - - #region Internal Methods - - internal static FieldDetails Create( - FieldDescription fieldDescription, - ILogger logger) - { - Type fieldType = - GetFieldTypeFromTypeName( - fieldDescription.ParameterAssemblyFullName, - logger); - - if (typeof(IList).GetTypeInfo().IsAssignableFrom(fieldType.GetTypeInfo())) - { - return new CollectionFieldDetails( - fieldDescription.Name, - fieldDescription.Label, - fieldType, - fieldDescription.IsMandatory, - fieldDescription.DefaultValue); - } - else if (typeof(PSCredential) == fieldType) - { - return new CredentialFieldDetails( - fieldDescription.Name, - fieldDescription.Label, - fieldType, - fieldDescription.IsMandatory, - fieldDescription.DefaultValue); - } - else - { - return new FieldDetails( - fieldDescription.Name, - fieldDescription.Label, - fieldType, - fieldDescription.IsMandatory, - fieldDescription.DefaultValue); - } - } - - private static Type GetFieldTypeFromTypeName( - string assemblyFullName, - ILogger logger) - { - Type fieldType = typeof(string); - - if (!string.IsNullOrEmpty(assemblyFullName)) - { - if (!LanguagePrimitives.TryConvertTo(assemblyFullName, out fieldType)) - { - logger.Write( - LogLevel.Warning, - string.Format( - "Could not resolve type of field: {0}", - assemblyFullName)); - } - } - - return fieldType; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Console/IConsoleOperations.cs deleted file mode 100644 index b3fb58561..000000000 --- a/src/PowerShellEditorServices/Console/IConsoleOperations.cs +++ /dev/null @@ -1,140 +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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides platform specific console utilities. - /// - public interface IConsoleOperations - { - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// An object that describes the constant and Unicode character, if any, - /// that correspond to the pressed console key. The object also - /// describes, in a bitwise combination of values, whether - /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. - /// - ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken); - - /// - /// Obtains the next character or function key pressed by the user asynchronously. - /// Does not block when other console API's are called. - /// - /// - /// Determines whether to display the pressed key in the console window. - /// to not display the pressed key; otherwise, . - /// - /// The CancellationToken to observe. - /// - /// A task that will complete with a result of the key pressed by the user. - /// - Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The horizontal position of the console cursor. - int GetCursorLeft(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The horizontal position of the console cursor. - int GetCursorLeft(CancellationToken cancellationToken); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - Task GetCursorLeftAsync(); - - /// - /// Obtains the horizontal position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the horizontal position - /// of the console cursor. - /// - Task GetCursorLeftAsync(CancellationToken cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The vertical position of the console cursor. - int GetCursorTop(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// The vertical position of the console cursor. - int GetCursorTop(CancellationToken cancellationToken); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - Task GetCursorTopAsync(); - - /// - /// Obtains the vertical position of the console cursor. Use this method - /// instead of to avoid triggering - /// pending calls to - /// on Unix platforms. - /// - /// The to observe. - /// - /// A representing the asynchronous operation. The - /// property will return the vertical position - /// of the console cursor. - /// - Task GetCursorTopAsync(CancellationToken cancellationToken); - } -} diff --git a/src/PowerShellEditorServices/Console/InputPromptHandler.cs b/src/PowerShellEditorServices/Console/InputPromptHandler.cs deleted file mode 100644 index 7d626c9ab..000000000 --- a/src/PowerShellEditorServices/Console/InputPromptHandler.cs +++ /dev/null @@ -1,331 +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.Collections.Generic; -using System.Globalization; -using System.Management.Automation; -using System.Security; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides a base implementation for IPromptHandler classes - /// that present the user a set of fields for which values - /// should be entered. - /// - public abstract class InputPromptHandler : PromptHandler - { - #region Private Fields - - private int currentFieldIndex = -1; - private FieldDetails currentField; - private CancellationTokenSource promptCancellationTokenSource = - new CancellationTokenSource(); - private TaskCompletionSource> cancelTask = - new TaskCompletionSource>(); - - #endregion - - /// - /// - /// - /// An ILogger implementation used for writing log messages. - public InputPromptHandler(ILogger logger) : base(logger) - { - } - - #region Properties - - /// - /// Gets the array of fields for which the user must enter values. - /// - protected FieldDetails[] Fields { get; private set; } - - #endregion - - #region Public Methods - - /// - /// Prompts the user for a line of input without writing any message or caption. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's input. - /// - public Task PromptForInputAsync( - CancellationToken cancellationToken) - { - Task> innerTask = - this.PromptForInputAsync( - null, - null, - new FieldDetails[] { new FieldDetails("", "", typeof(string), false, "") }, - cancellationToken); - - return - innerTask.ContinueWith( - task => - { - if (task.IsFaulted) - { - throw task.Exception; - } - else if (task.IsCanceled) - { - throw new TaskCanceledException(task); - } - - // Return the value of the sole field - return (string)task.Result[""]; - }); - } - - /// - /// Prompts the user for a line (or lines) of input. - /// - /// - /// A title shown before the series of input fields. - /// - /// - /// A descritpive message shown before the series of input fields. - /// - /// - /// An array of FieldDetails items to be displayed which prompt the - /// user for input of a specific type. - /// - /// - /// A CancellationToken that can be used to cancel the prompt. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's input. - /// - public async Task> PromptForInputAsync( - string promptCaption, - string promptMessage, - FieldDetails[] fields, - CancellationToken cancellationToken) - { - // Cancel the prompt if the caller cancels the task - cancellationToken.Register(this.CancelPrompt, true); - - this.Fields = fields; - - this.ShowPromptMessage(promptCaption, promptMessage); - - Task> promptTask = - this.StartPromptLoopAsync(this.promptCancellationTokenSource.Token); - - Task finishedTask = - await Task.WhenAny( - cancelTask.Task, - promptTask); - - if (this.cancelTask.Task.IsCanceled) - { - throw new PipelineStoppedException(); - } - - return promptTask.Result; - } - - /// - /// Prompts the user for a SecureString without writing any message or caption. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's input. - /// - public Task PromptForSecureInputAsync( - CancellationToken cancellationToken) - { - Task> innerTask = - this.PromptForInputAsync( - null, - null, - new FieldDetails[] { new FieldDetails("", "", typeof(SecureString), false, "") }, - cancellationToken); - - return - innerTask.ContinueWith( - task => - { - if (task.IsFaulted) - { - throw task.Exception; - } - else if (task.IsCanceled) - { - throw new TaskCanceledException(task); - } - - // Return the value of the sole field - return (SecureString)task.Result?[""]; - }); - } - - /// - /// Called when the active prompt should be cancelled. - /// - protected override void OnPromptCancelled() - { - // Cancel the prompt task - this.promptCancellationTokenSource.Cancel(); - this.cancelTask.TrySetCanceled(); - } - - #endregion - - #region Abstract Methods - - /// - /// Called when the prompt caption and message should be - /// displayed to the user. - /// - /// The caption string to be displayed. - /// The message string to be displayed. - protected abstract void ShowPromptMessage(string caption, string message); - - /// - /// Called when a prompt should be displayed for a specific - /// input field. - /// - /// The details of the field to be displayed. - protected abstract void ShowFieldPrompt(FieldDetails fieldDetails); - - /// - /// Reads an input string asynchronously from the console. - /// - /// - /// A CancellationToken that can be used to cancel the read. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's input. - /// - protected abstract Task ReadInputStringAsync(CancellationToken cancellationToken); - - /// - /// Reads a SecureString asynchronously from the console. - /// - /// - /// A CancellationToken that can be used to cancel the read. - /// - /// - /// A Task instance that can be monitored for completion to get - /// the user's input. - /// - protected abstract Task ReadSecureStringAsync(CancellationToken cancellationToken); - - /// - /// Called when an error should be displayed, such as when the - /// user types in a string with an incorrect format for the - /// current field. - /// - /// - /// The Exception containing the error to be displayed. - /// - protected abstract void ShowErrorMessage(Exception e); - - #endregion - - #region Private Methods - - private async Task> StartPromptLoopAsync( - CancellationToken cancellationToken) - { - this.GetNextField(); - - // Loop until there are no more prompts to process - while (this.currentField != null && !cancellationToken.IsCancellationRequested) - { - // Show current prompt - this.ShowFieldPrompt(this.currentField); - - bool enteredValue = false; - object responseValue = null; - string responseString = null; - - // Read input depending on field type - if (this.currentField.FieldType == typeof(SecureString)) - { - SecureString secureString = await this.ReadSecureStringAsync(cancellationToken); - responseValue = secureString; - enteredValue = secureString != null; - } - else - { - responseString = await this.ReadInputStringAsync(cancellationToken); - responseValue = responseString; - enteredValue = responseString != null && responseString.Length > 0; - - try - { - responseValue = - LanguagePrimitives.ConvertTo( - responseString, - this.currentField.FieldType, - CultureInfo.CurrentCulture); - } - catch (PSInvalidCastException e) - { - this.ShowErrorMessage(e.InnerException ?? e); - continue; - } - } - - // Set the field's value and get the next field - this.currentField.SetValue(responseValue, enteredValue); - this.GetNextField(); - } - - if (cancellationToken.IsCancellationRequested) - { - // Throw a TaskCanceledException to stop the pipeline - throw new TaskCanceledException(); - } - - // Return the field values - return this.GetFieldValues(); - } - - private FieldDetails GetNextField() - { - FieldDetails nextField = this.currentField?.GetNextField(); - - if (nextField == null) - { - this.currentFieldIndex++; - - // Have we shown all the prompts already? - if (this.currentFieldIndex < this.Fields.Length) - { - nextField = this.Fields[this.currentFieldIndex]; - } - } - - this.currentField = nextField; - return nextField; - } - - private Dictionary GetFieldValues() - { - Dictionary fieldValues = new Dictionary(); - - foreach (FieldDetails field in this.Fields) - { - fieldValues.Add(field.OriginalName, field.GetValue(this.Logger)); - } - - return fieldValues; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/PromptHandler.cs b/src/PowerShellEditorServices/Console/PromptHandler.cs deleted file mode 100644 index a40bd6e76..000000000 --- a/src/PowerShellEditorServices/Console/PromptHandler.cs +++ /dev/null @@ -1,55 +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 Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Defines an abstract base class for prompt handler implementations. - /// - public abstract class PromptHandler - { - /// - /// Gets the ILogger implementation used for this instance. - /// - protected ILogger Logger { get; private set; } - - /// - /// - /// - /// An ILogger implementation used for writing log messages. - public PromptHandler(ILogger logger) - { - this.Logger = logger; - } - - /// - /// Called when the active prompt should be cancelled. - /// - public void CancelPrompt() - { - // Allow the implementation to clean itself up - this.OnPromptCancelled(); - this.PromptCancelled?.Invoke(this, new EventArgs()); - } - - /// - /// An event that gets raised if the prompt is cancelled, either - /// by the user or due to a timeout. - /// - public event EventHandler PromptCancelled; - - /// - /// Implementation classes may override this method to perform - /// cleanup when the CancelPrompt method gets called. - /// - protected virtual void OnPromptCancelled() - { - } - } -} - diff --git a/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs deleted file mode 100644 index 1bf5a5cc4..000000000 --- a/src/PowerShellEditorServices/Console/TerminalChoicePromptHandler.cs +++ /dev/null @@ -1,63 +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.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides a standard implementation of ChoicePromptHandler - /// for use in the interactive console (REPL). - /// - internal class TerminalChoicePromptHandler : ConsoleChoicePromptHandler - { - #region Private Fields - - private ConsoleReadLine consoleReadLine; - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ConsoleChoicePromptHandler class. - /// - /// - /// The ConsoleReadLine instance to use for interacting with the terminal. - /// - /// - /// The IHostOutput implementation to use for writing to the - /// console. - /// - /// An ILogger implementation used for writing log messages. - public TerminalChoicePromptHandler( - ConsoleReadLine consoleReadLine, - IHostOutput hostOutput, - ILogger logger) - : base(hostOutput, logger) - { - this.hostOutput = hostOutput; - this.consoleReadLine = consoleReadLine; - } - - #endregion - - /// - /// Reads an input string from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - protected override async Task ReadInputStringAsync(CancellationToken cancellationToken) - { - string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); - this.hostOutput.WriteOutput(string.Empty); - - return inputString; - } - } -} diff --git a/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs deleted file mode 100644 index 67b58bc55..000000000 --- a/src/PowerShellEditorServices/Console/TerminalInputPromptHandler.cs +++ /dev/null @@ -1,80 +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.Security; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - /// - /// Provides a standard implementation of InputPromptHandler - /// for use in the interactive console (REPL). - /// - internal class TerminalInputPromptHandler : ConsoleInputPromptHandler - { - #region Private Fields - - private ConsoleReadLine consoleReadLine; - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ConsoleInputPromptHandler class. - /// - /// - /// The ConsoleReadLine instance to use for interacting with the terminal. - /// - /// - /// The IHostOutput implementation to use for writing to the - /// console. - /// - /// An ILogger implementation used for writing log messages. - public TerminalInputPromptHandler( - ConsoleReadLine consoleReadLine, - IHostOutput hostOutput, - ILogger logger) - : base(hostOutput, logger) - { - this.consoleReadLine = consoleReadLine; - } - - #endregion - - #region Public Methods - - /// - /// Reads an input string from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - protected override async Task ReadInputStringAsync(CancellationToken cancellationToken) - { - string inputString = await this.consoleReadLine.ReadSimpleLineAsync(cancellationToken); - this.hostOutput.WriteOutput(string.Empty); - - return inputString; - } - - /// - /// Reads a SecureString from the user. - /// - /// A CancellationToken that can be used to cancel the prompt. - /// A Task that can be awaited to get the user's response. - protected override async Task ReadSecureStringAsync(CancellationToken cancellationToken) - { - SecureString secureString = await this.consoleReadLine.ReadSecureLineAsync(cancellationToken); - this.hostOutput.WriteOutput(string.Empty); - - return secureString; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs deleted file mode 100644 index e0ef73b65..000000000 --- a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs +++ /dev/null @@ -1,297 +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.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; -using UnixConsoleEcho; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - internal class UnixConsoleOperations : IConsoleOperations - { - private const int LongWaitForKeySleepTime = 300; - - private const int ShortWaitForKeyTimeout = 5000; - - private const int ShortWaitForKeySpinUntilSleepTime = 30; - - private static readonly ManualResetEventSlim s_waitHandle = new ManualResetEventSlim(); - - private static readonly SemaphoreSlim s_readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - private static readonly SemaphoreSlim s_stdInHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - private Func WaitForKeyAvailable; - - private Func> WaitForKeyAvailableAsync; - - internal UnixConsoleOperations() - { - // Switch between long and short wait periods depending on if the - // user has recently (last 5 seconds) pressed a key to avoid preventing - // the CPU from entering low power mode. - WaitForKeyAvailable = LongWaitForKey; - WaitForKeyAvailableAsync = LongWaitForKeyAsync; - } - - public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) - { - s_readKeyHandle.Wait(cancellationToken); - - // On Unix platforms System.Console.ReadKey has an internal lock on stdin. Because - // of this, if a ReadKey call is pending in one thread and in another thread - // Console.CursorLeft is called, both threads block until a key is pressed. - - // To work around this we wait for a key to be pressed before actually calling Console.ReadKey. - // However, any pressed keys during this time will be echoed to the console. To get around - // this we use the UnixConsoleEcho package to disable echo prior to waiting. - if (Utils.IsPS6) - { - InputEcho.Disable(); - } - - try - { - // The WaitForKeyAvailable delegate switches between a long delay between waits and - // a short timeout depending on how recently a key has been pressed. This allows us - // to let the CPU enter low power mode without compromising responsiveness. - while (!WaitForKeyAvailable(cancellationToken)); - } - finally - { - if (Utils.IsPS6) - { - InputEcho.Disable(); - } - s_readKeyHandle.Release(); - } - - // A key has been pressed, so aquire a lock on our internal stdin handle. This is done - // so any of our calls to cursor position API's do not release ReadKey. - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.ReadKey(intercept); - } - finally - { - s_stdInHandle.Release(); - } - } - - public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) - { - await s_readKeyHandle.WaitAsync(cancellationToken); - - // I tried to replace this library with a call to `stty -echo`, but unfortunately - // the library also sets up allowing backspace to trigger `Console.KeyAvailable`. - if (Utils.IsPS6) - { - InputEcho.Disable(); - } - try - { - while (!await WaitForKeyAvailableAsync(cancellationToken)); - } - finally - { - if (Utils.IsPS6) - { - InputEcho.Enable(); - } - s_readKeyHandle.Release(); - } - - await s_stdInHandle.WaitAsync(cancellationToken); - try - { - return System.Console.ReadKey(intercept); - } - finally - { - s_stdInHandle.Release(); - } - } - - public int GetCursorLeft() - { - return GetCursorLeft(CancellationToken.None); - } - - public int GetCursorLeft(CancellationToken cancellationToken) - { - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.CursorLeft; - } - finally - { - s_stdInHandle.Release(); - } - } - - public async Task GetCursorLeftAsync() - { - return await GetCursorLeftAsync(CancellationToken.None); - } - - public async Task GetCursorLeftAsync(CancellationToken cancellationToken) - { - await s_stdInHandle.WaitAsync(cancellationToken); - try - { - return System.Console.CursorLeft; - } - finally - { - s_stdInHandle.Release(); - } - } - - public int GetCursorTop() - { - return GetCursorTop(CancellationToken.None); - } - - public int GetCursorTop(CancellationToken cancellationToken) - { - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.CursorTop; - } - finally - { - s_stdInHandle.Release(); - } - } - - public async Task GetCursorTopAsync() - { - return await GetCursorTopAsync(CancellationToken.None); - } - - public async Task GetCursorTopAsync(CancellationToken cancellationToken) - { - await s_stdInHandle.WaitAsync(cancellationToken); - try - { - return System.Console.CursorTop; - } - finally - { - s_stdInHandle.Release(); - } - } - - private bool LongWaitForKey(CancellationToken cancellationToken) - { - // Wait for a key to be buffered (in other words, wait for Console.KeyAvailable to become - // true) with a long delay between checks. - while (!IsKeyAvailable(cancellationToken)) - { - s_waitHandle.Wait(LongWaitForKeySleepTime, cancellationToken); - } - - // As soon as a key is buffered, return true and switch the wait logic to be more - // responsive, but also more expensive. - WaitForKeyAvailable = ShortWaitForKey; - return true; - } - - private async Task LongWaitForKeyAsync(CancellationToken cancellationToken) - { - while (!await IsKeyAvailableAsync(cancellationToken)) - { - await Task.Delay(LongWaitForKeySleepTime, cancellationToken); - } - - WaitForKeyAvailableAsync = ShortWaitForKeyAsync; - return true; - } - - private bool ShortWaitForKey(CancellationToken cancellationToken) - { - // Check frequently for a new key to be buffered. - if (SpinUntilKeyAvailable(ShortWaitForKeyTimeout, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - return true; - } - - // If the user has not pressed a key before the end of the SpinUntil timeout then - // the user is idle and we can switch back to long delays between KeyAvailable checks. - cancellationToken.ThrowIfCancellationRequested(); - WaitForKeyAvailable = LongWaitForKey; - return false; - } - - private async Task ShortWaitForKeyAsync(CancellationToken cancellationToken) - { - if (await SpinUntilKeyAvailableAsync(ShortWaitForKeyTimeout, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - return true; - } - - cancellationToken.ThrowIfCancellationRequested(); - WaitForKeyAvailableAsync = LongWaitForKeyAsync; - return false; - } - - private bool SpinUntilKeyAvailable(int millisecondsTimeout, CancellationToken cancellationToken) - { - return SpinWait.SpinUntil( - () => - { - s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); - return IsKeyAvailable(cancellationToken); - }, - millisecondsTimeout); - } - - private async Task SpinUntilKeyAvailableAsync(int millisecondsTimeout, CancellationToken cancellationToken) - { - return await Task.Factory.StartNew( - () => SpinWait.SpinUntil( - () => - { - // The wait handle is never set, it's just used to enable cancelling the wait. - s_waitHandle.Wait(ShortWaitForKeySpinUntilSleepTime, cancellationToken); - return IsKeyAvailable(cancellationToken); - }, - millisecondsTimeout)); - } - - private bool IsKeyAvailable(CancellationToken cancellationToken) - { - s_stdInHandle.Wait(cancellationToken); - try - { - return System.Console.KeyAvailable; - } - finally - { - s_stdInHandle.Release(); - } - } - - private async Task IsKeyAvailableAsync(CancellationToken cancellationToken) - { - await s_stdInHandle.WaitAsync(cancellationToken); - try - { - return System.Console.KeyAvailable; - } - finally - { - s_stdInHandle.Release(); - } - } - } -} diff --git a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs deleted file mode 100644 index 493e66930..000000000 --- a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs +++ /dev/null @@ -1,76 +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.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Console -{ - internal class WindowsConsoleOperations : IConsoleOperations - { - private ConsoleKeyInfo? _bufferedKey; - - private SemaphoreSlim _readKeyHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - public int GetCursorLeft() => System.Console.CursorLeft; - - public int GetCursorLeft(CancellationToken cancellationToken) => System.Console.CursorLeft; - - public Task GetCursorLeftAsync() => Task.FromResult(System.Console.CursorLeft); - - public Task GetCursorLeftAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorLeft); - - public int GetCursorTop() => System.Console.CursorTop; - - public int GetCursorTop(CancellationToken cancellationToken) => System.Console.CursorTop; - - public Task GetCursorTopAsync() => Task.FromResult(System.Console.CursorTop); - - public Task GetCursorTopAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorTop); - - public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) - { - await _readKeyHandle.WaitAsync(cancellationToken); - try - { - return - _bufferedKey.HasValue - ? _bufferedKey.Value - : await Task.Factory.StartNew( - () => (_bufferedKey = System.Console.ReadKey(intercept)).Value); - } - finally - { - _readKeyHandle.Release(); - - // Throw if we're cancelled so the buffered key isn't cleared. - cancellationToken.ThrowIfCancellationRequested(); - _bufferedKey = null; - } - } - - public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) - { - _readKeyHandle.Wait(cancellationToken); - try - { - return - _bufferedKey.HasValue - ? _bufferedKey.Value - : (_bufferedKey = System.Console.ReadKey(intercept)).Value; - } - finally - { - _readKeyHandle.Release(); - - // Throw if we're cancelled so the buffered key isn't cleared. - cancellationToken.ThrowIfCancellationRequested(); - _bufferedKey = null; - } - } - } -} diff --git a/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs deleted file mode 100644 index f89606a2a..000000000 --- a/src/PowerShellEditorServices/Debugging/BreakpointDetails.cs +++ /dev/null @@ -1,108 +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.Management.Automation; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about a breakpoint that is set in the - /// PowerShell debugger. - /// - public class BreakpointDetails : BreakpointDetailsBase - { - /// - /// Gets the unique ID of the breakpoint. - /// - /// - public int Id { get; private set; } - - /// - /// Gets the source where the breakpoint is located. Used only for debug purposes. - /// - public string Source { get; private set; } - - /// - /// Gets the line number at which the breakpoint is set. - /// - public int LineNumber { get; private set; } - - /// - /// Gets the column number at which the breakpoint is set. If null, the default of 1 is used. - /// - public int? ColumnNumber { get; private set; } - - private BreakpointDetails() - { - } - - /// - /// Creates an instance of the BreakpointDetails class from the individual - /// pieces of breakpoint information provided by the client. - /// - /// - /// - /// - /// - /// - /// - public static BreakpointDetails Create( - string source, - int line, - int? column = null, - string condition = null, - string hitCondition = null) - { - Validate.IsNotNull("source", source); - - return new BreakpointDetails - { - Verified = true, - Source = source, - LineNumber = line, - ColumnNumber = column, - Condition = condition, - HitCondition = hitCondition - }; - } - - /// - /// Creates an instance of the BreakpointDetails class from a - /// PowerShell Breakpoint object. - /// - /// The Breakpoint instance from which details will be taken. - /// A new instance of the BreakpointDetails class. - public static BreakpointDetails Create(Breakpoint breakpoint) - { - Validate.IsNotNull("breakpoint", breakpoint); - - LineBreakpoint lineBreakpoint = breakpoint as LineBreakpoint; - if (lineBreakpoint == null) - { - throw new ArgumentException( - "Unexpected breakpoint type: " + breakpoint.GetType().Name); - } - - var breakpointDetails = new BreakpointDetails - { - Id = breakpoint.Id, - Verified = true, - Source = lineBreakpoint.Script, - LineNumber = lineBreakpoint.Line, - ColumnNumber = lineBreakpoint.Column, - Condition = lineBreakpoint.Action?.ToString() - }; - - if (lineBreakpoint.Column > 0) - { - breakpointDetails.ColumnNumber = lineBreakpoint.Column; - } - - return breakpointDetails; - } - } -} diff --git a/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs b/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs deleted file mode 100644 index 9fa83c23e..000000000 --- a/src/PowerShellEditorServices/Debugging/BreakpointDetailsBase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// 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 -{ - /// - /// Provides details about a breakpoint that is set in the - /// PowerShell debugger. - /// - public abstract class BreakpointDetailsBase - { - /// - /// Gets or sets a boolean indicator that if true, breakpoint could be set - /// (but not necessarily at the desired location). - /// - public bool Verified { get; set; } - - /// - /// Gets or set an optional message about the state of the breakpoint. This is shown to the user - /// and can be used to explain why a breakpoint could not be verified. - /// - public string Message { get; set; } - - /// - /// Gets the breakpoint condition string. - /// - public string Condition { get; protected set; } - - /// - /// Gets the breakpoint hit condition string. - /// - public string HitCondition { get; protected set; } - } -} diff --git a/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs b/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs deleted file mode 100644 index 8197480c9..000000000 --- a/src/PowerShellEditorServices/Debugging/CommandBreakpointDetails.cs +++ /dev/null @@ -1,73 +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.Management.Automation; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about a command breakpoint that is set in the PowerShell debugger. - /// - public class CommandBreakpointDetails : BreakpointDetailsBase - { - /// - /// Gets the name of the command on which the command breakpoint has been set. - /// - public string Name { get; private set; } - - private CommandBreakpointDetails() - { - } - - /// - /// Creates an instance of the class from the individual - /// pieces of breakpoint information provided by the client. - /// - /// The name of the command to break on. - /// Condition string that would be applied to the breakpoint Action parameter. - /// Hit condition string that would be applied to the breakpoint Action parameter. - /// - public static CommandBreakpointDetails Create( - string name, - string condition = null, - string hitCondition = null) - { - Validate.IsNotNull(nameof(name), name); - - return new CommandBreakpointDetails { - Name = name, - Condition = condition - }; - } - - /// - /// Creates an instance of the class from a - /// PowerShell CommandBreakpoint object. - /// - /// The Breakpoint instance from which details will be taken. - /// A new instance of the BreakpointDetails class. - public static CommandBreakpointDetails Create(Breakpoint breakpoint) - { - Validate.IsNotNull("breakpoint", breakpoint); - - CommandBreakpoint commandBreakpoint = breakpoint as CommandBreakpoint; - if (commandBreakpoint == null) - { - throw new ArgumentException( - "Unexpected breakpoint type: " + breakpoint.GetType().Name); - } - - var breakpointDetails = new CommandBreakpointDetails { - Verified = true, - Name = commandBreakpoint.Command, - Condition = commandBreakpoint.Action?.ToString() - }; - - return breakpointDetails; - } - } -} diff --git a/src/PowerShellEditorServices/Debugging/DebugService.cs b/src/PowerShellEditorServices/Debugging/DebugService.cs deleted file mode 100644 index 98d47d7af..000000000 --- a/src/PowerShellEditorServices/Debugging/DebugService.cs +++ /dev/null @@ -1,1359 +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.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Debugging; -using Microsoft.PowerShell.EditorServices.Utility; -using Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Session.Capabilities; -using System.Threading; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a high-level service for interacting with the - /// PowerShell debugger in the runspace managed by a PowerShellContext. - /// - public class DebugService - { - #region Fields - - private const string PsesGlobalVariableNamePrefix = "__psEditorServices_"; - private const string TemporaryScriptFileName = "Script Listing.ps1"; - - private ILogger logger; - private PowerShellContext powerShellContext; - private RemoteFileManager remoteFileManager; - - // TODO: This needs to be managed per nested session - private Dictionary> breakpointsPerFile = - new Dictionary>(); - - private int nextVariableId; - private string temporaryScriptListingPath; - private List variables; - private VariableContainerDetails globalScopeVariables; - private VariableContainerDetails scriptScopeVariables; - private StackFrameDetails[] stackFrameDetails; - private PropertyInfo invocationTypeScriptPositionProperty; - - private static int breakpointHitCounter = 0; - - private SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - #endregion - - #region Properties - - /// - /// Gets or sets a boolean that indicates whether a debugger client is - /// currently attached to the debugger. - /// - public bool IsClientAttached { get; set; } - - /// - /// Gets a boolean that indicates whether the debugger is currently - /// stopped at a breakpoint. - /// - public bool IsDebuggerStopped => this.powerShellContext.IsDebuggerStopped; - - /// - /// Gets the current DebuggerStoppedEventArgs when the debugger - /// is stopped. - /// - public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the DebugService class and uses - /// the given PowerShellContext for all future operations. - /// - /// - /// The PowerShellContext to use for all debugging operations. - /// - /// An ILogger implementation used for writing log messages. - public DebugService(PowerShellContext powerShellContext, ILogger logger) - : this(powerShellContext, null, logger) - { - } - - /// - /// Initializes a new instance of the DebugService class and uses - /// the given PowerShellContext for all future operations. - /// - /// - /// The PowerShellContext to use for all debugging operations. - /// - /// - /// A RemoteFileManager instance to use for accessing files in remote sessions. - /// - /// An ILogger implementation used for writing log messages. - public DebugService( - PowerShellContext powerShellContext, - RemoteFileManager remoteFileManager, - ILogger logger) - { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - - this.logger = logger; - this.powerShellContext = powerShellContext; - this.powerShellContext.DebuggerStop += this.OnDebuggerStopAsync; - this.powerShellContext.DebuggerResumed += this.OnDebuggerResumed; - - this.powerShellContext.BreakpointUpdated += this.OnBreakpointUpdated; - - this.remoteFileManager = remoteFileManager; - - this.invocationTypeScriptPositionProperty = - typeof(InvocationInfo) - .GetProperty( - "ScriptPosition", - BindingFlags.NonPublic | BindingFlags.Instance); - } - - #endregion - - #region Public Methods - - /// - /// Sets the list of line breakpoints for the current debugging session. - /// - /// The ScriptFile in which breakpoints will be set. - /// BreakpointDetails for each breakpoint that will be set. - /// If true, causes all existing breakpoints to be cleared before setting new ones. - /// An awaitable Task that will provide details about the breakpoints that were set. - public async Task SetLineBreakpointsAsync( - ScriptFile scriptFile, - BreakpointDetails[] breakpoints, - bool clearExisting = true) - { - var resultBreakpointDetails = new List(); - - var dscBreakpoints = - this.powerShellContext - .CurrentRunspace - .GetCapability(); - - // Make sure we're using the remote script path - string scriptPath = scriptFile.FilePath; - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null) - { - if (!this.remoteFileManager.IsUnderRemoteTempPath(scriptPath)) - { - this.logger.Write( - LogLevel.Verbose, - $"Could not set breakpoints for local path '{scriptPath}' in a remote session."); - - return resultBreakpointDetails.ToArray(); - } - - string mappedPath = - this.remoteFileManager.GetMappedPath( - scriptPath, - this.powerShellContext.CurrentRunspace); - - scriptPath = mappedPath; - } - else if ( - this.temporaryScriptListingPath != null && - this.temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase)) - { - this.logger.Write( - LogLevel.Verbose, - $"Could not set breakpoint on temporary script listing path '{scriptPath}'."); - - return resultBreakpointDetails.ToArray(); - } - - // Fix for issue #123 - file paths that contain wildcard chars [ and ] need to - // quoted and have those wildcard chars escaped. - string escapedScriptPath = - PowerShellContext.WildcardEscapePath(scriptPath); - - if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath)) - { - if (clearExisting) - { - await this.ClearBreakpointsInFileAsync(scriptFile); - } - - foreach (BreakpointDetails breakpoint in breakpoints) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); - psCommand.AddParameter("Script", escapedScriptPath); - psCommand.AddParameter("Line", breakpoint.LineNumber); - - // Check if the user has specified the column number for the breakpoint. - if (breakpoint.ColumnNumber.HasValue && breakpoint.ColumnNumber.Value > 0) - { - // It bums me out that PowerShell will silently ignore a breakpoint - // where either the line or the column is invalid. I'd rather have an - // error or warning message I could relay back to the client. - psCommand.AddParameter("Column", breakpoint.ColumnNumber.Value); - } - - // Check if this is a "conditional" line breakpoint. - if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || - !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) - { - ScriptBlock actionScriptBlock = - GetBreakpointActionScriptBlock(breakpoint); - - // If there was a problem with the condition string, - // move onto the next breakpoint. - if (actionScriptBlock == null) - { - resultBreakpointDetails.Add(breakpoint); - continue; - } - - psCommand.AddParameter("Action", actionScriptBlock); - } - - IEnumerable configuredBreakpoints = - await this.powerShellContext.ExecuteCommandAsync(psCommand); - - // The order in which the breakpoints are returned is significant to the - // VSCode client and should match the order in which they are passed in. - resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(BreakpointDetails.Create)); - } - } - else - { - resultBreakpointDetails = - await dscBreakpoints.SetLineBreakpointsAsync( - this.powerShellContext, - escapedScriptPath, - breakpoints); - } - - return resultBreakpointDetails.ToArray(); - } - - /// - /// Sets the list of command breakpoints for the current debugging session. - /// - /// CommandBreakpointDetails for each command breakpoint that will be set. - /// If true, causes all existing function breakpoints to be cleared before setting new ones. - /// An awaitable Task that will provide details about the breakpoints that were set. - public async Task SetCommandBreakpointsAsync( - CommandBreakpointDetails[] breakpoints, - bool clearExisting = true) - { - var resultBreakpointDetails = new List(); - - if (clearExisting) - { - await this.ClearCommandBreakpointsAsync(); - } - - if (breakpoints.Length > 0) - { - foreach (CommandBreakpointDetails breakpoint in breakpoints) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint"); - psCommand.AddParameter("Command", breakpoint.Name); - - // Check if this is a "conditional" command breakpoint. - if (!String.IsNullOrWhiteSpace(breakpoint.Condition) || - !String.IsNullOrWhiteSpace(breakpoint.HitCondition)) - { - ScriptBlock actionScriptBlock = GetBreakpointActionScriptBlock(breakpoint); - - // If there was a problem with the condition string, - // move onto the next breakpoint. - if (actionScriptBlock == null) - { - resultBreakpointDetails.Add(breakpoint); - continue; - } - - psCommand.AddParameter("Action", actionScriptBlock); - } - - IEnumerable configuredBreakpoints = - await this.powerShellContext.ExecuteCommandAsync(psCommand); - - // The order in which the breakpoints are returned is significant to the - // VSCode client and should match the order in which they are passed in. - resultBreakpointDetails.AddRange( - configuredBreakpoints.Select(CommandBreakpointDetails.Create)); - } - } - - return resultBreakpointDetails.ToArray(); - } - - /// - /// Sends a "continue" action to the debugger when stopped. - /// - public void Continue() - { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.Continue); - } - - /// - /// Sends a "step over" action to the debugger when stopped. - /// - public void StepOver() - { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.StepOver); - } - - /// - /// Sends a "step in" action to the debugger when stopped. - /// - public void StepIn() - { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.StepInto); - } - - /// - /// Sends a "step out" action to the debugger when stopped. - /// - public void StepOut() - { - this.powerShellContext.ResumeDebugger( - DebuggerResumeAction.StepOut); - } - - /// - /// Causes the debugger to break execution wherever it currently - /// is at the time. This is equivalent to clicking "Pause" in a - /// debugger UI. - /// - public void Break() - { - // Break execution in the debugger - this.powerShellContext.BreakExecution(); - } - - /// - /// Aborts execution of the debugger while it is running, even while - /// it is stopped. Equivalent to calling PowerShellContext.AbortExecution. - /// - public void Abort() - { - this.powerShellContext.AbortExecution(shouldAbortDebugSession: true); - } - - /// - /// Gets the list of variables that are children of the scope or variable - /// that is identified by the given referenced ID. - /// - /// - /// An array of VariableDetails instances which describe the requested variables. - public VariableDetailsBase[] GetVariables(int variableReferenceId) - { - VariableDetailsBase[] childVariables; - this.debugInfoHandle.Wait(); - try - { - if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) - { - logger.Write(LogLevel.Warning, $"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); - return new VariableDetailsBase[0]; - } - - VariableDetailsBase parentVariable = this.variables[variableReferenceId]; - if (parentVariable.IsExpandable) - { - childVariables = parentVariable.GetChildren(this.logger); - foreach (var child in childVariables) - { - // Only add child if it hasn't already been added. - if (child.Id < 0) - { - child.Id = this.nextVariableId++; - this.variables.Add(child); - } - } - } - else - { - childVariables = new VariableDetailsBase[0]; - } - - return childVariables; - } - finally - { - this.debugInfoHandle.Release(); - } - } - - /// - /// Evaluates a variable expression in the context of the stopped - /// debugger. This method decomposes the variable expression to - /// walk the cached variable data for the specified stack frame. - /// - /// The variable expression string to evaluate. - /// The ID of the stack frame in which the expression should be evaluated. - /// A VariableDetailsBase object containing the result. - public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId) - { - // NOTE: From a watch we will get passed expressions that are not naked variables references. - // Probably the right way to do this woudld be to examine the AST of the expr before calling - // this method to make sure it is a VariableReference. But for the most part, non-naked variable - // references are very unlikely to find a matching variable e.g. "$i+5.2" will find no var matching "$i+5". - - // Break up the variable path - string[] variablePathParts = variableExpression.Split('.'); - - VariableDetailsBase resolvedVariable = null; - IEnumerable variableList; - - // Ensure debug info isn't currently being built. - this.debugInfoHandle.Wait(); - try - { - variableList = this.variables; - } - finally - { - this.debugInfoHandle.Release(); - } - - foreach (var variableName in variablePathParts) - { - if (variableList == null) - { - // If there are no children left to search, break out early - return null; - } - - resolvedVariable = - variableList.FirstOrDefault( - v => - string.Equals( - v.Name, - variableName, - StringComparison.CurrentCultureIgnoreCase)); - - if (resolvedVariable != null && - resolvedVariable.IsExpandable) - { - // Continue by searching in this variable's children - variableList = this.GetVariables(resolvedVariable.Id); - } - } - - return resolvedVariable; - } - - /// - /// Sets the specified variable by container variableReferenceId and variable name to the - /// specified new value. If the variable cannot be set or converted to that value this - /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or - /// SessionStateUnauthorizedAccessException. - /// - /// The container (Autos, Local, Script, Global) that holds the variable. - /// The name of the variable prefixed with $. - /// The new string value. This value must not be null. If you want to set the variable to $null - /// pass in the string "$null". - /// The string representation of the value the variable was set to. - public async Task SetVariableAsync(int variableContainerReferenceId, string name, string value) - { - Validate.IsNotNull(nameof(name), name); - Validate.IsNotNull(nameof(value), value); - - this.logger.Write(LogLevel.Verbose, $"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); - - // An empty or whitespace only value is not a valid expression for SetVariable. - if (value.Trim().Length == 0) - { - throw new InvalidPowerShellExpressionException("Expected an expression."); - } - - // Evaluate the expression to get back a PowerShell object from the expression string. - PSCommand psCommand = new PSCommand(); - psCommand.AddScript(value); - var errorMessages = new StringBuilder(); - var results = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, - errorMessages, - false, - false); - - // Check if PowerShell's evaluation of the expression resulted in an error. - object psobject = results.FirstOrDefault(); - if ((psobject == null) && (errorMessages.Length > 0)) - { - throw new InvalidPowerShellExpressionException(errorMessages.ToString()); - } - - // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. - // Ideally we would have a separate means from communicating error records apart from normal output. - ErrorRecord errorRecord = psobject as ErrorRecord; - if (errorRecord != null) - { - throw new InvalidPowerShellExpressionException(errorRecord.ToString()); - } - - // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. - // Get the variable referenced by variableContainerReferenceId and variable name. - VariableContainerDetails variableContainer = null; - await this.debugInfoHandle.WaitAsync(); - try - { - variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; - } - finally - { - this.debugInfoHandle.Release(); - } - - VariableDetailsBase variable = variableContainer.Children[name]; - // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. - string scope = null; - if (variableContainerReferenceId == this.scriptScopeVariables.Id) - { - scope = "Script"; - } - else if (variableContainerReferenceId == this.globalScopeVariables.Id) - { - scope = "Global"; - } - else - { - // Determine which stackframe's local scope the variable is in. - StackFrameDetails[] stackFrames = await this.GetStackFramesAsync(); - for (int i = 0; i < stackFrames.Length; i++) - { - var stackFrame = stackFrames[i]; - if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) - { - scope = i.ToString(); - break; - } - } - } - - if (scope == null) - { - // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. - throw new Exception("Could not find the scope for this variable."); - } - - // Now that we have the scope, get the associated PSVariable object for the variable to be set. - psCommand.Commands.Clear(); - psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); - psCommand.AddParameter("Name", name.TrimStart('$')); - psCommand.AddParameter("Scope", scope); - - IEnumerable result = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false); - PSVariable psVariable = result.FirstOrDefault(); - if (psVariable == null) - { - throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); - } - - // We have the PSVariable object for the variable the user wants to set and an object to assign to that variable. - // The last step is to determine whether the PSVariable is "strongly typed" which may require a conversion. - // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. - // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - - // ArgumentTransformationAttribute. - var argTypeConverterAttr = - psVariable.Attributes - .OfType() - .FirstOrDefault(a => a.GetType().Name.Equals("ArgumentTypeConverterAttribute")); - - if (argTypeConverterAttr != null) - { - // PSVariable is strongly typed. Need to apply the conversion/transform to the new value. - psCommand.Commands.Clear(); - psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); - psCommand.AddParameter("Name", "ExecutionContext"); - psCommand.AddParameter("ValueOnly"); - - errorMessages.Clear(); - - var getExecContextResults = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, - errorMessages, - sendErrorToHost: false); - - EngineIntrinsics executionContext = getExecContextResults.OfType().FirstOrDefault(); - - var msg = $"Setting variable '{name}' using conversion to value: {psobject ?? ""}"; - this.logger.Write(LogLevel.Verbose, msg); - - psVariable.Value = argTypeConverterAttr.Transform(executionContext, psobject); - } - else - { - // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. - var msg = $"Setting variable '{name}' directly to value: {psobject ?? ""} - previous type was {psVariable.Value?.GetType().Name ?? ""}"; - this.logger.Write(LogLevel.Verbose, msg); - psVariable.Value = psobject; - } - - // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. - // This makes the returned string consistent with the strings normally displayed for variables in the debugger. - var tempVariable = new VariableDetails(psVariable); - this.logger.Write(LogLevel.Verbose, $"Set variable '{name}' to: {tempVariable.ValueString ?? ""}"); - return tempVariable.ValueString; - } - - /// - /// Evaluates an expression in the context of the stopped - /// debugger. This method will execute the specified expression - /// PowerShellContext. - /// - /// The expression string to execute. - /// The ID of the stack frame in which the expression should be executed. - /// - /// If true, writes the expression result as host output rather than returning the results. - /// In this case, the return value of this function will be null. - /// A VariableDetails object containing the result. - public async Task EvaluateExpressionAsync( - string expressionString, - int stackFrameId, - bool writeResultAsOutput) - { - var results = - await this.powerShellContext.ExecuteScriptStringAsync( - expressionString, - false, - writeResultAsOutput); - - // Since this method should only be getting invoked in the debugger, - // we can assume that Out-String will be getting used to format results - // of command executions into string output. However, if null is returned - // then return null so that no output gets displayed. - string outputString = - results != null && results.Any() ? - string.Join(Environment.NewLine, results) : - null; - - // If we've written the result as output, don't return a - // VariableDetails instance. - return - writeResultAsOutput ? - null : - new VariableDetails( - expressionString, - outputString); - } - - /// - /// Gets the list of stack frames at the point where the - /// debugger sf stopped. - /// - /// - /// An array of StackFrameDetails instances that contain the stack trace. - /// - public StackFrameDetails[] GetStackFrames() - { - this.debugInfoHandle.Wait(); - try - { - return this.stackFrameDetails; - } - finally - { - this.debugInfoHandle.Release(); - } - } - - internal StackFrameDetails[] GetStackFrames(CancellationToken cancellationToken) - { - this.debugInfoHandle.Wait(cancellationToken); - try - { - return this.stackFrameDetails; - } - finally - { - this.debugInfoHandle.Release(); - } - } - - internal async Task GetStackFramesAsync() - { - await this.debugInfoHandle.WaitAsync(); - try - { - return this.stackFrameDetails; - } - finally - { - this.debugInfoHandle.Release(); - } - } - - internal async Task GetStackFramesAsync(CancellationToken cancellationToken) - { - await this.debugInfoHandle.WaitAsync(cancellationToken); - try - { - return this.stackFrameDetails; - } - finally - { - this.debugInfoHandle.Release(); - } - } - - /// - /// Gets the list of variable scopes for the stack frame that - /// is identified by the given ID. - /// - /// The ID of the stack frame at which variable scopes should be retrieved. - /// The list of VariableScope instances which describe the available variable scopes. - public VariableScope[] GetVariableScopes(int stackFrameId) - { - var stackFrames = this.GetStackFrames(); - int localStackFrameVariableId = stackFrames[stackFrameId].LocalVariables.Id; - int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; - - return new VariableScope[] - { - new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName), - new VariableScope(localStackFrameVariableId, VariableContainerDetails.LocalScopeName), - new VariableScope(this.scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName), - new VariableScope(this.globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName), - }; - } - - /// - /// Clears all breakpoints in the current session. - /// - public async Task ClearAllBreakpointsAsync() - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - - await this.powerShellContext.ExecuteCommandAsync(psCommand); - } - - #endregion - - #region Private Methods - - private async Task ClearBreakpointsInFileAsync(ScriptFile scriptFile) - { - List breakpoints = null; - - // Get the list of breakpoints for this file - if (this.breakpointsPerFile.TryGetValue(scriptFile.Id, out breakpoints)) - { - if (breakpoints.Count > 0) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - psCommand.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray()); - - await this.powerShellContext.ExecuteCommandAsync(psCommand); - - // Clear the existing breakpoints list for the file - breakpoints.Clear(); - } - } - } - - private async Task ClearCommandBreakpointsAsync() - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint"); - psCommand.AddParameter("Type", "Command"); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint"); - - await this.powerShellContext.ExecuteCommandAsync(psCommand); - } - - private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) - { - await this.debugInfoHandle.WaitAsync(); - try - { - this.nextVariableId = VariableDetailsBase.FirstVariableId; - this.variables = new List(); - - // Create a dummy variable for index 0, should never see this. - this.variables.Add(new VariableDetails("Dummy", null)); - - // Must retrieve global/script variales before stack frame variables - // as we check stack frame variables against globals. - await FetchGlobalAndScriptVariablesAsync(); - await FetchStackFramesAsync(scriptNameOverride); - } - finally - { - this.debugInfoHandle.Release(); - } - } - - private async Task FetchGlobalAndScriptVariablesAsync() - { - // Retrieve globals first as script variable retrieval needs to search globals. - this.globalScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null); - - this.scriptScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null); - } - - private async Task FetchVariableContainerAsync( - string scope, - VariableContainerDetails autoVariables) - { - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand("Get-Variable"); - psCommand.AddParameter("Scope", scope); - - var scopeVariableContainer = - new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); - this.variables.Add(scopeVariableContainer); - - var results = await this.powerShellContext.ExecuteCommandAsync(psCommand, sendErrorToHost: false); - if (results != null) - { - foreach (PSObject psVariableObject in results) - { - var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ }; - this.variables.Add(variableDetails); - scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); - - if ((autoVariables != null) && AddToAutoVariables(psVariableObject, scope)) - { - autoVariables.Children.Add(variableDetails.Name, variableDetails); - } - } - } - - return scopeVariableContainer; - } - - private bool AddToAutoVariables(PSObject psvariable, string scope) - { - if ((scope == VariableContainerDetails.GlobalScopeName) || - (scope == VariableContainerDetails.ScriptScopeName)) - { - // We don't A) have a good way of distinguishing built-in from user created variables - // and B) globalScopeVariables.Children.ContainsKey() doesn't work for built-in variables - // stored in a child variable container within the globals variable container. - return false; - } - - string variableName = psvariable.Properties["Name"].Value as string; - object variableValue = psvariable.Properties["Value"].Value; - - // Don't put any variables created by PSES in the Auto variable container. - if (variableName.StartsWith(PsesGlobalVariableNamePrefix) || - variableName.Equals("PSDebugContext")) - { - return false; - } - - ScopedItemOptions variableScope = ScopedItemOptions.None; - PSPropertyInfo optionsProperty = psvariable.Properties["Options"]; - if (string.Equals(optionsProperty.TypeNameOfValue, "System.String")) - { - if (!Enum.TryParse( - optionsProperty.Value as string, - out variableScope)) - { - this.logger.Write( - LogLevel.Warning, - $"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); - } - } - else if (optionsProperty.Value is ScopedItemOptions) - { - variableScope = (ScopedItemOptions)optionsProperty.Value; - } - - // Some local variables, if they exist, should be displayed by default - if (psvariable.TypeNames[0].EndsWith("LocalVariable")) - { - if (variableName.Equals("_")) - { - return true; - } - else if (variableName.Equals("args", StringComparison.OrdinalIgnoreCase)) - { - var array = variableValue as Array; - return array != null ? array.Length > 0 : false; - } - - return false; - } - else if (!psvariable.TypeNames[0].EndsWith(nameof(PSVariable))) - { - return false; - } - - var constantAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.Constant; - var readonlyAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.ReadOnly; - - if (((variableScope & constantAllScope) == constantAllScope) || - ((variableScope & readonlyAllScope) == readonlyAllScope)) - { - string prefixedVariableName = VariableDetails.DollarPrefix + variableName; - if (this.globalScopeVariables.Children.ContainsKey(prefixedVariableName)) - { - return false; - } - } - - return true; - } - - private async Task FetchStackFramesAsync(string scriptNameOverride) - { - PSCommand psCommand = new PSCommand(); - - // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame - // objects (or "deserialized" CallStackFrames) when attached to a runspace in another - // process. Without the intermediate variable Get-PSCallStack inexplicably returns - // an array of strings containing the formatted output of the CallStackFrame list. - var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; - psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); - - var results = await this.powerShellContext.ExecuteCommandAsync(psCommand); - - var callStackFrames = results.ToArray(); - - this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; - - for (int i = 0; i < callStackFrames.Length; i++) - { - VariableContainerDetails autoVariables = - new VariableContainerDetails( - this.nextVariableId++, - VariableContainerDetails.AutoVariablesName); - - this.variables.Add(autoVariables); - - VariableContainerDetails localVariables = - await FetchVariableContainerAsync(i.ToString(), autoVariables); - - // When debugging, this is the best way I can find to get what is likely the workspace root. - // This is controlled by the "cwd:" setting in the launch config. - string workspaceRootPath = this.powerShellContext.InitialWorkingDirectory; - - this.stackFrameDetails[i] = - StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath); - - string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath; - if (scriptNameOverride != null && - string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) - { - this.stackFrameDetails[i].ScriptPath = scriptNameOverride; - } - else if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null && - !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) - { - this.stackFrameDetails[i].ScriptPath = - this.remoteFileManager.GetMappedPath( - stackFrameScriptPath, - this.powerShellContext.CurrentRunspace); - } - } - } - - /// - /// Inspects the condition, putting in the appropriate scriptblock template - /// "if (expression) { break }". If errors are found in the condition, the - /// breakpoint passed in is updated to set Verified to false and an error - /// message is put into the breakpoint.Message property. - /// - /// - /// - private ScriptBlock GetBreakpointActionScriptBlock( - BreakpointDetailsBase breakpoint) - { - try - { - ScriptBlock actionScriptBlock; - int? hitCount = null; - - // If HitCondition specified, parse and verify it. - if (!(String.IsNullOrWhiteSpace(breakpoint.HitCondition))) - { - int parsedHitCount; - - if (Int32.TryParse(breakpoint.HitCondition, out parsedHitCount)) - { - hitCount = parsedHitCount; - } - else - { - breakpoint.Verified = false; - breakpoint.Message = $"The specified HitCount '{breakpoint.HitCondition}' is not valid. " + - "The HitCount must be an integer number."; - return null; - } - } - - // Create an Action scriptblock based on condition and/or hit count passed in. - if (hitCount.HasValue && String.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // In the HitCount only case, this is simple as we can just use the HitCount - // property on the breakpoint object which is represented by $_. - string action = $"if ($_.HitCount -eq {hitCount}) {{ break }}"; - actionScriptBlock = ScriptBlock.Create(action); - } - else if (!String.IsNullOrWhiteSpace(breakpoint.Condition)) - { - // Must be either condition only OR condition and hit count. - actionScriptBlock = ScriptBlock.Create(breakpoint.Condition); - - // Check for simple, common errors that ScriptBlock parsing will not catch - // e.g. $i == 3 and $i > 3 - string message; - if (!ValidateBreakpointConditionAst(actionScriptBlock.Ast, out message)) - { - breakpoint.Verified = false; - breakpoint.Message = message; - return null; - } - - // Check for "advanced" condition syntax i.e. if the user has specified - // a "break" or "continue" statement anywhere in their scriptblock, - // pass their scriptblock through to the Action parameter as-is. - Ast breakOrContinueStatementAst = - actionScriptBlock.Ast.Find( - ast => (ast is BreakStatementAst || ast is ContinueStatementAst), true); - - // If this isn't advanced syntax then the conditions string should be a simple - // expression that needs to be wrapped in a "if" test that conditionally executes - // a break statement. - if (breakOrContinueStatementAst == null) - { - string wrappedCondition; - - if (hitCount.HasValue) - { - string globalHitCountVarName = - $"$global:{PsesGlobalVariableNamePrefix}BreakHitCounter_{breakpointHitCounter++}"; - - wrappedCondition = - $"if ({breakpoint.Condition}) {{ if (++{globalHitCountVarName} -eq {hitCount}) {{ break }} }}"; - } - else - { - wrappedCondition = $"if ({breakpoint.Condition}) {{ break }}"; - } - - actionScriptBlock = ScriptBlock.Create(wrappedCondition); - } - } - else - { - // Shouldn't get here unless someone called this with no condition and no hit count. - actionScriptBlock = ScriptBlock.Create("break"); - this.logger.Write(LogLevel.Warning, "No condition and no hit count specified by caller."); - } - - return actionScriptBlock; - } - catch (ParseException ex) - { - // Failed to create conditional breakpoint likely because the user provided an - // invalid PowerShell expression. Let the user know why. - breakpoint.Verified = false; - breakpoint.Message = ExtractAndScrubParseExceptionMessage(ex, breakpoint.Condition); - return null; - } - } - - private bool ValidateBreakpointConditionAst(Ast conditionAst, out string message) - { - message = string.Empty; - - // We are only inspecting a few simple scenarios in the EndBlock only. - ScriptBlockAst scriptBlockAst = conditionAst as ScriptBlockAst; - if ((scriptBlockAst != null) && - (scriptBlockAst.BeginBlock == null) && - (scriptBlockAst.ProcessBlock == null) && - (scriptBlockAst.EndBlock != null) && - (scriptBlockAst.EndBlock.Statements.Count == 1)) - { - StatementAst statementAst = scriptBlockAst.EndBlock.Statements[0]; - string condition = statementAst.Extent.Text; - - if (statementAst is AssignmentStatementAst) - { - message = FormatInvalidBreakpointConditionMessage(condition, "Use '-eq' instead of '=='."); - return false; - } - - PipelineAst pipelineAst = statementAst as PipelineAst; - if ((pipelineAst != null) && (pipelineAst.PipelineElements.Count == 1) && - (pipelineAst.PipelineElements[0].Redirections.Count > 0)) - { - message = FormatInvalidBreakpointConditionMessage(condition, "Use '-gt' instead of '>'."); - return false; - } - } - - return true; - } - - private string ExtractAndScrubParseExceptionMessage(ParseException parseException, string condition) - { - string[] messageLines = parseException.Message.Split('\n'); - - // Skip first line - it is a location indicator "At line:1 char: 4" - for (int i = 1; i < messageLines.Length; i++) - { - string line = messageLines[i]; - if (line.StartsWith("+")) - { - continue; - } - - if (!string.IsNullOrWhiteSpace(line)) - { - // Note '==' and '>" do not generate parse errors - if (line.Contains("'!='")) - { - line += " Use operator '-ne' instead of '!='."; - } - else if (line.Contains("'<'") && condition.Contains("<=")) - { - line += " Use operator '-le' instead of '<='."; - } - else if (line.Contains("'<'")) - { - line += " Use operator '-lt' instead of '<'."; - } - else if (condition.Contains(">=")) - { - line += " Use operator '-ge' instead of '>='."; - } - - return FormatInvalidBreakpointConditionMessage(condition, line); - } - } - - // If the message format isn't in a form we expect, just return the whole message. - return FormatInvalidBreakpointConditionMessage(condition, parseException.Message); - } - - private string FormatInvalidBreakpointConditionMessage(string condition, string message) - { - return $"'{condition}' is not a valid PowerShell expression. {message}"; - } - - private string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength) - { - string scriptLine = scriptLineObj.ToString(); - - if (!string.IsNullOrWhiteSpace(scriptLine)) - { - if (prefixLength == 0) - { - // The prefix is a padded integer ending with ':', an asterisk '*' - // if this is the current line, and one character of padding - prefixLength = scriptLine.IndexOf(':') + 2; - } - - return scriptLine.Substring(prefixLength); - } - - return null; - } - - #endregion - - #region Events - - /// - /// Raised when the debugger stops execution at a breakpoint or when paused. - /// - public event EventHandler DebuggerStopped; - - private async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) - { - bool noScriptName = false; - string localScriptPath = e.InvocationInfo.ScriptName; - - // If there's no ScriptName, get the "list" of the current source - if (this.remoteFileManager != null && string.IsNullOrEmpty(localScriptPath)) - { - // Get the current script listing and create the buffer - PSCommand command = new PSCommand(); - command.AddScript($"list 1 {int.MaxValue}"); - - IEnumerable scriptListingLines = - await this.powerShellContext.ExecuteCommandAsync( - command, false, false); - - if (scriptListingLines != null) - { - int linePrefixLength = 0; - - string scriptListing = - string.Join( - Environment.NewLine, - scriptListingLines - .Select(o => this.TrimScriptListingLine(o, ref linePrefixLength)) - .Where(s => s != null)); - - this.temporaryScriptListingPath = - this.remoteFileManager.CreateTemporaryFile( - $"[{this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", - scriptListing, - this.powerShellContext.CurrentRunspace); - - localScriptPath = - this.temporaryScriptListingPath - ?? StackFrameDetails.NoFileScriptPath; - - noScriptName = localScriptPath != null; - } - else - { - this.logger.Write( - LogLevel.Warning, - $"Could not load script context"); - } - } - - // Get call stack and variables. - await this.FetchStackFramesAndVariablesAsync( - noScriptName ? localScriptPath : null); - - // If this is a remote connection and the debugger stopped at a line - // in a script file, get the file contents - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null && - !noScriptName) - { - localScriptPath = - await this.remoteFileManager.FetchRemoteFileAsync( - e.InvocationInfo.ScriptName, - this.powerShellContext.CurrentRunspace); - } - - if (this.stackFrameDetails.Length > 0) - { - // Augment the top stack frame with details from the stop event - IScriptExtent scriptExtent = - this.invocationTypeScriptPositionProperty - .GetValue(e.InvocationInfo) as IScriptExtent; - - if (scriptExtent != null) - { - this.stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; - this.stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; - this.stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; - this.stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; - } - } - - this.CurrentDebuggerStoppedEventArgs = - new DebuggerStoppedEventArgs( - e, - this.powerShellContext.CurrentRunspace, - localScriptPath); - - // Notify the host that the debugger is stopped - this.DebuggerStopped?.Invoke( - sender, - this.CurrentDebuggerStoppedEventArgs); - } - - private void OnDebuggerResumed(object sender, DebuggerResumeAction e) - { - this.CurrentDebuggerStoppedEventArgs = null; - } - - /// - /// Raised when a breakpoint is added/removed/updated in the debugger. - /// - public event EventHandler BreakpointUpdated; - - private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) - { - // This event callback also gets called when a CommandBreakpoint is modified. - // Only execute the following code for LineBreakpoint so we can keep track - // of which line breakpoints exist per script file. We use this later when - // we need to clear all breakpoints in a script file. We do not need to do - // this for CommandBreakpoint, as those span all script files. - LineBreakpoint lineBreakpoint = e.Breakpoint as LineBreakpoint; - if (lineBreakpoint != null) - { - List breakpoints; - - string scriptPath = lineBreakpoint.Script; - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && - this.remoteFileManager != null) - { - string mappedPath = - this.remoteFileManager.GetMappedPath( - scriptPath, - this.powerShellContext.CurrentRunspace); - - if (mappedPath == null) - { - this.logger.Write( - LogLevel.Error, - $"Could not map remote path '{scriptPath}' to a local path."); - - return; - } - - scriptPath = mappedPath; - } - - // Normalize the script filename for proper indexing - string normalizedScriptName = scriptPath.ToLower(); - - // Get the list of breakpoints for this file - if (!this.breakpointsPerFile.TryGetValue(normalizedScriptName, out breakpoints)) - { - breakpoints = new List(); - this.breakpointsPerFile.Add( - normalizedScriptName, - breakpoints); - } - - // Add or remove the breakpoint based on the update type - if (e.UpdateType == BreakpointUpdateType.Set) - { - breakpoints.Add(e.Breakpoint); - } - else if (e.UpdateType == BreakpointUpdateType.Removed) - { - breakpoints.Remove(e.Breakpoint); - } - else - { - // TODO: Do I need to switch out instances for updated breakpoints? - } - } - - this.BreakpointUpdated?.Invoke(sender, e); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs b/src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs deleted file mode 100644 index f53ab8323..000000000 --- a/src/PowerShellEditorServices/Debugging/DebuggerStoppedEventArgs.cs +++ /dev/null @@ -1,117 +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 Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Utility; -using System.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices.Debugging -{ - /// - /// Provides event arguments for the DebugService.DebuggerStopped event. - /// - public class DebuggerStoppedEventArgs - { - #region Properties - - /// - /// Gets the path of the script where the debugger has stopped execution. - /// If 'IsRemoteSession' returns true, this path will be a local filesystem - /// path containing the contents of the script that is executing remotely. - /// - public string ScriptPath { get; private set; } - - /// - /// Returns true if the breakpoint was raised from a remote debugging session. - /// - public bool IsRemoteSession - { - get { return this.RunspaceDetails.Location == RunspaceLocation.Remote; } - } - - /// - /// Gets the original script path if 'IsRemoteSession' returns true. - /// - public string RemoteScriptPath { get; private set; } - - /// - /// Gets the RunspaceDetails for the current runspace. - /// - public RunspaceDetails RunspaceDetails { get; private set; } - - /// - /// Gets the line number at which the debugger stopped execution. - /// - public int LineNumber - { - get - { - return this.OriginalEvent.InvocationInfo.ScriptLineNumber; - } - } - - /// - /// Gets the column number at which the debugger stopped execution. - /// - public int ColumnNumber - { - get - { - return this.OriginalEvent.InvocationInfo.OffsetInLine; - } - } - - /// - /// Gets the original DebuggerStopEventArgs from the PowerShell engine. - /// - public DebuggerStopEventArgs OriginalEvent { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the DebuggerStoppedEventArgs class. - /// - /// The original DebuggerStopEventArgs instance from which this instance is based. - /// The RunspaceDetails of the runspace which raised this event. - public DebuggerStoppedEventArgs( - DebuggerStopEventArgs originalEvent, - RunspaceDetails runspaceDetails) - : this(originalEvent, runspaceDetails, null) - { - } - - /// - /// Creates a new instance of the DebuggerStoppedEventArgs class. - /// - /// The original DebuggerStopEventArgs instance from which this instance is based. - /// The RunspaceDetails of the runspace which raised this event. - /// The local path of the remote script being debugged. - public DebuggerStoppedEventArgs( - DebuggerStopEventArgs originalEvent, - RunspaceDetails runspaceDetails, - string localScriptPath) - { - Validate.IsNotNull(nameof(originalEvent), originalEvent); - Validate.IsNotNull(nameof(runspaceDetails), runspaceDetails); - - if (!string.IsNullOrEmpty(localScriptPath)) - { - this.ScriptPath = localScriptPath; - this.RemoteScriptPath = originalEvent.InvocationInfo.ScriptName; - } - else - { - this.ScriptPath = originalEvent.InvocationInfo.ScriptName; - } - - this.OriginalEvent = originalEvent; - this.RunspaceDetails = runspaceDetails; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs b/src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs deleted file mode 100644 index fd6b13caf..000000000 --- a/src/PowerShellEditorServices/Debugging/InvalidPowerShellExpressionException.cs +++ /dev/null @@ -1,24 +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; - -namespace Microsoft.PowerShell.EditorServices.Debugging -{ - /// - /// Represents the exception that is thrown when an invalid expression is provided to the DebugService's SetVariable method. - /// - public class InvalidPowerShellExpressionException : Exception - { - /// - /// Initializes a new instance of the SetVariableExpressionException class. - /// - /// Message indicating why the expression is invalid. - public InvalidPowerShellExpressionException(string message) - : base(message) - { - } - } -} diff --git a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs deleted file mode 100644 index e217457fc..000000000 --- a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs +++ /dev/null @@ -1,135 +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.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Contains details pertaining to a single stack frame in - /// the current debugging session. - /// - public class StackFrameDetails - { - #region Fields - - /// - /// A constant string used in the ScriptPath field to represent a - /// stack frame with no associated script file. - /// - public const string NoFileScriptPath = ""; - - #endregion - - #region Properties - - /// - /// Gets the path to the script where the stack frame occurred. - /// - public string ScriptPath { get; internal set; } - - /// - /// Gets the name of the function where the stack frame occurred. - /// - public string FunctionName { get; private set; } - - /// - /// Gets the start line number of the script where the stack frame occurred. - /// - public int StartLineNumber { get; internal set; } - - /// - /// Gets the line number of the script where the stack frame occurred. - /// - public int? EndLineNumber { get; internal set; } - - /// - /// Gets the start column number of the line where the stack frame occurred. - /// - public int StartColumnNumber { get; internal set; } - - /// - /// Gets the end column number of the line where the stack frame occurred. - /// - public int? EndColumnNumber { get; internal set; } - - /// - /// Gets a boolean value indicating whether or not the stack frame is executing - /// in script external to the current workspace root. - /// - public bool IsExternalCode { get; internal set; } - - /// - /// Gets or sets the VariableContainerDetails that contains the auto variables. - /// - public VariableContainerDetails AutoVariables { get; private set; } - - /// - /// Gets or sets the VariableContainerDetails that contains the local variables. - /// - public VariableContainerDetails LocalVariables { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the StackFrameDetails class from a - /// CallStackFrame instance provided by the PowerShell engine. - /// - /// - /// A PSObject representing the CallStackFrame instance from which details will be obtained. - /// - /// - /// A variable container with all the filtered, auto variables for this stack frame. - /// - /// - /// A variable container with all the local variables for this stack frame. - /// - /// - /// Specifies the path to the root of an open workspace, if one is open. This path is used to - /// determine whether individua stack frames are external to the workspace. - /// - /// A new instance of the StackFrameDetails class. - static internal StackFrameDetails Create( - PSObject callStackFrameObject, - VariableContainerDetails autoVariables, - VariableContainerDetails localVariables, - string workspaceRootPath = null) - { - string moduleId = string.Empty; - var isExternal = false; - - var invocationInfo = callStackFrameObject.Properties["InvocationInfo"]?.Value as InvocationInfo; - string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath; - int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0); - - // TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add - // settings to control this feature. - //if (workspaceRootPath != null && - // invocationInfo != null && - // !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase)) - //{ - // isExternal = true; - //} - - return new StackFrameDetails - { - ScriptPath = scriptPath, - FunctionName = callStackFrameObject.Properties["FunctionName"].Value as string, - StartLineNumber = startLineNumber, - EndLineNumber = startLineNumber, // End line number isn't given in PowerShell stack frames - StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames - EndColumnNumber = 0, - AutoVariables = autoVariables, - LocalVariables = localVariables, - IsExternalCode = isExternal - }; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs b/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs deleted file mode 100644 index 2c90d8891..000000000 --- a/src/PowerShellEditorServices/Debugging/VariableContainerDetails.cs +++ /dev/null @@ -1,99 +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.Collections.Generic; -using System.Diagnostics; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Container for variables that is not itself a variable per se. However given how - /// VSCode uses an integer variable reference id for every node under the "Variables" tool - /// window, it is useful to treat containers, typically scope containers, as a variable. - /// Note that these containers are not necessarily always a scope container. Consider a - /// container such as "Auto" or "My". These aren't scope related but serve as just another - /// way to organize variables into a useful UI structure. - /// - [DebuggerDisplay("Name = {Name}, Id = {Id}, Count = {Children.Count}")] - public class VariableContainerDetails : VariableDetailsBase - { - /// - /// Provides a constant for the name of the Global scope. - /// - public const string AutoVariablesName = "Auto"; - - /// - /// Provides a constant for the name of the Global scope. - /// - public const string GlobalScopeName = "Global"; - - /// - /// Provides a constant for the name of the Local scope. - /// - public const string LocalScopeName = "Local"; - - /// - /// Provides a constant for the name of the Script scope. - /// - public const string ScriptScopeName = "Script"; - - private readonly Dictionary children; - - /// - /// Instantiates an instance of VariableScopeDetails. - /// - /// The variable reference id for this scope. - /// The name of the variable scope. - public VariableContainerDetails(int id, string name) - { - Validate.IsNotNull(name, "name"); - - this.Id = id; - this.Name = name; - this.IsExpandable = true; - this.ValueString = " "; // An empty string isn't enough due to a temporary bug in VS Code. - - this.children = new Dictionary(); - } - - /// - /// Gets the collection of child variables. - /// - public IDictionary Children - { - get { return this.children; } - } - - /// - /// Returns the details of the variable container's children. If empty, returns an empty array. - /// - /// - public override VariableDetailsBase[] GetChildren(ILogger logger) - { - var variablesArray = new VariableDetailsBase[this.children.Count]; - this.children.Values.CopyTo(variablesArray, 0); - return variablesArray; - } - - /// - /// Determines whether this variable container contains the specified variable by its referenceId. - /// - /// The variableReferenceId to search for. - /// Returns true if this variable container directly contains the specified variableReferenceId, false otherwise. - public bool ContainsVariable(int variableReferenceId) - { - foreach (VariableDetailsBase value in this.children.Values) - { - if (value.Id == variableReferenceId) - { - return true; - } - } - - return false; - } - } -} diff --git a/src/PowerShellEditorServices/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Debugging/VariableDetails.cs deleted file mode 100644 index 25ee71057..000000000 --- a/src/PowerShellEditorServices/Debugging/VariableDetails.cs +++ /dev/null @@ -1,415 +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.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Management.Automation; -using System.Reflection; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Contains details pertaining to a variable in the current - /// debugging session. - /// - [DebuggerDisplay("Name = {Name}, Id = {Id}, Value = {ValueString}")] - public class VariableDetails : VariableDetailsBase - { - #region Fields - - /// - /// Provides a constant for the dollar sign variable prefix string. - /// - public const string DollarPrefix = "$"; - - private object valueObject; - private VariableDetails[] cachedChildren; - - #endregion - - #region Constructors - - /// - /// Initializes an instance of the VariableDetails class from - /// the details contained in a PSVariable instance. - /// - /// - /// The PSVariable instance from which variable details will be obtained. - /// - public VariableDetails(PSVariable psVariable) - : this(DollarPrefix + psVariable.Name, psVariable.Value) - { - } - - /// - /// Initializes an instance of the VariableDetails class from - /// the name and value pair stored inside of a PSObject which - /// represents a PSVariable. - /// - /// - /// The PSObject which represents a PSVariable. - /// - public VariableDetails(PSObject psVariableObject) - : this( - DollarPrefix + psVariableObject.Properties["Name"].Value as string, - psVariableObject.Properties["Value"].Value) - { - } - - /// - /// Initializes an instance of the VariableDetails class from - /// the details contained in a PSPropertyInfo instance. - /// - /// - /// The PSPropertyInfo instance from which variable details will be obtained. - /// - public VariableDetails(PSPropertyInfo psProperty) - : this(psProperty.Name, psProperty.Value) - { - } - - /// - /// Initializes an instance of the VariableDetails class from - /// a given name/value pair. - /// - /// The variable's name. - /// The variable's value. - public VariableDetails(string name, object value) - { - this.valueObject = value; - - this.Id = -1; // Not been assigned a variable reference id yet - this.Name = name; - this.IsExpandable = GetIsExpandable(value); - - string typeName; - this.ValueString = GetValueStringAndType(value, this.IsExpandable, out typeName); - this.Type = typeName; - } - - #endregion - - #region Public Methods - - /// - /// If this variable instance is expandable, this method returns the - /// details of its children. Otherwise it returns an empty array. - /// - /// - public override VariableDetailsBase[] GetChildren(ILogger logger) - { - VariableDetails[] childVariables = null; - - if (this.IsExpandable) - { - if (this.cachedChildren == null) - { - this.cachedChildren = GetChildren(this.valueObject, logger); - } - - return this.cachedChildren; - } - else - { - childVariables = new VariableDetails[0]; - } - - return childVariables; - } - - #endregion - - #region Private Methods - - private static bool GetIsExpandable(object valueObject) - { - if (valueObject == null) - { - return false; - } - - // If a PSObject, unwrap it - var psobject = valueObject as PSObject; - if (psobject != null) - { - valueObject = psobject.BaseObject; - } - - Type valueType = - valueObject != null ? - valueObject.GetType() : - null; - - TypeInfo valueTypeInfo = valueType.GetTypeInfo(); - - return - valueObject != null && - !valueTypeInfo.IsPrimitive && - !valueTypeInfo.IsEnum && // Enums don't have any properties - !(valueObject is string) && // Strings get treated as IEnumerables - !(valueObject is decimal) && - !(valueObject is UnableToRetrievePropertyMessage); - } - - private static string GetValueStringAndType(object value, bool isExpandable, out string typeName) - { - string valueString = null; - typeName = null; - - if (value == null) - { - // Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural. - return "$null"; - } - - Type objType = value.GetType(); - typeName = $"[{objType.FullName}]"; - - if (value is bool) - { - // Set to identifier recognized by PowerShell to make setVariable from the debug UI more natural. - valueString = (bool) value ? "$true" : "$false"; - } - else if (isExpandable) - { - - // Get the "value" for an expandable object. - if (value is DictionaryEntry) - { - // For DictionaryEntry - display the key/value as the value. - var entry = (DictionaryEntry)value; - valueString = - string.Format( - "[{0}, {1}]", - entry.Key, - GetValueStringAndType(entry.Value, GetIsExpandable(entry.Value), out typeName)); - } - else - { - string valueToString = value.SafeToString(); - if (valueToString.Equals(objType.ToString())) - { - // If the ToString() matches the type name, then display the type - // name in PowerShell format. - string shortTypeName = objType.Name; - - // For arrays and ICollection, display the number of contained items. - if (value is Array) - { - var arr = value as Array; - if (arr.Rank == 1) - { - shortTypeName = InsertDimensionSize(shortTypeName, arr.Length); - } - } - else if (value is ICollection) - { - var collection = (ICollection)value; - shortTypeName = InsertDimensionSize(shortTypeName, collection.Count); - } - - valueString = $"[{shortTypeName}]"; - } - else - { - valueString = valueToString; - } - } - } - else - { - // Value is a scalar (not expandable). If it's a string, display it directly otherwise use SafeToString() - if (value is string) - { - valueString = "\"" + value + "\""; - } - else - { - valueString = value.SafeToString(); - } - } - - return valueString; - } - - private static string InsertDimensionSize(string value, int dimensionSize) - { - string result = value; - - int indexLastRBracket = value.LastIndexOf("]"); - if (indexLastRBracket > 0) - { - result = - value.Substring(0, indexLastRBracket) + - dimensionSize + - value.Substring(indexLastRBracket); - } - else - { - // Types like ArrayList don't use [] in type name so - // display value like so - [ArrayList: 5] - result = value + ": " + dimensionSize; - } - - return result; - } - - private VariableDetails[] GetChildren(object obj, ILogger logger) - { - List childVariables = new List(); - - if (obj == null) - { - return childVariables.ToArray(); - } - - try - { - PSObject psObject = obj as PSObject; - - if ((psObject != null) && - (psObject.TypeNames[0] == typeof(PSCustomObject).ToString())) - { - // PowerShell PSCustomObject's properties are completely defined by the ETS type system. - childVariables.AddRange( - psObject - .Properties - .Select(p => new VariableDetails(p))); - } - else - { - // If a PSObject other than a PSCustomObject, unwrap it. - if (psObject != null) - { - // First add the PSObject's ETS propeties - childVariables.AddRange( - psObject - .Properties - .Where(p => p.MemberType == PSMemberTypes.NoteProperty) - .Select(p => new VariableDetails(p))); - - obj = psObject.BaseObject; - } - - IDictionary dictionary = obj as IDictionary; - IEnumerable enumerable = obj as IEnumerable; - - // We're in the realm of regular, unwrapped .NET objects - if (dictionary != null) - { - // Buckle up kids, this is a bit weird. We could not use the LINQ - // operator OfType. Even though R# will squiggle the - // "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT! - // The reason is that LINQ extension methods work with objects of type - // IEnumerable. Objects of type Dictionary<,>, respond to iteration via - // IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic - // dictionaries like HashTable return DictionaryEntry objects. - // It turns out that iteration via C#'s foreach loop, operates on the variable's - // type which in this case is IDictionary. IDictionary was designed to always - // return DictionaryEntry objects upon iteration and the Dictionary<,> implementation - // honors that when the object is reintepreted as an IDictionary object. - // FYI, a test case for this is to open $PSBoundParameters when debugging a - // function that defines parameters and has been passed parameters. - // If you open the $PSBoundParameters variable node in this scenario and see nothing, - // this code is broken. - int i = 0; - foreach (DictionaryEntry entry in dictionary) - { - childVariables.Add( - new VariableDetails( - "[" + i++ + "]", - entry)); - } - } - else if (enumerable != null && !(obj is string)) - { - int i = 0; - foreach (var item in enumerable) - { - childVariables.Add( - new VariableDetails( - "[" + i++ + "]", - item)); - } - } - - AddDotNetProperties(obj, childVariables); - } - } - catch (GetValueInvocationException ex) - { - // This exception occurs when accessing the value of a - // variable causes a script to be executed. Right now - // we aren't loading children on the pipeline thread so - // this causes an exception to be raised. In this case, - // just return an empty list of children. - logger.Write(LogLevel.Warning, $"Failed to get properties of variable {this.Name}, value invocation was attempted: {ex.Message}"); - } - - return childVariables.ToArray(); - } - - private static void AddDotNetProperties(object obj, List childVariables) - { - Type objectType = obj.GetType(); - var properties = - objectType.GetProperties( - BindingFlags.Public | BindingFlags.Instance); - - foreach (var property in properties) - { - // Don't display indexer properties, it causes an exception anyway. - if (property.GetIndexParameters().Length > 0) - { - continue; - } - - try - { - childVariables.Add( - new VariableDetails( - property.Name, - property.GetValue(obj))); - } - catch (Exception ex) - { - // Some properties can throw exceptions, add the property - // name and info about the error. - if (ex.GetType() == typeof (TargetInvocationException)) - { - ex = ex.InnerException; - } - - childVariables.Add( - new VariableDetails( - property.Name, - new UnableToRetrievePropertyMessage( - "Error retrieving property - " + ex.GetType().Name))); - } - } - } - - #endregion - - private struct UnableToRetrievePropertyMessage - { - public UnableToRetrievePropertyMessage(string message) - { - this.Message = message; - } - - public string Message { get; } - - public override string ToString() - { - return "<" + Message + ">"; - } - } - } -} diff --git a/src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs b/src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs deleted file mode 100644 index aada558fa..000000000 --- a/src/PowerShellEditorServices/Debugging/VariableDetailsBase.cs +++ /dev/null @@ -1,59 +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 Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Defines the common details between a variable and a variable container such as a scope - /// in the current debugging session. - /// - public abstract class VariableDetailsBase - { - /// - /// Provides a constant that is used as the starting variable ID for all. - /// Avoid 0 as it indicates a variable node with no children. - /// variables. - /// - public const int FirstVariableId = 1; - - /// - /// Gets the numeric ID of the variable which can be used to refer - /// to it in future requests. - /// - public int Id { get; set; } - - /// - /// Gets the variable's name. - /// - public string Name { get; protected set; } - - /// - /// Gets the string representation of the variable's value. - /// If the variable is an expandable object, this string - /// will be empty. - /// - public string ValueString { get; protected set; } - - /// - /// Gets the type of the variable's value. - /// - public string Type { get; protected set; } - - /// - /// Returns true if the variable's value is expandable, meaning - /// that it has child properties or its contents can be enumerated. - /// - public bool IsExpandable { get; protected set; } - - /// - /// If this variable instance is expandable, this method returns the - /// details of its children. Otherwise it returns an empty array. - /// - /// - public abstract VariableDetailsBase[] GetChildren(ILogger logger); - } -} diff --git a/src/PowerShellEditorServices/Debugging/VariableScope.cs b/src/PowerShellEditorServices/Debugging/VariableScope.cs deleted file mode 100644 index 53c98058b..000000000 --- a/src/PowerShellEditorServices/Debugging/VariableScope.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 -{ - /// - /// Contains details pertaining to a variable scope in the current - /// debugging session. - /// - public class VariableScope - { - /// - /// Gets a numeric ID that can be used in future operations - /// relating to this scope. - /// - public int Id { get; private set; } - - /// - /// Gets a name that describes the variable scope. - /// - public string Name { get; private set; } - - /// - /// Initializes a new instance of the VariableScope class with - /// the given ID and name. - /// - /// The variable scope's ID. - /// The variable scope's name. - public VariableScope(int id, string name) - { - this.Id = id; - this.Name = name; - } - } -} diff --git a/src/PowerShellEditorServices/Extensions/EditorCommand.cs b/src/PowerShellEditorServices/Extensions/EditorCommand.cs deleted file mode 100644 index 8a7b80cef..000000000 --- a/src/PowerShellEditorServices/Extensions/EditorCommand.cs +++ /dev/null @@ -1,89 +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.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides details about a command that has been registered - /// with the editor. - /// - public class EditorCommand - { - #region Properties - - /// - /// Gets the name which uniquely identifies the command. - /// - public string Name { get; private set; } - - /// - /// Gets the display name for the command. - /// - public string DisplayName { get; private set; } - - /// - /// Gets the boolean which determines whether this command's - /// output should be suppressed. - /// - public bool SuppressOutput { get; private set; } - - /// - /// Gets the ScriptBlock which can be used to execute the command. - /// - public ScriptBlock ScriptBlock { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new EditorCommand instance that invokes a cmdlet or - /// function by name. - /// - /// The unique identifier name for the command. - /// The display name for the command. - /// If true, causes output to be suppressed for this command. - /// The name of the cmdlet or function which will be invoked by this command. - public EditorCommand( - string commandName, - string displayName, - bool suppressOutput, - string cmdletName) - : this( - commandName, - displayName, - suppressOutput, - ScriptBlock.Create( - string.Format( - "param($context) {0} $context", - cmdletName))) - { - } - - /// - /// Creates a new EditorCommand instance that invokes a ScriptBlock. - /// - /// The unique identifier name for the command. - /// The display name for the command. - /// If true, causes output to be suppressed for this command. - /// The ScriptBlock which will be invoked by this command. - public EditorCommand( - string commandName, - string displayName, - bool suppressOutput, - ScriptBlock scriptBlock) - { - this.Name = commandName; - this.DisplayName = displayName; - this.SuppressOutput = suppressOutput; - this.ScriptBlock = scriptBlock; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Extensions/EditorCommandAttribute.cs b/src/PowerShellEditorServices/Extensions/EditorCommandAttribute.cs deleted file mode 100644 index 8020cb844..000000000 --- a/src/PowerShellEditorServices/Extensions/EditorCommandAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides an attribute that can be used to target PowerShell - /// commands for import as editor commands. - /// - [AttributeUsage(AttributeTargets.Class)] - public class EditorCommandAttribute : Attribute - { - - #region Properties - - /// - /// Gets or sets the name which uniquely identifies the command. - /// - public string Name { get; set; } - - /// - /// Gets or sets the display name for the command. - /// - public string DisplayName { get; set; } - - /// - /// Gets or sets a value indicating whether this command's output - /// should be suppressed. - /// - public bool SuppressOutput { get; set; } - - #endregion - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Extensions/EditorContext.cs b/src/PowerShellEditorServices/Extensions/EditorContext.cs deleted file mode 100644 index c669acd19..000000000 --- a/src/PowerShellEditorServices/Extensions/EditorContext.cs +++ /dev/null @@ -1,117 +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.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides context for the host editor at the time of creation. - /// - public class EditorContext - { - #region Private Fields - - private IEditorOperations editorOperations; - - #endregion - - #region Properties - - /// - /// Gets the FileContext for the active file. - /// - public FileContext CurrentFile { get; private set; } - - /// - /// Gets the BufferRange representing the current selection in the file. - /// - public BufferRange SelectedRange { get; private set; } - - /// - /// Gets the FilePosition representing the current cursor position. - /// - public FilePosition CursorPosition { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the EditorContext class. - /// - /// An IEditorOperations implementation which performs operations in the editor. - /// The ScriptFile that is in the active editor buffer. - /// The position of the user's cursor in the active editor buffer. - /// The range of the user's selection in the active editor buffer. - /// Determines the language of the file.false If it is not specified, then it defaults to "Unknown" - public EditorContext( - IEditorOperations editorOperations, - ScriptFile currentFile, - BufferPosition cursorPosition, - BufferRange selectedRange, - string language = "Unknown") - { - this.editorOperations = editorOperations; - this.CurrentFile = new FileContext(currentFile, this, editorOperations, language); - this.SelectedRange = selectedRange; - this.CursorPosition = new FilePosition(currentFile, cursorPosition); - } - - #endregion - - #region Public Methods - - /// - /// Sets a selection in the host editor's active buffer. - /// - /// The 1-based starting line of the selection. - /// The 1-based starting column of the selection. - /// The 1-based ending line of the selection. - /// The 1-based ending column of the selection. - public void SetSelection( - int startLine, - int startColumn, - int endLine, - int endColumn) - { - this.SetSelection( - new BufferRange( - startLine, startColumn, - endLine, endColumn)); - } - - /// - /// Sets a selection in the host editor's active buffer. - /// - /// The starting position of the selection. - /// The ending position of the selection. - public void SetSelection( - BufferPosition startPosition, - BufferPosition endPosition) - { - this.SetSelection( - new BufferRange( - startPosition, - endPosition)); - } - - /// - /// Sets a selection in the host editor's active buffer. - /// - /// The range of the selection. - public void SetSelection(BufferRange selectionRange) - { - this.editorOperations - .SetSelectionAsync(selectionRange) - .Wait(); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Extensions/EditorObject.cs b/src/PowerShellEditorServices/Extensions/EditorObject.cs deleted file mode 100644 index 0013da684..000000000 --- a/src/PowerShellEditorServices/Extensions/EditorObject.cs +++ /dev/null @@ -1,112 +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 Microsoft.PowerShell.EditorServices.Components; -using System; -using System.Management.Automation; -using System.Reflection; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides the entry point of the extensibility API, inserted into - /// the PowerShell session as the "$psEditor" variable. - /// - public class EditorObject - { - #region Private Fields - - private ExtensionService extensionService; - private IEditorOperations editorOperations; - - #endregion - - #region Properties - - /// - /// Gets the version of PowerShell Editor Services. - /// - public Version EditorServicesVersion - { - get { return this.GetType().GetTypeInfo().Assembly.GetName().Version; } - } - - /// - /// Gets the workspace interface for the editor API. - /// - public EditorWorkspace Workspace { get; private set; } - - /// - /// Gets the window interface for the editor API. - /// - public EditorWindow Window { get; private set; } - - /// - /// Gets the component registry for the session. - /// - /// - public IComponentRegistry Components { get; private set; } - - #endregion - - /// - /// Creates a new instance of the EditorObject class. - /// - /// An ExtensionService which handles command registration. - /// An IEditorOperations implementation which handles operations in the host editor. - /// An IComponentRegistry instance which provides components in the session. - public EditorObject( - ExtensionService extensionService, - IEditorOperations editorOperations, - IComponentRegistry componentRegistry) - { - this.extensionService = extensionService; - this.editorOperations = editorOperations; - this.Components = componentRegistry; - - // Create API area objects - this.Workspace = new EditorWorkspace(this.editorOperations); - this.Window = new EditorWindow(this.editorOperations); - } - - /// - /// Registers a new command in the editor. - /// - /// The EditorCommand to be registered. - /// True if the command is newly registered, false if the command already exists. - public bool RegisterCommand(EditorCommand editorCommand) - { - return this.extensionService.RegisterCommand(editorCommand); - } - - /// - /// Unregisters an existing EditorCommand based on its registered name. - /// - /// The name of the command to be unregistered. - public void UnregisterCommand(string commandName) - { - this.extensionService.UnregisterCommand(commandName); - } - - /// - /// Returns all registered EditorCommands. - /// - /// An Array of all registered EditorCommands. - public EditorCommand[] GetCommands() - { - return this.extensionService.GetCommands(); - } - /// - /// Gets the EditorContext which contains the state of the editor - /// at the time this method is invoked. - /// - /// A instance of the EditorContext class. - public EditorContext GetEditorContext() - { - return this.editorOperations.GetEditorContextAsync().Result; - } - } -} - diff --git a/src/PowerShellEditorServices/Extensions/EditorWindow.cs b/src/PowerShellEditorServices/Extensions/EditorWindow.cs deleted file mode 100644 index 8d241559a..000000000 --- a/src/PowerShellEditorServices/Extensions/EditorWindow.cs +++ /dev/null @@ -1,83 +0,0 @@ -// -// 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.Extensions -{ - /// - /// Provides a PowerShell-facing API which allows scripts to - /// interact with the editor's window. - /// - public class EditorWindow - { - #region Private Fields - - private IEditorOperations editorOperations; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the EditorWindow class. - /// - /// An IEditorOperations implementation which handles operations in the host editor. - internal EditorWindow(IEditorOperations editorOperations) - { - this.editorOperations = editorOperations; - } - - #endregion - - #region Public Methods - - /// - /// Shows an informational message to the user. - /// - /// The message to be shown. - public void ShowInformationMessage(string message) - { - this.editorOperations.ShowInformationMessageAsync(message).Wait(); - } - - /// - /// Shows an error message to the user. - /// - /// The message to be shown. - public void ShowErrorMessage(string message) - { - this.editorOperations.ShowErrorMessageAsync(message).Wait(); - } - - /// - /// Shows a warning message to the user. - /// - /// The message to be shown. - public void ShowWarningMessage(string message) - { - this.editorOperations.ShowWarningMessageAsync(message).Wait(); - } - - /// - /// Sets the status bar message in the editor UI (if applicable). - /// - /// The message to be shown. - public void SetStatusBarMessage(string message) - { - this.editorOperations.SetStatusBarMessageAsync(message, null).Wait(); - } - - /// - /// Sets the status bar message in the editor UI (if applicable). - /// - /// The message to be shown. - /// A timeout in milliseconds for how long the message should remain visible. - public void SetStatusBarMessage(string message, int timeout) - { - this.editorOperations.SetStatusBarMessageAsync(message, timeout).Wait(); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs deleted file mode 100644 index ad679f371..000000000 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ /dev/null @@ -1,76 +0,0 @@ -// -// 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.Extensions -{ - /// - /// Provides a PowerShell-facing API which allows scripts to - /// interact with the editor's workspace. - /// - public class EditorWorkspace - { - #region Private Fields - - private IEditorOperations editorOperations; - - #endregion - - #region Properties - - /// - /// Gets the current workspace path if there is one or null otherwise. - /// - public string Path - { - get { return this.editorOperations.GetWorkspacePath(); } - } - - #endregion - - #region Constructors - - internal EditorWorkspace(IEditorOperations editorOperations) - { - this.editorOperations = editorOperations; - } - - #endregion - - #region Public Methods - - /// - /// Creates a new file in the editor - /// - public void NewFile() - { - this.editorOperations.NewFileAsync().Wait(); - } - - /// - /// Opens a file in the workspace. If the file is already open - /// its buffer will be made active. - /// - /// The path to the file to be opened. - public void OpenFile(string filePath) - { - this.editorOperations.OpenFileAsync(filePath).Wait(); - } - - /// - /// Opens a file in the workspace. If the file is already open - /// its buffer will be made active. - /// You can specify whether the file opens as a preview or as a durable editor. - /// - /// The path to the file to be opened. - /// Determines wether the file is opened as a preview or as a durable editor. - public void OpenFile(string filePath, bool preview) - { - this.editorOperations.OpenFileAsync(filePath, preview).Wait(); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Extensions/ExtensionService.cs b/src/PowerShellEditorServices/Extensions/ExtensionService.cs deleted file mode 100644 index cc4e01af4..000000000 --- a/src/PowerShellEditorServices/Extensions/ExtensionService.cs +++ /dev/null @@ -1,220 +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 Microsoft.PowerShell.EditorServices.Components; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; -using System.Management.Automation; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides a high-level service which enables PowerShell scripts - /// and modules to extend the behavior of the host editor. - /// - public class ExtensionService - { - #region Fields - - private Dictionary editorCommands = - new Dictionary(); - - #endregion - - #region Properties - - /// - /// Gets the IEditorOperations implementation used to invoke operations - /// in the host editor. - /// - public IEditorOperations EditorOperations { get; private set; } - - /// - /// Gets the EditorObject which exists in the PowerShell session as the - /// '$psEditor' variable. - /// - public EditorObject EditorObject { get; private set; } - - /// - /// Gets the PowerShellContext in which extension code will be executed. - /// - public PowerShellContext PowerShellContext { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ExtensionService which uses the provided - /// PowerShellContext for loading and executing extension code. - /// - /// A PowerShellContext used to execute extension code. - public ExtensionService(PowerShellContext powerShellContext) - { - this.PowerShellContext = powerShellContext; - } - - #endregion - - #region Public Methods - - /// - /// Initializes this ExtensionService using the provided IEditorOperations - /// implementation for future interaction with the host editor. - /// - /// An IEditorOperations implementation. - /// An IComponentRegistry instance which provides components in the session. - /// A Task that can be awaited for completion. - public async Task InitializeAsync( - IEditorOperations editorOperations, - IComponentRegistry componentRegistry) - { - this.EditorObject = - new EditorObject( - this, - editorOperations, - componentRegistry); - - // Register the editor object in the runspace - PSCommand variableCommand = new PSCommand(); - using (RunspaceHandle handle = await this.PowerShellContext.GetRunspaceHandleAsync()) - { - handle.Runspace.SessionStateProxy.PSVariable.Set( - "psEditor", - this.EditorObject); - } - } - - /// - /// Invokes the specified editor command against the provided EditorContext. - /// - /// The unique name of the command to be invoked. - /// The context in which the command is being invoked. - /// A Task that can be awaited for completion. - public async Task InvokeCommandAsync(string commandName, EditorContext editorContext) - { - EditorCommand editorCommand; - - if (this.editorCommands.TryGetValue(commandName, out editorCommand)) - { - PSCommand executeCommand = new PSCommand(); - executeCommand.AddCommand("Invoke-Command"); - executeCommand.AddParameter("ScriptBlock", editorCommand.ScriptBlock); - executeCommand.AddParameter("ArgumentList", new object[] { editorContext }); - - await this.PowerShellContext.ExecuteCommandAsync( - executeCommand, - !editorCommand.SuppressOutput, - true); - } - else - { - throw new KeyNotFoundException( - string.Format( - "Editor command not found: '{0}'", - commandName)); - } - } - - /// - /// Registers a new EditorCommand with the ExtensionService and - /// causes its details to be sent to the host editor. - /// - /// The details about the editor command to be registered. - /// True if the command is newly registered, false if the command already exists. - public bool RegisterCommand(EditorCommand editorCommand) - { - bool commandExists = - this.editorCommands.ContainsKey( - editorCommand.Name); - - // Add or replace the editor command - this.editorCommands[editorCommand.Name] = editorCommand; - - if (!commandExists) - { - this.OnCommandAdded(editorCommand); - } - else - { - this.OnCommandUpdated(editorCommand); - } - - return !commandExists; - } - - /// - /// Unregisters an existing EditorCommand based on its registered name. - /// - /// The name of the command to be unregistered. - public void UnregisterCommand(string commandName) - { - EditorCommand existingCommand = null; - if (this.editorCommands.TryGetValue(commandName, out existingCommand)) - { - this.editorCommands.Remove(commandName); - this.OnCommandRemoved(existingCommand); - } - else - { - throw new KeyNotFoundException( - string.Format( - "Command '{0}' is not registered", - commandName)); - } - } - - /// - /// Returns all registered EditorCommands. - /// - /// An Array of all registered EditorCommands. - public EditorCommand[] GetCommands() - { - EditorCommand[] commands = new EditorCommand[this.editorCommands.Count]; - this.editorCommands.Values.CopyTo(commands,0); - return commands; - } - #endregion - - #region Events - - /// - /// Raised when a new editor command is added. - /// - public event EventHandler CommandAdded; - - private void OnCommandAdded(EditorCommand command) - { - this.CommandAdded?.Invoke(this, command); - } - - /// - /// Raised when an existing editor command is updated. - /// - public event EventHandler CommandUpdated; - - private void OnCommandUpdated(EditorCommand command) - { - this.CommandUpdated?.Invoke(this, command); - } - - /// - /// Raised when an existing editor command is removed. - /// - public event EventHandler CommandRemoved; - - private void OnCommandRemoved(EditorCommand command) - { - this.CommandRemoved?.Invoke(this, command); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Extensions/FileContext.cs b/src/PowerShellEditorServices/Extensions/FileContext.cs deleted file mode 100644 index 3fc5ac120..000000000 --- a/src/PowerShellEditorServices/Extensions/FileContext.cs +++ /dev/null @@ -1,281 +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; -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides context for a file that is open in the editor. - /// - public class FileContext - { - #region Private Fields - - internal ScriptFile scriptFile; - private EditorContext editorContext; - private IEditorOperations editorOperations; - - #endregion - - #region Properties - - /// - /// Gets the parsed abstract syntax tree for the file. - /// - public Ast Ast - { - get { return this.scriptFile.ScriptAst; } - } - - /// - /// Gets a BufferRange which represents the entire content - /// range of the file. - /// - public BufferRange FileRange - { - get { return this.scriptFile.FileRange; } - } - - /// - /// Gets the language of the file. - /// - public string Language { get; private set; } - - /// - /// Gets the filesystem path of the file. - /// - public string Path - { - get { return this.scriptFile.FilePath; } - } - - /// - /// Gets the parsed token list for the file. - /// - public Token[] Tokens - { - get { return this.scriptFile.ScriptTokens; } - } - - /// - /// Gets the workspace-relative path of the file. - /// - public string WorkspacePath - { - get - { - return - this.editorOperations.GetWorkspaceRelativePath( - this.scriptFile.FilePath); - } - } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the FileContext class. - /// - /// The ScriptFile to which this file refers. - /// The EditorContext to which this file relates. - /// An IEditorOperations implementation which performs operations in the editor. - /// Determines the language of the file.false If it is not specified, then it defaults to "Unknown" - public FileContext( - ScriptFile scriptFile, - EditorContext editorContext, - IEditorOperations editorOperations, - string language = "Unknown") - { - if (string.IsNullOrWhiteSpace(language)) - { - language = "Unknown"; - } - - this.scriptFile = scriptFile; - this.editorContext = editorContext; - this.editorOperations = editorOperations; - this.Language = language; - } - - #endregion - - #region Text Accessors - - /// - /// Gets the complete file content as a string. - /// - /// A string containing the complete file content. - public string GetText() - { - return this.scriptFile.Contents; - } - - /// - /// Gets the file content in the specified range as a string. - /// - /// The buffer range for which content will be extracted. - /// A string with the specified range of content. - public string GetText(BufferRange bufferRange) - { - return - string.Join( - Environment.NewLine, - this.GetTextLines(bufferRange)); - } - - /// - /// Gets the complete file content as an array of strings. - /// - /// An array of strings, each representing a line in the file. - public string[] GetTextLines() - { - return this.scriptFile.FileLines.ToArray(); - } - - /// - /// Gets the file content in the specified range as an array of strings. - /// - /// The buffer range for which content will be extracted. - /// An array of strings, each representing a line in the file within the specified range. - public string[] GetTextLines(BufferRange bufferRange) - { - return this.scriptFile.GetLinesInRange(bufferRange); - } - - #endregion - - #region Text Manipulation - - /// - /// Inserts a text string at the current cursor position represented by - /// the parent EditorContext's CursorPosition property. - /// - /// The text string to insert. - public void InsertText(string textToInsert) - { - // Is there a selection? - if (this.editorContext.SelectedRange.HasRange) - { - this.InsertText( - textToInsert, - this.editorContext.SelectedRange); - } - else - { - this.InsertText( - textToInsert, - this.editorContext.CursorPosition); - } - } - - /// - /// Inserts a text string at the specified buffer position. - /// - /// The text string to insert. - /// The position at which the text will be inserted. - public void InsertText(string textToInsert, BufferPosition insertPosition) - { - this.InsertText( - textToInsert, - new BufferRange(insertPosition, insertPosition)); - } - - /// - /// Inserts a text string at the specified line and column numbers. - /// - /// The text string to insert. - /// The 1-based line number at which the text will be inserted. - /// The 1-based column number at which the text will be inserted. - public void InsertText(string textToInsert, int insertLine, int insertColumn) - { - this.InsertText( - textToInsert, - new BufferPosition(insertLine, insertColumn)); - } - - /// - /// Inserts a text string to replace the specified range, represented - /// by starting and ending line and column numbers. Can be used to - /// insert, replace, or delete text depending on the specified range - /// and text to insert. - /// - /// The text string to insert. - /// The 1-based starting line number where text will be replaced. - /// The 1-based starting column number where text will be replaced. - /// The 1-based ending line number where text will be replaced. - /// The 1-based ending column number where text will be replaced. - public void InsertText( - string textToInsert, - int startLine, - int startColumn, - int endLine, - int endColumn) - { - this.InsertText( - textToInsert, - new BufferRange( - startLine, - startColumn, - endLine, - endColumn)); - } - - /// - /// Inserts a text string to replace the specified range. Can be - /// used to insert, replace, or delete text depending on the specified - /// range and text to insert. - /// - /// The text string to insert. - /// The buffer range which will be replaced by the string. - public void InsertText(string textToInsert, BufferRange insertRange) - { - this.editorOperations - .InsertTextAsync(this.scriptFile.ClientFilePath, textToInsert, insertRange) - .Wait(); - } - - #endregion - - #region File Manipulation - - /// - /// Saves this file. - /// - public void Save() - { - this.editorOperations.SaveFileAsync(this.scriptFile.FilePath); - } - - /// - /// Save this file under a new path and open a new editor window on that file. - /// - /// - /// the path where the file should be saved, - /// including the file name with extension as the leaf - /// - public void SaveAs(string newFilePath) - { - // Do some validation here so that we can provide a helpful error if the path won't work - string absolutePath = System.IO.Path.IsPathRooted(newFilePath) ? - newFilePath : - System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(this.scriptFile.FilePath), newFilePath)); - - if (File.Exists(absolutePath)) - { - throw new IOException(String.Format("The file '{0}' already exists", absolutePath)); - } - - this.editorOperations.SaveFileAsync(this.scriptFile.FilePath, newFilePath); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs deleted file mode 100644 index 7ef6bdf0e..000000000 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ /dev/null @@ -1,128 +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.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Extensions -{ - /// - /// Provides an interface that must be implemented by an editor - /// host to perform operations invoked by extensions written in - /// PowerShell. - /// - public interface IEditorOperations - { - /// - /// Gets the EditorContext for the editor's current state. - /// - /// A new EditorContext object. - Task GetEditorContextAsync(); - - /// - /// Gets the path to the editor's active workspace. - /// - /// The workspace path or null if there isn't one. - string GetWorkspacePath(); - - /// - /// Resolves the given file path relative to the current workspace path. - /// - /// The file path to be resolved. - /// The resolved file path. - string GetWorkspaceRelativePath(string filePath); - - /// - /// Causes a new untitled file to be created in the editor. - /// - /// A task that can be awaited for completion. - Task NewFileAsync(); - - /// - /// Causes a file to be opened in the editor. If the file is - /// already open, the editor must switch to the file. - /// - /// The path of the file to be opened. - /// A Task that can be tracked for completion. - Task OpenFileAsync(string filePath); - - /// - /// Causes a file to be opened in the editor. If the file is - /// already open, the editor must switch to the file. - /// You can specify whether the file opens as a preview or as a durable editor. - /// - /// The path of the file to be opened. - /// Determines wether the file is opened as a preview or as a durable editor. - /// A Task that can be tracked for completion. - Task OpenFileAsync(string filePath, bool preview); - - /// - /// Causes a file to be closed in the editor. - /// - /// The path of the file to be closed. - /// A Task that can be tracked for completion. - Task CloseFileAsync(string filePath); - - /// - /// Causes a file to be saved in the editor. - /// - /// The path of the file to be saved. - /// A Task that can be tracked for completion. - Task SaveFileAsync(string filePath); - - /// - /// Causes a file to be saved as a new file in a new editor window. - /// - /// the path of the current file being saved - /// the path of the new file where the current window content will be saved - /// - Task SaveFileAsync(string oldFilePath, string newFilePath); - - /// - /// Inserts text into the specified range for the file at the specified path. - /// - /// The path of the file which will have text inserted. - /// The text to insert into the file. - /// The range in the file to be replaced. - /// A Task that can be tracked for completion. - Task InsertTextAsync(string filePath, string insertText, BufferRange insertRange); - - /// - /// Causes the selection to be changed in the editor's active file buffer. - /// - /// The range over which the selection will be made. - /// A Task that can be tracked for completion. - Task SetSelectionAsync(BufferRange selectionRange); - - /// - /// Shows an informational message to the user. - /// - /// The message to be shown. - /// A Task that can be tracked for completion. - Task ShowInformationMessageAsync(string message); - - /// - /// Shows an error message to the user. - /// - /// The message to be shown. - /// A Task that can be tracked for completion. - Task ShowErrorMessageAsync(string message); - - /// - /// Shows a warning message to the user. - /// - /// The message to be shown. - /// A Task that can be tracked for completion. - Task ShowWarningMessageAsync(string message); - - /// - /// Sets the status bar message in the editor UI (if applicable). - /// - /// The message to be shown. - /// If non-null, a timeout in milliseconds for how long the message should remain visible. - /// A Task that can be tracked for completion. - Task SetStatusBarMessageAsync(string message, int? timeout); - } -} - diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs deleted file mode 100644 index 354e5f188..000000000 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ /dev/null @@ -1,343 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Diagnostics; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Management.Automation.Language; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices -{ - using System.Management.Automation; - using System.Management.Automation.Language; - - /// - /// Provides common operations for the syntax tree of a parsed script. - /// - internal static class AstOperations - { - // TODO: When netstandard is upgraded to 2.0, see if - // Delegate.CreateDelegate can be used here instead - private static readonly MethodInfo s_extentCloneWithNewOffset = typeof(PSObject).GetTypeInfo().Assembly - .GetType("System.Management.Automation.Language.InternalScriptPosition") - .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); - - private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - /// - /// Gets completions for the symbol found in the Ast at - /// the given file offset. - /// - /// - /// The Ast which will be traversed to find a completable symbol. - /// - /// - /// The array of tokens corresponding to the scriptAst parameter. - /// - /// - /// The 1-based file offset at which a symbol will be located. - /// - /// - /// The PowerShellContext to use for gathering completions. - /// - /// An ILogger implementation used for writing log messages. - /// - /// A CancellationToken to cancel completion requests. - /// - /// - /// A CommandCompletion instance that contains completions for the - /// symbol at the given offset. - /// - static public async Task GetCompletionsAsync( - Ast scriptAst, - Token[] currentTokens, - int fileOffset, - PowerShellContext powerShellContext, - ILogger logger, - CancellationToken cancellationToken) - { - if (!s_completionHandle.Wait(0)) - { - return null; - } - - try - { - IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( - scriptAst.Extent.StartScriptPosition, - new object[] { fileOffset }); - - logger.Write( - LogLevel.Verbose, - string.Format( - "Getting completions at offset {0} (line: {1}, column: {2})", - fileOffset, - cursorPosition.LineNumber, - cursorPosition.ColumnNumber)); - - if (!powerShellContext.IsAvailable) - { - return null; - } - - var stopwatch = new Stopwatch(); - - // If the current runspace is out of process we can use - // CommandCompletion.CompleteInput because PSReadLine won't be taking up the - // main runspace. - if (powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - stopwatch.Start(); - try - { - return CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: powerShell); - } - finally - { - stopwatch.Stop(); - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - } - } - } - - CommandCompletion commandCompletion = null; - await powerShellContext.InvokeOnPipelineThreadAsync( - pwsh => - { - stopwatch.Start(); - commandCompletion = CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: pwsh); - }); - stopwatch.Stop(); - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - - return commandCompletion; - } - finally - { - s_completionHandle.Release(); - } - } - - /// - /// Finds the symbol at a given file location - /// - /// The abstract syntax tree of the given script - /// The line number of the cursor for the given script - /// The coulumn number of the cursor for the given script - /// Includes full function definition ranges in the search. - /// SymbolReference of found symbol - static public SymbolReference FindSymbolAtPosition( - Ast scriptAst, - int lineNumber, - int columnNumber, - bool includeFunctionDefinitions = false) - { - FindSymbolVisitor symbolVisitor = - new FindSymbolVisitor( - lineNumber, - columnNumber, - includeFunctionDefinitions); - - scriptAst.Visit(symbolVisitor); - - return symbolVisitor.FoundSymbolReference; - } - - /// - /// Finds the symbol (always Command type) at a given file location - /// - /// The abstract syntax tree of the given script - /// The line number of the cursor for the given script - /// The column number of the cursor for the given script - /// SymbolReference of found command - static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber) - { - FindCommandVisitor commandVisitor = new FindCommandVisitor(lineNumber, columnNumber); - scriptAst.Visit(commandVisitor); - - return commandVisitor.FoundCommandReference; - } - - /// - /// Finds all references (including aliases) in a script for the given symbol - /// - /// The abstract syntax tree of the given script - /// The symbol that we are looking for referneces of - /// Dictionary maping cmdlets to aliases for finding alias references - /// Dictionary maping aliases to cmdlets for finding alias references - /// - static public IEnumerable FindReferencesOfSymbol( - Ast scriptAst, - SymbolReference symbolReference, - Dictionary> CmdletToAliasDictionary, - Dictionary AliasToCmdletDictionary) - { - // find the symbol evaluators for the node types we are handling - FindReferencesVisitor referencesVisitor = - new FindReferencesVisitor( - symbolReference, - CmdletToAliasDictionary, - AliasToCmdletDictionary); - scriptAst.Visit(referencesVisitor); - - return referencesVisitor.FoundReferences; - } - - /// - /// Finds all references (not including aliases) in a script for the given symbol - /// - /// The abstract syntax tree of the given script - /// The symbol that we are looking for referneces of - /// If this reference search needs aliases. - /// This should always be false and used for occurence requests - /// A collection of SymbolReference objects that are refrences to the symbolRefrence - /// not including aliases - static public IEnumerable FindReferencesOfSymbol( - ScriptBlockAst scriptAst, - SymbolReference foundSymbol, - bool needsAliases) - { - FindReferencesVisitor referencesVisitor = - new FindReferencesVisitor(foundSymbol); - scriptAst.Visit(referencesVisitor); - - return referencesVisitor.FoundReferences; - } - - /// - /// Finds the definition of the symbol - /// - /// The abstract syntax tree of the given script - /// The symbol that we are looking for the definition of - /// A SymbolReference of the definition of the symbolReference - static public SymbolReference FindDefinitionOfSymbol( - Ast scriptAst, - SymbolReference symbolReference) - { - FindDeclarationVisitor declarationVisitor = - new FindDeclarationVisitor( - symbolReference); - scriptAst.Visit(declarationVisitor); - - return declarationVisitor.FoundDeclaration; - } - - /// - /// Finds all symbols in a script - /// - /// The abstract syntax tree of the given script - /// The PowerShell version the Ast was generated from - /// A collection of SymbolReference objects - static public IEnumerable FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion) - { - IEnumerable symbolReferences = null; - - // TODO: Restore this when we figure out how to support multiple - // PS versions in the new PSES-as-a-module world (issue #276) - // if (powerShellVersion >= new Version(5,0)) - // { - //#if PowerShellv5 - // FindSymbolsVisitor2 findSymbolsVisitor = new FindSymbolsVisitor2(); - // scriptAst.Visit(findSymbolsVisitor); - // symbolReferences = findSymbolsVisitor.SymbolReferences; - //#endif - // } - // else - - FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor(); - scriptAst.Visit(findSymbolsVisitor); - symbolReferences = findSymbolsVisitor.SymbolReferences; - return symbolReferences; - } - - /// - /// Checks if a given ast represents the root node of a *.psd1 file. - /// - /// The abstract syntax tree of the given script - /// true if the AST represts a *.psd1 file, otherwise false - static public bool IsPowerShellDataFileAst(Ast ast) - { - // sometimes we don't have reliable access to the filename - // so we employ heuristics to check if the contents are - // part of a psd1 file. - return IsPowerShellDataFileAstNode( - new { Item = ast, Children = new List() }, - new Type[] { - typeof(ScriptBlockAst), - typeof(NamedBlockAst), - typeof(PipelineAst), - typeof(CommandExpressionAst), - typeof(HashtableAst) }, - 0); - } - - static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap, int level) - { - var levelAstTypeMatch = node.Item.GetType().Equals(levelAstMap[level]); - if (!levelAstTypeMatch) - { - return false; - } - - if (level == levelAstMap.Length - 1) - { - return levelAstTypeMatch; - } - - var astsFound = (node.Item as Ast).FindAll(a => a is Ast, false); - if (astsFound != null) - { - foreach (var astFound in astsFound) - { - if (!astFound.Equals(node.Item) - && node.Item.Equals(astFound.Parent) - && IsPowerShellDataFileAstNode( - new { Item = astFound, Children = new List() }, - levelAstMap, - level + 1)) - { - return true; - } - } - } - - return false; - } - - /// - /// Finds all files dot sourced in a script - /// - /// The abstract syntax tree of the given script - /// Pre-calculated value of $PSScriptRoot - /// - static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot) - { - FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(psScriptRoot); - scriptAst.Visit(dotSourcedVisitor); - - return dotSourcedVisitor.DotSourcedFiles.ToArray(); - } - } -} diff --git a/src/PowerShellEditorServices/Language/CommandHelpers.cs b/src/PowerShellEditorServices/Language/CommandHelpers.cs deleted file mode 100644 index 9c72e0eef..000000000 --- a/src/PowerShellEditorServices/Language/CommandHelpers.cs +++ /dev/null @@ -1,114 +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 Microsoft.PowerShell.EditorServices.Utility; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides utility methods for working with PowerShell commands. - /// - public class CommandHelpers - { - private static HashSet NounBlackList = - new HashSet - { - "Module", - "Script", - "Package", - "PackageProvider", - "PackageSource", - "InstalledModule", - "InstalledScript", - "ScriptFileInfo", - "PSRepository" - }; - - /// - /// Gets the CommandInfo instance for a command with a particular name. - /// - /// The name of the command. - /// The PowerShellContext to use for running Get-Command. - /// A CommandInfo object with details about the specified command. - public static async Task GetCommandInfoAsync( - string commandName, - PowerShellContext powerShellContext) - { - Validate.IsNotNull(nameof(commandName), commandName); - - // Make sure the command's noun isn't blacklisted. This is - // currently necessary to make sure that Get-Command doesn't - // load PackageManagement or PowerShellGet because they cause - // a major slowdown in IntelliSense. - var commandParts = commandName.Split('-'); - if (commandParts.Length == 2 && NounBlackList.Contains(commandParts[1])) - { - return null; - } - - PSCommand command = new PSCommand(); - command.AddCommand(@"Microsoft.PowerShell.Core\Get-Command"); - command.AddArgument(commandName); - command.AddParameter("ErrorAction", "Ignore"); - - return - (await powerShellContext - .ExecuteCommandAsync(command, false, false)) - .Select(o => o.BaseObject) - .OfType() - .FirstOrDefault(); - } - - /// - /// Gets the command's "Synopsis" documentation section. - /// - /// The CommandInfo instance for the command. - /// The PowerShellContext to use for getting command documentation. - /// - public static async Task GetCommandSynopsisAsync( - CommandInfo commandInfo, - PowerShellContext powerShellContext) - { - string synopsisString = string.Empty; - - PSObject helpObject = null; - - if (commandInfo != null && - (commandInfo.CommandType == CommandTypes.Cmdlet || - commandInfo.CommandType == CommandTypes.Function || - commandInfo.CommandType == CommandTypes.Filter)) - { - PSCommand command = new PSCommand(); - command.AddCommand(@"Microsoft.PowerShell.Core\Get-Help"); - command.AddArgument(commandInfo); - command.AddParameter("ErrorAction", "Ignore"); - - var results = await powerShellContext.ExecuteCommandAsync(command, false, false); - helpObject = results.FirstOrDefault(); - - if (helpObject != null) - { - // Extract the synopsis string from the object - synopsisString = - (string)helpObject.Properties["synopsis"].Value ?? - string.Empty; - - // Ignore the placeholder value for this field - if (string.Equals(synopsisString, "SHORT DESCRIPTION", System.StringComparison.CurrentCultureIgnoreCase)) - { - synopsisString = string.Empty; - } - } - } - - return synopsisString; - } - } -} - diff --git a/src/PowerShellEditorServices/Language/CompletionResults.cs b/src/PowerShellEditorServices/Language/CompletionResults.cs deleted file mode 100644 index fc8d2eb00..000000000 --- a/src/PowerShellEditorServices/Language/CompletionResults.cs +++ /dev/null @@ -1,340 +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.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Management.Automation; -using System.Text.RegularExpressions; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides the results of a single code completion request. - /// - public sealed class CompletionResults - { - #region Properties - - /// - /// Gets the completions that were found during the - /// completion request. - /// - public CompletionDetails[] Completions { get; private set; } - - /// - /// Gets the range in the buffer that should be replaced by this - /// completion result. - /// - public BufferRange ReplacedRange { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an empty CompletionResults instance. - /// - public CompletionResults() - { - this.Completions = new CompletionDetails[0]; - this.ReplacedRange = new BufferRange(0, 0, 0, 0); - } - - internal static CompletionResults Create( - ScriptFile scriptFile, - CommandCompletion commandCompletion) - { - BufferRange replacedRange = null; - - // Only calculate the replacement range if there are completion results - if (commandCompletion.CompletionMatches.Count > 0) - { - replacedRange = - scriptFile.GetRangeBetweenOffsets( - commandCompletion.ReplacementIndex, - commandCompletion.ReplacementIndex + commandCompletion.ReplacementLength); - } - - return new CompletionResults - { - Completions = GetCompletionsArray(commandCompletion), - ReplacedRange = replacedRange - }; - } - - #endregion - - #region Private Methods - - private static CompletionDetails[] GetCompletionsArray( - CommandCompletion commandCompletion) - { - IEnumerable completionList = - commandCompletion.CompletionMatches.Select( - CompletionDetails.Create); - - return completionList.ToArray(); - } - - #endregion - } - - /// - /// Enumerates the completion types that may be returned. - /// - public enum CompletionType - { - /// - /// Completion type is unknown, either through being uninitialized or - /// having been created from an unsupported CompletionResult that was - /// returned by the PowerShell engine. - /// - Unknown = 0, - - /// - /// Identifies a completion for a command. - /// - Command, - - /// - /// Identifies a completion for a .NET method. - /// - Method, - - /// - /// Identifies a completion for a command parameter name. - /// - ParameterName, - - /// - /// Identifies a completion for a command parameter value. - /// - ParameterValue, - - /// - /// Identifies a completion for a .NET property. - /// - Property, - - /// - /// Identifies a completion for a variable name. - /// - Variable, - - /// - /// Identifies a completion for a namespace. - /// - Namespace, - - /// - /// Identifies a completion for a .NET type name. - /// - Type, - - /// - /// Identifies a completion for a PowerShell language keyword. - /// - Keyword, - - /// - /// Identifies a completion for a provider path (like a file system path) to a leaf item. - /// - File, - - /// - /// Identifies a completion for a provider path (like a file system path) to a container. - /// - Folder - } - - /// - /// Provides the details about a single completion result. - /// - [DebuggerDisplay("CompletionType = {CompletionType.ToString()}, CompletionText = {CompletionText}")] - public sealed class CompletionDetails - { - #region Properties - - /// - /// Gets the text that will be used to complete the statement - /// at the requested file offset. - /// - public string CompletionText { get; private set; } - - /// - /// Gets the text that should be dispayed in a drop-down completion list. - /// - public string ListItemText { get; private set; } - - /// - /// Gets the text that can be used to display a tooltip for - /// the statement at the requested file offset. - /// - public string ToolTipText { get; private set; } - - /// - /// Gets the name of the type which this symbol represents. - /// If the symbol doesn't have an inherent type, null will - /// be returned. - /// - public string SymbolTypeName { get; private set; } - - /// - /// Gets the CompletionType which identifies the type of this completion. - /// - public CompletionType CompletionType { get; private set; } - - #endregion - - #region Constructors - - internal static CompletionDetails Create(CompletionResult completionResult) - { - Validate.IsNotNull("completionResult", completionResult); - - // Some tooltips may have newlines or whitespace for unknown reasons - string toolTipText = completionResult.ToolTip; - if (toolTipText != null) - { - toolTipText = toolTipText.Trim(); - } - - return new CompletionDetails - { - CompletionText = completionResult.CompletionText, - ListItemText = completionResult.ListItemText, - ToolTipText = toolTipText, - SymbolTypeName = ExtractSymbolTypeNameFromToolTip(completionResult.ToolTip), - CompletionType = - ConvertCompletionResultType( - completionResult.ResultType) - }; - } - - internal static CompletionDetails Create( - string completionText, - CompletionType completionType, - string toolTipText = null, - string symbolTypeName = null, - string listItemText = null) - { - return new CompletionDetails - { - CompletionText = completionText, - CompletionType = completionType, - ListItemText = listItemText, - ToolTipText = toolTipText, - SymbolTypeName = symbolTypeName - }; - } - - #endregion - - #region Public Methods - - /// - /// Compares two CompletionResults instances for equality. - /// - /// The potential CompletionResults instance to compare. - /// True if the CompletionResults instances have the same details. - public override bool Equals(object obj) - { - CompletionDetails otherDetails = obj as CompletionDetails; - if (otherDetails == null) - { - return false; - } - - return - string.Equals(this.CompletionText, otherDetails.CompletionText) && - this.CompletionType == otherDetails.CompletionType && - string.Equals(this.ToolTipText, otherDetails.ToolTipText) && - string.Equals(this.SymbolTypeName, otherDetails.SymbolTypeName); - } - - /// - /// Returns the hash code for this CompletionResults instance. - /// - /// The hash code for this CompletionResults instance. - public override int GetHashCode() - { - return - string.Format( - "{0}{1}{2}{3}{4}", - this.CompletionText, - this.CompletionType, - this.ListItemText, - this.ToolTipText, - this.SymbolTypeName).GetHashCode(); - } - - #endregion - - #region Private Methods - - private static CompletionType ConvertCompletionResultType( - CompletionResultType completionResultType) - { - switch (completionResultType) - { - case CompletionResultType.Command: - return CompletionType.Command; - - case CompletionResultType.Method: - return CompletionType.Method; - - case CompletionResultType.ParameterName: - return CompletionType.ParameterName; - - case CompletionResultType.ParameterValue: - return CompletionType.ParameterValue; - - case CompletionResultType.Property: - return CompletionType.Property; - - case CompletionResultType.Variable: - return CompletionType.Variable; - - case CompletionResultType.Namespace: - return CompletionType.Namespace; - - case CompletionResultType.Type: - return CompletionType.Type; - - case CompletionResultType.Keyword: - return CompletionType.Keyword; - - case CompletionResultType.ProviderContainer: - return CompletionType.Folder; - - case CompletionResultType.ProviderItem: - return CompletionType.File; - - default: - // TODO: Trace the unsupported CompletionResultType - return CompletionType.Unknown; - } - } - - private static string ExtractSymbolTypeNameFromToolTip(string toolTipText) - { - // Tooltips returned from PowerShell contain the symbol type in - // brackets. Attempt to extract such strings for further processing. - var matches = Regex.Matches(toolTipText, @"^\[(.+)\]"); - - if (matches.Count > 0 && matches[0].Groups.Count > 1) - { - // Return the symbol type name - return matches[0].Groups[1].Value; - } - - return null; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Language/FindCommandVisitor.cs b/src/PowerShellEditorServices/Language/FindCommandVisitor.cs deleted file mode 100644 index 5e5fca624..000000000 --- a/src/PowerShellEditorServices/Language/FindCommandVisitor.cs +++ /dev/null @@ -1,87 +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.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// The vistior used to find the commandAst of a specific location in an AST - /// - internal class FindCommandVisitor : AstVisitor - { - private int lineNumber; - private int columnNumber; - - public SymbolReference FoundCommandReference { get; private set; } - - public FindCommandVisitor(int lineNumber, int columnNumber) - { - this.lineNumber = lineNumber; - this.columnNumber = columnNumber; - } - - public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) - { - if (this.lineNumber == pipelineAst.Extent.StartLineNumber) - { - // Which command is the cursor in? - foreach (var commandAst in pipelineAst.PipelineElements.OfType()) - { - int trueEndColumnNumber = commandAst.Extent.EndColumnNumber; - string currentLine = commandAst.Extent.StartScriptPosition.Line; - - if (currentLine.Length >= trueEndColumnNumber) - { - // Get the text left in the line after the command's extent - string remainingLine = - currentLine.Substring( - commandAst.Extent.EndColumnNumber); - - // Calculate the "true" end column number by finding out how many - // whitespace characters are between this command and the next (or - // the end of the line). - // NOTE: +1 is added to trueEndColumnNumber to account for the position - // just after the last character in the command string or script line. - int preTrimLength = remainingLine.Length; - int postTrimLength = remainingLine.TrimStart().Length; - trueEndColumnNumber = - commandAst.Extent.EndColumnNumber + - (preTrimLength - postTrimLength) + 1; - } - - if (commandAst.Extent.StartColumnNumber <= columnNumber && - trueEndColumnNumber >= columnNumber) - { - this.FoundCommandReference = - new SymbolReference( - SymbolType.Function, - commandAst.CommandElements[0].Extent); - - return AstVisitAction.StopVisit; - } - } - } - - return base.VisitPipeline(pipelineAst); - } - - /// - /// Is the position of the given location is in the range of the start - /// of the first element to the character before the second element - /// - /// The script extent of the first element of the command ast - /// The script extent of the second element of the command ast - /// True if the given position is in the range of the start of - /// the first element to the character before the second element - private bool IsPositionInExtent(IScriptExtent firstExtent, IScriptExtent secondExtent) - { - return (firstExtent.StartLineNumber == lineNumber && - firstExtent.StartColumnNumber <= columnNumber && - secondExtent.StartColumnNumber >= columnNumber - 1); - } - } -} diff --git a/src/PowerShellEditorServices/Language/FindDeclarationVisitor.cs b/src/PowerShellEditorServices/Language/FindDeclarationVisitor.cs deleted file mode 100644 index f8ecffc2e..000000000 --- a/src/PowerShellEditorServices/Language/FindDeclarationVisitor.cs +++ /dev/null @@ -1,148 +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.Collections.Generic; -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// The visitor used to find the definition of a symbol - /// - internal class FindDeclarationVisitor : AstVisitor - { - private SymbolReference symbolRef; - private string variableName; - - public SymbolReference FoundDeclaration{ get; private set; } - - public FindDeclarationVisitor(SymbolReference symbolRef) - { - this.symbolRef = symbolRef; - if (this.symbolRef.SymbolType == SymbolType.Variable) - { - // converts `$varName` to `varName` or of the form ${varName} to varName - variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); - } - } - - /// - /// Decides if the current function definition is the right definition - /// for the symbol being searched for. The definition of the symbol will be a of type - /// SymbolType.Function and have the same name as the symbol - /// - /// A FunctionDefinitionAst in the script's AST - /// A decision to stop searching if the right FunctionDefinitionAst was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - // Get the start column number of the function name, - // instead of the the start column of 'function' and create new extent for the functionName - int startColumnNumber = - functionDefinitionAst.Extent.Text.IndexOf( - functionDefinitionAst.Name) + 1; - - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, - StartColumnNumber = startColumnNumber, - EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, - EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length - }; - - if (symbolRef.SymbolType.Equals(SymbolType.Function) && - nameExtent.Text.Equals(symbolRef.ScriptRegion.Text, StringComparison.CurrentCultureIgnoreCase)) - { - this.FoundDeclaration = - new SymbolReference( - SymbolType.Function, - nameExtent); - - return AstVisitAction.StopVisit; - } - - return base.VisitFunctionDefinition(functionDefinitionAst); - } - - /// - /// Check if the left hand side of an assignmentStatementAst is a VariableExpressionAst - /// with the same name as that of symbolRef. - /// - /// An AssignmentStatementAst - /// A decision to stop searching if the right VariableExpressionAst was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) - { - if (variableName == null) - { - return AstVisitAction.Continue; - } - - // We want to check VariableExpressionAsts from within this AssignmentStatementAst so we visit it. - FindDeclarationVariableExpressionVisitor visitor = new FindDeclarationVariableExpressionVisitor(symbolRef); - assignmentStatementAst.Left.Visit(visitor); - - if (visitor.FoundDeclaration != null) - { - FoundDeclaration = visitor.FoundDeclaration; - return AstVisitAction.StopVisit; - } - return AstVisitAction.Continue; - } - - /// - /// The private visitor used to find the variable expression that matches a symbol - /// - private class FindDeclarationVariableExpressionVisitor : AstVisitor - { - private SymbolReference symbolRef; - private string variableName; - - public SymbolReference FoundDeclaration{ get; private set; } - - public FindDeclarationVariableExpressionVisitor(SymbolReference symbolRef) - { - this.symbolRef = symbolRef; - if (this.symbolRef.SymbolType == SymbolType.Variable) - { - // converts `$varName` to `varName` or of the form ${varName} to varName - variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); - } - } - - /// - /// Check if the VariableExpressionAst has the same name as that of symbolRef. - /// - /// A VariableExpressionAst - /// A decision to stop searching if the right VariableExpressionAst was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (variableExpressionAst.VariablePath.UserPath.Equals(variableName, StringComparison.OrdinalIgnoreCase)) - { - // TODO also find instances of set-variable - FoundDeclaration = new SymbolReference(SymbolType.Variable, variableExpressionAst.Extent); - return AstVisitAction.StopVisit; - } - return AstVisitAction.Continue; - } - - public override AstVisitAction VisitMemberExpression(MemberExpressionAst functionDefinitionAst) - { - // We don't want to discover any variables in member expressisons (`$something.Foo`) - return AstVisitAction.SkipChildren; - } - - public override AstVisitAction VisitIndexExpression(IndexExpressionAst functionDefinitionAst) - { - // We don't want to discover any variables in index expressions (`$something[0]`) - return AstVisitAction.SkipChildren; - } - } - } -} diff --git a/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs deleted file mode 100644 index c0295a228..000000000 --- a/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs +++ /dev/null @@ -1,90 +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.Collections.Generic; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// The vistor used to find the dont sourced files in an AST - /// - internal class FindDotSourcedVisitor : AstVisitor - { - private readonly string _psScriptRoot; - - /// - /// A hash set of the dot sourced files (because we don't want duplicates) - /// - public HashSet DotSourcedFiles { get; private set; } - - /// - /// Creates a new instance of the FindDotSourcedVisitor class. - /// - /// Pre-calculated value of $PSScriptRoot - public FindDotSourcedVisitor(string psScriptRoot) - { - DotSourcedFiles = new HashSet(StringComparer.CurrentCultureIgnoreCase); - _psScriptRoot = psScriptRoot; - } - - /// - /// Checks to see if the command invocation is a dot - /// in order to find a dot sourced file - /// - /// A CommandAst object in the script's AST - /// A decision to stop searching if the right commandAst was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - CommandElementAst commandElementAst = commandAst.CommandElements[0]; - if (commandAst.InvocationOperator.Equals(TokenKind.Dot)) - { - string path; - switch (commandElementAst) - { - case StringConstantExpressionAst stringConstantExpressionAst: - path = stringConstantExpressionAst.Value; - break; - - case ExpandableStringExpressionAst expandableStringExpressionAst: - path = GetPathFromExpandableStringExpression(expandableStringExpressionAst); - break; - - default: - path = null; - break; - } - - if (!string.IsNullOrWhiteSpace(path)) - { - DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(path)); - } - } - - return base.VisitCommand(commandAst); - } - - private string GetPathFromExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) - { - var path = expandableStringExpressionAst.Value; - foreach (var nestedExpression in expandableStringExpressionAst.NestedExpressions) - { - // If the string contains the variable $PSScriptRoot, we replace it with the corresponding value. - if (!(nestedExpression is VariableExpressionAst variableAst - && variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase))) - { - return null; // We return null instead of a partially evaluated ExpandableStringExpression. - } - - path = path.Replace(variableAst.ToString(), _psScriptRoot); - } - - return path; - } - } -} diff --git a/src/PowerShellEditorServices/Language/FindOccurrencesResult.cs b/src/PowerShellEditorServices/Language/FindOccurrencesResult.cs deleted file mode 100644 index 92f795ca4..000000000 --- a/src/PowerShellEditorServices/Language/FindOccurrencesResult.cs +++ /dev/null @@ -1,23 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// A class for the found occurences of a symbol. - /// It contains a collection of symbol references. - /// - public class FindOccurrencesResult - { - #region Properties - /// - /// Gets the collection of SymboleReferences for the all occurences of the symbol - /// - public IEnumerable FoundOccurrences { get; internal set; } - #endregion - } -} diff --git a/src/PowerShellEditorServices/Language/FindReferencesResult.cs b/src/PowerShellEditorServices/Language/FindReferencesResult.cs deleted file mode 100644 index 16396a75a..000000000 --- a/src/PowerShellEditorServices/Language/FindReferencesResult.cs +++ /dev/null @@ -1,33 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// A class to contain the found references of a symbol. - /// It contains a collection of symbol references, the symbol name, and the symbol's file offset - /// - public class FindReferencesResult - { - #region Properties - /// - /// Gets the name of the symbol - /// - public string SymbolName { get; internal set; } - - /// - /// Gets the file offset (location based on line and column number) of the symbol - /// - public int SymbolFileOffset { get; internal set; } - - /// - /// Gets the collection of SymboleReferences for the all references to the symbol - /// - public IEnumerable FoundReferences { get; internal set; } - #endregion - } -} diff --git a/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs b/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs deleted file mode 100644 index 2b7b160ea..000000000 --- a/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs +++ /dev/null @@ -1,189 +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.Collections.Generic; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// The visitor used to find the references of a symbol in a script's AST - /// - internal class FindReferencesVisitor : AstVisitor - { - private SymbolReference symbolRef; - private Dictionary> CmdletToAliasDictionary; - private Dictionary AliasToCmdletDictionary; - private string symbolRefCommandName; - private bool needsAliases; - - public List FoundReferences { get; set; } - - /// - /// Constructor used when searching for aliases is needed - /// - /// The found symbolReference that other symbols are being compared to - /// Dictionary maping cmdlets to aliases for finding alias references - /// Dictionary maping aliases to cmdlets for finding alias references - public FindReferencesVisitor( - SymbolReference symbolReference, - Dictionary> CmdletToAliasDictionary, - Dictionary AliasToCmdletDictionary) - { - this.symbolRef = symbolReference; - this.FoundReferences = new List(); - this.needsAliases = true; - this.CmdletToAliasDictionary = CmdletToAliasDictionary; - this.AliasToCmdletDictionary = AliasToCmdletDictionary; - - // Try to get the symbolReference's command name of an alias, - // if a command name does not exists (if the symbol isn't an alias to a command) - // set symbolRefCommandName to and empty string value - AliasToCmdletDictionary.TryGetValue(symbolReference.ScriptRegion.Text, out symbolRefCommandName); - if (symbolRefCommandName == null) { symbolRefCommandName = string.Empty; } - - } - - /// - /// Constructor used when searching for aliases is not needed - /// - /// The found symbolReference that other symbols are being compared to - public FindReferencesVisitor(SymbolReference foundSymbol) - { - this.symbolRef = foundSymbol; - this.FoundReferences = new List(); - this.needsAliases = false; - } - - /// - /// Decides if the current command is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Function - /// and have the same name as the symbol - /// - /// A CommandAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - Ast commandNameAst = commandAst.CommandElements[0]; - string commandName = commandNameAst.Extent.Text; - - if(symbolRef.SymbolType.Equals(SymbolType.Function)) - { - if (needsAliases) - { - // Try to get the commandAst's name and aliases, - // if a command does not exists (if the symbol isn't an alias to a command) - // set command to and empty string value string command - // if the aliases do not exist (if the symvol isn't a command that has aliases) - // set aliases to an empty List - string command; - List alaises; - CmdletToAliasDictionary.TryGetValue(commandName, out alaises); - AliasToCmdletDictionary.TryGetValue(commandName, out command); - if (alaises == null) { alaises = new List(); } - if (command == null) { command = string.Empty; } - - if (symbolRef.SymbolType.Equals(SymbolType.Function)) - { - // Check if the found symbol's name is the same as the commandAst's name OR - // if the symbol's name is an alias for this commandAst's name (commandAst is a cmdlet) OR - // if the symbol's name is the same as the commandAst's cmdlet name (commandAst is a alias) - if (commandName.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase) || - alaises.Contains(symbolRef.ScriptRegion.Text.ToLower()) || - command.Equals(symbolRef.ScriptRegion.Text, StringComparison.CurrentCultureIgnoreCase) || - (!command.Equals(string.Empty) && command.Equals(symbolRefCommandName, StringComparison.CurrentCultureIgnoreCase))) - { - this.FoundReferences.Add(new SymbolReference( - SymbolType.Function, - commandNameAst.Extent)); - } - } - - } - else // search does not include aliases - { - if (commandName.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - this.FoundReferences.Add(new SymbolReference( - SymbolType.Function, - commandNameAst.Extent)); - } - } - - } - return base.VisitCommand(commandAst); - } - - /// - /// Decides if the current function definition is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Function and have the same name as the symbol - /// - /// A functionDefinitionAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - // Get the start column number of the function name, - // instead of the the start column of 'function' and create new extent for the functionName - int startColumnNumber = - functionDefinitionAst.Extent.Text.IndexOf( - functionDefinitionAst.Name) + 1; - - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, - EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, - StartColumnNumber = startColumnNumber, - EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length - }; - - if (symbolRef.SymbolType.Equals(SymbolType.Function) && - nameExtent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - this.FoundReferences.Add(new SymbolReference( - SymbolType.Function, - nameExtent)); - } - return base.VisitFunctionDefinition(functionDefinitionAst); - } - - /// - /// Decides if the current function definition is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Parameter and have the same name as the symbol - /// - /// A commandParameterAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) - { - if (symbolRef.SymbolType.Equals(SymbolType.Parameter) && - commandParameterAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - this.FoundReferences.Add(new SymbolReference( - SymbolType.Parameter, - commandParameterAst.Extent)); - } - return AstVisitAction.Continue; - } - - /// - /// Decides if the current function definition is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Variable and have the same name as the symbol - /// - /// A variableExpressionAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if(symbolRef.SymbolType.Equals(SymbolType.Variable) && - variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - this.FoundReferences.Add(new SymbolReference( - SymbolType.Variable, - variableExpressionAst.Extent)); - } - return AstVisitAction.Continue; - } - } -} diff --git a/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs b/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs deleted file mode 100644 index 404f8d190..000000000 --- a/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs +++ /dev/null @@ -1,145 +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.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// The visitor used to find the the symbol at a specfic location in the AST - /// - internal class FindSymbolVisitor : AstVisitor - { - private int lineNumber; - private int columnNumber; - private bool includeFunctionDefinitions; - - public SymbolReference FoundSymbolReference { get; private set; } - - public FindSymbolVisitor( - int lineNumber, - int columnNumber, - bool includeFunctionDefinitions) - { - this.lineNumber = lineNumber; - this.columnNumber = columnNumber; - this.includeFunctionDefinitions = includeFunctionDefinitions; - } - - /// - /// Checks to see if this command ast is the symbol we are looking for. - /// - /// A CommandAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - Ast commandNameAst = commandAst.CommandElements[0]; - - if (this.IsPositionInExtent(commandNameAst.Extent)) - { - this.FoundSymbolReference = - new SymbolReference( - SymbolType.Function, - commandNameAst.Extent); - - return AstVisitAction.StopVisit; - } - - return base.VisitCommand(commandAst); - } - - /// - /// Checks to see if this function definition is the symbol we are looking for. - /// - /// A functionDefinitionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - int startColumnNumber = 1; - - if (!this.includeFunctionDefinitions) - { - startColumnNumber = - functionDefinitionAst.Extent.Text.IndexOf( - functionDefinitionAst.Name) + 1; - } - - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, - EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, - StartColumnNumber = startColumnNumber, - EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length - }; - - if (this.IsPositionInExtent(nameExtent)) - { - this.FoundSymbolReference = - new SymbolReference( - SymbolType.Function, - nameExtent); - - return AstVisitAction.StopVisit; - } - - return base.VisitFunctionDefinition(functionDefinitionAst); - } - - /// - /// Checks to see if this command parameter is the symbol we are looking for. - /// - /// A CommandParameterAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) - { - if (this.IsPositionInExtent(commandParameterAst.Extent)) - { - this.FoundSymbolReference = - new SymbolReference( - SymbolType.Parameter, - commandParameterAst.Extent); - return AstVisitAction.StopVisit; - } - return AstVisitAction.Continue; - } - - /// - /// Checks to see if this variable expression is the symbol we are looking for. - /// - /// A VariableExpressionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (this.IsPositionInExtent(variableExpressionAst.Extent)) - { - this.FoundSymbolReference = - new SymbolReference( - SymbolType.Variable, - variableExpressionAst.Extent); - - return AstVisitAction.StopVisit; - } - - return AstVisitAction.Continue; - } - - /// - /// Is the position of the given location is in the ast's extent - /// - /// The script extent of the element - /// True if the given position is in the range of the element's extent - private bool IsPositionInExtent(IScriptExtent extent) - { - return (extent.StartLineNumber == lineNumber && - extent.StartColumnNumber <= columnNumber && - extent.EndColumnNumber >= columnNumber); - } - } -} diff --git a/src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs b/src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs deleted file mode 100644 index 96f5c335c..000000000 --- a/src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs +++ /dev/null @@ -1,148 +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.Collections.Generic; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// The visitor used to find all the symbols (function and class defs) in the AST. - /// - /// - /// Requires PowerShell v3 or higher - /// - internal class FindSymbolsVisitor : AstVisitor - { - public List SymbolReferences { get; private set; } - - public FindSymbolsVisitor() - { - this.SymbolReferences = new List(); - } - - /// - /// Adds each function definition as a - /// - /// A functionDefinitionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - IScriptExtent nameExtent = new ScriptExtent() { - Text = functionDefinitionAst.Name, - StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, - EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, - StartColumnNumber = functionDefinitionAst.Extent.StartColumnNumber, - EndColumnNumber = functionDefinitionAst.Extent.EndColumnNumber - }; - - SymbolType symbolType = - functionDefinitionAst.IsWorkflow ? - SymbolType.Workflow : SymbolType.Function; - - this.SymbolReferences.Add( - new SymbolReference( - symbolType, - nameExtent)); - - return AstVisitAction.Continue; - } - - /// - /// Checks to see if this variable expression is the symbol we are looking for. - /// - /// A VariableExpressionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (!IsAssignedAtScriptScope(variableExpressionAst)) - { - return AstVisitAction.Continue; - } - - this.SymbolReferences.Add( - new SymbolReference( - SymbolType.Variable, - variableExpressionAst.Extent)); - - return AstVisitAction.Continue; - } - - private bool IsAssignedAtScriptScope(VariableExpressionAst variableExpressionAst) - { - Ast parent = variableExpressionAst.Parent; - if (!(parent is AssignmentStatementAst)) - { - return false; - } - - parent = parent.Parent; - if (parent == null || parent.Parent == null || parent.Parent.Parent == null) - { - return true; - } - - return false; - } - } - - /// - /// Visitor to find all the keys in Hashtable AST - /// - internal class FindHashtableSymbolsVisitor : AstVisitor - { - /// - /// List of symbols (keys) found in the hashtable - /// - public List SymbolReferences { get; private set; } - - /// - /// Initializes a new instance of FindHashtableSymbolsVisitor class - /// - public FindHashtableSymbolsVisitor() - { - SymbolReferences = new List(); - } - - /// - /// Adds keys in the input hashtable to the symbol reference - /// - public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) - { - if (hashtableAst.KeyValuePairs == null) - { - return AstVisitAction.Continue; - } - - foreach (var kvp in hashtableAst.KeyValuePairs) - { - var keyStrConstExprAst = kvp.Item1 as StringConstantExpressionAst; - if (keyStrConstExprAst != null) - { - IScriptExtent nameExtent = new ScriptExtent() - { - Text = keyStrConstExprAst.Value, - StartLineNumber = kvp.Item1.Extent.StartLineNumber, - EndLineNumber = kvp.Item2.Extent.EndLineNumber, - StartColumnNumber = kvp.Item1.Extent.StartColumnNumber, - EndColumnNumber = kvp.Item2.Extent.EndColumnNumber - }; - - SymbolType symbolType = SymbolType.HashtableKey; - - this.SymbolReferences.Add( - new SymbolReference( - symbolType, - nameExtent)); - - } - } - - return AstVisitAction.Continue; - } - } -} diff --git a/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs deleted file mode 100644 index 03628ee3e..000000000 --- a/src/PowerShellEditorServices/Language/FindSymbolsVisitor2.cs +++ /dev/null @@ -1,80 +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.Collections.Generic; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - // TODO: Restore this when we figure out how to support multiple - // PS versions in the new PSES-as-a-module world (issue #276) - - ///// - ///// The visitor used to find all the symbols (function and class defs) in the AST. - ///// - ///// - ///// Requires PowerShell v5 or higher - ///// - ///// - //internal class FindSymbolsVisitor2 : AstVisitor2 - //{ - // private FindSymbolsVisitor findSymbolsVisitor; - - // public List SymbolReferences - // { - // get - // { - // return this.findSymbolsVisitor.SymbolReferences; - // } - // } - - // public FindSymbolsVisitor2() - // { - // this.findSymbolsVisitor = new FindSymbolsVisitor(); - // } - - // /// - // /// Adds each function definition as a - // /// - // /// A functionDefinitionAst object in the script's AST - // /// A decision to stop searching if the right symbol was found, - // /// or a decision to continue if it wasn't found - // public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - // { - // return this.findSymbolsVisitor.VisitFunctionDefinition(functionDefinitionAst); - // } - - // /// - // /// Checks to see if this variable expression is the symbol we are looking for. - // /// - // /// A VariableExpressionAst object in the script's AST - // /// A decision to stop searching if the right symbol was found, - // /// or a decision to continue if it wasn't found - // public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - // { - // return this.findSymbolsVisitor.VisitVariableExpression(variableExpressionAst); - // } - - // public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) - // { - // IScriptExtent nameExtent = new ScriptExtent() - // { - // Text = configurationDefinitionAst.InstanceName.Extent.Text, - // StartLineNumber = configurationDefinitionAst.Extent.StartLineNumber, - // EndLineNumber = configurationDefinitionAst.Extent.EndLineNumber, - // StartColumnNumber = configurationDefinitionAst.Extent.StartColumnNumber, - // EndColumnNumber = configurationDefinitionAst.Extent.EndColumnNumber - // }; - - // this.findSymbolsVisitor.SymbolReferences.Add( - // new SymbolReference( - // SymbolType.Configuration, - // nameExtent)); - - // return AstVisitAction.Continue; - // } - //} -} - diff --git a/src/PowerShellEditorServices/Language/FoldingReference.cs b/src/PowerShellEditorServices/Language/FoldingReference.cs deleted file mode 100644 index 2b8a14502..000000000 --- a/src/PowerShellEditorServices/Language/FoldingReference.cs +++ /dev/null @@ -1,112 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// A class that holds the information for a foldable region of text in a document - /// - public class FoldingReference: IComparable - { - /// - /// The zero-based line number from where the folded range starts. - /// - public int StartLine { get; set; } - - /// - /// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. - /// - public int StartCharacter { get; set; } = 0; - - /// - /// The zero-based line number where the folded range ends. - /// - public int EndLine { get; set; } - - /// - /// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. - /// - public int EndCharacter { get; set; } = 0; - - /// - /// Describes the kind of the folding range such as `comment' or 'region'. - /// - public string Kind { get; set; } - - /// - /// A custom comparable method which can properly sort FoldingReference objects - /// - public int CompareTo(FoldingReference that) { - // Initially look at the start line - if (this.StartLine < that.StartLine) { return -1; } - if (this.StartLine > that.StartLine) { return 1; } - - // They have the same start line so now consider the end line. - // The biggest line range is sorted first - if (this.EndLine > that.EndLine) { return -1; } - if (this.EndLine < that.EndLine) { return 1; } - - // They have the same lines, but what about character offsets - if (this.StartCharacter < that.StartCharacter) { return -1; } - if (this.StartCharacter > that.StartCharacter) { return 1; } - if (this.EndCharacter < that.EndCharacter) { return -1; } - if (this.EndCharacter > that.EndCharacter) { return 1; } - - // They're the same range, but what about kind - return string.Compare(this.Kind, that.Kind); - } - } - - /// - /// A class that holds a list of FoldingReferences and ensures that when adding a reference that the - /// folding rules are obeyed, e.g. Only one fold per start line - /// - public class FoldingReferenceList - { - private readonly Dictionary references = new Dictionary(); - - /// - /// Return all references in the list - /// - public IEnumerable References - { - get - { - return references.Values; - } - } - - /// - /// Adds a FoldingReference to the list and enforces ordering rules e.g. Only one fold per start line - /// - public void SafeAdd(FoldingReference item) - { - if (item == null) { return; } - - // Only add the item if it hasn't been seen before or it's the largest range - if (references.TryGetValue(item.StartLine, out FoldingReference currentItem)) - { - if (currentItem.CompareTo(item) == 1) { references[item.StartLine] = item; } - } - else - { - references[item.StartLine] = item; - } - } - - /// - /// Helper method to easily convert the Dictionary Values into an array - /// - public FoldingReference[] ToArray() - { - var result = new FoldingReference[references.Count]; - references.Values.CopyTo(result, 0); - return result; - } - } -} diff --git a/src/PowerShellEditorServices/Language/FullScriptExtent.cs b/src/PowerShellEditorServices/Language/FullScriptExtent.cs deleted file mode 100644 index 3db5b9f5d..000000000 --- a/src/PowerShellEditorServices/Language/FullScriptExtent.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an IScriptExtent implementation that is aware of editor context - /// and can adjust to changes. - /// - public class FullScriptExtent : IScriptExtent - { - #region Properties - - /// - /// Gets the buffer range of the extent. - /// - public BufferRange BufferRange { get; private set; } - - /// - /// Gets the FileContext that this extent refers to. - /// - public FileContext FileContext { get; } - - /// - /// Gets the file path of the script file in which this extent is contained. - /// - public string File - { - get { return FileContext.Path; } - } - - /// - /// Gets the starting script position of the extent. - /// - public IScriptPosition StartScriptPosition - { - get { return new FullScriptPosition(FileContext, BufferRange.Start, StartOffset); } - } - - /// - /// Gets the ending script position of the extent. - /// - public IScriptPosition EndScriptPosition - { - get { return new FullScriptPosition(FileContext, BufferRange.End, EndOffset); } - } - - /// - /// Gets the starting line number of the extent. - /// - public int StartLineNumber - { - get { return BufferRange.Start.Line; } - } - - - /// - /// Gets the starting column number of the extent. - /// - public int StartColumnNumber - { - get { return BufferRange.Start.Column; } - } - - /// - /// Gets the ending line number of the extent. - /// - public int EndLineNumber - { - get { return BufferRange.End.Line; } - } - - /// - /// Gets the ending column number of the extent. - /// - public int EndColumnNumber - { - get { return BufferRange.End.Column; } - } - - /// - /// Gets the text that is contained within the extent. - /// - public string Text - { - get - { - // StartOffset can be > the length for the EOF token. - if (StartOffset > FileContext.scriptFile.Contents.Length) - { - return ""; - } - - return FileContext.GetText(BufferRange); - } - } - - /// - /// Gets the starting file offset of the extent. - /// - public int StartOffset { get; private set; } - - /// - /// Gets the ending file offset of the extent. - /// - public int EndOffset { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the FullScriptExtent class. - /// - /// The FileContext this extent refers to. - /// The buffer range this extent is located at. - public FullScriptExtent(FileContext fileContext, BufferRange bufferRange) - { - Validate.IsNotNull(nameof(fileContext), fileContext); - Validate.IsNotNull(nameof(bufferRange), bufferRange); - - BufferRange = bufferRange; - FileContext = fileContext; - - StartOffset = fileContext.scriptFile.GetOffsetAtPosition( - bufferRange.Start.Line, - bufferRange.Start.Column); - - EndOffset = fileContext.scriptFile.GetOffsetAtPosition( - bufferRange.End.Line, - bufferRange.End.Column); - } - - /// - /// Creates an new instance of the FullScriptExtent class. - /// - /// The FileContext this extent refers to. - /// The zero based offset this extent starts at. - /// The zero based offset this extent ends at. - public FullScriptExtent(FileContext fileContext, int startOffset, int endOffset) - { - Validate.IsNotNull(nameof(fileContext), fileContext); - Validate.IsNotNull(nameof(startOffset), startOffset); - Validate.IsNotNull(nameof(endOffset), endOffset); - - FileContext = fileContext; - StartOffset = startOffset; - EndOffset = endOffset; - BufferRange = fileContext.scriptFile.GetRangeBetweenOffsets(startOffset, endOffset); - } - - #endregion - - #region Public Methods - - /// - /// Return the text this extent refers to. - /// - public override string ToString() - { - return Text; - } - - /// - /// Moves the start and end positions of the extent by an offset. Can - /// be used to move forwards or backwards. - /// - /// The amount to move the extent. - public void AddOffset(int offset) { - StartOffset += offset; - EndOffset += offset; - - BufferRange = FileContext.scriptFile.GetRangeBetweenOffsets(StartOffset, EndOffset); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Language/FullScriptPosition.cs b/src/PowerShellEditorServices/Language/FullScriptPosition.cs deleted file mode 100644 index c9a24b6fd..000000000 --- a/src/PowerShellEditorServices/Language/FullScriptPosition.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Extensions; - -namespace Microsoft.PowerShell.EditorServices -{ - internal class FullScriptPosition : IScriptPosition - { - #region Fields - private readonly FileContext fileContext; - - private readonly BufferPosition bufferPosition; - - #endregion - - #region Properties - public string File - { - get { return fileContext.Path; } - } - public int LineNumber - { - get { return bufferPosition.Line; } - } - public int ColumnNumber - { - get { return bufferPosition.Column; } - } - public string Line - { - get { return fileContext.scriptFile.GetLine(LineNumber); } - } - public int Offset { get; } - - #endregion - - #region Constructors - - internal FullScriptPosition(FileContext context, BufferPosition position, int offset) - { - fileContext = context; - bufferPosition = position; - Offset = offset; - } - - #endregion - - - #region Public Methods - - public string GetFullScript() - { - return fileContext.GetText(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Language/GetDefinitionResult.cs b/src/PowerShellEditorServices/Language/GetDefinitionResult.cs deleted file mode 100644 index 753e027a1..000000000 --- a/src/PowerShellEditorServices/Language/GetDefinitionResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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 -{ - /// - /// A class to contain the found definition of a symbol. - /// It contains the symbol reference of the definition - /// - public class GetDefinitionResult - { - #region Properties - /// - /// Gets the symbolReference of the found definition - /// - public SymbolReference FoundDefinition { get; internal set; } - #endregion - - /// - /// Constructs an instance of a GetDefinitionResut - /// - /// The symbolRefernece for the found definition - public GetDefinitionResult(SymbolReference symRef) - { - FoundDefinition = symRef; - } - } -} diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs deleted file mode 100644 index 486306b58..000000000 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ /dev/null @@ -1,887 +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 Microsoft.PowerShell.EditorServices.Symbols; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Runtime.InteropServices; -using System.Security; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a high-level service for performing code completion and - /// navigation operations on PowerShell scripts. - /// - public class LanguageService - { - #region Private Fields - - const int DefaultWaitTimeoutMilliseconds = 5000; - - private readonly ILogger _logger; - - private readonly PowerShellContext _powerShellContext; - - private readonly Dictionary> _cmdletToAliasDictionary; - - private readonly Dictionary _aliasToCmdletDictionary; - - private readonly IDocumentSymbolProvider[] _documentSymbolProviders; - - private readonly SemaphoreSlim _aliasHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - private bool _areAliasesLoaded; - - private CompletionResults _mostRecentCompletions; - - private int _mostRecentRequestLine; - - private int _mostRecentRequestOffest; - - private string _mostRecentRequestFile; - - #endregion - - #region Constructors - - /// - /// Constructs an instance of the LanguageService class and uses - /// the given Runspace to execute language service operations. - /// - /// - /// The PowerShellContext in which language service operations will be executed. - /// - /// An ILogger implementation used for writing log messages. - public LanguageService( - PowerShellContext powerShellContext, - ILogger logger) - { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - - _powerShellContext = powerShellContext; - _logger = logger; - - _cmdletToAliasDictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); - _aliasToCmdletDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - _documentSymbolProviders = new IDocumentSymbolProvider[] - { - new ScriptDocumentSymbolProvider(powerShellContext.LocalPowerShellVersion.Version), - new PsdDocumentSymbolProvider(), - new PesterDocumentSymbolProvider() - }; - } - - #endregion - - #region Public Methods - - /// - /// Gets completions for a statement contained in the given - /// script file at the specified line and column position. - /// - /// - /// The script file in which completions will be gathered. - /// - /// - /// The 1-based line number at which completions will be gathered. - /// - /// - /// The 1-based column number at which completions will be gathered. - /// - /// - /// A CommandCompletion instance completions for the identified statement. - /// - public async Task GetCompletionsInFileAsync( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) - { - Validate.IsNotNull(nameof(scriptFile), scriptFile); - - // Get the offset at the specified position. This method - // will also validate the given position. - int fileOffset = - scriptFile.GetOffsetAtPosition( - lineNumber, - columnNumber); - - CommandCompletion commandCompletion = - await AstOperations.GetCompletionsAsync( - scriptFile.ScriptAst, - scriptFile.ScriptTokens, - fileOffset, - _powerShellContext, - _logger, - new CancellationTokenSource(DefaultWaitTimeoutMilliseconds).Token); - - if (commandCompletion == null) - { - return new CompletionResults(); - } - - try - { - CompletionResults completionResults = - CompletionResults.Create( - scriptFile, - commandCompletion); - - // save state of most recent completion - _mostRecentCompletions = completionResults; - _mostRecentRequestFile = scriptFile.Id; - _mostRecentRequestLine = lineNumber; - _mostRecentRequestOffest = columnNumber; - - return completionResults; - } - catch (ArgumentException e) - { - // Bad completion results could return an invalid - // replacement range, catch that here - _logger.Write( - LogLevel.Error, - $"Caught exception while trying to create CompletionResults:\n\n{e.ToString()}"); - - return new CompletionResults(); - } - } - - /// - /// Finds command completion details for the script given a file location - /// - /// The details and contents of a open script file - /// The name of the suggestion that needs details - /// CompletionResult object (contains information about the command completion) - public CompletionDetails GetCompletionDetailsInFile( - ScriptFile file, - string entryName) - { - if (!file.Id.Equals(_mostRecentRequestFile)) - { - return null; - } - - foreach (CompletionDetails completion in _mostRecentCompletions.Completions) - { - if (completion.CompletionText.Equals(entryName)) - { - return completion; - } - } - - // If we found no completions, return null - return null; - } - - /// - /// Finds the symbol in the script given a file location - /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The coulumn number of the cursor for the given script - /// A SymbolReference of the symbol found at the given location - /// or null if there is no symbol at that location - /// - public SymbolReference FindSymbolAtLocation( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) - { - SymbolReference symbolReference = - AstOperations.FindSymbolAtPosition( - scriptFile.ScriptAst, - lineNumber, - columnNumber); - - if (symbolReference != null) - { - symbolReference.FilePath = scriptFile.FilePath; - } - - return symbolReference; - } - - /// - /// Finds a function definition in the script given a file location - /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The coulumn number of the cursor for the given script - /// A SymbolReference of the symbol found at the given location - /// or null if there is no symbol at that location - /// - public SymbolReference FindFunctionDefinitionAtLocation( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) - { - SymbolReference symbolReference = - AstOperations.FindSymbolAtPosition( - scriptFile.ScriptAst, - lineNumber, - columnNumber, - includeFunctionDefinitions: true); - - if (symbolReference != null) - { - symbolReference.FilePath = scriptFile.FilePath; - } - - return symbolReference; - } - - /// - /// Finds the details of the symbol at the given script file location. - /// - /// The ScriptFile in which the symbol can be located. - /// The line number at which the symbol can be located. - /// The column number at which the symbol can be located. - /// - public async Task FindSymbolDetailsAtLocationAsync( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) - { - SymbolDetails symbolDetails = null; - SymbolReference symbolReference = - AstOperations.FindSymbolAtPosition( - scriptFile.ScriptAst, - lineNumber, - columnNumber); - - if (symbolReference == null) - { - // TODO #21: Return Result - return null; - } - - symbolReference.FilePath = scriptFile.FilePath; - symbolDetails = - await SymbolDetails.CreateAsync( - symbolReference, - _powerShellContext); - - return symbolDetails; - } - - /// - /// Finds all the symbols in a file. - /// - /// The ScriptFile in which the symbol can be located. - /// - public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) - { - Validate.IsNotNull(nameof(scriptFile), scriptFile); - - var foundOccurrences = new List(); - foreach (IDocumentSymbolProvider symbolProvider in _documentSymbolProviders) - { - foreach (SymbolReference reference in symbolProvider.ProvideDocumentSymbols(scriptFile)) - { - reference.SourceLine = scriptFile.GetLine(reference.ScriptRegion.StartLineNumber); - reference.FilePath = scriptFile.FilePath; - foundOccurrences.Add(reference); - } - } - - return new FindOccurrencesResult - { - FoundOccurrences = foundOccurrences - }; - } - - /// - /// Finds all the references of a symbol - /// - /// The symbol to find all references for - /// An array of scriptFiles too search for references in - /// The workspace that will be searched for symbols - /// FindReferencesResult - public async Task FindReferencesOfSymbolAsync( - SymbolReference foundSymbol, - ScriptFile[] referencedFiles, - Workspace workspace) - { - if (foundSymbol == null) - { - return null; - } - - int symbolOffset = referencedFiles[0].GetOffsetAtPosition( - foundSymbol.ScriptRegion.StartLineNumber, - foundSymbol.ScriptRegion.StartColumnNumber); - - // Make sure aliases have been loaded - await GetAliasesAsync(); - - // We want to look for references first in referenced files, hence we use ordered dictionary - // TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic - var fileMap = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? new OrderedDictionary() - : new OrderedDictionary(StringComparer.OrdinalIgnoreCase); - - foreach (ScriptFile scriptFile in referencedFiles) - { - fileMap[scriptFile.FilePath] = scriptFile; - } - - foreach (string filePath in workspace.EnumeratePSFiles()) - { - if (!fileMap.Contains(filePath)) - { - if (!workspace.TryGetFile(filePath, out ScriptFile scriptFile)) - { - // If we can't access the file for some reason, just ignore it - continue; - } - - fileMap[filePath] = scriptFile; - } - } - - var symbolReferences = new List(); - foreach (object fileName in fileMap.Keys) - { - var file = (ScriptFile)fileMap[fileName]; - await _aliasHandle.WaitAsync(); - try - { - - IEnumerable references = AstOperations.FindReferencesOfSymbol( - file.ScriptAst, - foundSymbol, - _cmdletToAliasDictionary, - _aliasToCmdletDictionary); - - foreach (SymbolReference reference in references) - { - try - { - reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); - } - catch (ArgumentOutOfRangeException e) - { - reference.SourceLine = string.Empty; - _logger.WriteException("Found reference is out of range in script file", e); - } - reference.FilePath = file.FilePath; - symbolReferences.Add(reference); - } - } - finally - { - _aliasHandle.Release(); - } - } - - return new FindReferencesResult - { - SymbolFileOffset = symbolOffset, - SymbolName = foundSymbol.SymbolName, - FoundReferences = symbolReferences - }; - } - - /// - /// Finds the definition of a symbol in the script file or any of the - /// files that it references. - /// - /// The initial script file to be searched for the symbol's definition. - /// The symbol for which a definition will be found. - /// The Workspace to which the ScriptFile belongs. - /// The resulting GetDefinitionResult for the symbol's definition. - public async Task GetDefinitionOfSymbolAsync( - ScriptFile sourceFile, - SymbolReference foundSymbol, - Workspace workspace) - { - Validate.IsNotNull(nameof(sourceFile), sourceFile); - Validate.IsNotNull(nameof(foundSymbol), foundSymbol); - Validate.IsNotNull(nameof(workspace), workspace); - - ScriptFile[] referencedFiles = - workspace.ExpandScriptReferences( - sourceFile); - - var filesSearched = new HashSet(StringComparer.OrdinalIgnoreCase); - - // look through the referenced files until definition is found - // or there are no more file to look through - SymbolReference foundDefinition = null; - foreach (ScriptFile scriptFile in referencedFiles) - { - foundDefinition = - AstOperations.FindDefinitionOfSymbol( - scriptFile.ScriptAst, - foundSymbol); - - filesSearched.Add(scriptFile.FilePath); - if (foundDefinition != null) - { - foundDefinition.FilePath = scriptFile.FilePath; - break; - } - - if (foundSymbol.SymbolType == SymbolType.Function) - { - // Dot-sourcing is parsed as a "Function" Symbol. - string dotSourcedPath = GetDotSourcedPath(foundSymbol, workspace, scriptFile); - if (scriptFile.FilePath == dotSourcedPath) - { - foundDefinition = new SymbolReference(SymbolType.Function, foundSymbol.SymbolName, scriptFile.ScriptAst.Extent, scriptFile.FilePath); - break; - } - } - } - - // if the definition the not found in referenced files - // look for it in all the files in the workspace - if (foundDefinition == null) - { - // Get a list of all powershell files in the workspace path - IEnumerable allFiles = workspace.EnumeratePSFiles(); - foreach (string file in allFiles) - { - if (filesSearched.Contains(file)) - { - continue; - } - - Token[] tokens = null; - ParseError[] parseErrors = null; - foundDefinition = - AstOperations.FindDefinitionOfSymbol( - Parser.ParseFile(file, out tokens, out parseErrors), - foundSymbol); - - filesSearched.Add(file); - if (foundDefinition != null) - { - foundDefinition.FilePath = file; - break; - } - - } - } - - // if definition is not found in file in the workspace - // look for it in the builtin commands - if (foundDefinition == null) - { - CommandInfo cmdInfo = - await CommandHelpers.GetCommandInfoAsync( - foundSymbol.SymbolName, - _powerShellContext); - - foundDefinition = - FindDeclarationForBuiltinCommand( - cmdInfo, - foundSymbol, - workspace); - } - - return foundDefinition != null ? - new GetDefinitionResult(foundDefinition) : - null; - } - - /// - /// Gets a path from a dot-source symbol. - /// - /// The symbol representing the dot-source expression. - /// The current workspace - /// The script file containing the symbol - /// - private static string GetDotSourcedPath(SymbolReference symbol, Workspace workspace, ScriptFile scriptFile) - { - string cleanedUpSymbol = PathUtils.NormalizePathSeparators(symbol.SymbolName.Trim('\'', '"')); - string psScriptRoot = Path.GetDirectoryName(scriptFile.FilePath); - return workspace.ResolveRelativeScriptPath(psScriptRoot, - Regex.Replace(cleanedUpSymbol, @"\$PSScriptRoot|\${PSScriptRoot}", psScriptRoot, RegexOptions.IgnoreCase)); - } - - /// - /// Finds all the occurences of a symbol in the script given a file location - /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The coulumn number of the cursor for the given script - /// FindOccurrencesResult - public FindOccurrencesResult FindOccurrencesInFile( - ScriptFile file, - int lineNumber, - int columnNumber) - { - SymbolReference foundSymbol = - AstOperations.FindSymbolAtPosition( - file.ScriptAst, - lineNumber, - columnNumber); - - if (foundSymbol == null) - { - return null; - } - - // find all references, and indicate that looking for aliases is not needed - IEnumerable symbolOccurrences = - AstOperations - .FindReferencesOfSymbol( - file.ScriptAst, - foundSymbol, - false); - - return new FindOccurrencesResult - { - FoundOccurrences = symbolOccurrences - }; - } - - /// - /// Finds the parameter set hints of a specific command (determined by a given file location) - /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The coulumn number of the cursor for the given script - /// ParameterSetSignatures - public async Task FindParameterSetsInFileAsync( - ScriptFile file, - int lineNumber, - int columnNumber) - { - SymbolReference foundSymbol = - AstOperations.FindCommandAtPosition( - file.ScriptAst, - lineNumber, - columnNumber); - - if (foundSymbol == null) - { - return null; - } - - CommandInfo commandInfo = - await CommandHelpers.GetCommandInfoAsync( - foundSymbol.SymbolName, - _powerShellContext); - - if (commandInfo == null) - { - return null; - } - - try - { - IEnumerable commandParamSets = commandInfo.ParameterSets; - return new ParameterSetSignatures(commandParamSets, foundSymbol); - } - catch (RuntimeException e) - { - // A RuntimeException will be thrown when an invalid attribute is - // on a parameter binding block and then that command/script has - // its signatures resolved by typing it into a script. - _logger.WriteException("RuntimeException encountered while accessing command parameter sets", e); - - return null; - } - catch (InvalidOperationException) - { - // For some commands there are no paramsets (like applications). Until - // the valid command types are better understood, catch this exception - // which gets raised when there are no ParameterSets for the command type. - return null; - } - } - - /// - /// Gets the smallest statment ast that contains the given script position as - /// indicated by lineNumber and columnNumber parameters. - /// - /// Open script file. - /// 1-based line number of the position. - /// 1-based column number of the position. - /// - public ScriptRegion FindSmallestStatementAstRegion( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) - { - Ast ast = FindSmallestStatementAst(scriptFile, lineNumber, columnNumber); - if (ast == null) - { - return null; - } - - return ScriptRegion.Create(ast.Extent); - } - - /// - /// Gets the function defined on a given line. - /// - /// Open script file. - /// The 1 based line on which to look for function definition. - /// If found, returns the function definition on the given line. Otherwise, returns null. - public FunctionDefinitionAst GetFunctionDefinitionAtLine( - ScriptFile scriptFile, - int lineNumber) - { - Ast functionDefinitionAst = scriptFile.ScriptAst.Find( - ast => ast is FunctionDefinitionAst && ast.Extent.StartLineNumber == lineNumber, - true); - - return functionDefinitionAst as FunctionDefinitionAst; - } - - /// - /// Finds a function definition that follows or contains the given line number. - /// - /// Open script file. - /// The 1 based line on which to look for function definition. - /// - /// If found, returns the function definition, otherwise, returns null. - public FunctionDefinitionAst GetFunctionDefinitionForHelpComment( - ScriptFile scriptFile, - int lineNumber, - out string helpLocation) - { - // check if the next line contains a function definition - FunctionDefinitionAst funcDefnAst = GetFunctionDefinitionAtLine(scriptFile, lineNumber + 1); - if (funcDefnAst != null) - { - helpLocation = "before"; - return funcDefnAst; - } - - // find all the script definitions that contain the line `lineNumber` - IEnumerable foundAsts = scriptFile.ScriptAst.FindAll( - ast => - { - var fdAst = ast as FunctionDefinitionAst; - if (fdAst == null) - { - return false; - } - - return fdAst.Body.Extent.StartLineNumber < lineNumber && - fdAst.Body.Extent.EndLineNumber > lineNumber; - }, - true); - - if (foundAsts == null || !foundAsts.Any()) - { - helpLocation = null; - return null; - } - - // of all the function definitions found, return the innermost function - // definition that contains `lineNumber` - foreach (FunctionDefinitionAst foundAst in foundAsts.Cast()) - { - if (funcDefnAst == null) - { - funcDefnAst = foundAst; - continue; - } - - if (funcDefnAst.Extent.StartOffset >= foundAst.Extent.StartOffset - && funcDefnAst.Extent.EndOffset <= foundAst.Extent.EndOffset) - { - funcDefnAst = foundAst; - } - } - - // TODO use tokens to check for non empty character instead of just checking for line offset - if (funcDefnAst.Body.Extent.StartLineNumber == lineNumber - 1) - { - helpLocation = "begin"; - return funcDefnAst; - } - - if (funcDefnAst.Body.Extent.EndLineNumber == lineNumber + 1) - { - helpLocation = "end"; - return funcDefnAst; - } - - // If we didn't find a function definition, then return null - helpLocation = null; - return null; - } - - #endregion - - #region Private Fields - - /// - /// Gets all aliases found in the runspace - /// - private async Task GetAliasesAsync() - { - if (_areAliasesLoaded) - { - return; - } - - await _aliasHandle.WaitAsync(); - try - { - if (_powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - _areAliasesLoaded = true; - return; - } - - var aliases = await _powerShellContext.ExecuteCommandAsync( - new PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Get-Command") - .AddParameter("CommandType", CommandTypes.Alias), - sendOutputToHost: false, - sendErrorToHost: false); - - foreach (AliasInfo aliasInfo in aliases) - { - // Using Get-Command will obtain aliases from modules not yet loaded, - // these aliases will not have a definition. - if (string.IsNullOrEmpty(aliasInfo.Definition)) - { - continue; - } - - if (!_cmdletToAliasDictionary.ContainsKey(aliasInfo.Definition)) - { - _cmdletToAliasDictionary.Add(aliasInfo.Definition, new List { aliasInfo.Name }); - } - else - { - _cmdletToAliasDictionary[aliasInfo.Definition].Add(aliasInfo.Name); - } - - _aliasToCmdletDictionary.Add(aliasInfo.Name, aliasInfo.Definition); - } - - _areAliasesLoaded = true; - } - catch (PSNotSupportedException e) - { - _logger.Write( - LogLevel.Warning, - $"Caught PSNotSupportedException while attempting to get aliases from remote session:\n\n{e.ToString()}"); - - // Prevent the aliases from being fetched again - no point if the remote doesn't support InvokeCommand. - _areAliasesLoaded = true; - } - catch (TaskCanceledException) - { - // The wait for a RunspaceHandle has timed out, skip aliases for now - } - finally - { - _aliasHandle.Release(); - } - } - - private ScriptFile[] GetBuiltinCommandScriptFiles( - PSModuleInfo moduleInfo, - Workspace workspace) - { - if (moduleInfo == null) - { - return new ScriptFile[0]; - } - - string modPath = moduleInfo.Path; - List scriptFiles = new List(); - ScriptFile newFile; - - // find any files where the moduleInfo's path ends with ps1 or psm1 - // and add it to allowed script files - if (modPath.EndsWith(@".ps1") || modPath.EndsWith(@".psm1")) - { - newFile = workspace.GetFile(modPath); - newFile.IsAnalysisEnabled = false; - scriptFiles.Add(newFile); - } - if (moduleInfo.NestedModules.Count > 0) - { - foreach (PSModuleInfo nestedInfo in moduleInfo.NestedModules) - { - string nestedModPath = nestedInfo.Path; - if (nestedModPath.EndsWith(@".ps1") || nestedModPath.EndsWith(@".psm1")) - { - newFile = workspace.GetFile(nestedModPath); - newFile.IsAnalysisEnabled = false; - scriptFiles.Add(newFile); - } - } - } - - return scriptFiles.ToArray(); - } - - private SymbolReference FindDeclarationForBuiltinCommand( - CommandInfo commandInfo, - SymbolReference foundSymbol, - Workspace workspace) - { - if (commandInfo == null) - { - return null; - } - - ScriptFile[] nestedModuleFiles = - GetBuiltinCommandScriptFiles( - commandInfo.Module, - workspace); - - SymbolReference foundDefinition = null; - foreach (ScriptFile nestedModuleFile in nestedModuleFiles) - { - foundDefinition = AstOperations.FindDefinitionOfSymbol( - nestedModuleFile.ScriptAst, - foundSymbol); - - if (foundDefinition != null) - { - foundDefinition.FilePath = nestedModuleFile.FilePath; - break; - } - } - - return foundDefinition; - } - - private Ast FindSmallestStatementAst(ScriptFile scriptFile, int lineNumber, int columnNumber) - { - IEnumerable asts = scriptFile.ScriptAst.FindAll(ast => - { - return ast is StatementAst && ast.Extent.Contains(lineNumber, columnNumber); - }, true); - - // Find the Ast with the smallest extent - Ast minAst = scriptFile.ScriptAst; - foreach (Ast ast in asts) - { - if (ast.Extent.ExtentWidthComparer(minAst.Extent) == -1) - { - minAst = ast; - } - } - - return minAst; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Language/ParameterSetSignatures.cs b/src/PowerShellEditorServices/Language/ParameterSetSignatures.cs deleted file mode 100644 index 68cd8279d..000000000 --- a/src/PowerShellEditorServices/Language/ParameterSetSignatures.cs +++ /dev/null @@ -1,150 +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.Collections.Generic; -using System.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// A class for containing the commandName, the command's - /// possible signatures, and the script extent of the command - /// - public class ParameterSetSignatures - { - #region Properties - /// - /// Gets the name of the command - /// - public string CommandName { get; internal set; } - - /// - /// Gets the collection of signatures for the command - /// - public ParameterSetSignature[] Signatures { get; internal set; } - - /// - /// Gets the script extent of the command - /// - public ScriptRegion ScriptRegion { get; internal set; } - #endregion - - /// - /// Constructs an instance of a ParameterSetSignatures object - /// - /// Collection of parameter set info - /// The SymbolReference of the command - public ParameterSetSignatures(IEnumerable commandInfoSet, SymbolReference foundSymbol) - { - List paramSetSignatures = new List(); - foreach (CommandParameterSetInfo setInfo in commandInfoSet) - { - paramSetSignatures.Add(new ParameterSetSignature(setInfo)); - } - Signatures = paramSetSignatures.ToArray(); - CommandName = foundSymbol.ScriptRegion.Text; - ScriptRegion = foundSymbol.ScriptRegion; - } - } - - /// - /// A class for containing the signature text and the collection of parameters for a signature - /// - public class ParameterSetSignature - { - private static HashSet commonParameterNames = - new HashSet - { - "Verbose", - "Debug", - "ErrorAction", - "WarningAction", - "InformationAction", - "ErrorVariable", - "WarningVariable", - "InformationVariable", - "OutVariable", - "OutBuffer", - "PipelineVariable", - }; - - #region Properties - /// - /// Gets the signature text - /// - public string SignatureText { get; internal set; } - - /// - /// Gets the collection of parameters for the signature - /// - public IEnumerable Parameters { get; internal set; } - #endregion - - /// - /// Constructs an instance of a ParameterSetSignature - /// - /// Collection of parameter info - public ParameterSetSignature(CommandParameterSetInfo commandParamInfoSet) - { - List parameterInfo = new List(); - foreach (CommandParameterInfo commandParameterInfo in commandParamInfoSet.Parameters) - { - if (!commonParameterNames.Contains(commandParameterInfo.Name)) - { - parameterInfo.Add(new ParameterInfo(commandParameterInfo)); - } - } - - SignatureText = commandParamInfoSet.ToString(); - Parameters = parameterInfo.ToArray(); - } - } - - /// - /// A class for containing the parameter info of a parameter - /// - public class ParameterInfo - { - #region Properties - /// - /// Gets the name of the parameter - /// - public string Name { get; internal set; } - - /// - /// Gets the type of the parameter - /// - public string ParameterType { get; internal set; } - - /// - /// Gets the position of the parameter - /// - public int Position { get; internal set; } - - /// - /// Gets a boolean for whetheer or not the parameter is required - /// - public bool IsMandatory { get; internal set; } - - /// - /// Gets the help message of the parameter - /// - public string HelpMessage { get; internal set; } - #endregion - - /// - /// Constructs an instance of a ParameterInfo object - /// - /// Parameter info of the parameter - public ParameterInfo(CommandParameterInfo parameterInfo) - { - this.Name = "-" + parameterInfo.Name; - this.ParameterType = parameterInfo.ParameterType.FullName; - this.Position = parameterInfo.Position; - this.IsMandatory = parameterInfo.IsMandatory; - this.HelpMessage = parameterInfo.HelpMessage; - } - } -} diff --git a/src/PowerShellEditorServices/Language/ScriptExtent.cs b/src/PowerShellEditorServices/Language/ScriptExtent.cs deleted file mode 100644 index 58f3ab84a..000000000 --- a/src/PowerShellEditorServices/Language/ScriptExtent.cs +++ /dev/null @@ -1,109 +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.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a default IScriptExtent implementation - /// containing details about a section of script content - /// in a file. - /// - public class ScriptExtent : IScriptExtent - { - #region Properties - - /// - /// Gets the file path of the script file in which this extent is contained. - /// - public string File - { - get; - set; - } - - /// - /// Gets or sets the starting column number of the extent. - /// - public int StartColumnNumber - { - get; - set; - } - - /// - /// Gets or sets the starting line number of the extent. - /// - public int StartLineNumber - { - get; - set; - } - - /// - /// Gets or sets the starting file offset of the extent. - /// - public int StartOffset - { - get; - set; - } - - /// - /// Gets or sets the starting script position of the extent. - /// - public IScriptPosition StartScriptPosition - { - get { throw new NotImplementedException(); } - } - /// - /// Gets or sets the text that is contained within the extent. - /// - public string Text - { - get; - set; - } - - /// - /// Gets or sets the ending column number of the extent. - /// - public int EndColumnNumber - { - get; - set; - } - - /// - /// Gets or sets the ending line number of the extent. - /// - public int EndLineNumber - { - get; - set; - } - - /// - /// Gets or sets the ending file offset of the extent. - /// - public int EndOffset - { - get; - set; - } - - /// - /// Gets the ending script position of the extent. - /// - public IScriptPosition EndScriptPosition - { - get { throw new NotImplementedException(); } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Language/SymbolDetails.cs b/src/PowerShellEditorServices/Language/SymbolDetails.cs deleted file mode 100644 index e0971c2ec..000000000 --- a/src/PowerShellEditorServices/Language/SymbolDetails.cs +++ /dev/null @@ -1,95 +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.Diagnostics; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides detailed information for a given symbol. - /// - [DebuggerDisplay("SymbolReference = {SymbolReference.SymbolType}/{SymbolReference.SymbolName}, DisplayString = {DisplayString}")] - public class SymbolDetails - { - #region Properties - - /// - /// Gets the original symbol reference which was used to gather details. - /// - public SymbolReference SymbolReference { get; private set; } - - /// - /// Gets the display string for this symbol. - /// - public string DisplayString { get; private set; } - - /// - /// Gets the documentation string for this symbol. Returns an - /// empty string if the symbol has no documentation. - /// - public string Documentation { get; private set; } - - #endregion - - #region Constructors - - static internal async Task CreateAsync( - SymbolReference symbolReference, - PowerShellContext powerShellContext) - { - SymbolDetails symbolDetails = new SymbolDetails(); - symbolDetails.SymbolReference = symbolReference; - - // If the symbol is a command, get its documentation - if (symbolReference.SymbolType == SymbolType.Function) - { - CommandInfo commandInfo = - await CommandHelpers.GetCommandInfoAsync( - symbolReference.SymbolName, - powerShellContext); - - if (commandInfo != null) - { - symbolDetails.Documentation = - await CommandHelpers.GetCommandSynopsisAsync( - commandInfo, - powerShellContext); - - if (commandInfo.CommandType == CommandTypes.Application) - { - symbolDetails.DisplayString = "(application) " + symbolReference.SymbolName; - } - else - { - symbolDetails.DisplayString = "function " + symbolReference.SymbolName; - } - } - else - { - // Command information can't be loaded. This is likely due to - // the symbol being a function that is defined in a file that - // hasn't been loaded in the runspace yet. - symbolDetails.DisplayString = "function " + symbolReference.SymbolName; - } - } - else if (symbolReference.SymbolType == SymbolType.Parameter) - { - // TODO: Get parameter help - symbolDetails.DisplayString = "(parameter) " + symbolReference.SymbolName; - } - else if (symbolReference.SymbolType == SymbolType.Variable) - { - symbolDetails.DisplayString = symbolReference.SymbolName; - } - - return symbolDetails; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Language/SymbolReference.cs b/src/PowerShellEditorServices/Language/SymbolReference.cs deleted file mode 100644 index af0551a9e..000000000 --- a/src/PowerShellEditorServices/Language/SymbolReference.cs +++ /dev/null @@ -1,89 +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.Diagnostics; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// A class that holds the type, name, script extent, and source line of a symbol - /// - [DebuggerDisplay("SymbolType = {SymbolType}, SymbolName = {SymbolName}")] - public class SymbolReference - { - #region Properties - - /// - /// Gets the symbol's type - /// - public SymbolType SymbolType { get; private set; } - - /// - /// Gets the name of the symbol - /// - public string SymbolName { get; private set; } - - /// - /// Gets the script extent of the symbol - /// - public ScriptRegion ScriptRegion { get; private set; } - - /// - /// Gets the contents of the line the given symbol is on - /// - public string SourceLine { get; internal set; } - - /// - /// Gets the path of the file in which the symbol was found. - /// - public string FilePath { get; internal set; } - - #endregion - - /// - /// Constructs and instance of a SymbolReference - /// - /// The higher level type of the symbol - /// The name of the symbol - /// The script extent of the symbol - /// The file path of the symbol - /// The line contents of the given symbol (defaults to empty string) - public SymbolReference( - SymbolType symbolType, - string symbolName, - IScriptExtent scriptExtent, - string filePath = "", - string sourceLine = "") - { - // TODO: Verify params - this.SymbolType = symbolType; - this.SymbolName = symbolName; - this.ScriptRegion = ScriptRegion.Create(scriptExtent); - this.FilePath = filePath; - this.SourceLine = sourceLine; - - // TODO: Make sure end column number usage is correct - - // Build the display string - //this.DisplayString = - // string.Format( - // "{0} {1}") - } - - /// - /// Constructs and instance of a SymbolReference - /// - /// The higher level type of the symbol - /// The script extent of the symbol - /// The file path of the symbol - /// The line contents of the given symbol (defaults to empty string) - public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent, string filePath = "", string sourceLine = "") - : this(symbolType, scriptExtent.Text, scriptExtent, filePath, sourceLine) - { - } - } -} diff --git a/src/PowerShellEditorServices/Language/SymbolType.cs b/src/PowerShellEditorServices/Language/SymbolType.cs deleted file mode 100644 index 2dba9a0a0..000000000 --- a/src/PowerShellEditorServices/Language/SymbolType.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -// 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 -{ - /// - /// A way to define symbols on a higher level - /// - public enum SymbolType - { - /// - /// The symbol type is unknown - /// - Unknown = 0, - - /// - /// The symbol is a vairable - /// - Variable, - - /// - /// The symbol is a function - /// - Function, - - /// - /// The symbol is a parameter - /// - Parameter, - - /// - /// The symbol is a DSC configuration - /// - Configuration, - - /// - /// The symbol is a workflow - /// - Workflow, - - /// - /// The symbol is a hashtable key - /// - HashtableKey - } -} diff --git a/src/PowerShellEditorServices/Language/TokenOperations.cs b/src/PowerShellEditorServices/Language/TokenOperations.cs deleted file mode 100644 index e62dca68c..000000000 --- a/src/PowerShellEditorServices/Language/TokenOperations.cs +++ /dev/null @@ -1,215 +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.Collections.Generic; -using System.Management.Automation.Language; -using System.Text.RegularExpressions; - -namespace Microsoft.PowerShell.EditorServices -{ - - /// - /// Provides common operations for the tokens of a parsed script. - /// - internal static class TokenOperations - { - // Region kinds to align with VSCode's region kinds - private const string RegionKindComment = "comment"; - private const string RegionKindRegion = "region"; - private const string RegionKindNone = null; - - // These regular expressions are used to match lines which mark the start and end of region comment in a PowerShell - // script. They are based on the defaults in the VS Code Language Configuration at; - // https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31 - // https://github.com/Microsoft/vscode/issues/49070 - static private readonly Regex s_startRegionTextRegex = new Regex( - @"^\s*#[rR]egion\b", RegexOptions.Compiled); - static private readonly Regex s_endRegionTextRegex = new Regex( - @"^\s*#[eE]nd[rR]egion\b", RegexOptions.Compiled); - - /// - /// Extracts all of the unique foldable regions in a script given the list tokens - /// - internal static FoldingReferenceList FoldableReferences( - Token[] tokens) - { - var refList = new FoldingReferenceList(); - - Stack tokenCurlyStack = new Stack(); - Stack tokenParenStack = new Stack(); - foreach (Token token in tokens) - { - switch (token.Kind) - { - // Find matching braces { -> } - // Find matching hashes @{ -> } - case TokenKind.LCurly: - case TokenKind.AtCurly: - tokenCurlyStack.Push(token); - break; - - case TokenKind.RCurly: - if (tokenCurlyStack.Count > 0) - { - refList.SafeAdd(CreateFoldingReference(tokenCurlyStack.Pop(), token, RegionKindNone)); - } - break; - - // Find matching parentheses ( -> ) - // Find matching array literals @( -> ) - // Find matching subexpressions $( -> ) - case TokenKind.LParen: - case TokenKind.AtParen: - case TokenKind.DollarParen: - tokenParenStack.Push(token); - break; - - case TokenKind.RParen: - if (tokenParenStack.Count > 0) - { - refList.SafeAdd(CreateFoldingReference(tokenParenStack.Pop(), token, RegionKindNone)); - } - break; - - // Find contiguous here strings @' -> '@ - // Find unopinionated variable names ${ \n \n } - // Find contiguous expandable here strings @" -> "@ - case TokenKind.HereStringLiteral: - case TokenKind.Variable: - case TokenKind.HereStringExpandable: - if (token.Extent.StartLineNumber != token.Extent.EndLineNumber) - { - refList.SafeAdd(CreateFoldingReference(token, token, RegionKindNone)); - } - break; - } - } - - // Find matching comment regions #region -> #endregion - // Given a list of tokens, find the tokens that are comments and - // the comment text is either `#region` or `#endregion`, and then use a stack to determine - // the ranges they span - // - // Find blocks of line comments # comment1\n# comment2\n... - // Finding blocks of comment tokens is more complicated as the newline characters are not - // classed as comments. To workaround this we search for valid block comments (See IsBlockCmment) - // and then determine contiguous line numbers from there - // - // Find comments regions <# -> #> - // Match the token start and end of kind TokenKind.Comment - var tokenCommentRegionStack = new Stack(); - Token blockStartToken = null; - int blockNextLine = -1; - - for (int index = 0; index < tokens.Length; index++) - { - Token token = tokens[index]; - if (token.Kind != TokenKind.Comment) { continue; } - - // Processing for comment regions <# -> #> - if (token.Extent.StartLineNumber != token.Extent.EndLineNumber) - { - refList.SafeAdd(CreateFoldingReference(token, token, RegionKindComment)); - continue; - } - - if (!IsBlockComment(index, tokens)) { continue; } - - // Regex's are very expensive. Use them sparingly! - // Processing for #region -> #endregion - if (s_startRegionTextRegex.IsMatch(token.Text)) - { - tokenCommentRegionStack.Push(token); - continue; - } - if (s_endRegionTextRegex.IsMatch(token.Text)) - { - // Mismatched regions in the script can cause bad stacks. - if (tokenCommentRegionStack.Count > 0) - { - refList.SafeAdd(CreateFoldingReference(tokenCommentRegionStack.Pop(), token, RegionKindRegion)); - } - continue; - } - - // If it's neither a start or end region then it could be block line comment - // Processing for blocks of line comments # comment1\n# comment2\n... - int thisLine = token.Extent.StartLineNumber - 1; - if ((blockStartToken != null) && (thisLine != blockNextLine)) - { - refList.SafeAdd(CreateFoldingReference(blockStartToken, blockNextLine - 1, RegionKindComment)); - blockStartToken = token; - } - if (blockStartToken == null) { blockStartToken = token; } - blockNextLine = thisLine + 1; - } - - // If we exit the token array and we're still processing comment lines, then the - // comment block simply ends at the end of document - if (blockStartToken != null) - { - refList.SafeAdd(CreateFoldingReference(blockStartToken, blockNextLine - 1, RegionKindComment)); - } - - return refList; - } - - /// - /// Creates an instance of a FoldingReference object from a start and end langauge Token - /// Returns null if the line range is invalid - /// - static private FoldingReference CreateFoldingReference( - Token startToken, - Token endToken, - string matchKind) - { - if (endToken.Extent.EndLineNumber == startToken.Extent.StartLineNumber) { return null; } - // Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions - return new FoldingReference { - StartLine = startToken.Extent.StartLineNumber - 1, - StartCharacter = startToken.Extent.StartColumnNumber - 1, - EndLine = endToken.Extent.EndLineNumber - 1, - EndCharacter = endToken.Extent.EndColumnNumber - 1, - Kind = matchKind - }; - } - - /// - /// Creates an instance of a FoldingReference object from a start token and an end line - /// Returns null if the line range is invalid - /// - static private FoldingReference CreateFoldingReference( - Token startToken, - int endLine, - string matchKind) - { - if (endLine == (startToken.Extent.StartLineNumber - 1)) { return null; } - // Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions - return new FoldingReference { - StartLine = startToken.Extent.StartLineNumber - 1, - StartCharacter = startToken.Extent.StartColumnNumber - 1, - EndLine = endLine, - EndCharacter = 0, - Kind = matchKind - }; - } - - /// - /// Returns true if a Token is a block comment; - /// - Must be a TokenKind.comment - /// - Must be preceeded by TokenKind.NewLine - /// - Token text must start with a '#'.false This is because comment regions - /// start with '<#' but have the same TokenKind - /// - static private bool IsBlockComment(int index, Token[] tokens) { - Token thisToken = tokens[index]; - if (thisToken.Kind != TokenKind.Comment) { return false; } - if (index == 0) { return true; } - if (tokens[index - 1].Kind != TokenKind.NewLine) { return false; } - return thisToken.Text.StartsWith("#"); - } - } -} diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj deleted file mode 100644 index f4b5be263..000000000 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - PowerShell Editor Services - Provides common PowerShell editor capabilities as a .NET library. - netstandard2.0 - Microsoft.PowerShell.EditorServices - Latest - - - - 1591,1573,1572 - bin\$(TargetFramework)\$(Configuration)\Microsoft.PowerShell.EditorServices.xml - - - - - - - - - - - - - - - $(DefineConstants);RELEASE - - diff --git a/src/PowerShellEditorServices/Properties/AssemblyInfo.cs b/src/PowerShellEditorServices/Properties/AssemblyInfo.cs deleted file mode 100644 index d7cc1beb5..000000000 --- a/src/PowerShellEditorServices/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +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.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Protocol")] -[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Test")] -[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Test.Shared")] - diff --git a/src/PowerShellEditorServices/Providers/FeatureProviderBase.cs b/src/PowerShellEditorServices/Providers/FeatureProviderBase.cs deleted file mode 100644 index ecf8e8ce2..000000000 --- a/src/PowerShellEditorServices/Providers/FeatureProviderBase.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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 -{ - /// - /// Provides a base implementation of IFeatureProvider. - /// - public abstract class FeatureProviderBase : IFeatureProvider - { - /// - /// Gets the provider class type's FullName as the - /// ProviderId. - /// - public string ProviderId => this.GetType().FullName; - } -} diff --git a/src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs b/src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs deleted file mode 100644 index fd4e1b1c9..000000000 --- a/src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs +++ /dev/null @@ -1,46 +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.Collections; -using System.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a default implementation of IFeatureProviderCollection. - /// - public class FeatureProviderCollection : IFeatureProviderCollection - where TProvider : IFeatureProvider - { - #region Private Fields - - private List providerList = new List(); - - #endregion - - #region IFeatureProviderCollection Implementation - - void IFeatureProviderCollection.Add(TProvider provider) - { - if (!this.providerList.Contains(provider)) - { - this.providerList.Add(provider); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.providerList.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.providerList.GetEnumerator(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Providers/IFeatureProvider.cs b/src/PowerShellEditorServices/Providers/IFeatureProvider.cs deleted file mode 100644 index bea42b821..000000000 --- a/src/PowerShellEditorServices/Providers/IFeatureProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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 -{ - /// - /// Defines the contract for a feature provider, particularly for provider identification. - /// - public interface IFeatureProvider - { - /// - /// Specifies a unique identifier for the feature provider, typically a - /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" - /// - string ProviderId { get; } - } -} diff --git a/src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs b/src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs deleted file mode 100644 index 29351a4a4..000000000 --- a/src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs +++ /dev/null @@ -1,22 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Defines the contract for a collection of provider implementations. - /// - public interface IFeatureProviderCollection : IEnumerable - where TProvider : IFeatureProvider - { - /// - /// Adds a provider to the collection. - /// - /// The provider to be added. - void Add(TProvider provider); - } -} diff --git a/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs deleted file mode 100644 index 998b58a79..000000000 --- a/src/PowerShellEditorServices/Session/Capabilities/DscBreakpointCapability.cs +++ /dev/null @@ -1,165 +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.Linq; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Session.Capabilities -{ - using Microsoft.PowerShell.EditorServices.Utility; - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Management.Automation; - - internal class DscBreakpointCapability : IRunspaceCapability - { - private string[] dscResourceRootPaths = new string[0]; - - private Dictionary breakpointsPerFile = - new Dictionary(); - - public async Task> SetLineBreakpointsAsync( - PowerShellContext powerShellContext, - string scriptPath, - BreakpointDetails[] breakpoints) - { - List resultBreakpointDetails = - new List(); - - // We always get the latest array of breakpoint line numbers - // so store that for future use - if (breakpoints.Length > 0) - { - // Set the breakpoints for this scriptPath - this.breakpointsPerFile[scriptPath] = - breakpoints.Select(b => b.LineNumber).ToArray(); - } - else - { - // No more breakpoints for this scriptPath, remove it - this.breakpointsPerFile.Remove(scriptPath); - } - - string hashtableString = - string.Join( - ", ", - this.breakpointsPerFile - .Select(file => $"@{{Path=\"{file.Key}\";Line=@({string.Join(",", file.Value)})}}")); - - // Run Enable-DscDebug as a script because running it as a PSCommand - // causes an error which states that the Breakpoint parameter has not - // been passed. - await powerShellContext.ExecuteScriptStringAsync( - hashtableString.Length > 0 - ? $"Enable-DscDebug -Breakpoint {hashtableString}" - : "Disable-DscDebug", - false, - false); - - // Verify all the breakpoints and return them - foreach (var breakpoint in breakpoints) - { - breakpoint.Verified = true; - } - - return breakpoints.ToList(); - } - - public bool IsDscResourcePath(string scriptPath) - { - return dscResourceRootPaths.Any( - dscResourceRootPath => - scriptPath.StartsWith( - dscResourceRootPath, - StringComparison.CurrentCultureIgnoreCase)); - } - - public static DscBreakpointCapability CheckForCapability( - RunspaceDetails runspaceDetails, - PowerShellContext powerShellContext, - ILogger logger) - { - DscBreakpointCapability capability = null; - - // DSC support is enabled only for Windows PowerShell. - if ((runspaceDetails.PowerShellVersion.Version.Major < 6) && - (runspaceDetails.Context != RunspaceContext.DebuggedRunspace)) - { - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspaceDetails.Runspace; - - // Attempt to import the updated DSC module - powerShell.AddCommand("Import-Module"); - powerShell.AddArgument(@"C:\Program Files\DesiredStateConfiguration\1.0.0.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psd1"); - powerShell.AddParameter("PassThru"); - powerShell.AddParameter("ErrorAction", "Ignore"); - - PSObject moduleInfo = null; - - try - { - moduleInfo = powerShell.Invoke().FirstOrDefault(); - } - catch (RuntimeException e) - { - logger.WriteException("Could not load the DSC module!", e); - } - - if (moduleInfo != null) - { - logger.Write(LogLevel.Verbose, "Side-by-side DSC module found, gathering DSC resource paths..."); - - // The module was loaded, add the breakpoint capability - capability = new DscBreakpointCapability(); - runspaceDetails.AddCapability(capability); - - powerShell.Commands.Clear(); - powerShell.AddScript("Write-Host \"Gathering DSC resource paths, this may take a while...\""); - powerShell.Invoke(); - - // Get the list of DSC resource paths - powerShell.Commands.Clear(); - powerShell.AddCommand("Get-DscResource"); - powerShell.AddCommand("Select-Object"); - powerShell.AddParameter("ExpandProperty", "ParentPath"); - - Collection resourcePaths = null; - - try - { - resourcePaths = powerShell.Invoke(); - } - catch (CmdletInvocationException e) - { - logger.WriteException("Get-DscResource failed!", e); - } - - if (resourcePaths != null) - { - capability.dscResourceRootPaths = - resourcePaths - .Select(o => (string)o.BaseObject) - .ToArray(); - - logger.Write(LogLevel.Verbose, $"DSC resources found: {resourcePaths.Count}"); - } - else - { - logger.Write(LogLevel.Verbose, $"No DSC resources found."); - } - } - else - { - logger.Write(LogLevel.Verbose, $"Side-by-side DSC module was not found."); - } - } - } - - return capability; - } - } -} diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs deleted file mode 100644 index 8bc751a9f..000000000 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ /dev/null @@ -1,192 +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 Microsoft.PowerShell.EditorServices.Components; -using Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Templates; -using Microsoft.PowerShell.EditorServices.Utility; -using System.IO; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Manages a single session for all editor services. This - /// includes managing all open script files for the session. - /// - public class EditorSession - { - #region Private Fields - - private ILogger logger; - - #endregion - - #region Properties - - /// - /// Gets the IHostInput implementation to use for this session. - /// - public IHostInput HostInput { get; private set; } - - /// - /// Gets the Workspace instance for this session. - /// - public Workspace Workspace { get; private set; } - - /// - /// Gets the PowerShellContext instance for this session. - /// - public PowerShellContext PowerShellContext { get; private set; } - - /// - /// Gets the LanguageService instance for this session. - /// - public LanguageService LanguageService { get; private set; } - - /// - /// Gets the AnalysisService instance for this session. - /// - public AnalysisService AnalysisService { get; private set; } - - /// - /// Gets the DebugService instance for this session. - /// - public DebugService DebugService { get; private set; } - - /// - /// Gets the ExtensionService instance for this session. - /// - public ExtensionService ExtensionService { get; private set; } - - /// - /// Gets the TemplateService instance for this session. - /// - public TemplateService TemplateService { get; private set; } - - /// - /// Gets the RemoteFileManager instance for this session. - /// - public RemoteFileManager RemoteFileManager { get; private set; } - - /// - /// Gets the IComponentCollection instance for this session. - /// - public IComponentRegistry Components { get; } = new ComponentRegistry(); - - #endregion - - #region Constructors - - /// - /// - /// - /// An ILogger implementation used for writing log messages. - public EditorSession(ILogger logger) - { - this.logger = logger; - } - - #endregion - - #region Public Methods - - /// - /// Starts the session using the provided IConsoleHost implementation - /// for the ConsoleService. - /// - /// - /// - public void StartSession( - PowerShellContext powerShellContext, - IHostInput hostInput) - { - this.PowerShellContext = powerShellContext; - this.HostInput = hostInput; - - // Initialize all services - this.LanguageService = new LanguageService(this.PowerShellContext, this.logger); - this.ExtensionService = new ExtensionService(this.PowerShellContext); - this.TemplateService = new TemplateService(this.PowerShellContext, this.logger); - - this.InstantiateAnalysisService(); - - // Create a workspace to contain open files - this.Workspace = new Workspace(this.PowerShellContext.LocalPowerShellVersion.Version, this.logger); - } - - /// - /// Starts a debug-only session using the provided IConsoleHost implementation - /// for the ConsoleService. - /// - /// - /// - /// - /// An IEditorOperations implementation used to interact with the editor. - /// - public void StartDebugSession( - PowerShellContext powerShellContext, - IHostInput hostInput, - IEditorOperations editorOperations) - { - this.PowerShellContext = powerShellContext; - this.HostInput = hostInput; - - // Initialize all services - this.RemoteFileManager = new RemoteFileManager(this.PowerShellContext, editorOperations, logger); - this.DebugService = new DebugService(this.PowerShellContext, this.RemoteFileManager, logger); - - // Create a workspace to contain open files - this.Workspace = new Workspace(this.PowerShellContext.LocalPowerShellVersion.Version, this.logger); - } - - /// - /// Starts the DebugService if it's not already strated - /// - /// - /// An IEditorOperations implementation used to interact with the editor. - /// - public void StartDebugService(IEditorOperations editorOperations) - { - if (this.DebugService == null) - { - this.RemoteFileManager = new RemoteFileManager(this.PowerShellContext, editorOperations, logger); - this.DebugService = new DebugService(this.PowerShellContext, this.RemoteFileManager, logger); - } - } - - internal void InstantiateAnalysisService(string settingsPath = null) - { - // Create the analysis service. If this fails, the result will be null -- any exceptions are caught and logged - this.AnalysisService = AnalysisService.Create(settingsPath, this.logger); - } - - #endregion - - #region IDisposable Implementation - - /// - /// Disposes of any Runspaces that were created for the - /// services used in this session. - /// - public void Dispose() - { - if (this.AnalysisService != null) - { - this.AnalysisService.Dispose(); - this.AnalysisService = null; - } - - if (this.PowerShellContext != null) - { - this.PowerShellContext.Dispose(); - this.PowerShellContext = null; - } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/ExecutionOptions.cs b/src/PowerShellEditorServices/Session/ExecutionOptions.cs deleted file mode 100644 index a1071606f..000000000 --- a/src/PowerShellEditorServices/Session/ExecutionOptions.cs +++ /dev/null @@ -1,92 +0,0 @@ -// -// 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 -{ - /// - /// Defines options for the execution of a command. - /// - public class ExecutionOptions - { - private bool? _shouldExecuteInOriginalRunspace; - - #region Properties - - /// - /// Gets or sets a boolean that determines whether command output - /// should be written to the host. - /// - public bool WriteOutputToHost { get; set; } - - /// - /// Gets or sets a boolean that determines whether command errors - /// should be written to the host. - /// - public bool WriteErrorsToHost { get; set; } - - /// - /// Gets or sets a boolean that determines whether the executed - /// command should be added to the command history. - /// - public bool AddToHistory { get; set; } - - /// - /// Gets or sets a boolean that determines whether the execution - /// of the command should interrupt the command prompt. Should - /// only be set if WriteOutputToHost is false but the command - /// should still interrupt the command prompt. - /// - public bool InterruptCommandPrompt { get; set; } - - /// - /// Gets or sets a value indicating whether the text of the command - /// should be written to the host as if it was ran interactively. - /// - public bool WriteInputToHost { get; set; } - - /// - /// Gets or sets a value indicating whether the command to - /// be executed is a console input prompt, such as the - /// PSConsoleHostReadLine function. - /// - internal bool IsReadLine { get; set; } - - /// - /// Gets or sets a value indicating whether the command should - /// be invoked in the original runspace. In the majority of cases - /// this should remain unset. - /// - internal bool ShouldExecuteInOriginalRunspace - { - get - { - return _shouldExecuteInOriginalRunspace ?? IsReadLine; - } - set - { - _shouldExecuteInOriginalRunspace = value; - } - } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ExecutionOptions class with - /// default settings configured. - /// - public ExecutionOptions() - { - this.WriteOutputToHost = true; - this.WriteErrorsToHost = true; - this.WriteInputToHost = false; - this.AddToHistory = false; - this.InterruptCommandPrompt = false; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/ExecutionStatus.cs b/src/PowerShellEditorServices/Session/ExecutionStatus.cs deleted file mode 100644 index 233d0499e..000000000 --- a/src/PowerShellEditorServices/Session/ExecutionStatus.cs +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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 -{ - /// - /// Enumerates the possible execution results that can occur after - /// executing a command or script. - /// - public enum ExecutionStatus - { - /// - /// Indicates that execution has not yet started. - /// - Pending, - - /// - /// Indicates that the command is executing. - /// - Running, - - /// - /// Indicates that execution has failed. - /// - Failed, - - /// - /// Indicates that execution was aborted by the user. - /// - Aborted, - - /// - /// Indicates that execution completed successfully. - /// - Completed - } -} diff --git a/src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs deleted file mode 100644 index cd2dcaaf2..000000000 --- a/src/PowerShellEditorServices/Session/ExecutionStatusChangedEventArgs.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -// 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 -{ - /// - /// Contains details about an executed - /// - public class ExecutionStatusChangedEventArgs - { - #region Properties - - /// - /// Gets the options used when the command was executed. - /// - public ExecutionOptions ExecutionOptions { get; private set; } - - /// - /// Gets the command execution's current status. - /// - public ExecutionStatus ExecutionStatus { get; private set; } - - /// - /// If true, the command execution had errors. - /// - public bool HadErrors { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the ExecutionStatusChangedEventArgs class. - /// - /// The command execution's current status. - /// The options used when the command was executed. - /// If execution has completed, indicates whether there were errors. - public ExecutionStatusChangedEventArgs( - ExecutionStatus executionStatus, - ExecutionOptions executionOptions, - bool hadErrors) - { - this.ExecutionStatus = executionStatus; - this.ExecutionOptions = executionOptions; - this.HadErrors = hadErrors || (executionStatus == ExecutionStatus.Failed); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/ExecutionTarget.cs b/src/PowerShellEditorServices/Session/ExecutionTarget.cs deleted file mode 100644 index 70ec3cb6f..000000000 --- a/src/PowerShellEditorServices/Session/ExecutionTarget.cs +++ /dev/null @@ -1,28 +0,0 @@ -// -// 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.Session -{ - /// - /// Represents the different API's available for executing commands. - /// - internal enum ExecutionTarget - { - /// - /// Indicates that the command should be invoked through the PowerShell debugger. - /// - Debugger, - - /// - /// Indicates that the command should be invoked via an instance of the PowerShell class. - /// - PowerShell, - - /// - /// Indicates that the command should be invoked through the PowerShell engine's event manager. - /// - InvocationEvent - } -} diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs deleted file mode 100644 index 7c32acdb3..000000000 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHost.cs +++ /dev/null @@ -1,372 +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 Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an implementation of the PSHost class for the - /// ConsoleService and routes its calls to an IConsoleHost - /// implementation. - /// - public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession - { - #region Private Fields - - private ILogger Logger; - private HostDetails hostDetails; - private Guid instanceId = Guid.NewGuid(); - private EditorServicesPSHostUserInterface hostUserInterface; - private IHostSupportsInteractiveSession hostSupportsInteractiveSession; - private PowerShellContext powerShellContext; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsoleServicePSHost class - /// with the given IConsoleHost implementation. - /// - /// - /// An implementation of IHostSupportsInteractiveSession for runspace management. - /// - /// - /// Provides details about the host application. - /// - /// - /// The EditorServicesPSHostUserInterface implementation to use for this host. - /// - /// An ILogger implementation to use for this host. - public EditorServicesPSHost( - PowerShellContext powerShellContext, - HostDetails hostDetails, - EditorServicesPSHostUserInterface hostUserInterface, - ILogger logger) - { - this.Logger = logger; - this.hostDetails = hostDetails; - this.hostUserInterface = hostUserInterface; - this.hostSupportsInteractiveSession = powerShellContext; - this.powerShellContext = powerShellContext; - } - - #endregion - - #region PSHost Implementation - - /// - /// - /// - public override Guid InstanceId - { - get { return this.instanceId; } - } - - /// - /// - /// - public override string Name - { - get { return this.hostDetails.Name; } - } - - internal class ConsoleColorProxy - { - private EditorServicesPSHostUserInterface _hostUserInterface; - - internal ConsoleColorProxy(EditorServicesPSHostUserInterface hostUserInterface) - { - if (hostUserInterface == null) throw new ArgumentNullException("hostUserInterface"); - _hostUserInterface = hostUserInterface; - } - - /// - /// The ForegroundColor for Error - /// - public ConsoleColor ErrorForegroundColor - { - get - { return _hostUserInterface.ErrorForegroundColor; } - set - { _hostUserInterface.ErrorForegroundColor = value; } - } - - /// - /// The BackgroundColor for Error - /// - public ConsoleColor ErrorBackgroundColor - { - get - { return _hostUserInterface.ErrorBackgroundColor; } - set - { _hostUserInterface.ErrorBackgroundColor = value; } - } - - /// - /// The ForegroundColor for Warning - /// - public ConsoleColor WarningForegroundColor - { - get - { return _hostUserInterface.WarningForegroundColor; } - set - { _hostUserInterface.WarningForegroundColor = value; } - } - - /// - /// The BackgroundColor for Warning - /// - public ConsoleColor WarningBackgroundColor - { - get - { return _hostUserInterface.WarningBackgroundColor; } - set - { _hostUserInterface.WarningBackgroundColor = value; } - } - - /// - /// The ForegroundColor for Debug - /// - public ConsoleColor DebugForegroundColor - { - get - { return _hostUserInterface.DebugForegroundColor; } - set - { _hostUserInterface.DebugForegroundColor = value; } - } - - /// - /// The BackgroundColor for Debug - /// - public ConsoleColor DebugBackgroundColor - { - get - { return _hostUserInterface.DebugBackgroundColor; } - set - { _hostUserInterface.DebugBackgroundColor = value; } - } - - /// - /// The ForegroundColor for Verbose - /// - public ConsoleColor VerboseForegroundColor - { - get - { return _hostUserInterface.VerboseForegroundColor; } - set - { _hostUserInterface.VerboseForegroundColor = value; } - } - - /// - /// The BackgroundColor for Verbose - /// - public ConsoleColor VerboseBackgroundColor - { - get - { return _hostUserInterface.VerboseBackgroundColor; } - set - { _hostUserInterface.VerboseBackgroundColor = value; } - } - - /// - /// The ForegroundColor for Progress - /// - public ConsoleColor ProgressForegroundColor - { - get - { return _hostUserInterface.ProgressForegroundColor; } - set - { _hostUserInterface.ProgressForegroundColor = value; } - } - - /// - /// The BackgroundColor for Progress - /// - public ConsoleColor ProgressBackgroundColor - { - get - { return _hostUserInterface.ProgressBackgroundColor; } - set - { _hostUserInterface.ProgressBackgroundColor = value; } - } - } - - /// - /// Return the actual console host object so that the user can get at - /// the unproxied methods. - /// - public override PSObject PrivateData - { - get - { - if (hostUserInterface == null) return null; - return _consoleColorProxy ?? (_consoleColorProxy = PSObject.AsPSObject(new ConsoleColorProxy(hostUserInterface))); - } - } - private PSObject _consoleColorProxy; - - /// - /// - /// - public override Version Version - { - get { return this.hostDetails.Version; } - } - - // TODO: Pull these from IConsoleHost - - /// - /// - /// - public override System.Globalization.CultureInfo CurrentCulture - { - get { return System.Globalization.CultureInfo.CurrentCulture; } - } - - /// - /// - /// - public override System.Globalization.CultureInfo CurrentUICulture - { - get { return System.Globalization.CultureInfo.CurrentUICulture; } - } - - /// - /// - /// - public override PSHostUserInterface UI - { - get { return this.hostUserInterface; } - } - - /// - /// - /// - public override void EnterNestedPrompt() - { - this.powerShellContext.EnterNestedPrompt(); - } - - /// - /// - /// - public override void ExitNestedPrompt() - { - this.powerShellContext.ExitNestedPrompt(); - } - - /// - /// - /// - public override void NotifyBeginApplication() - { - Logger.Write(LogLevel.Verbose, "NotifyBeginApplication() called."); - this.hostUserInterface.IsNativeApplicationRunning = true; - } - - /// - /// - /// - public override void NotifyEndApplication() - { - Logger.Write(LogLevel.Verbose, "NotifyEndApplication() called."); - this.hostUserInterface.IsNativeApplicationRunning = false; - } - - /// - /// - /// - /// - public override void SetShouldExit(int exitCode) - { - if (this.IsRunspacePushed) - { - this.PopRunspace(); - } - } - - #endregion - - #region IHostSupportsInteractiveSession Implementation - - /// - /// - /// - /// - public bool IsRunspacePushed - { - get - { - if (this.hostSupportsInteractiveSession != null) - { - return this.hostSupportsInteractiveSession.IsRunspacePushed; - } - else - { - throw new NotImplementedException(); - } - } - } - - /// - /// - /// - /// - public Runspace Runspace - { - get - { - if (this.hostSupportsInteractiveSession != null) - { - return this.hostSupportsInteractiveSession.Runspace; - } - else - { - throw new NotImplementedException(); - } - } - } - - /// - /// - /// - /// - public void PushRunspace(Runspace runspace) - { - if (this.hostSupportsInteractiveSession != null) - { - this.hostSupportsInteractiveSession.PushRunspace(runspace); - } - else - { - throw new NotImplementedException(); - } - } - - /// - /// - /// - public void PopRunspace() - { - if (this.hostSupportsInteractiveSession != null) - { - this.hostSupportsInteractiveSession.PopRunspace(); - } - else - { - throw new NotImplementedException(); - } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs deleted file mode 100644 index 24ea1c8db..000000000 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ /dev/null @@ -1,1070 +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.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Linq; -using System.Security; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Console; -using System.Threading; -using Microsoft.PowerShell.EditorServices.Utility; -using Microsoft.PowerShell.EditorServices.Session; -using System.Globalization; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an implementation of the PSHostUserInterface class - /// for the ConsoleService and routes its calls to an IConsoleHost - /// implementation. - /// - public abstract class EditorServicesPSHostUserInterface : - PSHostUserInterface, - IHostInput, - IHostOutput, - IHostUISupportsMultipleChoiceSelection - { - #region Private Fields - - private readonly ConcurrentDictionary currentProgressMessages = - new ConcurrentDictionary(); - private PromptHandler activePromptHandler; - private PSHostRawUserInterface rawUserInterface; - private CancellationTokenSource commandLoopCancellationToken; - - /// - /// The PowerShellContext to use for executing commands. - /// - protected PowerShellContext powerShellContext; - - #endregion - - #region Public Constants - - /// - /// Gets a const string for the console's debug message prefix. - /// - public const string DebugMessagePrefix = "DEBUG: "; - - /// - /// Gets a const string for the console's warning message prefix. - /// - public const string WarningMessagePrefix = "WARNING: "; - - /// - /// Gets a const string for the console's verbose message prefix. - /// - public const string VerboseMessagePrefix = "VERBOSE: "; - - #endregion - - #region Properties - -#if !PowerShellv3 && !PowerShellv4 && !PowerShellv5r1 // Only available in Windows 10 Update 1 or higher - /// - /// Returns true if the host supports VT100 output codes. - /// - public override bool SupportsVirtualTerminal => true; -#endif - - /// - /// Returns true if a native application is currently running. - /// - public bool IsNativeApplicationRunning { get; internal set; } - - private bool IsCommandLoopRunning { get; set; } - - /// - /// Gets the ILogger implementation used for this host. - /// - protected ILogger Logger { get; private set; } - - /// - /// Gets a value indicating whether writing progress is supported. - /// - internal protected virtual bool SupportsWriteProgress => false; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsoleServicePSHostUserInterface - /// class with the given IConsoleHost implementation. - /// - /// The PowerShellContext to use for executing commands. - /// The PSHostRawUserInterface implementation to use for this host. - /// An ILogger implementation to use for this host. - public EditorServicesPSHostUserInterface( - PowerShellContext powerShellContext, - PSHostRawUserInterface rawUserInterface, - ILogger logger) - { - this.Logger = logger; - this.powerShellContext = powerShellContext; - this.rawUserInterface = rawUserInterface; - - this.powerShellContext.DebuggerStop += PowerShellContext_DebuggerStop; - this.powerShellContext.DebuggerResumed += PowerShellContext_DebuggerResumed; - this.powerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChanged; - } - - #endregion - - #region Public Methods - - /// - /// Starts the host's interactive command loop. - /// - public void StartCommandLoop() - { - if (!this.IsCommandLoopRunning) - { - this.IsCommandLoopRunning = true; - this.ShowCommandPrompt(); - } - } - - /// - /// Stops the host's interactive command loop. - /// - public void StopCommandLoop() - { - if (this.IsCommandLoopRunning) - { - this.IsCommandLoopRunning = false; - this.CancelCommandPrompt(); - } - } - - private void ShowCommandPrompt() - { - if (this.commandLoopCancellationToken == null) - { - this.commandLoopCancellationToken = new CancellationTokenSource(); - - var commandLoopThreadTask = - Task.Factory.StartNew( - async () => - { - await this.StartReplLoopAsync(this.commandLoopCancellationToken.Token); - }); - } - else - { - Logger.Write(LogLevel.Verbose, "StartReadLoop called while read loop is already running"); - } - } - - private void CancelCommandPrompt() - { - if (this.commandLoopCancellationToken != null) - { - // Set this to false so that Ctrl+C isn't trapped by any - // lingering ReadKey - // TOOD: Move this to Terminal impl! - //Console.TreatControlCAsInput = false; - - this.commandLoopCancellationToken.Cancel(); - this.commandLoopCancellationToken = null; - } - } - - /// - /// Cancels the currently executing command or prompt. - /// - public void SendControlC() - { - if (this.activePromptHandler != null) - { - this.activePromptHandler.CancelPrompt(); - } - else - { - // Cancel the current execution - this.powerShellContext.AbortExecution(); - } - } - - #endregion - - #region Abstract Methods - - /// - /// Requests that the HostUI implementation read a command line - /// from the user to be executed in the integrated console command - /// loop. - /// - /// - /// A CancellationToken used to cancel the command line request. - /// - /// A Task that can be awaited for the resulting input string. - protected abstract Task ReadCommandLineAsync(CancellationToken cancellationToken); - - /// - /// Creates an InputPrompt handle to use for displaying input - /// prompts to the user. - /// - /// A new InputPromptHandler instance. - protected abstract InputPromptHandler OnCreateInputPromptHandler(); - - /// - /// Creates a ChoicePromptHandler to use for displaying a - /// choice prompt to the user. - /// - /// A new ChoicePromptHandler instance. - protected abstract ChoicePromptHandler OnCreateChoicePromptHandler(); - - /// - /// 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 abstract void WriteOutput( - string outputString, - bool includeNewLine, - OutputType outputType, - ConsoleColor foregroundColor, - ConsoleColor backgroundColor); - - /// - /// Sends a progress update event to the user. - /// - /// The source ID of the progress event. - /// The details of the activity's current progress. - protected abstract void UpdateProgress( - long sourceId, - ProgressDetails progressDetails); - - #endregion - - #region IHostInput Implementation - - #endregion - - #region PSHostUserInterface Implementation - - /// - /// - /// - /// - /// - /// - /// - public override Dictionary Prompt( - string promptCaption, - string promptMessage, - Collection fieldDescriptions) - { - FieldDetails[] fields = - fieldDescriptions - .Select(f => { return FieldDetails.Create(f, this.Logger); }) - .ToArray(); - - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task> promptTask = - this.CreateInputPromptHandler() - .PromptForInputAsync( - promptCaption, - promptMessage, - fields, - cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "Prompt", - cancellationToken); - - // Convert all values to PSObjects - var psObjectDict = new Dictionary(); - - // The result will be null if the prompt was cancelled - if (promptTask.Result != null) - { - // Convert all values to PSObjects - foreach (var keyValuePair in promptTask.Result) - { - psObjectDict.Add( - keyValuePair.Key, - keyValuePair.Value != null - ? PSObject.AsPSObject(keyValuePair.Value) - : null); - } - } - - // Return the result - return psObjectDict; - } - - /// - /// - /// - /// - /// - /// - /// - /// - public override int PromptForChoice( - string promptCaption, - string promptMessage, - Collection choiceDescriptions, - int defaultChoice) - { - ChoiceDetails[] choices = - choiceDescriptions - .Select(ChoiceDetails.Create) - .ToArray(); - - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task promptTask = - this.CreateChoicePromptHandler() - .PromptForChoiceAsync( - promptCaption, - promptMessage, - choices, - defaultChoice, - cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "PromptForChoice", - cancellationToken); - - // Return the result - return promptTask.Result; - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public override PSCredential PromptForCredential( - string promptCaption, - string promptMessage, - string userName, - string targetName, - PSCredentialTypes allowedCredentialTypes, - PSCredentialUIOptions options) - { - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - - Task> promptTask = - this.CreateInputPromptHandler() - .PromptForInputAsync( - promptCaption, - promptMessage, - new FieldDetails[] { new CredentialFieldDetails("Credential", "Credential", userName) }, - cancellationToken.Token); - - Task unpackTask = - promptTask.ContinueWith( - task => - { - if (task.IsFaulted) - { - throw task.Exception; - } - else if (task.IsCanceled) - { - throw new TaskCanceledException(task); - } - - // Return the value of the sole field - return (PSCredential)task.Result?["Credential"]; - }); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - unpackTask, - "PromptForCredential", - cancellationToken); - - return unpackTask.Result; - } - - /// - /// - /// - /// - /// - /// - /// - /// - public override PSCredential PromptForCredential( - string caption, - string message, - string userName, - string targetName) - { - return this.PromptForCredential( - caption, - message, - userName, - targetName, - PSCredentialTypes.Default, - PSCredentialUIOptions.Default); - } - - /// - /// - /// - /// - public override PSHostRawUserInterface RawUI - { - get { return this.rawUserInterface; } - } - - /// - /// - /// - /// - public override string ReadLine() - { - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - - Task promptTask = - this.CreateInputPromptHandler() - .PromptForInputAsync(cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "ReadLine", - cancellationToken); - - return promptTask.Result; - } - - /// - /// - /// - /// - public override SecureString ReadLineAsSecureString() - { - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - - Task promptTask = - this.CreateInputPromptHandler() - .PromptForSecureInputAsync(cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "ReadLineAsSecureString", - cancellationToken); - - return promptTask.Result; - } - - /// - /// - /// - /// - /// - /// - public override void Write( - ConsoleColor foregroundColor, - ConsoleColor backgroundColor, - string value) - { - this.WriteOutput( - value, - false, - OutputType.Normal, - foregroundColor, - backgroundColor); - } - - /// - /// - /// - /// - public override void Write(string value) - { - this.WriteOutput( - value, - false, - OutputType.Normal, - this.rawUserInterface.ForegroundColor, - this.rawUserInterface.BackgroundColor); - } - - /// - /// - /// - /// - public override void WriteLine(string value) - { - this.WriteOutput( - value, - true, - OutputType.Normal, - this.rawUserInterface.ForegroundColor, - this.rawUserInterface.BackgroundColor); - } - - /// - /// - /// - /// - public override void WriteDebugLine(string message) - { - this.WriteOutput( - DebugMessagePrefix + message, - true, - OutputType.Debug, - foregroundColor: this.DebugForegroundColor, - backgroundColor: this.DebugBackgroundColor); - } - - /// - /// - /// - /// - public override void WriteVerboseLine(string message) - { - this.WriteOutput( - VerboseMessagePrefix + message, - true, - OutputType.Verbose, - foregroundColor: this.VerboseForegroundColor, - backgroundColor: this.VerboseBackgroundColor); - } - - /// - /// - /// - /// - public override void WriteWarningLine(string message) - { - this.WriteOutput( - WarningMessagePrefix + message, - true, - OutputType.Warning, - foregroundColor: this.WarningForegroundColor, - backgroundColor: this.WarningBackgroundColor); - } - - /// - /// - /// - /// - public override void WriteErrorLine(string value) - { - this.WriteOutput( - value, - true, - OutputType.Error, - foregroundColor: this.ErrorForegroundColor, - backgroundColor: this.ErrorBackgroundColor); - } - - /// - /// Invoked by to display a progress record. - /// - /// - /// Unique identifier of the source of the record. An int64 is used because typically, - /// the 'this' pointer of the command from whence the record is originating is used, and - /// that may be from a remote Runspace on a 64-bit machine. - /// - /// - /// The record being reported to the host. - /// - public sealed override void WriteProgress( - long sourceId, - ProgressRecord record) - { - // Maintain old behavior if this isn't overridden. - if (!this.SupportsWriteProgress) - { - this.UpdateProgress(sourceId, ProgressDetails.Create(record)); - return; - } - - // Keep a list of progress records we write so we can automatically - // clean them up after the pipeline ends. - if (record.RecordType == ProgressRecordType.Completed) - { - this.currentProgressMessages.TryRemove(new ProgressKey(sourceId, record), out _); - } - else - { - // Adding with a value of null here because we don't actually need a dictionary. We're - // only using ConcurrentDictionary<,> becuase there is no ConcurrentHashSet<>. - this.currentProgressMessages.TryAdd(new ProgressKey(sourceId, record), null); - } - - this.WriteProgressImpl(sourceId, record); - } - - /// - /// Invoked by to display a progress record. - /// - /// - /// Unique identifier of the source of the record. An int64 is used because typically, - /// the 'this' pointer of the command from whence the record is originating is used, and - /// that may be from a remote Runspace on a 64-bit machine. - /// - /// - /// The record being reported to the host. - /// - protected virtual void WriteProgressImpl(long sourceId, ProgressRecord record) - { - } - - internal void ClearProgress() - { - const string nonEmptyString = "noop"; - if (!this.SupportsWriteProgress) - { - return; - } - - foreach (ProgressKey key in this.currentProgressMessages.Keys) - { - // This constructor throws if the activity description is empty even - // with completed records. - var record = new ProgressRecord( - key.ActivityId, - activity: nonEmptyString, - statusDescription: nonEmptyString); - - record.RecordType = ProgressRecordType.Completed; - this.WriteProgressImpl(key.SourceId, record); - } - - this.currentProgressMessages.Clear(); - } - - #endregion - - #region IHostUISupportsMultipleChoiceSelection Implementation - - /// - /// - /// - /// - /// - /// - /// - /// - public Collection PromptForChoice( - string promptCaption, - string promptMessage, - Collection choiceDescriptions, - IEnumerable defaultChoices) - { - ChoiceDetails[] choices = - choiceDescriptions - .Select(ChoiceDetails.Create) - .ToArray(); - - CancellationTokenSource cancellationToken = new CancellationTokenSource(); - Task promptTask = - this.CreateChoicePromptHandler() - .PromptForChoiceAsync( - promptCaption, - promptMessage, - choices, - defaultChoices.ToArray(), - cancellationToken.Token); - - // Run the prompt task and wait for it to return - this.WaitForPromptCompletion( - promptTask, - "PromptForChoice", - cancellationToken); - - // Return the result - return new Collection(promptTask.Result.ToList()); - } - - #endregion - - #region Private Methods - - private Coordinates lastPromptLocation; - - private async Task WritePromptStringToHostAsync(CancellationToken cancellationToken) - { - try - { - if (this.lastPromptLocation != null && - this.lastPromptLocation.X == await ConsoleProxy.GetCursorLeftAsync(cancellationToken) && - this.lastPromptLocation.Y == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) - { - return; - } - } - // When output is redirected (like when running tests) attempting to get - // the cursor position will throw. - catch (System.IO.IOException) - { - } - - PSCommand promptCommand = new PSCommand().AddScript("prompt"); - - cancellationToken.ThrowIfCancellationRequested(); - string promptString = - (await this.powerShellContext.ExecuteCommandAsync(promptCommand, false, false)) - .Select(pso => pso.BaseObject) - .OfType() - .FirstOrDefault() ?? "PS> "; - - // Add the [DBG] prefix if we're stopped in the debugger and the prompt doesn't already have [DBG] in it - if (this.powerShellContext.IsDebuggerStopped && !promptString.Contains("[DBG]")) - { - promptString = - string.Format( - CultureInfo.InvariantCulture, - "[DBG]: {0}", - promptString); - } - - // Update the stored prompt string if the session is remote - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote) - { - promptString = - string.Format( - CultureInfo.InvariantCulture, - "[{0}]: {1}", - this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo != null - ? this.powerShellContext.CurrentRunspace.Runspace.ConnectionInfo.ComputerName - : this.powerShellContext.CurrentRunspace.SessionDetails.ComputerName, - promptString); - } - - cancellationToken.ThrowIfCancellationRequested(); - - // Write the prompt string - this.WriteOutput(promptString, false); - this.lastPromptLocation = new Coordinates( - await ConsoleProxy.GetCursorLeftAsync(cancellationToken), - await ConsoleProxy.GetCursorTopAsync(cancellationToken)); - } - - private void WriteDebuggerBanner(DebuggerStopEventArgs eventArgs) - { - // TODO: What do we display when we don't know why we stopped? - - if (eventArgs.Breakpoints.Count > 0) - { - // The breakpoint classes have nice ToString output so use that - this.WriteOutput( - Environment.NewLine + $"Hit {eventArgs.Breakpoints[0].ToString()}\n", - true, - OutputType.Normal, - ConsoleColor.Blue); - } - } - - internal static ConsoleColor BackgroundColor { get; set; } - - internal ConsoleColor ErrorForegroundColor { get; set; } = ConsoleColor.Red; - internal ConsoleColor ErrorBackgroundColor { get; set; } = BackgroundColor; - - internal ConsoleColor WarningForegroundColor { get; set; } = ConsoleColor.Yellow; - internal ConsoleColor WarningBackgroundColor { get; set; } = BackgroundColor; - - internal ConsoleColor DebugForegroundColor { get; set; } = ConsoleColor.Yellow; - internal ConsoleColor DebugBackgroundColor { get; set; } = BackgroundColor; - - internal ConsoleColor VerboseForegroundColor { get; set; } = ConsoleColor.Yellow; - internal ConsoleColor VerboseBackgroundColor { get; set; } = BackgroundColor; - - internal ConsoleColor ProgressForegroundColor { get; set; } = ConsoleColor.Yellow; - internal ConsoleColor ProgressBackgroundColor { get; set; } = ConsoleColor.DarkCyan; - - private async Task StartReplLoopAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - string commandString = null; - int originalCursorTop = 0; - - try - { - await this.WritePromptStringToHostAsync(cancellationToken); - } - catch (OperationCanceledException) - { - break; - } - - try - { - originalCursorTop = await ConsoleProxy.GetCursorTopAsync(cancellationToken); - commandString = await this.ReadCommandLineAsync(cancellationToken); - } - catch (PipelineStoppedException) - { - this.WriteOutput( - "^C", - true, - OutputType.Normal, - foregroundColor: ConsoleColor.Red); - } - // Do nothing here, the while loop condition will exit. - catch (TaskCanceledException) - { } - catch (OperationCanceledException) - { } - catch (Exception e) // Narrow this if possible - { - this.WriteOutput( - $"\n\nAn error occurred while reading input:\n\n{e.ToString()}\n", - true, - OutputType.Error); - - Logger.WriteException("Caught exception while reading command line", e); - } - finally - { - if (!cancellationToken.IsCancellationRequested && - originalCursorTop == await ConsoleProxy.GetCursorTopAsync(cancellationToken)) - { - this.WriteLine(); - } - } - - if (!string.IsNullOrWhiteSpace(commandString)) - { - var unusedTask = - this.powerShellContext - .ExecuteScriptStringAsync( - commandString, - writeInputToHost: false, - writeOutputToHost: true, - addToHistory: true) - .ConfigureAwait(continueOnCapturedContext: false); - - break; - } - } - } - - private InputPromptHandler CreateInputPromptHandler() - { - if (this.activePromptHandler != null) - { - Logger.Write( - LogLevel.Error, - "Prompt handler requested while another prompt is already active."); - } - - InputPromptHandler inputPromptHandler = this.OnCreateInputPromptHandler(); - this.activePromptHandler = inputPromptHandler; - this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; - - return inputPromptHandler; - } - - private ChoicePromptHandler CreateChoicePromptHandler() - { - if (this.activePromptHandler != null) - { - Logger.Write( - LogLevel.Error, - "Prompt handler requested while another prompt is already active."); - } - - ChoicePromptHandler choicePromptHandler = this.OnCreateChoicePromptHandler(); - this.activePromptHandler = choicePromptHandler; - this.activePromptHandler.PromptCancelled += activePromptHandler_PromptCancelled; - - return choicePromptHandler; - } - - private void activePromptHandler_PromptCancelled(object sender, EventArgs e) - { - // Clean up the existing prompt - this.activePromptHandler.PromptCancelled -= activePromptHandler_PromptCancelled; - this.activePromptHandler = null; - } - private void WaitForPromptCompletion( - Task promptTask, - string promptFunctionName, - CancellationTokenSource cancellationToken) - { - try - { - // This will synchronously block on the prompt task - // method which gets run on another thread. - promptTask.Wait(); - - if (promptTask.Status == TaskStatus.WaitingForActivation) - { - // The Wait() call has timed out, cancel the prompt - cancellationToken.Cancel(); - - this.WriteOutput("\r\nPrompt has been cancelled due to a timeout.\r\n"); - throw new PipelineStoppedException(); - } - } - catch (AggregateException e) - { - // Find the right InnerException - Exception innerException = e.InnerException; - while (innerException is AggregateException) - { - innerException = innerException.InnerException; - } - - // Was the task cancelled? - if (innerException is TaskCanceledException) - { - // Stop the pipeline if the prompt was cancelled - throw new PipelineStoppedException(); - } - else if (innerException is PipelineStoppedException) - { - // The prompt is being cancelled, rethrow the exception - throw innerException; - } - else - { - // Rethrow the exception - throw new Exception( - string.Format( - "{0} failed, check inner exception for details", - promptFunctionName), - innerException); - } - } - } - - private void PowerShellContext_DebuggerStop(object sender, System.Management.Automation.DebuggerStopEventArgs e) - { - if (!this.IsCommandLoopRunning) - { - StartCommandLoop(); - return; - } - - // Cancel any existing prompt first - this.CancelCommandPrompt(); - - this.WriteDebuggerBanner(e); - this.ShowCommandPrompt(); - } - - private void PowerShellContext_DebuggerResumed(object sender, System.Management.Automation.DebuggerResumeAction e) - { - this.CancelCommandPrompt(); - } - - private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs eventArgs) - { - // The command loop should only be manipulated if it's already started - if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) - { - this.ClearProgress(); - - // When aborted, cancel any lingering prompts - if (this.activePromptHandler != null) - { - this.activePromptHandler.CancelPrompt(); - this.WriteOutput(string.Empty); - } - } - else if ( - eventArgs.ExecutionOptions.WriteOutputToHost || - eventArgs.ExecutionOptions.InterruptCommandPrompt) - { - // Any command which writes output to the host will affect - // the display of the prompt - if (eventArgs.ExecutionStatus != ExecutionStatus.Running) - { - this.ClearProgress(); - - // Execution has completed, start the input prompt - this.ShowCommandPrompt(); - StartCommandLoop(); - } - else - { - // A new command was started, cancel the input prompt - StopCommandLoop(); - this.CancelCommandPrompt(); - } - } - else if ( - eventArgs.ExecutionOptions.WriteErrorsToHost && - (eventArgs.ExecutionStatus == ExecutionStatus.Failed || - eventArgs.HadErrors)) - { - this.ClearProgress(); - this.WriteOutput(string.Empty, true); - var unusedTask = this.WritePromptStringToHostAsync(CancellationToken.None); - } - } - - #endregion - - private readonly struct ProgressKey : IEquatable - { - internal readonly long SourceId; - - internal readonly int ActivityId; - - internal readonly int ParentActivityId; - - internal ProgressKey(long sourceId, ProgressRecord record) - { - SourceId = sourceId; - ActivityId = record.ActivityId; - ParentActivityId = record.ParentActivityId; - } - - public bool Equals(ProgressKey other) - { - return SourceId == other.SourceId - && ActivityId == other.ActivityId - && ParentActivityId == other.ParentActivityId; - } - - public override int GetHashCode() - { - // Algorithm from https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations - unchecked - { - int hash = 17; - hash = hash * 31 + SourceId.GetHashCode(); - hash = hash * 31 + ActivityId.GetHashCode(); - hash = hash * 31 + ParentActivityId.GetHashCode(); - return hash; - } - } - } - } -} diff --git a/src/PowerShellEditorServices/Session/Host/IHostInput.cs b/src/PowerShellEditorServices/Session/Host/IHostInput.cs deleted file mode 100644 index 28c79839d..000000000 --- a/src/PowerShellEditorServices/Session/Host/IHostInput.cs +++ /dev/null @@ -1,28 +0,0 @@ -// -// 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 -{ - /// - /// Provides methods for integrating with the host's input system. - /// - public interface IHostInput - { - /// - /// Starts the host's interactive command loop. - /// - void StartCommandLoop(); - - /// - /// Stops the host's interactive command loop. - /// - void StopCommandLoop(); - - /// - /// Cancels the currently executing command or prompt. - /// - void SendControlC(); - } -} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Session/Host/IHostOutput.cs b/src/PowerShellEditorServices/Session/Host/IHostOutput.cs deleted file mode 100644 index 4f36bc54f..000000000 --- a/src/PowerShellEditorServices/Session/Host/IHostOutput.cs +++ /dev/null @@ -1,175 +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; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a simplified interface for writing output to a - /// PowerShell host implementation. - /// - public interface IHostOutput - { - /// - /// 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. - /// - void WriteOutput( - string outputString, - bool includeNewLine, - OutputType outputType, - ConsoleColor foregroundColor, - ConsoleColor backgroundColor); - } - - /// - /// Provides helpful extension methods for the IHostOutput interface. - /// - public static class IHostOutputExtensions - { - /// - /// Writes normal output with a newline to the user interface. - /// - /// - /// The IHostOutput implementation to use for WriteOutput calls. - /// - /// - /// The output string to be written. - /// - public static void WriteOutput( - this IHostOutput hostOutput, - string outputString) - { - hostOutput.WriteOutput(outputString, true); - } - - /// - /// Writes normal output to the user interface. - /// - /// - /// The IHostOutput implementation to use for WriteOutput calls. - /// - /// - /// The output string to be written. - /// - /// - /// If true, a newline should be appended to the output's contents. - /// - public static void WriteOutput( - this IHostOutput hostOutput, - string outputString, - bool includeNewLine) - { - hostOutput.WriteOutput( - outputString, - includeNewLine, - OutputType.Normal); - } - - /// - /// Writes output of a particular type to the user interface - /// with a newline ending. - /// - /// - /// The IHostOutput implementation to use for WriteOutput calls. - /// - /// - /// The output string to be written. - /// - /// - /// Specifies the type of output to be written. - /// - public static void WriteOutput( - this IHostOutput hostOutput, - string outputString, - OutputType outputType) - { - hostOutput.WriteOutput( - outputString, - true, - OutputType.Normal); - } - - /// - /// Writes output of a particular type to the user interface. - /// - /// - /// The IHostOutput implementation to use for WriteOutput calls. - /// - /// - /// 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. - /// - public static void WriteOutput( - this IHostOutput hostOutput, - string outputString, - bool includeNewLine, - OutputType outputType) - { - hostOutput.WriteOutput( - outputString, - includeNewLine, - outputType, - ConsoleColor.Gray, - (ConsoleColor)(-1)); // -1 indicates the console's raw background color - } - - /// - /// Writes output of a particular type to the user interface using - /// a particular foreground color. - /// - /// - /// The IHostOutput implementation to use for WriteOutput calls. - /// - /// - /// 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. - /// - public static void WriteOutput( - this IHostOutput hostOutput, - string outputString, - bool includeNewLine, - OutputType outputType, - ConsoleColor foregroundColor) - { - hostOutput.WriteOutput( - outputString, - includeNewLine, - outputType, - foregroundColor, - (ConsoleColor)(-1)); // -1 indicates the console's raw background color - } - } -} diff --git a/src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs deleted file mode 100644 index 89895a776..000000000 --- a/src/PowerShellEditorServices/Session/Host/SimplePSHostRawUserInterface.cs +++ /dev/null @@ -1,230 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Management.Automation.Host; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an simple implementation of the PSHostRawUserInterface class. - /// - public class SimplePSHostRawUserInterface : PSHostRawUserInterface - { - #region Private Fields - - private const int DefaultConsoleHeight = 100; - private const int DefaultConsoleWidth = 120; - - private ILogger Logger; - - private Size currentBufferSize = new Size(DefaultConsoleWidth, DefaultConsoleHeight); - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the SimplePSHostRawUserInterface - /// class with the given IConsoleHost implementation. - /// - /// The ILogger implementation to use for this instance. - public SimplePSHostRawUserInterface(ILogger logger) - { - this.Logger = logger; - this.ForegroundColor = ConsoleColor.White; - this.BackgroundColor = ConsoleColor.Black; - } - - #endregion - - #region PSHostRawUserInterface Implementation - - /// - /// Gets or sets the background color of the console. - /// - public override ConsoleColor BackgroundColor - { - get; - set; - } - - /// - /// Gets or sets the foreground color of the console. - /// - public override ConsoleColor ForegroundColor - { - get; - set; - } - - /// - /// Gets or sets the size of the console buffer. - /// - public override Size BufferSize - { - get - { - return this.currentBufferSize; - } - set - { - this.currentBufferSize = value; - } - } - - /// - /// Gets or sets the cursor's position in the console buffer. - /// - public override Coordinates CursorPosition - { - get; - set; - } - - /// - /// Gets or sets the size of the cursor in the console buffer. - /// - public override int CursorSize - { - get; - set; - } - - /// - /// Gets or sets the position of the console's window. - /// - public override Coordinates WindowPosition - { - get; - set; - } - - /// - /// Gets or sets the size of the console's window. - /// - public override Size WindowSize - { - get; - set; - } - - /// - /// Gets or sets the console window's title. - /// - public override string WindowTitle - { - get; - set; - } - - /// - /// Gets a boolean that determines whether a keypress is available. - /// - public override bool KeyAvailable - { - get { return false; } - } - - /// - /// Gets the maximum physical size of the console window. - /// - public override Size MaxPhysicalWindowSize - { - get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } - } - - /// - /// Gets the maximum size of the console window. - /// - public override Size MaxWindowSize - { - get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } - } - - /// - /// Reads the current key pressed in the console. - /// - /// Options for reading the current keypress. - /// A KeyInfo struct with details about the current keypress. - public override KeyInfo ReadKey(ReadKeyOptions options) - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.ReadKey was called"); - - throw new System.NotImplementedException(); - } - - /// - /// Flushes the current input buffer. - /// - public override void FlushInputBuffer() - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.FlushInputBuffer was called"); - } - - /// - /// Gets the contents of the console buffer in a rectangular area. - /// - /// The rectangle inside which buffer contents will be accessed. - /// A BufferCell array with the requested buffer contents. - public override BufferCell[,] GetBufferContents(Rectangle rectangle) - { - return new BufferCell[0,0]; - } - - /// - /// Scrolls the contents of the console buffer. - /// - /// The source rectangle to scroll. - /// The destination coordinates by which to scroll. - /// The rectangle inside which the scrolling will be clipped. - /// The cell with which the buffer will be filled. - public override void ScrollBufferContents( - Rectangle source, - Coordinates destination, - Rectangle clip, - BufferCell fill) - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.ScrollBufferContents was called"); - } - - /// - /// Sets the contents of the buffer inside the specified rectangle. - /// - /// The rectangle inside which buffer contents will be filled. - /// The BufferCell which will be used to fill the requested space. - public override void SetBufferContents( - Rectangle rectangle, - BufferCell fill) - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.SetBufferContents was called"); - } - - /// - /// Sets the contents of the buffer at the given coordinate. - /// - /// The coordinate at which the buffer will be changed. - /// The new contents for the buffer at the given coordinate. - public override void SetBufferContents( - Coordinates origin, - BufferCell[,] contents) - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.SetBufferContents was called"); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs deleted file mode 100644 index be217c9d9..000000000 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs +++ /dev/null @@ -1,331 +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 Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Threading; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an implementation of the PSHostRawUserInterface class - /// for the ConsoleService and routes its calls to an IConsoleHost - /// implementation. - /// - internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface - { - #region Private Fields - - private readonly PSHostRawUserInterface internalRawUI; - private ILogger Logger; - private KeyInfo? lastKeyDown; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the TerminalPSHostRawUserInterface - /// class with the given IConsoleHost implementation. - /// - /// The ILogger implementation to use for this instance. - /// The InternalHost instance from the origin runspace. - public TerminalPSHostRawUserInterface(ILogger logger, PSHost internalHost) - { - this.Logger = logger; - this.internalRawUI = internalHost.UI.RawUI; - } - - #endregion - - #region PSHostRawUserInterface Implementation - - /// - /// Gets or sets the background color of the console. - /// - public override ConsoleColor BackgroundColor - { - get { return System.Console.BackgroundColor; } - set { System.Console.BackgroundColor = value; } - } - - /// - /// Gets or sets the foreground color of the console. - /// - public override ConsoleColor ForegroundColor - { - get { return System.Console.ForegroundColor; } - set { System.Console.ForegroundColor = value; } - } - - /// - /// Gets or sets the size of the console buffer. - /// - public override Size BufferSize - { - get => this.internalRawUI.BufferSize; - set => this.internalRawUI.BufferSize = value; - } - - /// - /// Gets or sets the cursor's position in the console buffer. - /// - public override Coordinates CursorPosition - { - get - { - return new Coordinates( - ConsoleProxy.GetCursorLeft(), - ConsoleProxy.GetCursorTop()); - } - - set => this.internalRawUI.CursorPosition = value; - } - - /// - /// Gets or sets the size of the cursor in the console buffer. - /// - public override int CursorSize - { - get => this.internalRawUI.CursorSize; - set => this.internalRawUI.CursorSize = value; - } - - /// - /// Gets or sets the position of the console's window. - /// - public override Coordinates WindowPosition - { - get => this.internalRawUI.WindowPosition; - set => this.internalRawUI.WindowPosition = value; - } - - /// - /// Gets or sets the size of the console's window. - /// - public override Size WindowSize - { - get => this.internalRawUI.WindowSize; - set => this.internalRawUI.WindowSize = value; - } - - /// - /// Gets or sets the console window's title. - /// - public override string WindowTitle - { - get => this.internalRawUI.WindowTitle; - set => this.internalRawUI.WindowTitle = value; - } - - /// - /// Gets a boolean that determines whether a keypress is available. - /// - public override bool KeyAvailable => this.internalRawUI.KeyAvailable; - - /// - /// Gets the maximum physical size of the console window. - /// - public override Size MaxPhysicalWindowSize => this.internalRawUI.MaxPhysicalWindowSize; - - /// - /// Gets the maximum size of the console window. - /// - public override Size MaxWindowSize => this.internalRawUI.MaxWindowSize; - - /// - /// Reads the current key pressed in the console. - /// - /// Options for reading the current keypress. - /// A KeyInfo struct with details about the current keypress. - public override KeyInfo ReadKey(ReadKeyOptions options) - { - - bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; - - // Key Up was requested and we have a cached key down we can return. - if (includeUp && this.lastKeyDown != null) - { - KeyInfo info = this.lastKeyDown.Value; - this.lastKeyDown = null; - return new KeyInfo( - info.VirtualKeyCode, - info.Character, - info.ControlKeyState, - keyDown: false); - } - - bool intercept = (options & ReadKeyOptions.NoEcho) != 0; - bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0; - if (!(includeDown || includeUp)) - { - throw new PSArgumentException( - "Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.", - nameof(options)); - } - - // Allow ControlC as input so we can emulate pipeline stop requests. We can't actually - // determine if a stop is requested without using non-public API's. - bool oldValue = System.Console.TreatControlCAsInput; - try - { - System.Console.TreatControlCAsInput = true; - ConsoleKeyInfo key = ConsoleProxy.ReadKey(intercept, default(CancellationToken)); - - if (IsCtrlC(key)) - { - // Caller wants CtrlC as input so return it. - if ((options & ReadKeyOptions.AllowCtrlC) != 0) - { - return ProcessKey(key, includeDown); - } - - // Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate - // a real stop. This will not show an exception to a script based caller and it - // will avoid having to return something like default(KeyInfo). - throw new PipelineStoppedException(); - } - - return ProcessKey(key, includeDown); - } - finally - { - System.Console.TreatControlCAsInput = oldValue; - } - } - - /// - /// Flushes the current input buffer. - /// - public override void FlushInputBuffer() - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.FlushInputBuffer was called"); - } - - /// - /// Gets the contents of the console buffer in a rectangular area. - /// - /// The rectangle inside which buffer contents will be accessed. - /// A BufferCell array with the requested buffer contents. - public override BufferCell[,] GetBufferContents(Rectangle rectangle) - { - return this.internalRawUI.GetBufferContents(rectangle); - } - - /// - /// Scrolls the contents of the console buffer. - /// - /// The source rectangle to scroll. - /// The destination coordinates by which to scroll. - /// The rectangle inside which the scrolling will be clipped. - /// The cell with which the buffer will be filled. - public override void ScrollBufferContents( - Rectangle source, - Coordinates destination, - Rectangle clip, - BufferCell fill) - { - this.internalRawUI.ScrollBufferContents(source, destination, clip, fill); - } - - /// - /// Sets the contents of the buffer inside the specified rectangle. - /// - /// The rectangle inside which buffer contents will be filled. - /// The BufferCell which will be used to fill the requested space. - public override void SetBufferContents( - Rectangle rectangle, - BufferCell fill) - { - // If the rectangle is all -1s then it means clear the visible buffer - if (rectangle.Top == -1 && - rectangle.Bottom == -1 && - rectangle.Left == -1 && - rectangle.Right == -1) - { - System.Console.Clear(); - return; - } - - this.internalRawUI.SetBufferContents(rectangle, fill); - } - - /// - /// Sets the contents of the buffer at the given coordinate. - /// - /// The coordinate at which the buffer will be changed. - /// The new contents for the buffer at the given coordinate. - public override void SetBufferContents( - Coordinates origin, - BufferCell[,] contents) - { - this.internalRawUI.SetBufferContents(origin, contents); - } - - #endregion - - /// - /// Determines if a key press represents the input Ctrl + C. - /// - /// The key to test. - /// - /// if the key represents the input Ctrl + C, - /// otherwise . - /// - private static bool IsCtrlC(ConsoleKeyInfo keyInfo) - { - // In the VSCode terminal Ctrl C is processed as virtual key code "3", which - // is not a named value in the ConsoleKey enum. - if ((int)keyInfo.Key == 3) - { - return true; - } - - return keyInfo.Key == ConsoleKey.C && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0; - } - - /// - /// Converts objects to objects and caches - /// key down events for the next key up request. - /// - /// The key to convert. - /// - /// A value indicating whether the result should be a key down event. - /// - /// The converted value. - private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) - { - // Translate ConsoleModifiers to ControlKeyStates - ControlKeyStates states = default; - if ((key.Modifiers & ConsoleModifiers.Alt) != 0) - { - states |= ControlKeyStates.LeftAltPressed; - } - - if ((key.Modifiers & ConsoleModifiers.Control) != 0) - { - states |= ControlKeyStates.LeftCtrlPressed; - } - - if ((key.Modifiers & ConsoleModifiers.Shift) != 0) - { - states |= ControlKeyStates.ShiftPressed; - } - - var result = new KeyInfo((int)key.Key, key.KeyChar, states, isDown); - if (isDown) - { - this.lastKeyDown = result; - } - - return result; - } - } -} diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs deleted file mode 100644 index 82dab2d1e..000000000 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs +++ /dev/null @@ -1,180 +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 Microsoft.PowerShell.EditorServices.Console; - -namespace Microsoft.PowerShell.EditorServices -{ - using System; - using System.Management.Automation; - using System.Management.Automation.Host; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.PowerShell.EditorServices.Utility; - - /// - /// Provides an EditorServicesPSHostUserInterface implementation - /// that integrates with the user's terminal UI. - /// - public class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface - { - #region Private Fields - - private readonly PSHostUserInterface internalHostUI; - private ConsoleReadLine consoleReadLine; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ConsoleServicePSHostUserInterface - /// class with the given IConsoleHost implementation. - /// - /// The PowerShellContext to use for executing commands. - /// An ILogger implementation to use for this host. - /// The InternalHost instance from the origin runspace. - public TerminalPSHostUserInterface( - PowerShellContext powerShellContext, - ILogger logger, - PSHost internalHost) - : base( - powerShellContext, - new TerminalPSHostRawUserInterface(logger, internalHost), - logger) - { - this.internalHostUI = internalHost.UI; - this.consoleReadLine = new ConsoleReadLine(powerShellContext); - - // Set the output encoding to UTF-8 so that special - // characters are written to the console correctly - System.Console.OutputEncoding = System.Text.Encoding.UTF8; - - System.Console.CancelKeyPress += - (obj, args) => - { - if (!this.IsNativeApplicationRunning) - { - // We'll handle Ctrl+C - args.Cancel = true; - this.SendControlC(); - } - }; - } - - #endregion - - /// - /// Gets a value indicating whether writing progress is supported. - /// - internal protected override bool SupportsWriteProgress => true; - - /// - /// Requests that the HostUI implementation read a command line - /// from the user to be executed in the integrated console command - /// loop. - /// - /// - /// A CancellationToken used to cancel the command line request. - /// - /// A Task that can be awaited for the resulting input string. - protected override Task ReadCommandLineAsync(CancellationToken cancellationToken) - { - return this.consoleReadLine.ReadCommandLineAsync(cancellationToken); - } - - /// - /// Creates an InputPrompt handle to use for displaying input - /// prompts to the user. - /// - /// A new InputPromptHandler instance. - protected override InputPromptHandler OnCreateInputPromptHandler() - { - return new TerminalInputPromptHandler( - this.consoleReadLine, - this, - this.Logger); - } - - /// - /// Creates a ChoicePromptHandler to use for displaying a - /// choice prompt to the user. - /// - /// A new ChoicePromptHandler instance. - protected override ChoicePromptHandler OnCreateChoicePromptHandler() - { - return new TerminalChoicePromptHandler( - this.consoleReadLine, - this, - this.Logger); - } - - /// - /// 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) - { - ConsoleColor oldForegroundColor = System.Console.ForegroundColor; - ConsoleColor oldBackgroundColor = System.Console.BackgroundColor; - - System.Console.ForegroundColor = foregroundColor; - System.Console.BackgroundColor = ((int)backgroundColor != -1) ? backgroundColor : oldBackgroundColor; - - System.Console.Write(outputString + (includeNewLine ? Environment.NewLine : "")); - - System.Console.ForegroundColor = oldForegroundColor; - System.Console.BackgroundColor = oldBackgroundColor; - } - - /// - /// Invoked by to display a progress record. - /// - /// - /// Unique identifier of the source of the record. An int64 is used because typically, - /// the 'this' pointer of the command from whence the record is originating is used, and - /// that may be from a remote Runspace on a 64-bit machine. - /// - /// - /// The record being reported to the host. - /// - protected override void WriteProgressImpl(long sourceId, ProgressRecord record) - { - this.internalHostUI.WriteProgress(sourceId, record); - } - - /// - /// 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) - { - } - } -} diff --git a/src/PowerShellEditorServices/Session/HostDetails.cs b/src/PowerShellEditorServices/Session/HostDetails.cs deleted file mode 100644 index a20bc8cf3..000000000 --- a/src/PowerShellEditorServices/Session/HostDetails.cs +++ /dev/null @@ -1,92 +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; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Contains details about the current host application (most - /// likely the editor which is using the host process). - /// - public class HostDetails - { - #region Constants - - /// - /// The default host name for PowerShell Editor Services. Used - /// if no host name is specified by the host application. - /// - public const string DefaultHostName = "PowerShell Editor Services Host"; - - /// - /// The default host ID for PowerShell Editor Services. Used - /// for the host-specific profile path if no host ID is specified. - /// - public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; - - /// - /// The default host version for PowerShell Editor Services. If - /// no version is specified by the host application, we use 0.0.0 - /// to indicate a lack of version. - /// - public static readonly Version DefaultHostVersion = new Version("0.0.0"); - - /// - /// The default host details in a HostDetails object. - /// - public static readonly HostDetails Default = new HostDetails(null, null, null); - - #endregion - - #region Properties - - /// - /// Gets the name of the host. - /// - public string Name { get; private set; } - - /// - /// Gets the profile ID of the host, used to determine the - /// host-specific profile path. - /// - public string ProfileId { get; private set; } - - /// - /// Gets the version of the host. - /// - public Version Version { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the HostDetails class. - /// - /// - /// The display name for the host, typically in the form of - /// "[Application Name] Host". - /// - /// - /// The identifier of the PowerShell host to use for its profile path. - /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' - /// where 'X' represents the value of hostProfileId. If null, a default - /// will be used. - /// - /// The host application's version. - public HostDetails( - string name, - string profileId, - Version version) - { - this.Name = name ?? DefaultHostName; - this.ProfileId = profileId ?? DefaultHostProfileId; - this.Version = version ?? DefaultHostVersion; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/IPromptContext.cs b/src/PowerShellEditorServices/Session/IPromptContext.cs deleted file mode 100644 index 157715e7d..000000000 --- a/src/PowerShellEditorServices/Session/IPromptContext.cs +++ /dev/null @@ -1,67 +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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Provides methods for interacting with implementations of ReadLine. - /// - public interface IPromptContext - { - /// - /// Read a string that has been input by the user. - /// - /// Indicates if ReadLine should act like a command REPL. - /// - /// The cancellation token can be used to cancel reading user input. - /// - /// - /// A task object that represents the completion of reading input. The Result property will - /// return the input string. - /// - Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken); - - /// - /// Performs any additional actions required to cancel the current ReadLine invocation. - /// - void AbortReadLine(); - - /// - /// Creates a task that completes when the current ReadLine invocation has been aborted. - /// - /// - /// A task object that represents the abortion of the current ReadLine invocation. - /// - Task AbortReadLineAsync(); - - /// - /// Blocks until the current ReadLine invocation has exited. - /// - void WaitForReadLineExit(); - - /// - /// Creates a task that completes when the current ReadLine invocation has exited. - /// - /// - /// A task object that represents the exit of the current ReadLine invocation. - /// - Task WaitForReadLineExitAsync(); - - /// - /// Adds the specified command to the history managed by the ReadLine implementation. - /// - /// The command to record. - void AddToHistory(string command); - - /// - /// Forces the prompt handler to trigger PowerShell event handling, reliquishing control - /// of the pipeline thread during event processing. - /// - void ForcePSEventHandling(); - } -} diff --git a/src/PowerShellEditorServices/Session/IRunspaceCapability.cs b/src/PowerShellEditorServices/Session/IRunspaceCapability.cs deleted file mode 100644 index 38d14fb96..000000000 --- a/src/PowerShellEditorServices/Session/IRunspaceCapability.cs +++ /dev/null @@ -1,12 +0,0 @@ -// -// 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.Session -{ - internal interface IRunspaceCapability - { - // NOTE: This interface is intentionally empty for now. - } -} diff --git a/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs deleted file mode 100644 index 55540ba9d..000000000 --- a/src/PowerShellEditorServices/Session/IVersionSpecificOperations.cs +++ /dev/null @@ -1,33 +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.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal interface IVersionSpecificOperations - { - void ConfigureDebugger(Runspace runspace); - - void PauseDebugger(Runspace runspace); - - IEnumerable ExecuteCommandInDebugger( - PowerShellContext powerShellContext, - Runspace currentRunspace, - PSCommand psCommand, - bool sendOutputToHost, - out DebuggerResumeAction? debuggerResumeAction); - - void StopCommandInDebugger(PowerShellContext powerShellContext); - - bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace); - - void ExitNestedPrompt(PSHost host); - } -} - diff --git a/src/PowerShellEditorServices/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices/Session/InvocationEventQueue.cs deleted file mode 100644 index b6e9a9c0b..000000000 --- a/src/PowerShellEditorServices/Session/InvocationEventQueue.cs +++ /dev/null @@ -1,263 +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.Collections.Generic; -using System.Management.Automation.Runspaces; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using System.Threading; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - using System.Management.Automation; - - /// - /// Provides the ability to take over the current pipeline in a runspace. - /// - internal class InvocationEventQueue - { - private const string ShouldProcessInExecutionThreadPropertyName = "ShouldProcessInExecutionThread"; - - private static readonly PropertyInfo s_shouldProcessInExecutionThreadProperty = - typeof(PSEventSubscriber) - .GetProperty( - ShouldProcessInExecutionThreadPropertyName, - BindingFlags.Instance | BindingFlags.NonPublic); - - private readonly PromptNest _promptNest; - - private readonly Runspace _runspace; - - private readonly PowerShellContext _powerShellContext; - - private InvocationRequest _invocationRequest; - - private SemaphoreSlim _lock = AsyncUtils.CreateSimpleLockingSemaphore(); - - private InvocationEventQueue(PowerShellContext powerShellContext, PromptNest promptNest) - { - _promptNest = promptNest; - _powerShellContext = powerShellContext; - _runspace = powerShellContext.CurrentRunspace.Runspace; - } - - internal static InvocationEventQueue Create(PowerShellContext powerShellContext, PromptNest promptNest) - { - var eventQueue = new InvocationEventQueue(powerShellContext, promptNest); - eventQueue.CreateInvocationSubscriber(); - return eventQueue; - } - - /// - /// Executes a command on the main pipeline thread through - /// eventing. A event subscriber will - /// be created that creates a nested PowerShell instance for - /// to utilize. - /// - /// - /// Avoid using this method directly if possible. - /// will route commands - /// through this method if required. - /// - /// The expected result type. - /// The to be executed. - /// - /// Error messages from PowerShell will be written to the . - /// - /// Specifies options to be used when executing this command. - /// - /// An awaitable which will provide results once the command - /// execution completes. - /// - internal async Task> ExecuteCommandOnIdleAsync( - PSCommand psCommand, - StringBuilder errorMessages, - ExecutionOptions executionOptions) - { - var request = new PipelineExecutionRequest( - _powerShellContext, - psCommand, - errorMessages, - executionOptions); - - await SetInvocationRequestAsync( - new InvocationRequest( - pwsh => request.ExecuteAsync().GetAwaiter().GetResult())); - - try - { - return await request.Results; - } - finally - { - await SetInvocationRequestAsync(request: null); - } - } - - /// - /// Marshals a to run on the pipeline thread. A new - /// will be created for the invocation. - /// - /// - /// The to invoke on the pipeline thread. The nested - /// instance for the created - /// will be passed as an argument. - /// - /// - /// An awaitable that the caller can use to know when execution completes. - /// - internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) - { - var request = new InvocationRequest(pwsh => - { - using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: false)) - { - pwsh.Runspace = _runspace; - invocationAction(pwsh); - } - }); - - await SetInvocationRequestAsync(request); - try - { - await request.Task; - } - finally - { - await SetInvocationRequestAsync(null); - } - } - - private async Task WaitForExistingRequestAsync() - { - InvocationRequest existingRequest; - await _lock.WaitAsync(); - try - { - existingRequest = _invocationRequest; - if (existingRequest == null || existingRequest.Task.IsCompleted) - { - return; - } - } - finally - { - _lock.Release(); - } - - await existingRequest.Task; - } - - private async Task SetInvocationRequestAsync(InvocationRequest request) - { - await WaitForExistingRequestAsync(); - await _lock.WaitAsync(); - try - { - _invocationRequest = request; - } - finally - { - _lock.Release(); - } - - _powerShellContext.ForcePSEventHandling(); - } - - private void OnPowerShellIdle(object sender, EventArgs e) - { - if (!_lock.Wait(0)) - { - return; - } - - InvocationRequest currentRequest = null; - try - { - if (_invocationRequest == null) - { - return; - } - - currentRequest = _invocationRequest; - } - finally - { - _lock.Release(); - } - - _promptNest.PushPromptContext(); - try - { - currentRequest.Invoke(_promptNest.GetPowerShell()); - } - finally - { - _promptNest.PopPromptContext(); - } - } - - private PSEventSubscriber CreateInvocationSubscriber() - { - PSEventSubscriber subscriber = _runspace.Events.SubscribeEvent( - source: null, - eventName: PSEngineEvent.OnIdle, - sourceIdentifier: PSEngineEvent.OnIdle, - data: null, - handlerDelegate: OnPowerShellIdle, - supportEvent: true, - forwardEvent: false); - - SetSubscriberExecutionThreadWithReflection(subscriber); - - subscriber.Unsubscribed += OnInvokerUnsubscribed; - - return subscriber; - } - - private void OnInvokerUnsubscribed(object sender, PSEventUnsubscribedEventArgs e) - { - CreateInvocationSubscriber(); - } - - private void SetSubscriberExecutionThreadWithReflection(PSEventSubscriber subscriber) - { - // We need to create the PowerShell object in the same thread so we can get a nested - // PowerShell. This is the only way to consistently take control of the pipeline. The - // alternative is to make the subscriber a script block and have that create and process - // the PowerShell object, but that puts us in a different SessionState and is a lot slower. - s_shouldProcessInExecutionThreadProperty.SetValue(subscriber, true); - } - - private class InvocationRequest : TaskCompletionSource - { - private readonly Action _invocationAction; - - internal InvocationRequest(Action invocationAction) - { - _invocationAction = invocationAction; - } - - internal void Invoke(PowerShell pwsh) - { - try - { - _invocationAction(pwsh); - - // Ensure the result is set in another thread otherwise the caller - // may take over the pipeline thread. - System.Threading.Tasks.Task.Run(() => SetResult(true)); - } - catch (Exception e) - { - System.Threading.Tasks.Task.Run(() => SetException(e)); - } - } - } - } -} diff --git a/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs deleted file mode 100644 index ad68d0512..000000000 --- a/src/PowerShellEditorServices/Session/LegacyReadLineContext.cs +++ /dev/null @@ -1,56 +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.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Console; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal class LegacyReadLineContext : IPromptContext - { - private readonly ConsoleReadLine _legacyReadLine; - - internal LegacyReadLineContext(PowerShellContext powerShellContext) - { - _legacyReadLine = new ConsoleReadLine(powerShellContext); - } - - public Task AbortReadLineAsync() - { - return Task.FromResult(true); - } - - public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - return await _legacyReadLine.InvokeLegacyReadLineAsync(isCommandLine, cancellationToken); - } - - public Task WaitForReadLineExitAsync() - { - return Task.FromResult(true); - } - - public void AddToHistory(string command) - { - // Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine. - } - - public void AbortReadLine() - { - // Do nothing, no additional actions are needed to cancel ReadLine. - } - - public void WaitForReadLineExit() - { - // Do nothing, ReadLine cancellation is instant or not appliciable. - } - - public void ForcePSEventHandling() - { - // Do nothing, the pipeline thread is not occupied by legacy ReadLine. - } - } -} diff --git a/src/PowerShellEditorServices/Session/OutputType.cs b/src/PowerShellEditorServices/Session/OutputType.cs deleted file mode 100644 index ad67f6891..000000000 --- a/src/PowerShellEditorServices/Session/OutputType.cs +++ /dev/null @@ -1,41 +0,0 @@ -// -// 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 -{ - /// - /// Enumerates the types of output lines that will be sent - /// to an IConsoleHost implementation. - /// - public enum OutputType - { - /// - /// A normal output line, usually written with the or Write-Host or - /// Write-Output cmdlets. - /// - Normal, - - /// - /// A debug output line, written with the Write-Debug cmdlet. - /// - Debug, - - /// - /// A verbose output line, written with the Write-Verbose cmdlet. - /// - Verbose, - - /// - /// A warning output line, written with the Write-Warning cmdlet. - /// - Warning, - - /// - /// An error output line, written with the Write-Error cmdlet or - /// as a result of some error during PowerShell pipeline execution. - /// - Error - } -} diff --git a/src/PowerShellEditorServices/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices/Session/OutputWrittenEventArgs.cs deleted file mode 100644 index b1408a991..000000000 --- a/src/PowerShellEditorServices/Session/OutputWrittenEventArgs.cs +++ /dev/null @@ -1,65 +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; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about output that has been written to the - /// PowerShell host. - /// - public class OutputWrittenEventArgs - { - /// - /// Gets the text of the output. - /// - public string OutputText { get; private set; } - - /// - /// Gets the type of the output. - /// - public OutputType OutputType { get; private set; } - - /// - /// Gets a boolean which indicates whether a newline - /// should be written after the output. - /// - public bool IncludeNewLine { get; private set; } - - /// - /// Gets the foreground color of the output text. - /// - public ConsoleColor ForegroundColor { get; private set; } - - /// - /// Gets the background color of the output text. - /// - public ConsoleColor BackgroundColor { get; private set; } - - /// - /// Creates an instance of the OutputWrittenEventArgs class. - /// - /// The text of the output. - /// A boolean which indicates whether a newline should be written after the output. - /// The type of the output. - /// The foreground color of the output text. - /// The background color of the output text. - public OutputWrittenEventArgs( - string outputText, - bool includeNewLine, - OutputType outputType, - ConsoleColor foregroundColor, - ConsoleColor backgroundColor) - { - this.OutputText = outputText; - this.IncludeNewLine = includeNewLine; - this.OutputType = outputType; - this.ForegroundColor = foregroundColor; - this.BackgroundColor = backgroundColor; - } - } -} - diff --git a/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs deleted file mode 100644 index a71e32ed9..000000000 --- a/src/PowerShellEditorServices/Session/PSReadLinePromptContext.cs +++ /dev/null @@ -1,203 +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.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using System; -using System.Management.Automation.Runspaces; -using Microsoft.PowerShell.EditorServices.Console; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session { - using System.Management.Automation; - - internal class PSReadLinePromptContext : IPromptContext { - private const string ReadLineScript = @" - [System.Diagnostics.DebuggerHidden()] - [System.Diagnostics.DebuggerStepThrough()] - param() - return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null]::ReadLine( - $Host.Runspace, - $ExecutionContext, - $args[0])"; - - private const string ReadLineInitScript = @" - [System.Diagnostics.DebuggerHidden()] - [System.Diagnostics.DebuggerStepThrough()] - param() - end { - $module = Get-Module -ListAvailable PSReadLine | - Where-Object Version -eq '2.0.0' | - Where-Object { $_.PrivateData.PSData.Prerelease -notin 'beta1','beta2','beta3' } | - Sort-Object -Descending Version | - Select-Object -First 1 - if (-not $module) { - return - } - - Import-Module -ModuleInfo $module - return [Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null] - }"; - - private readonly PowerShellContext _powerShellContext; - - private PromptNest _promptNest; - - private InvocationEventQueue _invocationEventQueue; - - private ConsoleReadLine _consoleReadLine; - - private CancellationTokenSource _readLineCancellationSource; - - private PSReadLineProxy _readLineProxy; - - internal PSReadLinePromptContext( - PowerShellContext powerShellContext, - PromptNest promptNest, - InvocationEventQueue invocationEventQueue, - PSReadLineProxy readLineProxy) - { - _promptNest = promptNest; - _powerShellContext = powerShellContext; - _invocationEventQueue = invocationEventQueue; - _consoleReadLine = new ConsoleReadLine(powerShellContext); - _readLineProxy = readLineProxy; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return; - } - - _readLineProxy.OverrideReadKey( - intercept => ConsoleProxy.UnixReadKey( - intercept, - _readLineCancellationSource.Token)); - } - - internal static bool TryGetPSReadLineProxy( - ILogger logger, - Runspace runspace, - out PSReadLineProxy readLineProxy) - { - readLineProxy = null; - using (var pwsh = PowerShell.Create()) - { - pwsh.Runspace = runspace; - var psReadLineType = pwsh - .AddScript(ReadLineInitScript) - .Invoke() - .FirstOrDefault(); - - if (psReadLineType == null) - { - return false; - } - - try - { - readLineProxy = new PSReadLineProxy(psReadLineType, logger); - } - catch (InvalidOperationException) - { - // The Type we got back from PowerShell doesn't have the members we expected. - // Could be an older version, a custom build, or something a newer version with - // breaking changes. - return false; - } - } - - return true; - } - - public async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - _readLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var localTokenSource = _readLineCancellationSource; - if (localTokenSource.Token.IsCancellationRequested) - { - throw new TaskCanceledException(); - } - - try - { - if (!isCommandLine) - { - return await _consoleReadLine.InvokeLegacyReadLineAsync( - false, - _readLineCancellationSource.Token); - } - - var result = (await _powerShellContext.ExecuteCommandAsync( - new PSCommand() - .AddScript(ReadLineScript) - .AddArgument(_readLineCancellationSource.Token), - null, - new ExecutionOptions() - { - WriteErrorsToHost = false, - WriteOutputToHost = false, - InterruptCommandPrompt = false, - AddToHistory = false, - IsReadLine = isCommandLine - })) - .FirstOrDefault(); - - return cancellationToken.IsCancellationRequested - ? string.Empty - : result; - } - finally - { - _readLineCancellationSource = null; - } - } - - public void AbortReadLine() - { - if (_readLineCancellationSource == null) - { - return; - } - - _readLineCancellationSource.Cancel(); - - WaitForReadLineExit(); - } - - public async Task AbortReadLineAsync() { - if (_readLineCancellationSource == null) - { - return; - } - - _readLineCancellationSource.Cancel(); - - await WaitForReadLineExitAsync(); - } - - public void WaitForReadLineExit() - { - using (_promptNest.GetRunspaceHandle(CancellationToken.None, isReadLine: true)) - { } - } - - public async Task WaitForReadLineExitAsync() { - using (await _promptNest.GetRunspaceHandleAsync(CancellationToken.None, isReadLine: true)) - { } - } - - public void AddToHistory(string command) - { - _readLineProxy.AddToHistory(command); - } - - public void ForcePSEventHandling() - { - _readLineProxy.ForcePSEventHandling(); - } - } -} diff --git a/src/PowerShellEditorServices/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices/Session/PSReadLineProxy.cs deleted file mode 100644 index 50aaf4af3..000000000 --- a/src/PowerShellEditorServices/Session/PSReadLineProxy.cs +++ /dev/null @@ -1,119 +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.Reflection; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal class PSReadLineProxy - { - private const string FieldMemberType = "field"; - - private const string MethodMemberType = "method"; - - private const string AddToHistoryMethodName = "AddToHistory"; - - private const string SetKeyHandlerMethodName = "SetKeyHandler"; - - private const string ReadKeyOverrideFieldName = "_readKeyOverride"; - - private const string VirtualTerminalTypeName = "Microsoft.PowerShell.Internal.VirtualTerminal"; - - private const string ForcePSEventHandlingMethodName = "ForcePSEventHandling"; - - private static readonly Type[] s_setKeyHandlerTypes = - { - typeof(string[]), - typeof(Action), - typeof(string), - typeof(string) - }; - - private static readonly Type[] s_addToHistoryTypes = { typeof(string) }; - - private readonly FieldInfo _readKeyOverrideField; - - internal PSReadLineProxy(Type psConsoleReadLine, ILogger logger) - { - ForcePSEventHandling = - (Action)psConsoleReadLine.GetMethod( - ForcePSEventHandlingMethodName, - BindingFlags.Static | BindingFlags.NonPublic) - ?.CreateDelegate(typeof(Action)); - - AddToHistory = (Action)psConsoleReadLine.GetMethod( - AddToHistoryMethodName, - s_addToHistoryTypes) - ?.CreateDelegate(typeof(Action)); - - SetKeyHandler = - (Action, string, string>)psConsoleReadLine.GetMethod( - SetKeyHandlerMethodName, - s_setKeyHandlerTypes) - ?.CreateDelegate(typeof(Action, string, string>)); - - _readKeyOverrideField = psConsoleReadLine.GetTypeInfo().Assembly - .GetType(VirtualTerminalTypeName) - ?.GetField(ReadKeyOverrideFieldName, BindingFlags.Static | BindingFlags.NonPublic); - - if (_readKeyOverrideField == null) - { - throw NewInvalidPSReadLineVersionException( - FieldMemberType, - ReadKeyOverrideFieldName, - logger); - } - - if (SetKeyHandler == null) - { - throw NewInvalidPSReadLineVersionException( - MethodMemberType, - SetKeyHandlerMethodName, - logger); - } - - if (AddToHistory == null) - { - throw NewInvalidPSReadLineVersionException( - MethodMemberType, - AddToHistoryMethodName, - logger); - } - - if (ForcePSEventHandling == null) - { - throw NewInvalidPSReadLineVersionException( - MethodMemberType, - ForcePSEventHandlingMethodName, - logger); - } - } - - internal Action AddToHistory { get; } - - internal Action, object>, string, string> SetKeyHandler { get; } - - internal Action ForcePSEventHandling { get; } - - internal void OverrideReadKey(Func readKeyFunc) - { - _readKeyOverrideField.SetValue(null, readKeyFunc); - } - - private static InvalidOperationException NewInvalidPSReadLineVersionException( - string memberType, - string memberName, - ILogger logger) - { - logger.Write( - LogLevel.Error, - $"The loaded version of PSReadLine is not supported. The {memberType} \"{memberName}\" was not found."); - - return new InvalidOperationException(); - } - } -} diff --git a/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs deleted file mode 100644 index 1c69f6a15..000000000 --- a/src/PowerShellEditorServices/Session/PipelineExecutionRequest.cs +++ /dev/null @@ -1,80 +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.Collections.Generic; -using System.Management.Automation; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal interface IPipelineExecutionRequest - { - Task ExecuteAsync(); - - Task WaitTask { get; } - } - - /// - /// Contains details relating to a request to execute a - /// command on the PowerShell pipeline thread. - /// - /// The expected result type of the execution. - internal class PipelineExecutionRequest : IPipelineExecutionRequest - { - private PowerShellContext _powerShellContext; - private PSCommand _psCommand; - private StringBuilder _errorMessages; - private ExecutionOptions _executionOptions; - private TaskCompletionSource> _resultsTask; - - public Task> Results - { - get { return this._resultsTask.Task; } - } - - public Task WaitTask { get { return Results; } } - - public PipelineExecutionRequest( - PowerShellContext powerShellContext, - PSCommand psCommand, - StringBuilder errorMessages, - bool sendOutputToHost) - : this( - powerShellContext, - psCommand, - errorMessages, - new ExecutionOptions() - { - WriteOutputToHost = sendOutputToHost - }) - { } - - - public PipelineExecutionRequest( - PowerShellContext powerShellContext, - PSCommand psCommand, - StringBuilder errorMessages, - ExecutionOptions executionOptions) - { - _powerShellContext = powerShellContext; - _psCommand = psCommand; - _errorMessages = errorMessages; - _executionOptions = executionOptions; - _resultsTask = new TaskCompletionSource>(); - } - - public async Task ExecuteAsync() - { - var results = - await _powerShellContext.ExecuteCommandAsync( - _psCommand, - _errorMessages, - _executionOptions); - - var unusedTask = Task.Run(() => _resultsTask.SetResult(results)); - } - } -} diff --git a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices/Session/PowerShell5Operations.cs deleted file mode 100644 index 20954ac21..000000000 --- a/src/PowerShellEditorServices/Session/PowerShell5Operations.cs +++ /dev/null @@ -1,107 +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.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - internal class PowerShell5Operations : IVersionSpecificOperations - { - public void ConfigureDebugger(Runspace runspace) - { - if (runspace.Debugger != null) - { - runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); - } - } - - public virtual void PauseDebugger(Runspace runspace) - { - if (runspace.Debugger != null) - { - runspace.Debugger.SetDebuggerStepMode(true); - } - } - - public virtual bool IsDebuggerStopped(PromptNest promptNest, Runspace runspace) - { - return runspace.Debugger.InBreakpoint || (promptNest.IsRemote && promptNest.IsInDebugger); - } - - public IEnumerable ExecuteCommandInDebugger( - PowerShellContext powerShellContext, - Runspace currentRunspace, - PSCommand psCommand, - bool sendOutputToHost, - out DebuggerResumeAction? debuggerResumeAction) - { - debuggerResumeAction = null; - PSDataCollection outputCollection = new PSDataCollection(); - - if (sendOutputToHost) - { - outputCollection.DataAdded += - (obj, e) => - { - for (int i = e.Index; i < outputCollection.Count; i++) - { - powerShellContext.WriteOutput( - outputCollection[i].ToString(), - true); - } - }; - } - - DebuggerCommandResults commandResults = - currentRunspace.Debugger.ProcessCommand( - psCommand, - outputCollection); - - // Pass along the debugger's resume action if the user's - // command caused one to be returned - debuggerResumeAction = commandResults.ResumeAction; - - IEnumerable results = null; - if (typeof(TResult) != typeof(PSObject)) - { - results = - outputCollection - .Select(pso => pso.BaseObject) - .Cast(); - } - else - { - results = outputCollection.Cast(); - } - - return results; - } - - public void StopCommandInDebugger(PowerShellContext powerShellContext) - { - // If the RunspaceAvailability is None, the runspace is dead and we should not try to run anything in it. - if (powerShellContext.CurrentRunspace.Runspace.RunspaceAvailability != RunspaceAvailability.None) - { - powerShellContext.CurrentRunspace.Runspace.Debugger.StopProcessCommand(); - } - } - - public void ExitNestedPrompt(PSHost host) - { - try - { - host.ExitNestedPrompt(); - } - catch (FlowControlException) - { - } - } - } -} - diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs deleted file mode 100644 index fbfe46cb2..000000000 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ /dev/null @@ -1,2532 +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.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; -using System.Runtime.InteropServices; -using System.Linq; -using System.Management.Automation.Host; -using System.Management.Automation.Remoting; -using System.Management.Automation.Runspaces; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Session; -using Microsoft.PowerShell.EditorServices.Session.Capabilities; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - using System.ComponentModel; - using System.Management.Automation; - - /// - /// Manages the lifetime and usage of a PowerShell session. - /// Handles nested PowerShell prompts and also manages execution of - /// commands whether inside or outside of the debugger. - /// - public class PowerShellContext : IDisposable, IHostSupportsInteractiveSession - { - private static readonly Action s_runspaceApartmentStateSetter; - - static PowerShellContext() - { - // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection - if (!Utils.IsNetCore) - { - MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); - Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); - s_runspaceApartmentStateSetter = (Action)setter; - } - } - - #region Fields - - private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - - private bool isPSReadLineEnabled; - private ILogger logger; - private PowerShell powerShell; - private bool ownsInitialRunspace; - private RunspaceDetails initialRunspace; - private SessionDetails mostRecentSessionDetails; - - private ProfilePaths profilePaths; - - private IVersionSpecificOperations versionSpecificOperations; - - private Stack runspaceStack = new Stack(); - - private int isCommandLoopRestarterSet; - - #endregion - - #region Properties - - private IPromptContext PromptContext { get; set; } - - private PromptNest PromptNest { get; set; } - - private InvocationEventQueue InvocationEventQueue { get; set; } - - private EngineIntrinsics EngineIntrinsics { get; set; } - - private PSHost ExternalHost { get; set; } - - /// - /// Gets a boolean that indicates whether the debugger is currently stopped, - /// either at a breakpoint or because the user broke execution. - /// - public bool IsDebuggerStopped => - this.versionSpecificOperations.IsDebuggerStopped( - PromptNest, - CurrentRunspace.Runspace); - - /// - /// Gets the current state of the session. - /// - public PowerShellContextState SessionState - { - get; - private set; - } - - /// - /// Gets the PowerShell version details for the initial local runspace. - /// - public PowerShellVersionDetails LocalPowerShellVersion - { - get; - private set; - } - - /// - /// Gets or sets an IHostOutput implementation for use in - /// writing output to the console. - /// - private IHostOutput ConsoleWriter { get; set; } - - private IHostInput ConsoleReader { get; set; } - - /// - /// Gets details pertaining to the current runspace. - /// - public RunspaceDetails CurrentRunspace - { - get; - private set; - } - - /// - /// Gets a value indicating whether the current runspace - /// is ready for a command - /// - public bool IsAvailable => this.SessionState == PowerShellContextState.Ready; - - /// - /// Gets the working directory path the PowerShell context was inititially set when the debugger launches. - /// This path is used to determine whether a script in the call stack is an "external" script. - /// - public string InitialWorkingDirectory { get; private set; } - - #endregion - - #region Constructors - - /// - /// - /// - /// An ILogger implementation used for writing log messages. - /// - /// Indicates whether PSReadLine should be used if possible - /// - public PowerShellContext(ILogger logger, bool isPSReadLineEnabled) - { - this.logger = logger; - this.isPSReadLineEnabled = isPSReadLineEnabled; - } - - /// - /// - /// - /// - /// - /// - /// The EditorServicesPSHostUserInterface to use for this instance. - /// - /// An ILogger implementation to use for this instance. - /// - public static Runspace CreateRunspace( - HostDetails hostDetails, - PowerShellContext powerShellContext, - EditorServicesPSHostUserInterface hostUserInterface, - ILogger logger) - { - var psHost = new EditorServicesPSHost(powerShellContext, hostDetails, hostUserInterface, logger); - powerShellContext.ConsoleWriter = hostUserInterface; - powerShellContext.ConsoleReader = hostUserInterface; - return CreateRunspace(psHost); - } - - /// - /// - /// - /// - /// - public static Runspace CreateRunspace(PSHost psHost) - { - InitialSessionState initialSessionState; - if (Environment.GetEnvironmentVariable("PSES_TEST_USE_CREATE_DEFAULT") == "1") { - initialSessionState = InitialSessionState.CreateDefault(); - } else { - initialSessionState = InitialSessionState.CreateDefault2(); - } - - Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); - - // Windows PowerShell must be hosted in STA mode - // This must be set on the runspace *before* it is opened - if (s_runspaceApartmentStateSetter != null) - { - s_runspaceApartmentStateSetter(runspace, ApartmentState.STA); - } - - runspace.ThreadOptions = PSThreadOptions.ReuseThread; - runspace.Open(); - - return runspace; - } - - /// - /// Initializes a new instance of the PowerShellContext class using - /// an existing runspace for the session. - /// - /// An object containing the profile paths for the session. - /// The initial runspace to use for this instance. - /// If true, the PowerShellContext owns this runspace. - public void Initialize( - ProfilePaths profilePaths, - Runspace initialRunspace, - bool ownsInitialRunspace) - { - this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, null); - } - - /// - /// Initializes a new instance of the PowerShellContext class using - /// an existing runspace for the session. - /// - /// An object containing the profile paths for the session. - /// The initial runspace to use for this instance. - /// If true, the PowerShellContext owns this runspace. - /// An IHostOutput implementation. Optional. - public void Initialize( - ProfilePaths profilePaths, - Runspace initialRunspace, - bool ownsInitialRunspace, - IHostOutput consoleHost) - { - Validate.IsNotNull("initialRunspace", initialRunspace); - - this.ownsInitialRunspace = ownsInitialRunspace; - this.SessionState = PowerShellContextState.NotStarted; - this.ConsoleWriter = consoleHost; - this.ConsoleReader = consoleHost as IHostInput; - - // Get the PowerShell runtime version - this.LocalPowerShellVersion = - PowerShellVersionDetails.GetVersionDetails( - initialRunspace, - this.logger); - - this.powerShell = PowerShell.Create(); - this.powerShell.Runspace = initialRunspace; - - this.initialRunspace = - new RunspaceDetails( - initialRunspace, - this.GetSessionDetailsInRunspace(initialRunspace), - this.LocalPowerShellVersion, - RunspaceLocation.Local, - RunspaceContext.Original, - null); - this.CurrentRunspace = this.initialRunspace; - - // Write out the PowerShell version for tracking purposes - this.logger.Write( - LogLevel.Normal, - string.Format( - "PowerShell runtime version: {0}, edition: {1}", - this.LocalPowerShellVersion.Version, - this.LocalPowerShellVersion.Edition)); - - Version powerShellVersion = this.LocalPowerShellVersion.Version; - if (powerShellVersion >= new Version(5, 0)) - { - this.versionSpecificOperations = new PowerShell5Operations(); - } - else - { - throw new NotSupportedException( - "This computer has an unsupported version of PowerShell installed: " + - powerShellVersion.ToString()); - } - - if (this.LocalPowerShellVersion.Edition != "Linux") - { - // TODO: Should this be configurable? - this.SetExecutionPolicy(ExecutionPolicy.RemoteSigned); - } - - // Set up the runspace - this.ConfigureRunspace(this.CurrentRunspace); - - // Add runspace capabilities - this.ConfigureRunspaceCapabilities(this.CurrentRunspace); - - // Set the $profile variable in the runspace - this.profilePaths = profilePaths; - if (this.profilePaths != null) - { - this.SetProfileVariableInCurrentRunspace(profilePaths); - } - - // Now that initialization is complete we can watch for InvocationStateChanged - this.SessionState = PowerShellContextState.Ready; - - // EngineIntrinsics is used in some instances to interact with the initial - // runspace without having to wait for PSReadLine to check for events. - this.EngineIntrinsics = - initialRunspace - .SessionStateProxy - .PSVariable - .GetValue("ExecutionContext") - as EngineIntrinsics; - - // The external host is used to properly exit from a nested prompt that - // was entered by the user. - this.ExternalHost = - initialRunspace - .SessionStateProxy - .PSVariable - .GetValue("Host") - as PSHost; - - // Now that the runspace is ready, enqueue it for first use - this.PromptNest = new PromptNest( - this, - this.powerShell, - this.ConsoleReader, - this.versionSpecificOperations); - this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); - - if (powerShellVersion.Major >= 5 && - this.isPSReadLineEnabled && - PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) - { - this.PromptContext = new PSReadLinePromptContext( - this, - this.PromptNest, - this.InvocationEventQueue, - proxy); - } - else - { - this.PromptContext = new LegacyReadLineContext(this); - } - } - - /// - /// Imports the PowerShellEditorServices.Commands module into - /// the runspace. This method will be moved somewhere else soon. - /// - /// - /// - public Task ImportCommandsModuleAsync(string moduleBasePath) - { - PSCommand importCommand = new PSCommand(); - importCommand - .AddCommand("Import-Module") - .AddArgument( - Path.Combine( - moduleBasePath, - "PowerShellEditorServices.Commands.psd1")); - - return this.ExecuteCommandAsync(importCommand, false, false); - } - - private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) - { - // The only types of runspaces that need to be configured are: - // - Locally created runspaces - // - Local process entered with Enter-PSHostProcess - // - Remote session entered with Enter-PSSession - return - (runspaceDetails.Location == RunspaceLocation.Local && - (runspaceDetails.Context == RunspaceContext.Original || - runspaceDetails.Context == RunspaceContext.EnteredProcess)) || - (runspaceDetails.Location == RunspaceLocation.Remote && runspaceDetails.Context == RunspaceContext.Original); - } - - private void ConfigureRunspace(RunspaceDetails runspaceDetails) - { - runspaceDetails.Runspace.StateChanged += this.HandleRunspaceStateChanged; - if (runspaceDetails.Runspace.Debugger != null) - { - runspaceDetails.Runspace.Debugger.BreakpointUpdated += OnBreakpointUpdated; - runspaceDetails.Runspace.Debugger.DebuggerStop += OnDebuggerStop; - } - - this.versionSpecificOperations.ConfigureDebugger(runspaceDetails.Runspace); - } - - private void CleanupRunspace(RunspaceDetails runspaceDetails) - { - runspaceDetails.Runspace.StateChanged -= this.HandleRunspaceStateChanged; - if (runspaceDetails.Runspace.Debugger != null) - { - runspaceDetails.Runspace.Debugger.BreakpointUpdated -= OnBreakpointUpdated; - runspaceDetails.Runspace.Debugger.DebuggerStop -= OnDebuggerStop; - } - } - - #endregion - - #region Public Methods - - /// - /// Gets a RunspaceHandle for the session's runspace. This - /// handle is used to gain temporary ownership of the runspace - /// so that commands can be executed against it directly. - /// - /// A RunspaceHandle instance that gives access to the session's runspace. - public Task GetRunspaceHandleAsync() - { - return this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine: false); - } - - /// - /// Gets a RunspaceHandle for the session's runspace. This - /// handle is used to gain temporary ownership of the runspace - /// so that commands can be executed against it directly. - /// - /// A CancellationToken that can be used to cancel the request. - /// A RunspaceHandle instance that gives access to the session's runspace. - public Task GetRunspaceHandleAsync(CancellationToken cancellationToken) - { - return this.GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); - } - - /// - /// Executes a PSCommand against the session's runspace and returns - /// a collection of results of the expected type. - /// - /// The expected result type. - /// The PSCommand to be executed. - /// - /// If true, causes any output written during command execution to be written to the host. - /// - /// - /// If true, causes any errors encountered during command execution to be written to the host. - /// - /// - /// An awaitable Task which will provide results once the command - /// execution completes. - /// - public async Task> ExecuteCommandAsync( - PSCommand psCommand, - bool sendOutputToHost = false, - bool sendErrorToHost = true) - { - return await ExecuteCommandAsync(psCommand, null, sendOutputToHost, sendErrorToHost); - } - - /// - /// Executes a PSCommand against the session's runspace and returns - /// a collection of results of the expected type. - /// - /// The expected result type. - /// The PSCommand to be executed. - /// Error messages from PowerShell will be written to the StringBuilder. - /// - /// If true, causes any output written during command execution to be written to the host. - /// - /// - /// If true, causes any errors encountered during command execution to be written to the host. - /// - /// - /// If true, adds the command to the user's command history. - /// - /// - /// An awaitable Task which will provide results once the command - /// execution completes. - /// - public Task> ExecuteCommandAsync( - PSCommand psCommand, - StringBuilder errorMessages, - bool sendOutputToHost = false, - bool sendErrorToHost = true, - bool addToHistory = false) - { - return - this.ExecuteCommandAsync( - psCommand, - errorMessages, - new ExecutionOptions - { - WriteOutputToHost = sendOutputToHost, - WriteErrorsToHost = sendErrorToHost, - AddToHistory = addToHistory - }); - } - - /// - /// Executes a PSCommand against the session's runspace and returns - /// a collection of results of the expected type. - /// - /// The expected result type. - /// The PSCommand to be executed. - /// Error messages from PowerShell will be written to the StringBuilder. - /// Specifies options to be used when executing this command. - /// - /// An awaitable Task which will provide results once the command - /// execution completes. - /// - public async Task> ExecuteCommandAsync( - PSCommand psCommand, - StringBuilder errorMessages, - ExecutionOptions executionOptions) - { - // Add history to PSReadLine before cancelling, otherwise it will be restored as the - // cancelled prompt when it's called again. - if (executionOptions.AddToHistory) - { - this.PromptContext.AddToHistory(psCommand.Commands[0].CommandText); - } - - bool hadErrors = false; - RunspaceHandle runspaceHandle = null; - ExecutionTarget executionTarget = ExecutionTarget.PowerShell; - IEnumerable executionResult = Enumerable.Empty(); - var shouldCancelReadLine = - executionOptions.InterruptCommandPrompt || - executionOptions.WriteOutputToHost; - - // If the debugger is active and the caller isn't on the pipeline - // thread, send the command over to that thread to be executed. - // Determine if execution should take place in a different thread - // using the following criteria: - // 1. The current frame in the prompt nest has a thread controller - // (meaning it is a nested prompt or is in the debugger) - // 2. We aren't already on the thread in question - // 3. The command is not a candidate for background invocation - // via PowerShell eventing - // 4. The command cannot be for a PSReadLine pipeline while we - // are currently in a out of process runspace - var threadController = PromptNest.GetThreadController(); - if (!(threadController == null || - !threadController.IsPipelineThread || - threadController.IsCurrentThread() || - this.ShouldExecuteWithEventing(executionOptions) || - (PromptNest.IsRemote && executionOptions.IsReadLine))) - { - this.logger.Write(LogLevel.Verbose, "Passing command execution to pipeline thread."); - - if (shouldCancelReadLine && PromptNest.IsReadLineBusy()) - { - // If a ReadLine pipeline is running in the debugger then we'll hang here - // if we don't cancel it. Typically we can rely on OnExecutionStatusChanged but - // the pipeline request won't even start without clearing the current task. - this.ConsoleReader?.StopCommandLoop(); - } - - // Send the pipeline execution request to the pipeline thread - return await threadController.RequestPipelineExecutionAsync( - new PipelineExecutionRequest( - this, - psCommand, - errorMessages, - executionOptions)); - } - else - { - try - { - // Instruct PowerShell to send output and errors to the host - if (executionOptions.WriteOutputToHost) - { - psCommand.Commands[0].MergeMyResults( - PipelineResultTypes.Error, - PipelineResultTypes.Output); - - psCommand.Commands.Add( - this.GetOutputCommand( - endOfStatement: false)); - } - - executionTarget = GetExecutionTarget(executionOptions); - - // If a ReadLine pipeline is running we can still execute commands that - // don't write output (e.g. command completion) - if (executionTarget == ExecutionTarget.InvocationEvent) - { - return (await this.InvocationEventQueue.ExecuteCommandOnIdleAsync( - psCommand, - errorMessages, - executionOptions)); - } - - // Prompt is stopped and started based on the execution status, so naturally - // we don't want PSReadLine pipelines to factor in. - if (!executionOptions.IsReadLine) - { - this.OnExecutionStatusChanged( - ExecutionStatus.Running, - executionOptions, - false); - } - - runspaceHandle = await this.GetRunspaceHandleAsync(executionOptions.IsReadLine); - if (executionOptions.WriteInputToHost) - { - this.WriteOutput(psCommand.Commands[0].CommandText, true); - } - - if (executionTarget == ExecutionTarget.Debugger) - { - // Manually change the session state for debugger commands because - // we don't have an invocation state event to attach to. - if (!executionOptions.IsReadLine) - { - this.OnSessionStateChanged( - this, - new SessionStateChangedEventArgs( - PowerShellContextState.Running, - PowerShellExecutionResult.NotFinished, - null)); - } - try - { - return this.ExecuteCommandInDebugger( - psCommand, - executionOptions.WriteOutputToHost); - } - catch (Exception e) - { - logger.Write( - LogLevel.Error, - "Exception occurred while executing debugger command:\r\n\r\n" + e.ToString()); - } - finally - { - if (!executionOptions.IsReadLine) - { - this.OnSessionStateChanged( - this, - new SessionStateChangedEventArgs( - PowerShellContextState.Ready, - PowerShellExecutionResult.Stopped, - null)); - } - } - } - - var invocationSettings = new PSInvocationSettings() - { - AddToHistory = executionOptions.AddToHistory - }; - - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Attempting to execute command(s):\r\n\r\n{0}", - GetStringForPSCommand(psCommand))); - - - PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine); - shell.Commands = psCommand; - - // Don't change our SessionState for ReadLine. - if (!executionOptions.IsReadLine) - { - shell.InvocationStateChanged += powerShell_InvocationStateChanged; - } - - shell.Runspace = executionOptions.ShouldExecuteInOriginalRunspace - ? this.initialRunspace.Runspace - : this.CurrentRunspace.Runspace; - try - { - // Nested PowerShell instances can't be invoked asynchronously. This occurs - // in nested prompts and pipeline requests from eventing. - if (shell.IsNested) - { - return shell.Invoke(null, invocationSettings); - } - - return await Task.Factory.StartNew>( - () => shell.Invoke(null, invocationSettings), - CancellationToken.None, // Might need a cancellation token - TaskCreationOptions.None, - TaskScheduler.Default); - } - finally - { - if (!executionOptions.IsReadLine) - { - shell.InvocationStateChanged -= powerShell_InvocationStateChanged; - } - - if (shell.HadErrors) - { - var strBld = new StringBuilder(1024); - strBld.AppendFormat("Execution of the following command(s) completed with errors:\r\n\r\n{0}\r\n", - GetStringForPSCommand(psCommand)); - - int i = 1; - foreach (var error in shell.Streams.Error) - { - if (i > 1) strBld.Append("\r\n\r\n"); - strBld.Append($"Error #{i++}:\r\n"); - strBld.Append(error.ToString() + "\r\n"); - strBld.Append("ScriptStackTrace:\r\n"); - strBld.Append((error.ScriptStackTrace ?? "") + "\r\n"); - strBld.Append($"Exception:\r\n {error.Exception?.ToString() ?? ""}"); - Exception innerEx = error.Exception?.InnerException; - while (innerEx != null) - { - strBld.Append($"InnerException:\r\n {innerEx.ToString()}"); - innerEx = innerEx.InnerException; - } - } - - // We've reported these errors, clear them so they don't keep showing up. - shell.Streams.Error.Clear(); - - var errorMessage = strBld.ToString(); - - errorMessages?.Append(errorMessage); - this.logger.Write(LogLevel.Error, errorMessage); - - hadErrors = true; - } - else - { - this.logger.Write( - LogLevel.Verbose, - "Execution completed successfully."); - } - } - } - catch (PSRemotingDataStructureException e) - { - this.logger.Write( - LogLevel.Error, - "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); - - errorMessages?.Append(e.Message); - } - catch (PipelineStoppedException e) - { - this.logger.Write( - LogLevel.Error, - "Pipeline stopped while executing command:\r\n\r\n" + e.ToString()); - - errorMessages?.Append(e.Message); - } - catch (RuntimeException e) - { - this.logger.Write( - LogLevel.Warning, - "Runtime exception occurred while executing command:\r\n\r\n" + e.ToString()); - - hadErrors = true; - errorMessages?.Append(e.Message); - - if (executionOptions.WriteErrorsToHost) - { - // Write the error to the host - this.WriteExceptionToHost(e); - } - } - catch (Exception e) - { - this.OnExecutionStatusChanged( - ExecutionStatus.Failed, - executionOptions, - true); - - throw; - } - finally - { - // If the RunspaceAvailability is None, it means that the runspace we're in is dead. - // If this is the case, we should abort the execution which will clean up the runspace - // (and clean up the debugger) and then pop it off the stack. - // An example of when this happens is when the "attach" debug config is used and the - // process you're attached to dies randomly. - if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.None) - { - this.AbortExecution(shouldAbortDebugSession: true); - this.PopRunspace(); - } - - // Get the new prompt before releasing the runspace handle - if (executionOptions.WriteOutputToHost) - { - SessionDetails sessionDetails = null; - - // Get the SessionDetails and then write the prompt - if (executionTarget == ExecutionTarget.Debugger) - { - sessionDetails = this.GetSessionDetailsInDebugger(); - } - else if (this.CurrentRunspace.Runspace.RunspaceAvailability == RunspaceAvailability.Available) - { - // This state can happen if the user types a command that causes the - // debugger to exit before we reach this point. No RunspaceHandle - // will exist already so we need to create one and then use it - if (runspaceHandle == null) - { - runspaceHandle = await this.GetRunspaceHandleAsync(); - } - - sessionDetails = this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); - } - else - { - sessionDetails = this.GetSessionDetailsInNestedPipeline(); - } - - // Check if the runspace has changed - this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails); - } - - // Dispose of the execution context - if (runspaceHandle != null) - { - runspaceHandle.Dispose(); - } - - this.OnExecutionStatusChanged( - ExecutionStatus.Completed, - executionOptions, - hadErrors); - } - } - - return executionResult; - } - - /// - /// Executes a PSCommand in the session's runspace without - /// expecting to receive any result. - /// - /// The PSCommand to be executed. - /// - /// An awaitable Task that the caller can use to know when - /// execution completes. - /// - public Task ExecuteCommandAsync(PSCommand psCommand) - { - return this.ExecuteCommandAsync(psCommand); - } - - /// - /// Executes a script string in the session's runspace. - /// - /// The script string to execute. - /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptStringAsync( - string scriptString) - { - return this.ExecuteScriptStringAsync(scriptString, false, true); - } - - /// - /// Executes a script string in the session's runspace. - /// - /// The script string to execute. - /// Error messages from PowerShell will be written to the StringBuilder. - /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptStringAsync( - string scriptString, - StringBuilder errorMessages) - { - return this.ExecuteScriptStringAsync(scriptString, errorMessages, false, true, false); - } - - /// - /// Executes a script string in the session's runspace. - /// - /// The script string to execute. - /// If true, causes the script string to be written to the host. - /// If true, causes the script output to be written to the host. - /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptStringAsync( - string scriptString, - bool writeInputToHost, - bool writeOutputToHost) - { - return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, false); - } - - /// - /// Executes a script string in the session's runspace. - /// - /// The script string to execute. - /// If true, causes the script string to be written to the host. - /// If true, causes the script output to be written to the host. - /// If true, adds the command to the user's command history. - /// A Task that can be awaited for the script completion. - public Task> ExecuteScriptStringAsync( - string scriptString, - bool writeInputToHost, - bool writeOutputToHost, - bool addToHistory) - { - return this.ExecuteScriptStringAsync(scriptString, null, writeInputToHost, writeOutputToHost, addToHistory); - } - - /// - /// Executes a script string in the session's runspace. - /// - /// The script string to execute. - /// Error messages from PowerShell will be written to the StringBuilder. - /// If true, causes the script string to be written to the host. - /// If true, causes the script output to be written to the host. - /// If true, adds the command to the user's command history. - /// A Task that can be awaited for the script completion. - public async Task> ExecuteScriptStringAsync( - string scriptString, - StringBuilder errorMessages, - bool writeInputToHost, - bool writeOutputToHost, - bool addToHistory) - { - return await this.ExecuteCommandAsync( - new PSCommand().AddScript(scriptString.Trim()), - errorMessages, - new ExecutionOptions() - { - WriteOutputToHost = writeOutputToHost, - AddToHistory = addToHistory, - WriteInputToHost = writeInputToHost - }); - } - - /// - /// Executes a script file at the specified path. - /// - /// The script execute. - /// Arguments to pass to the script. - /// Writes the executed script path and arguments to the host. - /// A Task that can be awaited for completion. - public async Task ExecuteScriptWithArgsAsync(string script, string arguments = null, bool writeInputToHost = false) - { - PSCommand command = new PSCommand(); - - if (arguments != null) - { - // Need to determine If the script string is a path to a script file. - string scriptAbsPath = string.Empty; - try - { - // Assume we can only debug scripts from the FileSystem provider - string workingDir = (await ExecuteCommandAsync( - new PSCommand() - .AddCommand("Microsoft.PowerShell.Management\\Get-Location") - .AddParameter("PSProvider", "FileSystem"), - false, - false)) - .FirstOrDefault() - .ProviderPath; - - workingDir = workingDir.TrimEnd(Path.DirectorySeparatorChar); - scriptAbsPath = workingDir + Path.DirectorySeparatorChar + script; - } - catch (System.Management.Automation.DriveNotFoundException e) - { - this.logger.Write( - LogLevel.Error, - "Could not determine current filesystem location:\r\n\r\n" + e.ToString()); - } - - var strBld = new StringBuilder(); - - // The script parameter can refer to either a "script path" or a "command name". If it is a - // script path, we can determine that by seeing if the path exists. If so, we always single - // quote that path in case it includes special PowerShell characters like ', &, (, ), [, ] and - // . Any embedded single quotes are escaped. - // If the provided path is already quoted, then File.Exists will not find it. - // This keeps us from quoting an already quoted path. - // Related to issue #123. - if (File.Exists(script) || File.Exists(scriptAbsPath)) - { - // Dot-source the launched script path and single quote the path in case it includes - strBld.Append(". ").Append(QuoteEscapeString(script)); - } - else - { - strBld.Append(script); - } - - // Add arguments - strBld.Append(' ').Append(arguments); - - var launchedScript = strBld.ToString(); - this.logger.Write(LogLevel.Verbose, $"Launch script is: {launchedScript}"); - - command.AddScript(launchedScript, false); - } - else - { - // AddCommand can handle script paths including those with special chars e.g.: - // ".\foo & [bar]\foo.ps1" and it can handle arbitrary commands, like "Invoke-Pester" - command.AddCommand(script, false); - } - - if (writeInputToHost) - { - this.WriteOutput( - script + Environment.NewLine, - true); - } - - await this.ExecuteCommandAsync( - command, - null, - sendOutputToHost: true, - addToHistory: true); - } - - /// - /// Forces the to trigger PowerShell event handling, - /// reliquishing control of the pipeline thread during event processing. - /// - /// - /// This method is called automatically by and - /// . Consider using them instead of this method directly when - /// possible. - /// - internal void ForcePSEventHandling() - { - PromptContext.ForcePSEventHandling(); - } - - /// - /// Marshals a to run on the pipeline thread. A new - /// will be created for the invocation. - /// - /// - /// The to invoke on the pipeline thread. The nested - /// instance for the created - /// will be passed as an argument. - /// - /// - /// An awaitable that the caller can use to know when execution completes. - /// - /// - /// This method is called automatically by . Consider using - /// that method instead of calling this directly when possible. - /// - internal async Task InvokeOnPipelineThreadAsync(Action invocationAction) - { - if (this.PromptNest.IsReadLineBusy()) - { - await this.InvocationEventQueue.InvokeOnPipelineThreadAsync(invocationAction); - return; - } - - // If this is invoked when ReadLine isn't busy then there shouldn't be any running - // pipelines. Right now this method is only used by command completion which doesn't - // actually require running on the pipeline thread, as long as nothing else is running. - invocationAction.Invoke(this.PromptNest.GetPowerShell()); - } - - internal async Task InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) - { - return await PromptContext.InvokeReadLineAsync( - isCommandLine, - cancellationToken); - } - - internal static TResult ExecuteScriptAndGetItem(string scriptToExecute, Runspace runspace, TResult defaultValue = default(TResult)) - { - using (PowerShell pwsh = PowerShell.Create()) - { - pwsh.Runspace = runspace; - IEnumerable results = pwsh.AddScript(scriptToExecute).Invoke(); - return results.DefaultIfEmpty(defaultValue).First(); - } - } - - /// - /// Loads PowerShell profiles for the host from the specified - /// profile locations. Only the profile paths which exist are - /// loaded. - /// - /// A Task that can be awaited for completion. - public async Task LoadHostProfilesAsync() - { - if (this.profilePaths != null) - { - // Load any of the profile paths that exist - PSCommand command = null; - foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths()) - { - command = new PSCommand(); - command.AddCommand(profilePath, false); - await this.ExecuteCommandAsync(command, true, true); - } - - // Gather the session details (particularly the prompt) after - // loading the user's profiles. - await this.GetSessionDetailsInRunspaceAsync(); - } - } - - /// - /// Causes the most recent execution to be aborted no matter what state - /// it is currently in. - /// - public void AbortExecution() - { - this.AbortExecution(shouldAbortDebugSession: false); - } - - /// - /// Causes the most recent execution to be aborted no matter what state - /// it is currently in. - /// - /// - /// A value indicating whether a debug session should be aborted if one - /// is currently active. - /// - public void AbortExecution(bool shouldAbortDebugSession) - { - if (this.SessionState != PowerShellContextState.Aborting && - this.SessionState != PowerShellContextState.Disposed) - { - this.logger.Write(LogLevel.Verbose, "Execution abort requested..."); - - if (shouldAbortDebugSession) - { - this.ExitAllNestedPrompts(); - } - - if (this.PromptNest.IsInDebugger) - { - if (shouldAbortDebugSession) - { - this.versionSpecificOperations.StopCommandInDebugger(this); - this.ResumeDebugger(DebuggerResumeAction.Stop); - } - else - { - this.versionSpecificOperations.StopCommandInDebugger(this); - } - } - else - { - this.PromptNest.GetPowerShell(isReadLine: false).BeginStop(null, null); - } - - this.SessionState = PowerShellContextState.Aborting; - - this.OnExecutionStatusChanged( - ExecutionStatus.Aborted, - null, - false); - } - else - { - this.logger.Write( - LogLevel.Verbose, - string.Format( - $"Execution abort requested when already aborted (SessionState = {this.SessionState})")); - } - } - - /// - /// Exit all consecutive nested prompts that the user has entered. - /// - internal void ExitAllNestedPrompts() - { - while (this.PromptNest.IsNestedPrompt) - { - this.PromptNest.WaitForCurrentFrameExit(frame => this.ExitNestedPrompt()); - this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); - } - } - - /// - /// Exit all consecutive nested prompts that the user has entered. - /// - /// - /// A task object that represents all nested prompts being exited - /// - internal async Task ExitAllNestedPromptsAsync() - { - while (this.PromptNest.IsNestedPrompt) - { - await this.PromptNest.WaitForCurrentFrameExitAsync(frame => this.ExitNestedPrompt()); - this.versionSpecificOperations.ExitNestedPrompt(ExternalHost); - } - } - - /// - /// Causes the debugger to break execution wherever it currently is. - /// This method is internal because the real Break API is provided - /// by the DebugService. - /// - internal void BreakExecution() - { - this.logger.Write(LogLevel.Verbose, "Debugger break requested..."); - - // Pause the debugger - this.versionSpecificOperations.PauseDebugger( - this.CurrentRunspace.Runspace); - } - - internal void ResumeDebugger(DebuggerResumeAction resumeAction) - { - ResumeDebugger(resumeAction, shouldWaitForExit: true); - } - - private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitForExit) - { - resumeRequestHandle.Wait(); - try - { - if (this.PromptNest.IsNestedPrompt) - { - this.ExitAllNestedPrompts(); - } - - if (this.PromptNest.IsInDebugger) - { - // Set the result so that the execution thread resumes. - // The execution thread will clean up the task. - if (shouldWaitForExit) - { - this.PromptNest.WaitForCurrentFrameExit( - frame => - { - frame.ThreadController.StartThreadExit(resumeAction); - this.ConsoleReader?.StopCommandLoop(); - if (this.SessionState != PowerShellContextState.Ready) - { - this.versionSpecificOperations.StopCommandInDebugger(this); - } - }); - } - else - { - this.PromptNest.GetThreadController().StartThreadExit(resumeAction); - this.ConsoleReader?.StopCommandLoop(); - if (this.SessionState != PowerShellContextState.Ready) - { - this.versionSpecificOperations.StopCommandInDebugger(this); - } - } - } - else - { - this.logger.Write( - LogLevel.Error, - $"Tried to resume debugger with action {resumeAction} but there was no debuggerStoppedTask."); - } - } - finally - { - resumeRequestHandle.Release(); - } - } - - /// - /// Disposes the runspace and any other resources being used - /// by this PowerShellContext. - /// - public void Dispose() - { - this.PromptNest.Dispose(); - this.SessionState = PowerShellContextState.Disposed; - - // Clean up the active runspace - this.CleanupRunspace(this.CurrentRunspace); - - // Push the active runspace so it will be included in the loop - this.runspaceStack.Push(this.CurrentRunspace); - - while (this.runspaceStack.Count > 0) - { - RunspaceDetails poppedRunspace = this.runspaceStack.Pop(); - - // Close the popped runspace if it isn't the initial runspace - // or if it is the initial runspace and we own that runspace - if (this.initialRunspace != poppedRunspace || this.ownsInitialRunspace) - { - this.CloseRunspace(poppedRunspace); - } - - this.OnRunspaceChanged( - this, - new RunspaceChangedEventArgs( - RunspaceChangeAction.Shutdown, - poppedRunspace, - null)); - } - - this.initialRunspace = null; - } - - private async Task GetRunspaceHandleAsync(bool isReadLine) - { - return await this.GetRunspaceHandleImplAsync(CancellationToken.None, isReadLine); - } - - private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) - { - return await this.PromptNest.GetRunspaceHandleAsync(cancellationToken, isReadLine); - } - - private ExecutionTarget GetExecutionTarget(ExecutionOptions options = null) - { - if (options == null) - { - options = new ExecutionOptions(); - } - - var noBackgroundInvocation = - options.InterruptCommandPrompt || - options.WriteOutputToHost || - options.IsReadLine || - PromptNest.IsRemote; - - // Take over the pipeline if PSReadLine is running, we aren't trying to run PSReadLine, and - // we aren't in a remote session. - if (!noBackgroundInvocation && PromptNest.IsReadLineBusy() && PromptNest.IsMainThreadBusy()) - { - return ExecutionTarget.InvocationEvent; - } - - // We can't take the pipeline from PSReadLine if it's in a remote session, so we need to - // invoke locally in that case. - if (IsDebuggerStopped && PromptNest.IsInDebugger && !(options.IsReadLine && PromptNest.IsRemote)) - { - return ExecutionTarget.Debugger; - } - - return ExecutionTarget.PowerShell; - } - - private bool ShouldExecuteWithEventing(ExecutionOptions executionOptions) - { - return - this.PromptNest.IsReadLineBusy() && - this.PromptNest.IsMainThreadBusy() && - !(executionOptions.IsReadLine || - executionOptions.InterruptCommandPrompt || - executionOptions.WriteOutputToHost || - IsCurrentRunspaceOutOfProcess()); - } - - private void CloseRunspace(RunspaceDetails runspaceDetails) - { - string exitCommand = null; - - switch (runspaceDetails.Context) - { - case RunspaceContext.Original: - if (runspaceDetails.Location == RunspaceLocation.Local) - { - runspaceDetails.Runspace.Close(); - runspaceDetails.Runspace.Dispose(); - } - else - { - exitCommand = "Exit-PSSession"; - } - - break; - - case RunspaceContext.EnteredProcess: - exitCommand = "Exit-PSHostProcess"; - break; - - case RunspaceContext.DebuggedRunspace: - // An attached runspace will be detached when the - // running pipeline is aborted - break; - } - - if (exitCommand != null) - { - Exception exitException = null; - - try - { - using (PowerShell ps = PowerShell.Create()) - { - ps.Runspace = runspaceDetails.Runspace; - ps.AddCommand(exitCommand); - ps.Invoke(); - } - } - catch (RemoteException e) - { - exitException = e; - } - catch (RuntimeException e) - { - exitException = e; - } - - if (exitException != null) - { - this.logger.Write( - LogLevel.Error, - $"Caught {exitException.GetType().Name} while exiting {runspaceDetails.Location} runspace:\r\n{exitException.ToString()}"); - } - } - } - - internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) - { - Validate.IsNotNull("runspaceHandle", runspaceHandle); - - if (PromptNest.IsMainThreadBusy() || (runspaceHandle.IsReadLine && PromptNest.IsReadLineBusy())) - { - var unusedTask = PromptNest - .ReleaseRunspaceHandleAsync(runspaceHandle) - .ConfigureAwait(false); - } - else - { - // Write the situation to the log since this shouldn't happen - this.logger.Write( - LogLevel.Error, - "ReleaseRunspaceHandle was called when the main thread was not busy."); - } - } - - /// - /// Determines if the current runspace is out of process. - /// - /// - /// A value indicating whether the current runspace is out of process. - /// - internal bool IsCurrentRunspaceOutOfProcess() - { - return - CurrentRunspace.Context == RunspaceContext.EnteredProcess || - CurrentRunspace.Context == RunspaceContext.DebuggedRunspace || - CurrentRunspace.Location == RunspaceLocation.Remote; - } - - /// - /// Called by the external PSHost when $Host.EnterNestedPrompt is called. - /// - internal void EnterNestedPrompt() - { - if (this.IsCurrentRunspaceOutOfProcess()) - { - throw new NotSupportedException(); - } - - this.PromptNest.PushPromptContext(PromptNestFrameType.NestedPrompt); - var localThreadController = this.PromptNest.GetThreadController(); - this.OnSessionStateChanged( - this, - new SessionStateChangedEventArgs( - PowerShellContextState.Ready, - PowerShellExecutionResult.Stopped, - null)); - - // Reset command loop mainly for PSReadLine - this.ConsoleReader?.StopCommandLoop(); - this.ConsoleReader?.StartCommandLoop(); - - var localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); - var localDebuggerStoppedTask = localThreadController.Exit(); - - // Wait for off-thread pipeline requests and/or ExitNestedPrompt - while (true) - { - int taskIndex = Task.WaitAny( - localPipelineExecutionTask, - localDebuggerStoppedTask); - - if (taskIndex == 0) - { - var localExecutionTask = localPipelineExecutionTask.GetAwaiter().GetResult(); - localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); - localExecutionTask.ExecuteAsync().GetAwaiter().GetResult(); - continue; - } - - this.ConsoleReader?.StopCommandLoop(); - this.PromptNest.PopPromptContext(); - break; - } - } - - /// - /// Called by the external PSHost when $Host.ExitNestedPrompt is called. - /// - internal void ExitNestedPrompt() - { - if (this.PromptNest.NestedPromptLevel == 1 || !this.PromptNest.IsNestedPrompt) - { - this.logger.Write( - LogLevel.Error, - "ExitNestedPrompt was called outside of a nested prompt."); - return; - } - - // Stop the command input loop so PSReadLine isn't invoked between ExitNestedPrompt - // being invoked and EnterNestedPrompt getting the message to exit. - this.ConsoleReader?.StopCommandLoop(); - this.PromptNest.GetThreadController().StartThreadExit(DebuggerResumeAction.Stop); - } - - /// - /// Sets the current working directory of the powershell context. The path should be - /// unescaped before calling this method. - /// - /// - public async Task SetWorkingDirectoryAsync(string path) - { - await this.SetWorkingDirectoryAsync(path, true); - } - - /// - /// Sets the current working directory of the powershell context. - /// - /// - /// Specify false to have the path escaped, otherwise specify true if the path has already been escaped. - public async Task SetWorkingDirectoryAsync(string path, bool isPathAlreadyEscaped) - { - this.InitialWorkingDirectory = path; - - if (!isPathAlreadyEscaped) - { - path = WildcardEscapePath(path); - } - - await ExecuteCommandAsync( - new PSCommand().AddCommand("Set-Location").AddParameter("Path", path), - null, - sendOutputToHost: false, - sendErrorToHost: false, - addToHistory: false); - } - - /// - /// Fully escape a given path for use in PowerShell script. - /// Note: this will not work with PowerShell.AddParameter() - /// - /// The path to escape. - /// An escaped version of the path that can be embedded in PowerShell script. - internal static string FullyPowerShellEscapePath(string path) - { - string wildcardEscapedPath = WildcardEscapePath(path); - return QuoteEscapeString(wildcardEscapedPath); - } - - /// - /// Wrap a string in quotes to make it safe to use in scripts. - /// - /// The glob-escaped path to wrap in quotes. - /// The given path wrapped in quotes appropriately. - internal static string QuoteEscapeString(string escapedPath) - { - var sb = new StringBuilder(escapedPath.Length + 2); // Length of string plus two quotes - sb.Append('\''); - if (!escapedPath.Contains('\'')) - { - sb.Append(escapedPath); - } - else - { - foreach (char c in escapedPath) - { - if (c == '\'') - { - sb.Append("''"); - continue; - } - - sb.Append(c); - } - } - sb.Append('\''); - return sb.ToString(); - } - - /// - /// Return the given path with all PowerShell globbing characters escaped, - /// plus optionally the whitespace. - /// - /// The path to process. - /// Specify True to escape spaces in the path, otherwise False. - /// The path with [ and ] escaped. - internal static string WildcardEscapePath(string path, bool escapeSpaces = false) - { - var sb = new StringBuilder(); - for (int i = 0; i < path.Length; i++) - { - char curr = path[i]; - switch (curr) - { - // Escape '[', ']', '?' and '*' with '`' - case '[': - case ']': - case '*': - case '?': - case '`': - sb.Append('`').Append(curr); - break; - - default: - // Escape whitespace if required - if (escapeSpaces && char.IsWhiteSpace(curr)) - { - sb.Append('`').Append(curr); - break; - } - sb.Append(curr); - break; - } - } - - return sb.ToString(); - } - - /// - /// Returns the passed in path with the [ and ] characters escaped. Escaping spaces is optional. - /// - /// The path to process. - /// Specify True to escape spaces in the path, otherwise False. - /// The path with [ and ] escaped. - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This API is not meant for public usage and should not be used.")] - public static string EscapePath(string path, bool escapeSpaces) - { - return WildcardEscapePath(path, escapeSpaces); - } - - internal static string UnescapeWildcardEscapedPath(string wildcardEscapedPath) - { - // Prevent relying on my implementation if we can help it - if (!wildcardEscapedPath.Contains('`')) - { - return wildcardEscapedPath; - } - - var sb = new StringBuilder(wildcardEscapedPath.Length); - for (int i = 0; i < wildcardEscapedPath.Length; i++) - { - // If we see a backtick perform a lookahead - char curr = wildcardEscapedPath[i]; - if (curr == '`' && i + 1 < wildcardEscapedPath.Length) - { - // If the next char is an escapable one, don't add this backtick to the new string - char next = wildcardEscapedPath[i + 1]; - switch (next) - { - case '[': - case ']': - case '?': - case '*': - continue; - - default: - if (char.IsWhiteSpace(next)) - { - continue; - } - break; - } - } - - sb.Append(curr); - } - - return sb.ToString(); - } - - /// - /// Unescapes any escaped [, ] or space characters. Typically use this before calling a - /// .NET API that doesn't understand PowerShell escaped chars. - /// - /// The path to unescape. - /// The path with the ` character before [, ] and spaces removed. - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This API is not meant for public usage and should not be used.")] - public static string UnescapePath(string path) - { - return UnescapeWildcardEscapedPath(path); - } - - #endregion - - #region Events - - /// - /// Raised when the state of the session has changed. - /// - public event EventHandler SessionStateChanged; - - private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e) - { - if (this.SessionState != PowerShellContextState.Disposed) - { - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Session state changed --\r\n\r\n Old state: {0}\r\n New state: {1}\r\n Result: {2}", - this.SessionState.ToString(), - e.NewSessionState.ToString(), - e.ExecutionResult)); - - this.SessionState = e.NewSessionState; - this.SessionStateChanged?.Invoke(sender, e); - } - else - { - this.logger.Write( - LogLevel.Warning, - $"Received session state change to {e.NewSessionState} when already disposed"); - } - } - - /// - /// Raised when the runspace changes by entering a remote session or one in a different process. - /// - public event EventHandler RunspaceChanged; - - private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e) - { - this.RunspaceChanged?.Invoke(sender, e); - } - - /// - /// Raised when the status of an executed command changes. - /// - public event EventHandler ExecutionStatusChanged; - - private void OnExecutionStatusChanged( - ExecutionStatus executionStatus, - ExecutionOptions executionOptions, - bool hadErrors) - { - this.ExecutionStatusChanged?.Invoke( - this, - new ExecutionStatusChangedEventArgs( - executionStatus, - executionOptions, - hadErrors)); - } - - #endregion - - #region Private Methods - - private IEnumerable ExecuteCommandInDebugger(PSCommand psCommand, bool sendOutputToHost) - { - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Attempting to execute command(s) in the debugger:\r\n\r\n{0}", - GetStringForPSCommand(psCommand))); - - IEnumerable output = - this.versionSpecificOperations.ExecuteCommandInDebugger( - this, - this.CurrentRunspace.Runspace, - psCommand, - sendOutputToHost, - out DebuggerResumeAction? debuggerResumeAction); - - if (debuggerResumeAction.HasValue) - { - // Resume the debugger with the specificed action - this.ResumeDebugger( - debuggerResumeAction.Value, - shouldWaitForExit: false); - } - - return output; - } - - internal void WriteOutput(string outputString, bool includeNewLine) - { - this.WriteOutput( - outputString, - includeNewLine, - OutputType.Normal); - } - - internal void WriteOutput( - string outputString, - bool includeNewLine, - OutputType outputType) - { - if (this.ConsoleWriter != null) - { - this.ConsoleWriter.WriteOutput( - outputString, - includeNewLine, - outputType); - } - } - - private void WriteExceptionToHost(Exception e) - { - const string ExceptionFormat = - "{0}\r\n{1}\r\n + CategoryInfo : {2}\r\n + FullyQualifiedErrorId : {3}"; - - IContainsErrorRecord containsErrorRecord = e as IContainsErrorRecord; - - if (containsErrorRecord == null || - containsErrorRecord.ErrorRecord == null) - { - this.WriteError(e.Message, null, 0, 0); - return; - } - - ErrorRecord errorRecord = containsErrorRecord.ErrorRecord; - if (errorRecord.InvocationInfo == null) - { - this.WriteError(errorRecord.ToString(), String.Empty, 0, 0); - return; - } - - string errorRecordString = errorRecord.ToString(); - if ((errorRecord.InvocationInfo.PositionMessage != null) && - errorRecordString.IndexOf(errorRecord.InvocationInfo.PositionMessage, StringComparison.Ordinal) != -1) - { - this.WriteError(errorRecordString); - return; - } - - string message = - string.Format( - CultureInfo.InvariantCulture, - ExceptionFormat, - errorRecord.ToString(), - errorRecord.InvocationInfo.PositionMessage, - errorRecord.CategoryInfo, - errorRecord.FullyQualifiedErrorId); - - this.WriteError(message); - } - - private void WriteError( - string errorMessage, - string filePath, - int lineNumber, - int columnNumber) - { - const string ErrorLocationFormat = "At {0}:{1} char:{2}"; - - this.WriteError( - errorMessage + - Environment.NewLine + - string.Format( - ErrorLocationFormat, - String.IsNullOrEmpty(filePath) ? "line" : filePath, - lineNumber, - columnNumber)); - } - - private void WriteError(string errorMessage) - { - if (this.ConsoleWriter != null) - { - this.ConsoleWriter.WriteOutput( - errorMessage, - true, - OutputType.Error, - ConsoleColor.Red, - ConsoleColor.Black); - } - } - - void powerShell_InvocationStateChanged(object sender, PSInvocationStateChangedEventArgs e) - { - SessionStateChangedEventArgs eventArgs = TranslateInvocationStateInfo(e.InvocationStateInfo); - this.OnSessionStateChanged(this, eventArgs); - } - - private static SessionStateChangedEventArgs TranslateInvocationStateInfo(PSInvocationStateInfo invocationState) - { - PowerShellContextState newState = PowerShellContextState.Unknown; - PowerShellExecutionResult executionResult = PowerShellExecutionResult.NotFinished; - - switch (invocationState.State) - { - case PSInvocationState.NotStarted: - newState = PowerShellContextState.NotStarted; - break; - - case PSInvocationState.Failed: - newState = PowerShellContextState.Ready; - executionResult = PowerShellExecutionResult.Failed; - break; - - case PSInvocationState.Disconnected: - // TODO: Any extra work to do in this case? - // TODO: Is this a unique state that can be re-connected? - newState = PowerShellContextState.Disposed; - executionResult = PowerShellExecutionResult.Stopped; - break; - - case PSInvocationState.Running: - newState = PowerShellContextState.Running; - break; - - case PSInvocationState.Completed: - newState = PowerShellContextState.Ready; - executionResult = PowerShellExecutionResult.Completed; - break; - - case PSInvocationState.Stopping: - newState = PowerShellContextState.Aborting; - break; - - case PSInvocationState.Stopped: - newState = PowerShellContextState.Ready; - executionResult = PowerShellExecutionResult.Aborted; - break; - - default: - newState = PowerShellContextState.Unknown; - break; - } - - return - new SessionStateChangedEventArgs( - newState, - executionResult, - invocationState.Reason); - } - - private Command GetOutputCommand(bool endOfStatement) - { - Command outputCommand = - new Command( - command: this.PromptNest.IsInDebugger ? "Out-String" : "Out-Default", - isScript: false, - useLocalScope: true); - - if (this.PromptNest.IsInDebugger) - { - // Out-String needs the -Stream parameter added - outputCommand.Parameters.Add("Stream"); - } - - return outputCommand; - } - - private static string GetStringForPSCommand(PSCommand psCommand) - { - StringBuilder stringBuilder = new StringBuilder(); - - foreach (var command in psCommand.Commands) - { - stringBuilder.Append(" "); - stringBuilder.Append(command.CommandText); - foreach (var param in command.Parameters) - { - if (param.Name != null) - { - stringBuilder.Append($" -{param.Name} {param.Value}"); - } - else - { - stringBuilder.Append($" {param.Value}"); - } - } - - stringBuilder.AppendLine(); - } - - return stringBuilder.ToString(); - } - - private void SetExecutionPolicy(ExecutionPolicy desiredExecutionPolicy) - { - var currentPolicy = ExecutionPolicy.Undefined; - - // Get the current execution policy so that we don't set it higher than it already is - this.powerShell.Commands.AddCommand("Get-ExecutionPolicy"); - - var result = this.powerShell.Invoke(); - if (result.Count > 0) - { - currentPolicy = result.FirstOrDefault(); - } - - if (desiredExecutionPolicy < currentPolicy || - desiredExecutionPolicy == ExecutionPolicy.Bypass || - currentPolicy == ExecutionPolicy.Undefined) - { - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Setting execution policy:\r\n Current = ExecutionPolicy.{0}\r\n Desired = ExecutionPolicy.{1}", - currentPolicy, - desiredExecutionPolicy)); - - this.powerShell.Commands.Clear(); - this.powerShell - .AddCommand("Set-ExecutionPolicy") - .AddParameter("ExecutionPolicy", desiredExecutionPolicy) - .AddParameter("Scope", ExecutionPolicyScope.Process) - .AddParameter("Force"); - - try - { - this.powerShell.Invoke(); - } - catch (CmdletInvocationException e) - { - this.logger.WriteException( - $"An error occurred while calling Set-ExecutionPolicy, the desired policy of {desiredExecutionPolicy} may not be set.", - e); - } - - this.powerShell.Commands.Clear(); - } - else - { - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Current execution policy: ExecutionPolicy.{0}", - currentPolicy)); - - } - } - - private SessionDetails GetSessionDetails(Func invokeAction) - { - try - { - this.mostRecentSessionDetails = - new SessionDetails( - invokeAction( - SessionDetails.GetDetailsCommand())); - - return this.mostRecentSessionDetails; - } - catch (RuntimeException e) - { - this.logger.Write( - LogLevel.Verbose, - "Runtime exception occurred while gathering runspace info:\r\n\r\n" + e.ToString()); - } - catch (ArgumentNullException) - { - this.logger.Write( - LogLevel.Error, - "Could not retrieve session details but no exception was thrown."); - } - - // TODO: Return a harmless object if necessary - this.mostRecentSessionDetails = null; - return this.mostRecentSessionDetails; - } - - private async Task GetSessionDetailsInRunspaceAsync() - { - using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandleAsync()) - { - return this.GetSessionDetailsInRunspace(runspaceHandle.Runspace); - } - } - - private SessionDetails GetSessionDetailsInRunspace(Runspace runspace) - { - SessionDetails sessionDetails = - this.GetSessionDetails( - command => - { - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspace; - powerShell.Commands = command; - - return - powerShell - .Invoke() - .FirstOrDefault(); - } - }); - - return sessionDetails; - } - - private SessionDetails GetSessionDetailsInDebugger() - { - return this.GetSessionDetails( - command => - { - // Use LastOrDefault to get the last item returned. This - // is necessary because advanced prompt functions (like those - // in posh-git) may return multiple objects in the result. - return - this.ExecuteCommandInDebugger(command, false) - .LastOrDefault(); - }); - } - - private SessionDetails GetSessionDetailsInNestedPipeline() - { - // We don't need to check what thread we're on here. If it's a local - // nested pipeline then we will already be on the correct thread, and - // non-debugger nested pipelines aren't supported in remote runspaces. - return this.GetSessionDetails( - command => - { - using (var localPwsh = PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - localPwsh.Commands = command; - return localPwsh.Invoke().FirstOrDefault(); - } - }); - } - - private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) - { - // Create the $profile variable - PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost); - - profile.Members.Add( - new PSNoteProperty( - nameof(profilePaths.AllUsersAllHosts), - profilePaths.AllUsersAllHosts)); - - profile.Members.Add( - new PSNoteProperty( - nameof(profilePaths.AllUsersCurrentHost), - profilePaths.AllUsersCurrentHost)); - - profile.Members.Add( - new PSNoteProperty( - nameof(profilePaths.CurrentUserAllHosts), - profilePaths.CurrentUserAllHosts)); - - profile.Members.Add( - new PSNoteProperty( - nameof(profilePaths.CurrentUserCurrentHost), - profilePaths.CurrentUserCurrentHost)); - - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Setting $profile variable in runspace. Current user host profile path: {0}", - profilePaths.CurrentUserCurrentHost)); - - // Set the variable in the runspace - this.powerShell.Commands.Clear(); - this.powerShell - .AddCommand("Set-Variable") - .AddParameter("Name", "profile") - .AddParameter("Value", profile) - .AddParameter("Option", "None"); - this.powerShell.Invoke(); - this.powerShell.Commands.Clear(); - } - - private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs args) - { - switch (args.RunspaceStateInfo.State) - { - case RunspaceState.Opening: - case RunspaceState.Opened: - // These cases don't matter, just return - return; - - case RunspaceState.Closing: - case RunspaceState.Closed: - case RunspaceState.Broken: - // If the runspace closes or fails, pop the runspace - ((IHostSupportsInteractiveSession)this).PopRunspace(); - break; - } - } - - #endregion - - #region Events - - // NOTE: This event is 'internal' because the DebugService provides - // the publicly consumable event. - internal event EventHandler DebuggerStop; - - /// - /// Raised when the debugger is resumed after it was previously stopped. - /// - public event EventHandler DebuggerResumed; - - private void StartCommandLoopOnRunspaceAvailable() - { - if (Interlocked.CompareExchange(ref this.isCommandLoopRestarterSet, 1, 1) == 1) - { - return; - } - - EventHandler handler = null; - handler = (runspace, eventArgs) => - { - if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || - this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) - { - return; - } - - ((Runspace)runspace).AvailabilityChanged -= handler; - Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); - this.ConsoleReader?.StartCommandLoop(); - }; - - this.CurrentRunspace.Runspace.AvailabilityChanged += handler; - Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 1); - } - - private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) - { - if (CurrentRunspace.Context == RunspaceContext.Original) - { - StartCommandLoopOnRunspaceAvailable(); - } - - this.logger.Write(LogLevel.Verbose, "Debugger stopped execution."); - - PromptNest.PushPromptContext( - IsCurrentRunspaceOutOfProcess() - ? PromptNestFrameType.Debug | PromptNestFrameType.Remote - : PromptNestFrameType.Debug); - - ThreadController localThreadController = PromptNest.GetThreadController(); - - // Update the session state - this.OnSessionStateChanged( - this, - new SessionStateChangedEventArgs( - PowerShellContextState.Ready, - PowerShellExecutionResult.Stopped, - null)); - - // Get the session details and push the current - // runspace if the session has changed - SessionDetails sessionDetails = null; - try - { - sessionDetails = this.GetSessionDetailsInDebugger(); - } - catch (InvalidOperationException) - { - this.logger.Write( - LogLevel.Verbose, - "Attempting to get session details failed, most likely due to a running pipeline that is attempting to stop."); - } - - if (!localThreadController.FrameExitTask.Task.IsCompleted) - { - // Push the current runspace if the session has changed - this.UpdateRunspaceDetailsIfSessionChanged(sessionDetails, isDebuggerStop: true); - - // Raise the event for the debugger service - this.DebuggerStop?.Invoke(sender, e); - } - - this.logger.Write(LogLevel.Verbose, "Starting pipeline thread message loop..."); - - Task localPipelineExecutionTask = - localThreadController.TakeExecutionRequestAsync(); - Task localDebuggerStoppedTask = - localThreadController.Exit(); - while (true) - { - int taskIndex = - Task.WaitAny( - localDebuggerStoppedTask, - localPipelineExecutionTask); - - if (taskIndex == 0) - { - // Write a new output line before continuing - this.WriteOutput("", true); - - e.ResumeAction = localDebuggerStoppedTask.GetAwaiter().GetResult(); - this.logger.Write(LogLevel.Verbose, "Received debugger resume action " + e.ResumeAction.ToString()); - - // Notify listeners that the debugger has resumed - this.DebuggerResumed?.Invoke(this, e.ResumeAction); - - // Pop the current RunspaceDetails if we were attached - // to a runspace and the resume action is Stop - if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace && - e.ResumeAction == DebuggerResumeAction.Stop) - { - this.PopRunspace(); - } - else if (e.ResumeAction != DebuggerResumeAction.Stop) - { - // Update the session state - this.OnSessionStateChanged( - this, - new SessionStateChangedEventArgs( - PowerShellContextState.Running, - PowerShellExecutionResult.NotFinished, - null)); - } - - break; - } - else if (taskIndex == 1) - { - this.logger.Write(LogLevel.Verbose, "Received pipeline thread execution request."); - - IPipelineExecutionRequest executionRequest = localPipelineExecutionTask.Result; - localPipelineExecutionTask = localThreadController.TakeExecutionRequestAsync(); - executionRequest.ExecuteAsync().GetAwaiter().GetResult(); - - this.logger.Write(LogLevel.Verbose, "Pipeline thread execution completed."); - - if (!this.versionSpecificOperations.IsDebuggerStopped( - this.PromptNest, - this.CurrentRunspace.Runspace)) - { - if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace) - { - // Notify listeners that the debugger has resumed - this.DebuggerResumed?.Invoke(this, DebuggerResumeAction.Stop); - - // We're detached from the runspace now, send a runspace update. - this.PopRunspace(); - } - - // If the executed command caused the debugger to exit, break - // from the pipeline loop - break; - } - } - else - { - // TODO: How to handle this? - } - } - - PromptNest.PopPromptContext(); - } - - // NOTE: This event is 'internal' because the DebugService provides - // the publicly consumable event. - internal event EventHandler BreakpointUpdated; - - private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) - { - this.BreakpointUpdated?.Invoke(sender, e); - } - - #endregion - - #region Nested Classes - - private void ConfigureRunspaceCapabilities(RunspaceDetails runspaceDetails) - { - DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); - } - - private void PushRunspace(RunspaceDetails newRunspaceDetails) - { - this.logger.Write( - LogLevel.Verbose, - $"Pushing {this.CurrentRunspace.Location} ({this.CurrentRunspace.Context}), new runspace is {newRunspaceDetails.Location} ({newRunspaceDetails.Context}), connection: {newRunspaceDetails.ConnectionString}"); - - RunspaceDetails previousRunspace = this.CurrentRunspace; - - if (newRunspaceDetails.Context == RunspaceContext.DebuggedRunspace) - { - this.WriteOutput( - $"Entering debugged runspace on {newRunspaceDetails.Location.ToString().ToLower()} machine {newRunspaceDetails.SessionDetails.ComputerName}", - true); - } - - // Switch out event handlers if necessary - if (CheckIfRunspaceNeedsEventHandlers(newRunspaceDetails)) - { - this.CleanupRunspace(previousRunspace); - this.ConfigureRunspace(newRunspaceDetails); - } - - this.runspaceStack.Push(previousRunspace); - this.CurrentRunspace = newRunspaceDetails; - - // Check for runspace capabilities - this.ConfigureRunspaceCapabilities(newRunspaceDetails); - - this.OnRunspaceChanged( - this, - new RunspaceChangedEventArgs( - RunspaceChangeAction.Enter, - previousRunspace, - this.CurrentRunspace)); - } - - private void UpdateRunspaceDetailsIfSessionChanged(SessionDetails sessionDetails, bool isDebuggerStop = false) - { - RunspaceDetails newRunspaceDetails = null; - - // If we've exited an entered process or debugged runspace, pop what we've - // got before we evaluate where we're at - if ( - (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace && - this.CurrentRunspace.SessionDetails.InstanceId != sessionDetails.InstanceId) || - (this.CurrentRunspace.Context == RunspaceContext.EnteredProcess && - this.CurrentRunspace.SessionDetails.ProcessId != sessionDetails.ProcessId)) - { - this.PopRunspace(); - } - - // Are we in a new session that the PushRunspace command won't - // notify us about? - // - // Possible cases: - // - Debugged runspace in a local or remote session - // - Entered process in a remote session - // - // We don't need additional logic to check for the cases that - // PowerShell would have notified us about because the CurrentRunspace - // will already be updated by PowerShell by the time we reach - // these checks. - - if (this.CurrentRunspace.SessionDetails.InstanceId != sessionDetails.InstanceId && isDebuggerStop) - { - // Are we on a local or remote computer? - bool differentComputer = - !string.Equals( - sessionDetails.ComputerName, - this.initialRunspace.SessionDetails.ComputerName, - StringComparison.CurrentCultureIgnoreCase); - - // We started debugging a runspace - newRunspaceDetails = - RunspaceDetails.CreateFromDebugger( - this.CurrentRunspace, - differentComputer ? RunspaceLocation.Remote : RunspaceLocation.Local, - RunspaceContext.DebuggedRunspace, - sessionDetails); - } - else if (this.CurrentRunspace.SessionDetails.ProcessId != sessionDetails.ProcessId) - { - // We entered a different PowerShell host process - newRunspaceDetails = - RunspaceDetails.CreateFromContext( - this.CurrentRunspace, - RunspaceContext.EnteredProcess, - sessionDetails); - } - - if (newRunspaceDetails != null) - { - this.PushRunspace(newRunspaceDetails); - } - } - - private void PopRunspace() - { - if (this.SessionState != PowerShellContextState.Disposed) - { - if (this.runspaceStack.Count > 0) - { - RunspaceDetails previousRunspace = this.CurrentRunspace; - this.CurrentRunspace = this.runspaceStack.Pop(); - - this.logger.Write( - LogLevel.Verbose, - $"Popping {previousRunspace.Location} ({previousRunspace.Context}), new runspace is {this.CurrentRunspace.Location} ({this.CurrentRunspace.Context}), connection: {this.CurrentRunspace.ConnectionString}"); - - if (previousRunspace.Context == RunspaceContext.DebuggedRunspace) - { - this.WriteOutput( - $"Leaving debugged runspace on {previousRunspace.Location.ToString().ToLower()} machine {previousRunspace.SessionDetails.ComputerName}", - true); - } - - // Switch out event handlers if necessary - if (CheckIfRunspaceNeedsEventHandlers(previousRunspace)) - { - this.CleanupRunspace(previousRunspace); - this.ConfigureRunspace(this.CurrentRunspace); - } - - this.OnRunspaceChanged( - this, - new RunspaceChangedEventArgs( - RunspaceChangeAction.Exit, - previousRunspace, - this.CurrentRunspace)); - } - else - { - this.logger.Write( - LogLevel.Error, - "Caller attempted to pop a runspace when no runspaces are on the stack."); - } - } - } - - #endregion - - #region IHostSupportsInteractiveSession Implementation - - bool IHostSupportsInteractiveSession.IsRunspacePushed - { - get - { - return this.runspaceStack.Count > 0; - } - } - - Runspace IHostSupportsInteractiveSession.Runspace - { - get - { - return this.CurrentRunspace.Runspace; - } - } - - void IHostSupportsInteractiveSession.PushRunspace(Runspace runspace) - { - // Get the session details for the new runspace - SessionDetails sessionDetails = this.GetSessionDetailsInRunspace(runspace); - - this.PushRunspace( - RunspaceDetails.CreateFromRunspace( - runspace, - sessionDetails, - this.logger)); - } - - void IHostSupportsInteractiveSession.PopRunspace() - { - this.PopRunspace(); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/PowerShellContextState.cs b/src/PowerShellEditorServices/Session/PowerShellContextState.cs deleted file mode 100644 index 2075c04d5..000000000 --- a/src/PowerShellEditorServices/Session/PowerShellContextState.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// 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 -{ - /// - /// Enumerates the possible states for a PowerShellContext. - /// - public enum PowerShellContextState - { - /// - /// Indicates an unknown, potentially uninitialized state. - /// - Unknown = 0, - - /// - /// Indicates the state where the session is starting but - /// not yet fully initialized. - /// - NotStarted, - - /// - /// Indicates that the session is ready to accept commands - /// for execution. - /// - Ready, - - /// - /// Indicates that the session is currently running a command. - /// - Running, - - /// - /// Indicates that the session is aborting the current execution. - /// - Aborting, - - /// - /// Indicates that the session is already disposed and cannot - /// accept further execution requests. - /// - Disposed - } -} - diff --git a/src/PowerShellEditorServices/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices/Session/PowerShellExecutionResult.cs deleted file mode 100644 index ee15ae97a..000000000 --- a/src/PowerShellEditorServices/Session/PowerShellExecutionResult.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// 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 -{ - /// - /// Enumerates the possible execution results that can occur after - /// executing a command or script. - /// - public enum PowerShellExecutionResult - { - /// - /// Indicates that execution is not yet finished. - /// - NotFinished, - - /// - /// Indicates that execution has failed. - /// - Failed, - - /// - /// Indicates that execution was aborted by the user. - /// - Aborted, - - /// - /// Indicates that execution was stopped by the debugger. - /// - Stopped, - - /// - /// Indicates that execution completed successfully. - /// - Completed - } -} - diff --git a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs deleted file mode 100644 index efd5e5feb..000000000 --- a/src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs +++ /dev/null @@ -1,167 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Defines the possible enumeration values for the PowerShell process architecture. - /// - public enum PowerShellProcessArchitecture - { - /// - /// The processor architecture is unknown or wasn't accessible. - /// - Unknown, - - /// - /// The processor architecture is 32-bit. - /// - X86, - - /// - /// The processor architecture is 64-bit. - /// - X64 - } - - /// - /// Provides details about the version of the PowerShell runtime. - /// - public class PowerShellVersionDetails - { - #region Properties - - /// - /// Gets the version of the PowerShell runtime. - /// - public Version Version { get; private set; } - - /// - /// Gets the full version string, either the ToString of the Version - /// property or the GitCommitId for open-source PowerShell releases. - /// - public string VersionString { get; private set; } - - /// - /// Gets the PowerShell edition (generally Desktop or Core). - /// - public string Edition { get; private set; } - - /// - /// Gets the architecture of the PowerShell process. - /// - public PowerShellProcessArchitecture Architecture { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the PowerShellVersionDetails class. - /// - /// The version of the PowerShell runtime. - /// A string representation of the PowerShell version. - /// The string representation of the PowerShell edition. - /// The processor architecture. - public PowerShellVersionDetails( - Version version, - string versionString, - string editionString, - PowerShellProcessArchitecture architecture) - { - this.Version = version; - this.VersionString = versionString; - this.Edition = editionString; - this.Architecture = architecture; - } - - #endregion - - #region Public Methods - - /// - /// Gets the PowerShell version details for the given runspace. - /// - /// The runspace for which version details will be gathered. - /// An ILogger implementation used for writing log messages. - /// A new PowerShellVersionDetails instance. - public static PowerShellVersionDetails GetVersionDetails(Runspace runspace, ILogger logger) - { - Version powerShellVersion = new Version(5, 0); - string versionString = null; - string powerShellEdition = "Desktop"; - var architecture = PowerShellProcessArchitecture.Unknown; - - try - { - var psVersionTable = PowerShellContext.ExecuteScriptAndGetItem("$PSVersionTable", runspace); - if (psVersionTable != null) - { - var edition = psVersionTable["PSEdition"] as string; - if (edition != null) - { - powerShellEdition = edition; - } - - // The PSVersion value will either be of Version or SemanticVersion. - // In the former case, take the value directly. In the latter case, - // generate a Version from its string representation. - var version = psVersionTable["PSVersion"]; - if (version is Version) - { - powerShellVersion = (Version)version; - } - else if (version != null) - { - // Expected version string format is 6.0.0-alpha so build a simpler version from that - powerShellVersion = new Version(version.ToString().Split('-')[0]); - } - - var gitCommitId = psVersionTable["GitCommitId"] as string; - if (gitCommitId != null) - { - versionString = gitCommitId; - } - else - { - versionString = powerShellVersion.ToString(); - } - - var arch = PowerShellContext.ExecuteScriptAndGetItem("$env:PROCESSOR_ARCHITECTURE", runspace); - if (arch != null) - { - if (string.Equals(arch, "AMD64", StringComparison.CurrentCultureIgnoreCase)) - { - architecture = PowerShellProcessArchitecture.X64; - } - else if (string.Equals(arch, "x86", StringComparison.CurrentCultureIgnoreCase)) - { - architecture = PowerShellProcessArchitecture.X86; - } - } - } - } - catch (Exception ex) - { - logger.Write( - LogLevel.Warning, - "Failed to look up PowerShell version, defaulting to version 5.\r\n\r\n" + ex.ToString()); - } - - return new PowerShellVersionDetails( - powerShellVersion, - versionString, - powerShellEdition, - architecture); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/ProfilePaths.cs b/src/PowerShellEditorServices/Session/ProfilePaths.cs deleted file mode 100644 index ef94092ec..000000000 --- a/src/PowerShellEditorServices/Session/ProfilePaths.cs +++ /dev/null @@ -1,110 +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.Collections.Generic; -using System.IO; -using System.Linq; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Provides profile path resolution behavior relative to the name - /// of a particular PowerShell host. - /// - public class ProfilePaths - { - #region Constants - - /// - /// The file name for the "all hosts" profile. Also used as the - /// suffix for the host-specific profile filenames. - /// - public const string AllHostsProfileName = "profile.ps1"; - - #endregion - - #region Properties - - /// - /// Gets the profile path for all users, all hosts. - /// - public string AllUsersAllHosts { get; private set; } - - /// - /// Gets the profile path for all users, current host. - /// - public string AllUsersCurrentHost { get; private set; } - - /// - /// Gets the profile path for the current user, all hosts. - /// - public string CurrentUserAllHosts { get; private set; } - - /// - /// Gets the profile path for the current user and host. - /// - public string CurrentUserCurrentHost { get; private set; } - - #endregion - - #region Public Methods - - /// - /// Creates a new instance of the ProfilePaths class. - /// - /// - /// The identifier of the host used in the host-specific X_profile.ps1 filename. - /// - /// The base path to use for constructing AllUsers profile paths. - /// The base path to use for constructing CurrentUser profile paths. - public ProfilePaths( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath); - } - - private void Initialize( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - string currentHostProfileName = - string.Format( - "{0}_{1}", - hostProfileId, - AllHostsProfileName); - - this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName); - this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName); - this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName); - this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName); - } - - /// - /// Gets the list of profile paths that exist on the filesystem. - /// - /// An IEnumerable of profile path strings to be loaded. - public IEnumerable GetLoadableProfilePaths() - { - var profilePaths = - new string[] - { - this.AllUsersAllHosts, - this.AllUsersCurrentHost, - this.CurrentUserAllHosts, - this.CurrentUserCurrentHost - }; - - return profilePaths.Where(p => File.Exists(p)); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Session/ProgressDetails.cs b/src/PowerShellEditorServices/Session/ProgressDetails.cs deleted file mode 100644 index b88d7c1ae..000000000 --- a/src/PowerShellEditorServices/Session/ProgressDetails.cs +++ /dev/null @@ -1,33 +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.Management.Automation; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about the progress of a particular activity. - /// - public class ProgressDetails - { - /// - /// Gets the percentage of the activity that has been completed. - /// - public int PercentComplete { get; private set; } - - internal static ProgressDetails Create(ProgressRecord progressRecord) - { - //progressRecord.RecordType == ProgressRecordType.Completed; - //progressRecord.Activity; - //progressRecord. - - return new ProgressDetails - { - PercentComplete = progressRecord.PercentComplete - }; - } - } -} - diff --git a/src/PowerShellEditorServices/Session/PromptNest.cs b/src/PowerShellEditorServices/Session/PromptNest.cs deleted file mode 100644 index 9cf4437f2..000000000 --- a/src/PowerShellEditorServices/Session/PromptNest.cs +++ /dev/null @@ -1,564 +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.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - using System; - using System.Management.Automation; - - /// - /// Represents the stack of contexts in which PowerShell commands can be invoked. - /// - internal class PromptNest : IDisposable - { - private ConcurrentStack _frameStack; - - private PromptNestFrame _readLineFrame; - - private IHostInput _consoleReader; - - private PowerShellContext _powerShellContext; - - private IVersionSpecificOperations _versionSpecificOperations; - - private bool _isDisposed; - - private object _syncObject = new object(); - - private object _disposeSyncObject = new object(); - - /// - /// Initializes a new instance of the class. - /// - /// - /// The to track prompt status for. - /// - /// - /// The instance for the first frame. - /// - /// - /// The input handler. - /// - /// - /// The for the calling - /// instance. - /// - /// - /// This constructor should only be called when - /// is set to the initial runspace. - /// - internal PromptNest( - PowerShellContext powerShellContext, - PowerShell initialPowerShell, - IHostInput consoleReader, - IVersionSpecificOperations versionSpecificOperations) - { - _versionSpecificOperations = versionSpecificOperations; - _consoleReader = consoleReader; - _powerShellContext = powerShellContext; - _frameStack = new ConcurrentStack(); - _frameStack.Push( - new PromptNestFrame( - initialPowerShell, - NewHandleQueue())); - - var readLineShell = PowerShell.Create(); - readLineShell.Runspace = powerShellContext.CurrentRunspace.Runspace; - _readLineFrame = new PromptNestFrame( - readLineShell, - new AsyncQueue()); - - ReleaseRunspaceHandleImpl(isReadLine: true); - } - - /// - /// Gets a value indicating whether the current frame was created by a debugger stop event. - /// - internal bool IsInDebugger => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Debug); - - /// - /// Gets a value indicating whether the current frame was created for an out of process runspace. - /// - internal bool IsRemote => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.Remote); - - /// - /// Gets a value indicating whether the current frame was created by PSHost.EnterNestedPrompt(). - /// - internal bool IsNestedPrompt => CurrentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt); - - /// - /// Gets a value indicating the current number of frames managed by this PromptNest. - /// - internal int NestedPromptLevel => _frameStack.Count; - - private PromptNestFrame CurrentFrame - { - get - { - _frameStack.TryPeek(out PromptNestFrame currentFrame); - return _isDisposed ? _readLineFrame : currentFrame; - } - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - lock (_disposeSyncObject) - { - if (_isDisposed || !disposing) - { - return; - } - - while (NestedPromptLevel > 1) - { - _consoleReader?.StopCommandLoop(); - var currentFrame = CurrentFrame; - if (currentFrame.FrameType.HasFlag(PromptNestFrameType.Debug)) - { - _versionSpecificOperations.StopCommandInDebugger(_powerShellContext); - currentFrame.ThreadController.StartThreadExit(DebuggerResumeAction.Stop); - currentFrame.WaitForFrameExit(CancellationToken.None); - continue; - } - - if (currentFrame.FrameType.HasFlag(PromptNestFrameType.NestedPrompt)) - { - _powerShellContext.ExitAllNestedPrompts(); - continue; - } - - currentFrame.PowerShell.BeginStop(null, null); - currentFrame.WaitForFrameExit(CancellationToken.None); - } - - _consoleReader?.StopCommandLoop(); - _readLineFrame.Dispose(); - CurrentFrame.Dispose(); - _frameStack.Clear(); - _powerShellContext = null; - _consoleReader = null; - _isDisposed = true; - } - } - - /// - /// Gets the for the current frame. - /// - /// - /// The for the current frame, or - /// if the current frame does not have one. - /// - internal ThreadController GetThreadController() - { - if (_isDisposed) - { - return null; - } - - return CurrentFrame.IsThreadController ? CurrentFrame.ThreadController : null; - } - - /// - /// Create a new and set it as the current frame. - /// - internal void PushPromptContext() - { - if (_isDisposed) - { - return; - } - - PushPromptContext(PromptNestFrameType.Normal); - } - - /// - /// Create a new and set it as the current frame. - /// - /// The frame type. - internal void PushPromptContext(PromptNestFrameType frameType) - { - if (_isDisposed) - { - return; - } - - _frameStack.Push( - new PromptNestFrame( - frameType.HasFlag(PromptNestFrameType.Remote) - ? PowerShell.Create() - : PowerShell.Create(RunspaceMode.CurrentRunspace), - NewHandleQueue(), - frameType)); - } - - /// - /// Dispose of the current and revert to the previous frame. - /// - internal void PopPromptContext() - { - PromptNestFrame currentFrame; - lock (_syncObject) - { - if (_isDisposed || _frameStack.Count == 1) - { - return; - } - - _frameStack.TryPop(out currentFrame); - } - - currentFrame.Dispose(); - } - - /// - /// Get the instance for the current - /// . - /// - /// Indicates whether this is for a PSReadLine command. - /// The instance for the current frame. - internal PowerShell GetPowerShell(bool isReadLine = false) - { - if (_isDisposed) - { - return null; - } - - // Typically we want to run PSReadLine on the current nest frame. - // The exception is when the current frame is remote, in which - // case we need to run it in it's own frame because we can't take - // over a remote pipeline through event invocation. - if (NestedPromptLevel > 1 && !IsRemote) - { - return CurrentFrame.PowerShell; - } - - return isReadLine ? _readLineFrame.PowerShell : CurrentFrame.PowerShell; - } - - /// - /// Get the for the current . - /// - /// - /// The that can be used to cancel the request. - /// - /// Indicates whether this is for a PSReadLine command. - /// The for the current frame. - internal RunspaceHandle GetRunspaceHandle(CancellationToken cancellationToken, bool isReadLine) - { - if (_isDisposed) - { - return null; - } - - // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace - // is in process. - if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - GetRunspaceHandleImpl(cancellationToken, isReadLine: false); - } - - return GetRunspaceHandleImpl(cancellationToken, isReadLine); - } - - - /// - /// Get the for the current . - /// - /// - /// The that will be checked prior to - /// completing the returned task. - /// - /// Indicates whether this is for a PSReadLine command. - /// - /// A object representing the asynchronous operation. - /// The property will return the - /// for the current frame. - /// - internal async Task GetRunspaceHandleAsync(CancellationToken cancellationToken, bool isReadLine) - { - if (_isDisposed) - { - return null; - } - - // Also grab the main runspace handle if this is for a ReadLine pipeline and the runspace - // is in process. - if (isReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - await GetRunspaceHandleImplAsync(cancellationToken, isReadLine: false); - } - - return await GetRunspaceHandleImplAsync(cancellationToken, isReadLine); - } - - /// - /// Releases control of the runspace aquired via the . - /// - /// - /// The representing the control to release. - /// - internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle) - { - if (_isDisposed) - { - return; - } - - ReleaseRunspaceHandleImpl(runspaceHandle.IsReadLine); - if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - ReleaseRunspaceHandleImpl(isReadLine: false); - } - } - - /// - /// Releases control of the runspace aquired via the . - /// - /// - /// The representing the control to release. - /// - /// - /// A object representing the release of the - /// . - /// - internal async Task ReleaseRunspaceHandleAsync(RunspaceHandle runspaceHandle) - { - if (_isDisposed) - { - return; - } - - await ReleaseRunspaceHandleImplAsync(runspaceHandle.IsReadLine); - if (runspaceHandle.IsReadLine && !_powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - await ReleaseRunspaceHandleImplAsync(isReadLine: false); - } - } - - /// - /// Determines if the current frame is unavailable for commands. - /// - /// - /// A value indicating whether the current frame is unavailable for commands. - /// - internal bool IsMainThreadBusy() - { - return !_isDisposed && CurrentFrame.Queue.IsEmpty; - } - - /// - /// Determines if a PSReadLine command is currently running. - /// - /// - /// A value indicating whether a PSReadLine command is currently running. - /// - internal bool IsReadLineBusy() - { - return !_isDisposed && _readLineFrame.Queue.IsEmpty; - } - - /// - /// Blocks until the current frame has been disposed. - /// - /// - /// A delegate that when invoked initates the exit of the current frame. - /// - internal void WaitForCurrentFrameExit(Action initiator) - { - if (_isDisposed) - { - return; - } - - var currentFrame = CurrentFrame; - try - { - initiator.Invoke(currentFrame); - } - finally - { - currentFrame.WaitForFrameExit(CancellationToken.None); - } - } - - /// - /// Blocks until the current frame has been disposed. - /// - internal void WaitForCurrentFrameExit() - { - if (_isDisposed) - { - return; - } - - CurrentFrame.WaitForFrameExit(CancellationToken.None); - } - - /// - /// Blocks until the current frame has been disposed. - /// - /// - /// The used the exit the block prior to - /// the current frame being disposed. - /// - internal void WaitForCurrentFrameExit(CancellationToken cancellationToken) - { - if (_isDisposed) - { - return; - } - - CurrentFrame.WaitForFrameExit(cancellationToken); - } - - /// - /// Creates a task that is completed when the current frame has been disposed. - /// - /// - /// A delegate that when invoked initates the exit of the current frame. - /// - /// - /// A object representing the current frame being disposed. - /// - internal async Task WaitForCurrentFrameExitAsync(Func initiator) - { - if (_isDisposed) - { - return; - } - - var currentFrame = CurrentFrame; - try - { - await initiator.Invoke(currentFrame); - } - finally - { - await currentFrame.WaitForFrameExitAsync(CancellationToken.None); - } - } - - /// - /// Creates a task that is completed when the current frame has been disposed. - /// - /// - /// A delegate that when invoked initates the exit of the current frame. - /// - /// - /// A object representing the current frame being disposed. - /// - internal async Task WaitForCurrentFrameExitAsync(Action initiator) - { - if (_isDisposed) - { - return; - } - - var currentFrame = CurrentFrame; - try - { - initiator.Invoke(currentFrame); - } - finally - { - await currentFrame.WaitForFrameExitAsync(CancellationToken.None); - } - } - - /// - /// Creates a task that is completed when the current frame has been disposed. - /// - /// - /// A object representing the current frame being disposed. - /// - internal async Task WaitForCurrentFrameExitAsync() - { - if (_isDisposed) - { - return; - } - - await WaitForCurrentFrameExitAsync(CancellationToken.None); - } - - /// - /// Creates a task that is completed when the current frame has been disposed. - /// - /// - /// The used the exit the block prior to the current frame being disposed. - /// - /// - /// A object representing the current frame being disposed. - /// - internal async Task WaitForCurrentFrameExitAsync(CancellationToken cancellationToken) - { - if (_isDisposed) - { - return; - } - - await CurrentFrame.WaitForFrameExitAsync(cancellationToken); - } - - private AsyncQueue NewHandleQueue() - { - var queue = new AsyncQueue(); - queue.Enqueue(new RunspaceHandle(_powerShellContext)); - return queue; - } - - private RunspaceHandle GetRunspaceHandleImpl(CancellationToken cancellationToken, bool isReadLine) - { - if (isReadLine) - { - return _readLineFrame.Queue.Dequeue(cancellationToken); - } - - return CurrentFrame.Queue.Dequeue(cancellationToken); - } - - private async Task GetRunspaceHandleImplAsync(CancellationToken cancellationToken, bool isReadLine) - { - if (isReadLine) - { - return await _readLineFrame.Queue.DequeueAsync(cancellationToken); - } - - return await CurrentFrame.Queue.DequeueAsync(cancellationToken); - } - - private void ReleaseRunspaceHandleImpl(bool isReadLine) - { - if (isReadLine) - { - _readLineFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, true)); - return; - } - - CurrentFrame.Queue.Enqueue(new RunspaceHandle(_powerShellContext, false)); - } - - private async Task ReleaseRunspaceHandleImplAsync(bool isReadLine) - { - if (isReadLine) - { - await _readLineFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, true)); - return; - } - - await CurrentFrame.Queue.EnqueueAsync(new RunspaceHandle(_powerShellContext, false)); - } - } -} diff --git a/src/PowerShellEditorServices/Session/PromptNestFrame.cs b/src/PowerShellEditorServices/Session/PromptNestFrame.cs deleted file mode 100644 index cae7dfb8a..000000000 --- a/src/PowerShellEditorServices/Session/PromptNestFrame.cs +++ /dev/null @@ -1,137 +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.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - using System.Management.Automation; - - /// - /// Represents a single frame in the . - /// - internal class PromptNestFrame : IDisposable - { - private const PSInvocationState IndisposableStates = PSInvocationState.Stopping | PSInvocationState.Running; - - private SemaphoreSlim _frameExited = new SemaphoreSlim(initialCount: 0); - - private bool _isDisposed = false; - - /// - /// Gets the instance. - /// - internal PowerShell PowerShell { get; } - - /// - /// Gets the queue that controls command invocation order. - /// - internal AsyncQueue Queue { get; } - - /// - /// Gets the frame type. - /// - internal PromptNestFrameType FrameType { get; } - - /// - /// Gets the . - /// - internal ThreadController ThreadController { get; } - - /// - /// Gets a value indicating whether the frame requires command invocations - /// to be routed to a specific thread. - /// - internal bool IsThreadController { get; } - - internal PromptNestFrame(PowerShell powerShell, AsyncQueue handleQueue) - : this(powerShell, handleQueue, PromptNestFrameType.Normal) - { } - - internal PromptNestFrame( - PowerShell powerShell, - AsyncQueue handleQueue, - PromptNestFrameType frameType) - { - PowerShell = powerShell; - Queue = handleQueue; - FrameType = frameType; - IsThreadController = (frameType & (PromptNestFrameType.Debug | PromptNestFrameType.NestedPrompt)) != 0; - if (!IsThreadController) - { - return; - } - - ThreadController = new ThreadController(this); - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (_isDisposed) - { - return; - } - - if (disposing) - { - if (IndisposableStates.HasFlag(PowerShell.InvocationStateInfo.State)) - { - PowerShell.BeginStop( - asyncResult => - { - PowerShell.Runspace = null; - PowerShell.Dispose(); - }, - state: null); - } - else - { - PowerShell.Runspace = null; - PowerShell.Dispose(); - } - - _frameExited.Release(); - } - - _isDisposed = true; - } - - /// - /// Blocks until the frame has been disposed. - /// - /// - /// The that will exit the block when cancelled. - /// - internal void WaitForFrameExit(CancellationToken cancellationToken) - { - _frameExited.Wait(cancellationToken); - _frameExited.Release(); - } - - /// - /// Creates a task object that is completed when the frame has been disposed. - /// - /// - /// The that will be checked prior to completing - /// the returned task. - /// - /// - /// A object that represents this frame being disposed. - /// - internal async Task WaitForFrameExitAsync(CancellationToken cancellationToken) - { - await _frameExited.WaitAsync(cancellationToken); - _frameExited.Release(); - } - } -} diff --git a/src/PowerShellEditorServices/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices/Session/PromptNestFrameType.cs deleted file mode 100644 index b42b42098..000000000 --- a/src/PowerShellEditorServices/Session/PromptNestFrameType.cs +++ /dev/null @@ -1,21 +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; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - [Flags] - internal enum PromptNestFrameType - { - Normal = 0, - - NestedPrompt = 1, - - Debug = 2, - - Remote = 4 - } -} diff --git a/src/PowerShellEditorServices/Session/RemoteFileManager.cs b/src/PowerShellEditorServices/Session/RemoteFileManager.cs deleted file mode 100644 index c9885b84f..000000000 --- a/src/PowerShellEditorServices/Session/RemoteFileManager.cs +++ /dev/null @@ -1,789 +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 Microsoft.PowerShell.EditorServices.Extensions; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Manages files that are accessed from a remote PowerShell session. - /// Also manages the registration and handling of the 'psedit' function. - /// - public class RemoteFileManager - { - #region Fields - - private ILogger logger; - private string remoteFilesPath; - private string processTempPath; - private PowerShellContext powerShellContext; - private IEditorOperations editorOperations; - - private Dictionary filesPerComputer = - new Dictionary(); - - private const string RemoteSessionOpenFile = "PSESRemoteSessionOpenFile"; - - private const string PSEditModule = @"<# - .SYNOPSIS - Opens the specified files in your editor window - .DESCRIPTION - Opens the specified files in your editor window - .EXAMPLE - PS > Open-EditorFile './foo.ps1' - Opens foo.ps1 in your editor - .EXAMPLE - PS > gci ./myDir | Open-EditorFile - Opens everything in 'myDir' in your editor - .INPUTS - Path - an array of files you want to open in your editor - #> - function Open-EditorFile { - param ( - [Parameter(Mandatory=$true, ValueFromPipeline=$true)] - [String[]] - $Path - ) - - begin { - $Paths = @() - } - - process { - $Paths += $Path - } - - end { - if ($Paths.Count -gt 1) { - $preview = $false - } else { - $preview = $true - } - - foreach ($fileName in $Paths) - { - Microsoft.PowerShell.Management\Get-ChildItem $fileName | Where-Object { ! $_.PSIsContainer } | Foreach-Object { - $filePathName = $_.FullName - - # Get file contents - $params = @{ Path=$filePathName; Raw=$true } - if ($PSVersionTable.PSEdition -eq 'Core') - { - $params['AsByteStream']=$true - } - else - { - $params['Encoding']='Byte' - } - - $contentBytes = Microsoft.PowerShell.Management\Get-Content @params - - # Notify client for file open. - Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($filePathName, $contentBytes, $preview) > $null - } - } - } - } - - <# - .SYNOPSIS - Creates new files and opens them in your editor window - .DESCRIPTION - Creates new files and opens them in your editor window - .EXAMPLE - PS > New-EditorFile './foo.ps1' - Creates and opens a new foo.ps1 in your editor - .EXAMPLE - PS > Get-Process | New-EditorFile proc.txt - Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process - .EXAMPLE - PS > Get-Process | New-EditorFile proc.txt -Force - Creates and opens a new foo.ps1 in your editor with the contents of the call to Get-Process. Overwrites the file if it already exists - .INPUTS - Path - an array of files you want to open in your editor - Value - The content you want in the new files - Force - Overwrites a file if it exists - #> - function New-EditorFile { - [CmdletBinding()] - param ( - [Parameter()] - [String[]] - [ValidateNotNullOrEmpty()] - $Path, - - [Parameter(ValueFromPipeline=$true)] - $Value, - - [Parameter()] - [switch] - $Force - ) - - begin { - $valueList = @() - } - - process { - $valueList += $Value - } - - end { - if ($Path) { - foreach ($fileName in $Path) - { - if (-not (Microsoft.PowerShell.Management\Test-Path $fileName) -or $Force) { - $valueList > $fileName - - # Get file contents - $params = @{ Path=$fileName; Raw=$true } - if ($PSVersionTable.PSEdition -eq 'Core') - { - $params['AsByteStream']=$true - } - else - { - $params['Encoding']='Byte' - } - - $contentBytes = Microsoft.PowerShell.Management\Get-Content @params - - if ($Path.Count -gt 1) { - $preview = $false - } else { - $preview = $true - } - - # Notify client for file open. - Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($fileName, $contentBytes, $preview) > $null - } else { - $PSCmdlet.WriteError( ( - Microsoft.PowerShell.Utility\New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList @( - [System.Exception]'File already exists.' - $Null - [System.Management.Automation.ErrorCategory]::ResourceExists - $fileName ) ) ) - } - } - } else { - $bytes = [System.Text.Encoding]::UTF8.GetBytes(($valueList | Microsoft.PowerShell.Utility\Out-String)) - Microsoft.PowerShell.Utility\New-Event -SourceIdentifier PSESRemoteSessionOpenFile -EventArguments @($null, $bytes) > $null - } - } - } - - Microsoft.PowerShell.Utility\Set-Alias psedit Open-EditorFile -Scope Global - Microsoft.PowerShell.Core\Export-ModuleMember -Function Open-EditorFile, New-EditorFile - "; - - // This script is templated so that the '-Forward' parameter can be added - // to the script when in non-local sessions - private const string CreatePSEditFunctionScript = @" - param ( - [string] $PSEditModule - ) - - Microsoft.PowerShell.Utility\Register-EngineEvent -SourceIdentifier PSESRemoteSessionOpenFile -Forward -SupportEvent - Microsoft.PowerShell.Core\New-Module -ScriptBlock ([Scriptblock]::Create($PSEditModule)) -Name PSEdit | - Microsoft.PowerShell.Core\Import-Module -Global - "; - - private const string RemovePSEditFunctionScript = @" - Microsoft.PowerShell.Core\Get-Module PSEdit | Microsoft.PowerShell.Core\Remove-Module - - Microsoft.PowerShell.Utility\Unregister-Event -SourceIdentifier PSESRemoteSessionOpenFile -Force -ErrorAction Ignore - "; - - private const string SetRemoteContentsScript = @" - param( - [string] $RemoteFilePath, - [byte[]] $Content - ) - - # Set file contents - $params = @{ Path=$RemoteFilePath; Value=$Content; Force=$true } - if ($PSVersionTable.PSEdition -eq 'Core') - { - $params['AsByteStream']=$true - } - else - { - $params['Encoding']='Byte' - } - - Microsoft.PowerShell.Management\Set-Content @params 2>&1 - "; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the RemoteFileManager class. - /// - /// - /// The PowerShellContext to use for file loading operations. - /// - /// - /// The IEditorOperations instance to use for opening/closing files in the editor. - /// - /// An ILogger implementation used for writing log messages. - public RemoteFileManager( - PowerShellContext powerShellContext, - IEditorOperations editorOperations, - ILogger logger) - { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - - this.logger = logger; - this.powerShellContext = powerShellContext; - this.powerShellContext.RunspaceChanged += HandleRunspaceChangedAsync; - - this.editorOperations = editorOperations; - - this.processTempPath = - Path.Combine( - Path.GetTempPath(), - "PSES-" + Process.GetCurrentProcess().Id); - - this.remoteFilesPath = Path.Combine(this.processTempPath, "RemoteFiles"); - - // Delete existing temporary file cache path if it already exists - this.TryDeleteTemporaryPath(); - - // Register the psedit function in the current runspace - this.RegisterPSEditFunction(this.powerShellContext.CurrentRunspace); - } - - #endregion - - #region Public Methods - - /// - /// Opens a remote file, fetching its contents if necessary. - /// - /// - /// The remote file path to be opened. - /// - /// - /// The runspace from which where the remote file will be fetched. - /// - /// - /// The local file path where the remote file's contents have been stored. - /// - public async Task FetchRemoteFileAsync( - string remoteFilePath, - RunspaceDetails runspaceDetails) - { - string localFilePath = null; - - if (!string.IsNullOrEmpty(remoteFilePath)) - { - try - { - RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); - localFilePath = this.GetMappedPath(remoteFilePath, runspaceDetails); - - if (!pathMappings.IsRemotePathOpened(remoteFilePath)) - { - // Does the local file already exist? - if (!File.Exists(localFilePath)) - { - // Load the file contents from the remote machine and create the buffer - PSCommand command = new PSCommand(); - command.AddCommand("Microsoft.PowerShell.Management\\Get-Content"); - command.AddParameter("Path", remoteFilePath); - command.AddParameter("Raw"); - command.AddParameter("Encoding", "Byte"); - - byte[] fileContent = - (await this.powerShellContext.ExecuteCommandAsync(command, false, false)) - .FirstOrDefault(); - - if (fileContent != null) - { - this.StoreRemoteFile(localFilePath, fileContent, pathMappings); - } - else - { - this.logger.Write( - LogLevel.Warning, - $"Could not load contents of remote file '{remoteFilePath}'"); - } - } - } - } - catch (IOException e) - { - this.logger.Write( - LogLevel.Error, - $"Caught {e.GetType().Name} while attempting to get remote file at path '{remoteFilePath}'\r\n\r\n{e.ToString()}"); - } - } - - return localFilePath; - } - - /// - /// Saves the contents of the file under the temporary local - /// file cache to its corresponding remote file. - /// - /// - /// The local file whose contents will be saved. It is assumed - /// that the editor has saved the contents of the local cache - /// file to disk before this method is called. - /// - /// A Task to be awaited for completion. - public async Task SaveRemoteFileAsync(string localFilePath) - { - string remoteFilePath = - this.GetMappedPath( - localFilePath, - this.powerShellContext.CurrentRunspace); - - this.logger.Write( - LogLevel.Verbose, - $"Saving remote file {remoteFilePath} (local path: {localFilePath})"); - - byte[] localFileContents = null; - try - { - localFileContents = File.ReadAllBytes(localFilePath); - } - catch (IOException e) - { - this.logger.WriteException( - "Failed to read contents of local copy of remote file", - e); - - return; - } - - PSCommand saveCommand = new PSCommand(); - saveCommand - .AddScript(SetRemoteContentsScript) - .AddParameter("RemoteFilePath", remoteFilePath) - .AddParameter("Content", localFileContents); - - StringBuilder errorMessages = new StringBuilder(); - - await this.powerShellContext.ExecuteCommandAsync( - saveCommand, - errorMessages, - false, - false); - - if (errorMessages.Length > 0) - { - this.logger.Write(LogLevel.Error, $"Remote file save failed due to an error:\r\n\r\n{errorMessages}"); - } - } - - /// - /// Creates a temporary file with the given name and contents - /// corresponding to the specified runspace. - /// - /// - /// The name of the file to be created under the session path. - /// - /// - /// The contents of the file to be created. - /// - /// - /// The runspace for which the temporary file relates. - /// - /// The full temporary path of the file if successful, null otherwise. - public string CreateTemporaryFile(string fileName, string fileContents, RunspaceDetails runspaceDetails) - { - string temporaryFilePath = Path.Combine(this.processTempPath, fileName); - - try - { - File.WriteAllText(temporaryFilePath, fileContents); - - RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); - pathMappings.AddOpenedLocalPath(temporaryFilePath); - } - catch (IOException e) - { - this.logger.Write( - LogLevel.Error, - $"Caught {e.GetType().Name} while attempting to write temporary file at path '{temporaryFilePath}'\r\n\r\n{e.ToString()}"); - - temporaryFilePath = null; - } - - return temporaryFilePath; - } - - /// - /// For a remote or local cache path, get the corresponding local or - /// remote file path. - /// - /// - /// The remote or local file path. - /// - /// - /// The runspace from which the remote file was fetched. - /// - /// The mapped file path. - public string GetMappedPath( - string filePath, - RunspaceDetails runspaceDetails) - { - RemotePathMappings remotePathMappings = this.GetPathMappings(runspaceDetails); - return remotePathMappings.GetMappedPath(filePath); - } - - /// - /// Returns true if the given file path is under the remote files - /// path in the temporary folder. - /// - /// The local file path to check. - /// - /// True if the file path is under the temporary remote files path. - /// - public bool IsUnderRemoteTempPath(string filePath) - { - return filePath.StartsWith( - this.remoteFilesPath, - System.StringComparison.CurrentCultureIgnoreCase); - } - - #endregion - - #region Private Methods - - private string StoreRemoteFile( - string remoteFilePath, - byte[] fileContent, - RunspaceDetails runspaceDetails) - { - RemotePathMappings pathMappings = this.GetPathMappings(runspaceDetails); - string localFilePath = pathMappings.GetMappedPath(remoteFilePath); - - this.StoreRemoteFile( - localFilePath, - fileContent, - pathMappings); - - return localFilePath; - } - - private void StoreRemoteFile( - string localFilePath, - byte[] fileContent, - RemotePathMappings pathMappings) - { - File.WriteAllBytes(localFilePath, fileContent); - pathMappings.AddOpenedLocalPath(localFilePath); - } - - private RemotePathMappings GetPathMappings(RunspaceDetails runspaceDetails) - { - RemotePathMappings remotePathMappings = null; - string computerName = runspaceDetails.SessionDetails.ComputerName; - - if (!this.filesPerComputer.TryGetValue(computerName, out remotePathMappings)) - { - remotePathMappings = new RemotePathMappings(runspaceDetails, this); - this.filesPerComputer.Add(computerName, remotePathMappings); - } - - return remotePathMappings; - } - - private async void HandleRunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) - { - if (e.ChangeAction == RunspaceChangeAction.Enter) - { - this.RegisterPSEditFunction(e.NewRunspace); - } - else - { - // Close any remote files that were opened - if (e.PreviousRunspace.Location == RunspaceLocation.Remote && - (e.ChangeAction == RunspaceChangeAction.Shutdown || - !string.Equals( - e.NewRunspace.SessionDetails.ComputerName, - e.PreviousRunspace.SessionDetails.ComputerName, - StringComparison.CurrentCultureIgnoreCase))) - { - RemotePathMappings remotePathMappings; - if (this.filesPerComputer.TryGetValue(e.PreviousRunspace.SessionDetails.ComputerName, out remotePathMappings)) - { - foreach (string remotePath in remotePathMappings.OpenedPaths) - { - await this.editorOperations?.CloseFileAsync(remotePath); - } - } - } - - if (e.PreviousRunspace != null) - { - this.RemovePSEditFunction(e.PreviousRunspace); - } - } - } - - private async void HandlePSEventReceivedAsync(object sender, PSEventArgs args) - { - if (string.Equals(RemoteSessionOpenFile, args.SourceIdentifier, StringComparison.CurrentCultureIgnoreCase)) - { - try - { - if (args.SourceArgs.Length >= 1) - { - string localFilePath = string.Empty; - string remoteFilePath = args.SourceArgs[0] as string; - - // Is this a local process runspace? Treat as a local file - if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Local) - { - localFilePath = remoteFilePath; - } - else - { - byte[] fileContent = null; - - if (args.SourceArgs.Length >= 2) - { - // Try to cast as a PSObject to get the BaseObject, if not, then try to case as a byte[] - PSObject sourceObj = args.SourceArgs[1] as PSObject; - if (sourceObj != null) - { - fileContent = sourceObj.BaseObject as byte[]; - } - else - { - fileContent = args.SourceArgs[1] as byte[]; - } - } - - // If fileContent is still null after trying to - // unpack the contents, just return an empty byte - // array. - fileContent = fileContent ?? new byte[0]; - - if (remoteFilePath != null) - { - localFilePath = - this.StoreRemoteFile( - remoteFilePath, - fileContent, - this.powerShellContext.CurrentRunspace); - } - else - { - await this.editorOperations?.NewFileAsync(); - EditorContext context = await this.editorOperations?.GetEditorContextAsync(); - context?.CurrentFile.InsertText(Encoding.UTF8.GetString(fileContent, 0, fileContent.Length)); - } - } - - bool preview = true; - if (args.SourceArgs.Length >= 3) - { - bool? previewCheck = args.SourceArgs[2] as bool?; - preview = previewCheck ?? true; - } - - // Open the file in the editor - this.editorOperations?.OpenFileAsync(localFilePath, preview); - } - } - catch (NullReferenceException e) - { - this.logger.WriteException("Could not store null remote file content", e); - } - } - } - - private void RegisterPSEditFunction(RunspaceDetails runspaceDetails) - { - if (runspaceDetails.Location == RunspaceLocation.Remote && - runspaceDetails.Context == RunspaceContext.Original) - { - try - { - runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived += HandlePSEventReceivedAsync; - - PSCommand createCommand = new PSCommand(); - createCommand - .AddScript(CreatePSEditFunctionScript) - .AddParameter("PSEditModule", PSEditModule); - - if (runspaceDetails.Context == RunspaceContext.DebuggedRunspace) - { - this.powerShellContext.ExecuteCommandAsync(createCommand).Wait(); - } - else - { - using (var powerShell = System.Management.Automation.PowerShell.Create()) - { - powerShell.Runspace = runspaceDetails.Runspace; - powerShell.Commands = createCommand; - powerShell.Invoke(); - } - } - } - catch (RemoteException e) - { - this.logger.WriteException("Could not create psedit function.", e); - } - } - } - - private void RemovePSEditFunction(RunspaceDetails runspaceDetails) - { - if (runspaceDetails.Location == RunspaceLocation.Remote && - runspaceDetails.Context == RunspaceContext.Original) - { - try - { - if (runspaceDetails.Runspace.Events != null) - { - runspaceDetails.Runspace.Events.ReceivedEvents.PSEventReceived -= HandlePSEventReceivedAsync; - } - - if (runspaceDetails.Runspace.RunspaceStateInfo.State == RunspaceState.Opened) - { - using (var powerShell = System.Management.Automation.PowerShell.Create()) - { - powerShell.Runspace = runspaceDetails.Runspace; - powerShell.Commands.AddScript(RemovePSEditFunctionScript); - powerShell.Invoke(); - } - } - } - catch (Exception e) when (e is RemoteException || e is PSInvalidOperationException) - { - this.logger.WriteException("Could not remove psedit function.", e); - } - } - } - - private void TryDeleteTemporaryPath() - { - try - { - if (Directory.Exists(this.processTempPath)) - { - Directory.Delete(this.processTempPath, true); - } - - Directory.CreateDirectory(this.processTempPath); - } - catch (IOException e) - { - this.logger.WriteException( - $"Could not delete temporary folder for current process: {this.processTempPath}", e); - } - } - - #endregion - - #region Nested Classes - - private class RemotePathMappings - { - private RunspaceDetails runspaceDetails; - private RemoteFileManager remoteFileManager; - private HashSet openedPaths = new HashSet(); - private Dictionary pathMappings = new Dictionary(); - - public IEnumerable OpenedPaths - { - get { return openedPaths; } - } - - public RemotePathMappings( - RunspaceDetails runspaceDetails, - RemoteFileManager remoteFileManager) - { - this.runspaceDetails = runspaceDetails; - this.remoteFileManager = remoteFileManager; - } - - public void AddPathMapping(string remotePath, string localPath) - { - // Add mappings in both directions - this.pathMappings[localPath.ToLower()] = remotePath; - this.pathMappings[remotePath.ToLower()] = localPath; - } - - public void AddOpenedLocalPath(string openedLocalPath) - { - this.openedPaths.Add(openedLocalPath); - } - - public bool IsRemotePathOpened(string remotePath) - { - return this.openedPaths.Contains(remotePath); - } - - public string GetMappedPath(string filePath) - { - string mappedPath = filePath; - - if (!this.pathMappings.TryGetValue(filePath.ToLower(), out mappedPath)) - { - // If the path isn't mapped yet, generate it - if (!filePath.StartsWith(this.remoteFileManager.remoteFilesPath)) - { - mappedPath = - this.MapRemotePathToLocal( - filePath, - runspaceDetails.SessionDetails.ComputerName); - - this.AddPathMapping(filePath, mappedPath); - } - } - - return mappedPath; - } - - private string MapRemotePathToLocal(string remotePath, string connectionString) - { - // The path generated by this code will look something like - // %TEMP%\PSES-[PID]\RemoteFiles\1205823508\computer-name\MyFile.ps1 - // The "path hash" is just the hashed representation of the remote - // file's full path (sans directory) to try and ensure some amount of - // uniqueness across different files on the remote machine. We put - // the "connection string" after the path slug so that it can be used - // as the differentiator string in editors like VS Code when more than - // one tab has the same filename. - - var sessionDir = Directory.CreateDirectory(this.remoteFileManager.remoteFilesPath); - var pathHashDir = - sessionDir.CreateSubdirectory( - Path.GetDirectoryName(remotePath).GetHashCode().ToString()); - - var remoteFileDir = pathHashDir.CreateSubdirectory(connectionString); - - return - Path.Combine( - remoteFileDir.FullName, - Path.GetFileName(remotePath)); - } - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs deleted file mode 100644 index 12dc76ade..000000000 --- a/src/PowerShellEditorServices/Session/RunspaceChangedEventArgs.cs +++ /dev/null @@ -1,69 +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 Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Defines the set of actions that will cause the runspace to be changed. - /// - public enum RunspaceChangeAction - { - /// - /// The runspace change was caused by entering a new session. - /// - Enter, - - /// - /// The runspace change was caused by exiting the current session. - /// - Exit, - - /// - /// The runspace change was caused by shutting down the service. - /// - Shutdown - } - - /// - /// Provides arguments for the PowerShellContext.RunspaceChanged event. - /// - public class RunspaceChangedEventArgs - { - /// - /// Gets the RunspaceChangeAction which caused this event. - /// - public RunspaceChangeAction ChangeAction { get; private set; } - - /// - /// Gets a RunspaceDetails object describing the previous runspace. - /// - public RunspaceDetails PreviousRunspace { get; private set; } - - /// - /// Gets a RunspaceDetails object describing the new runspace. - /// - public RunspaceDetails NewRunspace { get; private set; } - - /// - /// Creates a new instance of the RunspaceChangedEventArgs class. - /// - /// The action which caused the runspace to change. - /// The previously active runspace. - /// The newly active runspace. - public RunspaceChangedEventArgs( - RunspaceChangeAction changeAction, - RunspaceDetails previousRunspace, - RunspaceDetails newRunspace) - { - Validate.IsNotNull(nameof(previousRunspace), previousRunspace); - - this.ChangeAction = changeAction; - this.PreviousRunspace = previousRunspace; - this.NewRunspace = newRunspace; - } - } -} diff --git a/src/PowerShellEditorServices/Session/RunspaceDetails.cs b/src/PowerShellEditorServices/Session/RunspaceDetails.cs deleted file mode 100644 index d016e2fe8..000000000 --- a/src/PowerShellEditorServices/Session/RunspaceDetails.cs +++ /dev/null @@ -1,321 +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 Microsoft.CSharp.RuntimeBinder; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Management.Automation.Runspaces; -using System.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Specifies the possible types of a runspace. - /// - public enum RunspaceLocation - { - /// - /// A runspace on the local machine. - /// - Local, - - /// - /// A runspace on a different machine. - /// - Remote - } - - /// - /// Specifies the context in which the runspace was encountered. - /// - public enum RunspaceContext - { - /// - /// The original runspace in a local or remote session. - /// - Original, - - /// - /// A runspace in a process that was entered with Enter-PSHostProcess. - /// - EnteredProcess, - - /// - /// A runspace that is being debugged with Debug-Runspace. - /// - DebuggedRunspace - } - - /// - /// Provides details about a runspace being used in the current - /// editing session. - /// - public class RunspaceDetails - { - #region Private Fields - - private Dictionary capabilities = - new Dictionary(); - - #endregion - - #region Properties - - /// - /// Gets the Runspace instance for which this class contains details. - /// - internal Runspace Runspace { get; private set; } - - /// - /// Gets the PowerShell version of the new runspace. - /// - public PowerShellVersionDetails PowerShellVersion { get; private set; } - - /// - /// Gets the runspace location, either Local or Remote. - /// - public RunspaceLocation Location { get; private set; } - - /// - /// Gets the context in which the runspace was encountered. - /// - public RunspaceContext Context { get; private set; } - - /// - /// Gets the "connection string" for the runspace, generally the - /// ComputerName for a remote runspace or the ProcessId of an - /// "Attach" runspace. - /// - public string ConnectionString { get; private set; } - - /// - /// Gets the details of the runspace's session at the time this - /// RunspaceDetails object was created. - /// - public SessionDetails SessionDetails { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the RunspaceDetails class. - /// - /// - /// The runspace for which this instance contains details. - /// - /// - /// The SessionDetails for the runspace. - /// - /// - /// The PowerShellVersionDetails of the runspace. - /// - /// - /// The RunspaceLocation of the runspace. - /// - /// - /// The RunspaceContext of the runspace. - /// - /// - /// The connection string of the runspace. - /// - public RunspaceDetails( - Runspace runspace, - SessionDetails sessionDetails, - PowerShellVersionDetails powerShellVersion, - RunspaceLocation runspaceLocation, - RunspaceContext runspaceContext, - string connectionString) - { - this.Runspace = runspace; - this.SessionDetails = sessionDetails; - this.PowerShellVersion = powerShellVersion; - this.Location = runspaceLocation; - this.Context = runspaceContext; - this.ConnectionString = connectionString; - } - - #endregion - - #region Public Methods - - internal void AddCapability(TCapability capability) - where TCapability : IRunspaceCapability - { - this.capabilities.Add(typeof(TCapability), capability); - } - - internal TCapability GetCapability() - where TCapability : IRunspaceCapability - { - TCapability capability = default(TCapability); - this.TryGetCapability(out capability); - return capability; - } - - internal bool TryGetCapability(out TCapability capability) - where TCapability : IRunspaceCapability - { - IRunspaceCapability capabilityAsInterface = default(TCapability); - if (this.capabilities.TryGetValue(typeof(TCapability), out capabilityAsInterface)) - { - capability = (TCapability)capabilityAsInterface; - return true; - } - - capability = default(TCapability); - return false; - } - - internal bool HasCapability() - { - return this.capabilities.ContainsKey(typeof(TCapability)); - } - - /// - /// Creates and populates a new RunspaceDetails instance for the given runspace. - /// - /// - /// The runspace for which details will be gathered. - /// - /// - /// The SessionDetails for the runspace. - /// - /// An ILogger implementation used for writing log messages. - /// A new RunspaceDetails instance. - internal static RunspaceDetails CreateFromRunspace( - Runspace runspace, - SessionDetails sessionDetails, - ILogger logger) - { - Validate.IsNotNull(nameof(runspace), runspace); - Validate.IsNotNull(nameof(sessionDetails), sessionDetails); - - var runspaceLocation = RunspaceLocation.Local; - var runspaceContext = RunspaceContext.Original; - var versionDetails = PowerShellVersionDetails.GetVersionDetails(runspace, logger); - - string connectionString = null; - - if (runspace.ConnectionInfo != null) - { - // Use 'dynamic' to avoid missing NamedPipeRunspaceConnectionInfo - // on PS v3 and v4 - try - { - dynamic connectionInfo = runspace.ConnectionInfo; - if (connectionInfo.ProcessId != null) - { - connectionString = connectionInfo.ProcessId.ToString(); - runspaceContext = RunspaceContext.EnteredProcess; - } - } - catch (RuntimeBinderException) - { - // ProcessId property isn't on the object, move on. - } - - // Grab the $host.name which will tell us if we're in a PSRP session or not - string hostName = - PowerShellContext.ExecuteScriptAndGetItem( - "$Host.Name", - runspace, - defaultValue: string.Empty); - - // hostname is 'ServerRemoteHost' when the user enters a session. - // ex. Enter-PSSession - // Attaching to process currently needs to be marked as a local session - // so we skip this if block if the runspace is from Enter-PSHostProcess - if (hostName.Equals("ServerRemoteHost", StringComparison.Ordinal) - && runspace.OriginalConnectionInfo?.GetType().ToString() != "System.Management.Automation.Runspaces.NamedPipeConnectionInfo") - { - runspaceLocation = RunspaceLocation.Remote; - connectionString = - runspace.ConnectionInfo.ComputerName + - (connectionString != null ? $"-{connectionString}" : string.Empty); - } - } - - return - new RunspaceDetails( - runspace, - sessionDetails, - versionDetails, - runspaceLocation, - runspaceContext, - connectionString); - } - - /// - /// Creates a clone of the given runspace through which another - /// runspace was attached. Sets the IsAttached property of the - /// resulting RunspaceDetails object to true. - /// - /// - /// The RunspaceDetails object which the new object based. - /// - /// - /// The RunspaceContext of the runspace. - /// - /// - /// The SessionDetails for the runspace. - /// - /// - /// A new RunspaceDetails instance for the attached runspace. - /// - public static RunspaceDetails CreateFromContext( - RunspaceDetails runspaceDetails, - RunspaceContext runspaceContext, - SessionDetails sessionDetails) - { - return - new RunspaceDetails( - runspaceDetails.Runspace, - sessionDetails, - runspaceDetails.PowerShellVersion, - runspaceDetails.Location, - runspaceContext, - runspaceDetails.ConnectionString); - } - - /// - /// Creates a new RunspaceDetails object from a remote - /// debugging session. - /// - /// - /// The RunspaceDetails object which the new object based. - /// - /// - /// The RunspaceLocation of the runspace. - /// - /// - /// The RunspaceContext of the runspace. - /// - /// - /// The SessionDetails for the runspace. - /// - /// - /// A new RunspaceDetails instance for the attached runspace. - /// - public static RunspaceDetails CreateFromDebugger( - RunspaceDetails runspaceDetails, - RunspaceLocation runspaceLocation, - RunspaceContext runspaceContext, - SessionDetails sessionDetails) - { - // TODO: Get the PowerShellVersion correctly! - return - new RunspaceDetails( - runspaceDetails.Runspace, - sessionDetails, - runspaceDetails.PowerShellVersion, - runspaceLocation, - runspaceContext, - runspaceDetails.ConnectionString); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Session/RunspaceHandle.cs b/src/PowerShellEditorServices/Session/RunspaceHandle.cs deleted file mode 100644 index 4947eadbe..000000000 --- a/src/PowerShellEditorServices/Session/RunspaceHandle.cs +++ /dev/null @@ -1,60 +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.Management.Automation.Host; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides a handle to the runspace that is managed by - /// a PowerShellContext. The holder of this handle. - /// - public class RunspaceHandle : IDisposable - { - private PowerShellContext powerShellContext; - - /// - /// Gets the runspace that is held by this handle. - /// - public Runspace Runspace - { - get - { - return ((IHostSupportsInteractiveSession)this.powerShellContext).Runspace; - } - } - - internal bool IsReadLine { get; } - - /// - /// Initializes a new instance of the RunspaceHandle class using the - /// given runspace. - /// - /// The PowerShellContext instance which manages the runspace. - public RunspaceHandle(PowerShellContext powerShellContext) - : this(powerShellContext, false) - { } - - internal RunspaceHandle(PowerShellContext powerShellContext, bool isReadLine) - { - this.powerShellContext = powerShellContext; - this.IsReadLine = isReadLine; - } - - /// - /// Disposes the RunspaceHandle once the holder is done using it. - /// Causes the handle to be released back to the PowerShellContext. - /// - public void Dispose() - { - // Release the handle and clear the runspace so that - // no further operations can be performed on it. - this.powerShellContext.ReleaseRunspaceHandle(this); - } - } -} - diff --git a/src/PowerShellEditorServices/Session/SessionDetails.cs b/src/PowerShellEditorServices/Session/SessionDetails.cs deleted file mode 100644 index 708e6ec9d..000000000 --- a/src/PowerShellEditorServices/Session/SessionDetails.cs +++ /dev/null @@ -1,64 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Management.Automation; -using System.Collections; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Provides details about the current PowerShell session. - /// - public class SessionDetails - { - /// - /// Gets the process ID of the current process. - /// - public int? ProcessId { get; private set; } - - /// - /// Gets the name of the current computer. - /// - public string ComputerName { get; private set; } - - /// - /// Gets the current PSHost instance ID. - /// - public Guid? InstanceId { get; private set; } - - /// - /// Creates an instance of SessionDetails using the information - /// contained in the PSObject which was obtained using the - /// PSCommand returned by GetDetailsCommand. - /// - /// - public SessionDetails(PSObject detailsObject) - { - Validate.IsNotNull(nameof(detailsObject), detailsObject); - - Hashtable innerHashtable = detailsObject.BaseObject as Hashtable; - - this.ProcessId = (int)innerHashtable["processId"] as int?; - this.ComputerName = innerHashtable["computerName"] as string; - this.InstanceId = innerHashtable["instanceId"] as Guid?; - } - - /// - /// Gets the PSCommand that gathers details from the - /// current session. - /// - /// A PSCommand used to gather session details. - public static PSCommand GetDetailsCommand() - { - PSCommand infoCommand = new PSCommand(); - infoCommand.AddScript( - "@{ 'computerName' = if ([Environment]::MachineName) {[Environment]::MachineName} else {'localhost'}; 'processId' = $PID; 'instanceId' = $host.InstanceId }"); - - return infoCommand; - } - } -} diff --git a/src/PowerShellEditorServices/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices/Session/SessionStateChangedEventArgs.cs deleted file mode 100644 index 9a0fed0f3..000000000 --- a/src/PowerShellEditorServices/Session/SessionStateChangedEventArgs.cs +++ /dev/null @@ -1,48 +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; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about a change in state of a PowerShellContext. - /// - public class SessionStateChangedEventArgs - { - /// - /// Gets the new state for the session. - /// - public PowerShellContextState NewSessionState { get; private set; } - - /// - /// Gets the execution result of the operation that caused - /// the state change. - /// - public PowerShellExecutionResult ExecutionResult { get; private set; } - - /// - /// Gets the exception that caused a failure state or null otherwise. - /// - public Exception ErrorException { get; private set; } - - /// - /// Creates a new instance of the SessionStateChangedEventArgs class. - /// - /// The new session state. - /// The result of the operation that caused the state change. - /// An exception that describes the failure, if any. - public SessionStateChangedEventArgs( - PowerShellContextState newSessionState, - PowerShellExecutionResult executionResult, - Exception errorException) - { - this.NewSessionState = newSessionState; - this.ExecutionResult = executionResult; - this.ErrorException = errorException; - } - } -} - diff --git a/src/PowerShellEditorServices/Session/ThreadController.cs b/src/PowerShellEditorServices/Session/ThreadController.cs deleted file mode 100644 index 8720e3fa7..000000000 --- a/src/PowerShellEditorServices/Session/ThreadController.cs +++ /dev/null @@ -1,131 +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.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Session -{ - /// - /// Provides the ability to route PowerShell command invocations to a specific thread. - /// - internal class ThreadController - { - private PromptNestFrame _nestFrame; - - internal AsyncQueue PipelineRequestQueue { get; } - - internal TaskCompletionSource FrameExitTask { get; } - - internal int ManagedThreadId { get; } - - internal bool IsPipelineThread { get; } - - /// - /// Initializes an new instance of the ThreadController class. This constructor should only - /// ever been called from the thread it is meant to control. - /// - /// The parent PromptNestFrame object. - internal ThreadController(PromptNestFrame nestFrame) - { - _nestFrame = nestFrame; - PipelineRequestQueue = new AsyncQueue(); - FrameExitTask = new TaskCompletionSource(); - ManagedThreadId = Thread.CurrentThread.ManagedThreadId; - - // If the debugger stop is triggered on a thread with no default runspace we - // shouldn't attempt to route commands to it. - IsPipelineThread = Runspace.DefaultRunspace != null; - } - - /// - /// Determines if the caller is already on the thread that this object maintains. - /// - /// - /// A value indicating if the caller is already on the thread maintained by this object. - /// - internal bool IsCurrentThread() - { - return Thread.CurrentThread.ManagedThreadId == ManagedThreadId; - } - - /// - /// Requests the invocation of a PowerShell command on the thread maintained by this object. - /// - /// The execution request to send. - /// - /// A task object representing the asynchronous operation. The Result property will return - /// the output of the command invocation. - /// - internal async Task> RequestPipelineExecutionAsync( - PipelineExecutionRequest executionRequest) - { - await PipelineRequestQueue.EnqueueAsync(executionRequest); - return await executionRequest.Results; - } - - /// - /// Retrieves the first currently queued execution request. If there are no pending - /// execution requests then the task will be completed when one is requested. - /// - /// - /// A task object representing the asynchronous operation. The Result property will return - /// the retrieved pipeline execution request. - /// - internal async Task TakeExecutionRequestAsync() - { - return await PipelineRequestQueue.DequeueAsync(); - } - - /// - /// Marks the thread to be exited. - /// - /// - /// The resume action for the debugger. If the frame is not a debugger frame this parameter - /// is ignored. - /// - internal void StartThreadExit(DebuggerResumeAction action) - { - StartThreadExit(action, waitForExit: false); - } - - /// - /// Marks the thread to be exited. - /// - /// - /// The resume action for the debugger. If the frame is not a debugger frame this parameter - /// is ignored. - /// - /// - /// Indicates whether the method should block until the exit is completed. - /// - internal void StartThreadExit(DebuggerResumeAction action, bool waitForExit) - { - Task.Run(() => FrameExitTask.TrySetResult(action)); - if (!waitForExit) - { - return; - } - - _nestFrame.WaitForFrameExit(CancellationToken.None); - } - - /// - /// Creates a task object that completes when the thread has be marked for exit. - /// - /// - /// A task object representing the frame receiving a request to exit. The Result property - /// will return the DebuggerResumeAction supplied with the request. - /// - internal async Task Exit() - { - return await FrameExitTask.Task.ConfigureAwait(false); - } - } -} diff --git a/src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs deleted file mode 100644 index 02638c5b0..000000000 --- a/src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs +++ /dev/null @@ -1,24 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices.Symbols -{ - /// - /// Specifies the contract for a document symbols provider. - /// - public interface IDocumentSymbolProvider : IFeatureProvider - { - /// - /// Provides a list of symbols for the given document. - /// - /// - /// The document for which SymbolReferences should be provided. - /// - /// An IEnumerable collection of SymbolReferences. - IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); - } -} diff --git a/src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs b/src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs deleted file mode 100644 index 6c1937627..000000000 --- a/src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs +++ /dev/null @@ -1,31 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices.Symbols -{ - /// - /// Specifies the contract for an implementation of - /// the IDocumentSymbols component. - /// - public interface IDocumentSymbols - { - /// - /// Gets the collection of IDocumentSymbolsProvider implementations - /// that are registered with this component. - /// - IFeatureProviderCollection Providers { get; } - - /// - /// Provides a list of symbols for the given document. - /// - /// - /// The document for which SymbolReferences should be provided. - /// - /// An IEnumerable collection of SymbolReferences. - IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); - } -} diff --git a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs deleted file mode 100644 index 762cc002b..000000000 --- a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs +++ /dev/null @@ -1,223 +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.Collections.Generic; -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Symbols -{ - /// - /// Provides an IDocumentSymbolProvider implementation for - /// enumerating test symbols in Pester test (tests.ps1) files. - /// - public class PesterDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider - { - - IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( - ScriptFile scriptFile) - { - if (!scriptFile.FilePath.EndsWith( - "tests.ps1", - StringComparison.OrdinalIgnoreCase)) - { - return Enumerable.Empty(); - } - - // Find plausible Pester commands - IEnumerable commandAsts = scriptFile.ScriptAst.FindAll(IsNamedCommandWithArguments, true); - - return commandAsts.OfType() - .Where(IsPesterCommand) - .Select(ast => ConvertPesterAstToSymbolReference(scriptFile, ast)); - } - - /// - /// Test if the given Ast is a regular CommandAst with arguments - /// - /// the PowerShell Ast to test - /// true if the Ast represents a PowerShell command with arguments, false otherwise - private static bool IsNamedCommandWithArguments(Ast ast) - { - CommandAst commandAst = ast as CommandAst; - - return commandAst != null && - commandAst.InvocationOperator != TokenKind.Dot && - PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue && - commandAst.CommandElements.Count >= 2; - } - - /// - /// Test whether the given CommandAst represents a Pester command - /// - /// the CommandAst to test - /// true if the CommandAst represents a Pester command, false otherwise - private static bool IsPesterCommand(CommandAst commandAst) - { - if (commandAst == null) - { - return false; - } - - // Ensure the first word is a Pester keyword - if (!PesterSymbolReference.PesterKeywords.ContainsKey(commandAst.GetCommandName())) - { - return false; - } - - // Ensure that the last argument of the command is a scriptblock - if (!(commandAst.CommandElements[commandAst.CommandElements.Count-1] is ScriptBlockExpressionAst)) - { - return false; - } - - return true; - } - - /// - /// Convert a CommandAst known to represent a Pester command and a reference to the scriptfile - /// it is in into symbol representing a Pester call for code lens - /// - /// the scriptfile the Pester call occurs in - /// the CommandAst representing the Pester call - /// a symbol representing the Pester call containing metadata for CodeLens to use - private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFile scriptFile, CommandAst pesterCommandAst) - { - string testLine = scriptFile.GetLine(pesterCommandAst.Extent.StartLineNumber); - PesterCommandType? commandName = PesterSymbolReference.GetCommandType(pesterCommandAst.GetCommandName()); - if (commandName == null) - { - return null; - } - - // Search for a name for the test - // If the test has more than one argument for names, we set it to null - string testName = null; - bool alreadySawName = false; - for (int i = 1; i < pesterCommandAst.CommandElements.Count; i++) - { - CommandElementAst currentCommandElement = pesterCommandAst.CommandElements[i]; - - // Check for an explicit "-Name" parameter - if (currentCommandElement is CommandParameterAst parameterAst) - { - // Found -Name parameter, move to next element which is the argument for -TestName - i++; - - if (!alreadySawName && TryGetTestNameArgument(pesterCommandAst.CommandElements[i], out testName)) - { - alreadySawName = true; - } - - continue; - } - - // Otherwise, if an argument is given with no parameter, we assume it's the name - // If we've already seen a name, we set the name to null - if (!alreadySawName && TryGetTestNameArgument(pesterCommandAst.CommandElements[i], out testName)) - { - alreadySawName = true; - } - } - - return new PesterSymbolReference( - scriptFile, - commandName.Value, - testLine, - testName, - pesterCommandAst.Extent - ); - } - - private static bool TryGetTestNameArgument(CommandElementAst commandElementAst, out string testName) - { - testName = null; - - if (commandElementAst is StringConstantExpressionAst testNameStrAst) - { - testName = testNameStrAst.Value; - return true; - } - - return (commandElementAst is ExpandableStringExpressionAst); - } - } - - /// - /// Defines command types for Pester test blocks. - /// - public enum PesterCommandType - { - /// - /// Identifies a Describe block. - /// - Describe, - - /// - /// Identifies a Context block. - /// - Context, - - /// - /// Identifies an It block. - /// - It - } - - /// - /// Provides a specialization of SymbolReference containing - /// extra information about Pester test symbols. - /// - public class PesterSymbolReference : SymbolReference - { - /// - /// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself - /// - internal static readonly IReadOnlyDictionary PesterKeywords = - Enum.GetValues(typeof(PesterCommandType)) - .Cast() - .ToDictionary(pct => pct.ToString(), pct => pct, StringComparer.OrdinalIgnoreCase); - - private static char[] DefinitionTrimChars = new char[] { ' ', '{' }; - - /// - /// Gets the name of the test - /// - public string TestName { get; private set; } - - /// - /// Gets the test's command type. - /// - public PesterCommandType Command { get; private set; } - - internal PesterSymbolReference( - ScriptFile scriptFile, - PesterCommandType commandType, - string testLine, - string testName, - IScriptExtent scriptExtent) - : base( - SymbolType.Function, - testLine.TrimEnd(DefinitionTrimChars), - scriptExtent, - scriptFile.FilePath, - testLine) - { - this.Command = commandType; - this.TestName = testName; - } - - internal static PesterCommandType? GetCommandType(string commandName) - { - PesterCommandType pesterCommandType; - if (commandName == null || !PesterKeywords.TryGetValue(commandName, out pesterCommandType)) - { - return null; - } - return pesterCommandType; - } - } -} diff --git a/src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.cs deleted file mode 100644 index e6e0c4a32..000000000 --- a/src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.cs +++ /dev/null @@ -1,33 +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.Collections.Generic; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Symbols -{ - /// - /// Provides an IDocumentSymbolProvider implementation for - /// enumerating symbols in .psd1 files. - /// - public class PsdDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider - { - IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( - ScriptFile scriptFile) - { - if ((scriptFile.FilePath != null && - scriptFile.FilePath.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase)) || - AstOperations.IsPowerShellDataFileAst(scriptFile.ScriptAst)) - { - var findHashtableSymbolsVisitor = new FindHashtableSymbolsVisitor(); - scriptFile.ScriptAst.Visit(findHashtableSymbolsVisitor); - return findHashtableSymbolsVisitor.SymbolReferences; - } - - return Enumerable.Empty(); - } - } -} diff --git a/src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs deleted file mode 100644 index 9405bf479..000000000 --- a/src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs +++ /dev/null @@ -1,47 +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.Collections.Generic; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Symbols -{ - /// - /// Provides an IDocumentSymbolProvider implementation for - /// enumerating symbols in script (.psd1, .psm1) files. - /// - public class ScriptDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider - { - private Version powerShellVersion; - - /// - /// Creates an instance of the ScriptDocumentSymbolProvider to - /// target the specified PowerShell version. - /// - /// The target PowerShell version. - public ScriptDocumentSymbolProvider(Version powerShellVersion) - { - this.powerShellVersion = powerShellVersion; - } - - IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( - ScriptFile scriptFile) - { - if (scriptFile != null && - scriptFile.FilePath != null && - (scriptFile.FilePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase) || - scriptFile.FilePath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase))) - { - return - AstOperations.FindSymbolsInDocument( - scriptFile.ScriptAst, - this.powerShellVersion); - } - - return Enumerable.Empty(); - } - } -} diff --git a/src/PowerShellEditorServices/Templates/TemplateDetails.cs b/src/PowerShellEditorServices/Templates/TemplateDetails.cs deleted file mode 100644 index c15d58e29..000000000 --- a/src/PowerShellEditorServices/Templates/TemplateDetails.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -// 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.Templates -{ - /// - /// Provides details about a file or project template. - /// - public class TemplateDetails - { - /// - /// Gets or sets the title of the template. - /// - public string Title { get; set; } - - /// - /// Gets or sets the author of the template. - /// - public string Author { get; set; } - - /// - /// Gets or sets the version of the template. - /// - public string Version { get; set; } - - /// - /// Gets or sets the description of the template. - /// - public string Description { get; set; } - - /// - /// Gets or sets the template's comma-delimited string of tags. - /// - public string Tags { get; set; } - - /// - /// Gets or sets the template's folder path. - /// - public string TemplatePath { get; set; } - } -} diff --git a/src/PowerShellEditorServices/Templates/TemplateService.cs b/src/PowerShellEditorServices/Templates/TemplateService.cs deleted file mode 100644 index 18f58085c..000000000 --- a/src/PowerShellEditorServices/Templates/TemplateService.cs +++ /dev/null @@ -1,217 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Linq; -using System.Management.Automation; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Templates -{ - /// - /// Provides a service for listing PowerShell project templates and creating - /// new projects from those templates. This service leverages the Plaster - /// module for creating projects from templates. - /// - public class TemplateService - { - #region Private Fields - - private ILogger logger; - private bool isPlasterLoaded; - private bool? isPlasterInstalled; - private PowerShellContext powerShellContext; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the TemplateService class. - /// - /// The PowerShellContext to use for this service. - /// An ILogger implementation used for writing log messages. - public TemplateService(PowerShellContext powerShellContext, ILogger logger) - { - Validate.IsNotNull(nameof(powerShellContext), powerShellContext); - - this.logger = logger; - this.powerShellContext = powerShellContext; - } - - #endregion - - #region Public Methods - - /// - /// Checks if Plaster is installed on the user's machine. - /// - /// A Task that can be awaited until the check is complete. The result will be true if Plaster is installed. - public async Task ImportPlasterIfInstalledAsync() - { - if (!this.isPlasterInstalled.HasValue) - { - PSCommand psCommand = new PSCommand(); - - psCommand - .AddCommand("Get-Module") - .AddParameter("ListAvailable") - .AddParameter("Name", "Plaster"); - - psCommand - .AddCommand("Sort-Object") - .AddParameter("Descending") - .AddParameter("Property", "Version"); - - psCommand - .AddCommand("Select-Object") - .AddParameter("First", 1); - - this.logger.Write(LogLevel.Verbose, "Checking if Plaster is installed..."); - - var getResult = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, false, false); - - PSObject moduleObject = getResult.First(); - this.isPlasterInstalled = moduleObject != null; - string installedQualifier = - this.isPlasterInstalled.Value - ? string.Empty : "not "; - - this.logger.Write( - LogLevel.Verbose, - $"Plaster is {installedQualifier}installed!"); - - // Attempt to load plaster - if (this.isPlasterInstalled.Value && this.isPlasterLoaded == false) - { - this.logger.Write(LogLevel.Verbose, "Loading Plaster..."); - - psCommand = new PSCommand(); - psCommand - .AddCommand("Import-Module") - .AddParameter("ModuleInfo", (PSModuleInfo)moduleObject.ImmediateBaseObject) - .AddParameter("PassThru"); - - var importResult = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, false, false); - - this.isPlasterLoaded = importResult.Any(); - string loadedQualifier = - this.isPlasterInstalled.Value - ? "was" : "could not be"; - - this.logger.Write( - LogLevel.Verbose, - $"Plaster {loadedQualifier} loaded successfully!"); - } - } - - return this.isPlasterInstalled.Value; - } - - /// - /// Gets the available file or project templates on the user's - /// machine. - /// - /// - /// If true, searches the user's installed PowerShell modules for - /// included templates. - /// - /// A Task which can be awaited for the TemplateDetails list to be returned. - public async Task GetAvailableTemplatesAsync( - bool includeInstalledModules) - { - if (!this.isPlasterLoaded) - { - throw new InvalidOperationException("Plaster is not loaded, templates cannot be accessed."); - }; - - PSCommand psCommand = new PSCommand(); - psCommand.AddCommand("Get-PlasterTemplate"); - - if (includeInstalledModules) - { - psCommand.AddParameter("IncludeModules"); - } - - var templateObjects = - await this.powerShellContext.ExecuteCommandAsync( - psCommand, false, false); - - this.logger.Write( - LogLevel.Verbose, - $"Found {templateObjects.Count()} Plaster templates"); - - return - templateObjects - .Select(CreateTemplateDetails) - .ToArray(); - } - - /// - /// Creates a new file or project from a specified template and - /// places it in the destination path. This ultimately calls - /// Invoke-Plaster in PowerShell. - /// - /// The folder path containing the template. - /// The folder path where the files will be created. - /// A boolean-returning Task which communicates success or failure. - public async Task CreateFromTemplateAsync( - string templatePath, - string destinationPath) - { - this.logger.Write( - LogLevel.Verbose, - $"Invoking Plaster...\n\n TemplatePath: {templatePath}\n DestinationPath: {destinationPath}"); - - PSCommand command = new PSCommand(); - command.AddCommand("Invoke-Plaster"); - command.AddParameter("TemplatePath", templatePath); - command.AddParameter("DestinationPath", destinationPath); - - var errorString = new System.Text.StringBuilder(); - await this.powerShellContext.ExecuteCommandAsync( - command, - errorString, - new ExecutionOptions - { - WriteOutputToHost = false, - WriteErrorsToHost = true, - InterruptCommandPrompt = true - }); - - // If any errors were written out, creation was not successful - return errorString.Length == 0; - } - - #endregion - - #region Private Methods - - private static TemplateDetails CreateTemplateDetails(PSObject psObject) - { - object[] tags = psObject.Members["Tags"].Value as object[]; - - return new TemplateDetails - { - Title = psObject.Members["Title"].Value as string, - Author = psObject.Members["Author"].Value as string, - Version = psObject.Members["Version"].Value.ToString(), - Description = psObject.Members["Description"].Value as string, - TemplatePath = psObject.Members["TemplatePath"].Value as string, - Tags = - tags != null - ? string.Join(", ", tags) - : string.Empty - }; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Utility/AsyncContext.cs b/src/PowerShellEditorServices/Utility/AsyncContext.cs deleted file mode 100644 index ece383173..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncContext.cs +++ /dev/null @@ -1,53 +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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Simplifies the setup of a SynchronizationContext for the use - /// of async calls in the current thread. - /// - public static class AsyncContext - { - /// - /// Starts a new ThreadSynchronizationContext, attaches it to - /// the thread, and then runs the given async main function. - /// - /// - /// The Task-returning Func which represents the "main" function - /// for the thread. - /// - /// An ILogger implementation used for writing log messages. - public static void Start(Func asyncMainFunc, ILogger logger) - { - // Is there already a synchronization context? - if (SynchronizationContext.Current != null) - { - throw new InvalidOperationException( - "A SynchronizationContext is already assigned on this thread."); - } - - // Create and register a synchronization context for this thread - var threadSyncContext = new ThreadSynchronizationContext(logger); - SynchronizationContext.SetSynchronizationContext(threadSyncContext); - - // Get the main task and act on its completion - Task asyncMainTask = asyncMainFunc(); - asyncMainTask.ContinueWith( - t => threadSyncContext.EndLoop(), - TaskScheduler.Default); - - // Start the synchronization context's request loop and - // wait for the main task to complete - threadSyncContext.RunLoopOnCurrentThread(); - asyncMainTask.GetAwaiter().GetResult(); - } - } -} - diff --git a/src/PowerShellEditorServices/Utility/AsyncContextThread.cs b/src/PowerShellEditorServices/Utility/AsyncContextThread.cs deleted file mode 100644 index c5ca20039..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncContextThread.cs +++ /dev/null @@ -1,86 +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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Provides a simplified interface for creating a new thread - /// and establishing an AsyncContext in it. - /// - public class AsyncContextThread - { - #region Private Fields - - private Task threadTask; - private string threadName; - private CancellationTokenSource threadCancellationToken = - new CancellationTokenSource(); - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the AsyncContextThread class. - /// - /// - /// The name of the thread for debugging purposes. - /// - public AsyncContextThread(string threadName) - { - this.threadName = threadName; - } - - #endregion - - #region Public Methods - - /// - /// Runs a task on the AsyncContextThread. - /// - /// - /// A Func which returns the task to be run on the thread. - /// - /// An ILogger implementation used for writing log messages. - /// - /// A Task which can be used to monitor the thread for completion. - /// - public Task Run(Func taskReturningFunc, ILogger logger) - { - // Start up a long-running task with the action as the - // main entry point for the thread - this.threadTask = - Task.Factory.StartNew( - () => - { - // Set the thread's name to help with debugging - Thread.CurrentThread.Name = "AsyncContextThread: " + this.threadName; - - // Set up an AsyncContext to run the task - AsyncContext.Start(taskReturningFunc, logger); - }, - this.threadCancellationToken.Token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - return this.threadTask; - } - - /// - /// Stops the thread task. - /// - public void Stop() - { - this.threadCancellationToken.Cancel(); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs b/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs deleted file mode 100644 index 9644eff77..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncDebouncer.cs +++ /dev/null @@ -1,169 +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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Restricts the invocation of an operation to a specified time - /// interval. Can also cause previous requests to be cancelled - /// by new requests within that time window. Typically used for - /// buffering information for an operation or ensuring that an - /// operation only runs after some interval. - /// - /// The argument type for the Invoke method. - public abstract class AsyncDebouncer - { - #region Private Fields - - private int flushInterval; - private bool restartOnInvoke; - - private Task currentTimerTask; - private CancellationTokenSource timerCancellationSource; - - private AsyncLock asyncLock = new AsyncLock(); - - #endregion - - #region Public Methods - - /// - /// Creates a new instance of the AsyncDebouncer class with the - /// specified flush interval. If restartOnInvoke is true, any - /// calls to Invoke will cancel previous calls which have not yet - /// passed the flush interval. - /// - /// - /// A millisecond interval to use for flushing prior Invoke calls. - /// - /// - /// If true, Invoke calls will reset prior calls which haven't passed the flush interval. - /// - public AsyncDebouncer(int flushInterval, bool restartOnInvoke) - { - this.flushInterval = flushInterval; - this.restartOnInvoke = restartOnInvoke; - } - - /// - /// Invokes the debouncer with the given input. The debouncer will - /// wait for the specified interval before calling the Flush method - /// to complete the operation. - /// - /// - /// The argument for this implementation's Invoke method. - /// - /// A Task to be awaited until the Invoke is queued. - public async Task InvokeAsync(TInvokeArgs invokeArgument) - { - using (await this.asyncLock.LockAsync()) - { - // Invoke the implementor - await this.OnInvokeAsync(invokeArgument); - - // If there's no timer, start one - if (this.currentTimerTask == null) - { - this.StartTimer(); - } - else if (this.currentTimerTask != null && this.restartOnInvoke) - { - // Restart the existing timer - if (this.CancelTimer()) - { - this.StartTimer(); - } - } - } - } - - /// - /// Flushes the latest state regardless of the current interval. - /// An AsyncDebouncer MUST NOT invoke its own Flush method otherwise - /// deadlocks could occur. - /// - /// A Task to be awaited until Flush completes. - public async Task FlushAsync() - { - using (await this.asyncLock.LockAsync()) - { - // Cancel the current timer - this.CancelTimer(); - - // Flush the current output - await this.OnFlushAsync(); - } - } - - #endregion - - #region Abstract Methods - - /// - /// Implemented by the subclass to take the argument for the - /// future operation that will be performed by OnFlush. - /// - /// - /// The argument for this implementation's OnInvoke method. - /// - /// A Task to be awaited for the invoke to complete. - protected abstract Task OnInvokeAsync(TInvokeArgs invokeArgument); - - /// - /// Implemented by the subclass to complete the current operation. - /// - /// A Task to be awaited for the operation to complete. - protected abstract Task OnFlushAsync(); - - #endregion - - #region Private Methods - - private void StartTimer() - { - this.timerCancellationSource = new CancellationTokenSource(); - - this.currentTimerTask = - Task.Delay(this.flushInterval, this.timerCancellationSource.Token) - .ContinueWith( - t => - { - if (!t.IsCanceled) - { - return this.FlushAsync(); - } - else - { - return Task.FromResult(true); - } - }); - } - - private bool CancelTimer() - { - if (this.timerCancellationSource != null) - { - // Attempt to cancel the timer task - this.timerCancellationSource.Cancel(); - } - - // Was the task cancelled? - bool wasCancelled = - this.currentTimerTask == null || - this.currentTimerTask.IsCanceled; - - // Clear the current task so that another may be created - this.currentTimerTask = null; - - return wasCancelled; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Utility/AsyncLock.cs b/src/PowerShellEditorServices/Utility/AsyncLock.cs deleted file mode 100644 index 5eba1b24f..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncLock.cs +++ /dev/null @@ -1,128 +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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Provides a simple wrapper over a SemaphoreSlim to allow - /// synchronization locking inside of async calls. Cannot be - /// used recursively. - /// - public class AsyncLock - { - #region Fields - - private Task lockReleaseTask; - private SemaphoreSlim lockSemaphore = new SemaphoreSlim(1, 1); - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the AsyncLock class. - /// - public AsyncLock() - { - this.lockReleaseTask = - Task.FromResult( - (IDisposable)new LockReleaser(this)); - } - - #endregion - - #region Public Methods - - /// - /// Locks - /// - /// A task which has an IDisposable - public Task LockAsync() - { - return this.LockAsync(CancellationToken.None); - } - - /// - /// Obtains or waits for a lock which can be used to synchronize - /// access to a resource. The wait may be cancelled with the - /// given CancellationToken. - /// - /// - /// A CancellationToken which can be used to cancel the lock. - /// - /// - public Task LockAsync(CancellationToken cancellationToken) - { - Task waitTask = lockSemaphore.WaitAsync(cancellationToken); - - return waitTask.IsCompleted ? - this.lockReleaseTask : - waitTask.ContinueWith( - (t, releaser) => - { - return (IDisposable)releaser; - }, - this.lockReleaseTask.Result, - cancellationToken, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - - /// - /// Obtains or waits for a lock which can be used to synchronize - /// access to a resource. - /// - /// - public IDisposable Lock() - { - return Lock(CancellationToken.None); - } - - /// - /// Obtains or waits for a lock which can be used to synchronize - /// access to a resource. The wait may be cancelled with the - /// given CancellationToken. - /// - /// - /// A CancellationToken which can be used to cancel the lock. - /// - /// - public IDisposable Lock(CancellationToken cancellationToken) - { - lockSemaphore.Wait(cancellationToken); - return this.lockReleaseTask.Result; - } - - #endregion - - #region Private Classes - - /// - /// Provides an IDisposable wrapper around an AsyncLock so - /// that it can easily be used inside of a 'using' block. - /// - private class LockReleaser : IDisposable - { - private AsyncLock lockToRelease; - - internal LockReleaser(AsyncLock lockToRelease) - { - this.lockToRelease = lockToRelease; - } - - public void Dispose() - { - this.lockToRelease.lockSemaphore.Release(); - } - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Utility/AsyncQueue.cs b/src/PowerShellEditorServices/Utility/AsyncQueue.cs deleted file mode 100644 index 85bbc1592..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncQueue.cs +++ /dev/null @@ -1,224 +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.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Provides a synchronized queue which can be used from within async - /// operations. This is primarily used for producer/consumer scenarios. - /// - /// The type of item contained in the queue. - public class AsyncQueue - { - #region Private Fields - - private AsyncLock queueLock = new AsyncLock(); - private Queue itemQueue; - private Queue> requestQueue; - - #endregion - - #region Properties - - /// - /// Returns true if the queue is currently empty. - /// - public bool IsEmpty { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes an empty instance of the AsyncQueue class. - /// - public AsyncQueue() : this(Enumerable.Empty()) - { - } - - /// - /// Initializes an instance of the AsyncQueue class, pre-populated - /// with the given collection of items. - /// - /// - /// An IEnumerable containing the initial items with which the queue will - /// be populated. - /// - public AsyncQueue(IEnumerable initialItems) - { - this.itemQueue = new Queue(initialItems); - this.requestQueue = new Queue>(); - } - - #endregion - - #region Public Methods - - /// - /// Enqueues an item onto the end of the queue. - /// - /// The item to be added to the queue. - /// - /// A Task which can be awaited until the synchronized enqueue - /// operation completes. - /// - public async Task EnqueueAsync(T item) - { - using (await queueLock.LockAsync()) - { - TaskCompletionSource requestTaskSource = null; - - // Are any requests waiting? - while (this.requestQueue.Count > 0) - { - // Is the next request cancelled already? - requestTaskSource = this.requestQueue.Dequeue(); - if (!requestTaskSource.Task.IsCanceled) - { - // Dispatch the item - requestTaskSource.SetResult(item); - return; - } - } - - // No more requests waiting, queue the item for a later request - this.itemQueue.Enqueue(item); - this.IsEmpty = false; - } - } - - /// - /// Enqueues an item onto the end of the queue. - /// - /// The item to be added to the queue. - public void Enqueue(T item) - { - using (queueLock.Lock()) - { - while (this.requestQueue.Count > 0) - { - var requestTaskSource = this.requestQueue.Dequeue(); - if (requestTaskSource.Task.IsCanceled) - { - continue; - } - - requestTaskSource.SetResult(item); - return; - } - } - - this.itemQueue.Enqueue(item); - this.IsEmpty = false; - } - - /// - /// Dequeues an item from the queue or waits asynchronously - /// until an item is available. - /// - /// - /// A Task which can be awaited until a value can be dequeued. - /// - public Task DequeueAsync() - { - return this.DequeueAsync(CancellationToken.None); - } - - /// - /// Dequeues an item from the queue or waits asynchronously - /// until an item is available. The wait can be cancelled - /// using the given CancellationToken. - /// - /// - /// A CancellationToken with which a dequeue wait can be cancelled. - /// - /// - /// A Task which can be awaited until a value can be dequeued. - /// - public async Task DequeueAsync(CancellationToken cancellationToken) - { - Task requestTask; - - using (await queueLock.LockAsync(cancellationToken)) - { - if (this.itemQueue.Count > 0) - { - // Items are waiting to be taken so take one immediately - T item = this.itemQueue.Dequeue(); - this.IsEmpty = this.itemQueue.Count == 0; - - return item; - } - else - { - // Queue the request for the next item - var requestTaskSource = new TaskCompletionSource(); - this.requestQueue.Enqueue(requestTaskSource); - - // Register the wait task for cancel notifications - cancellationToken.Register( - () => requestTaskSource.TrySetCanceled()); - - requestTask = requestTaskSource.Task; - } - } - - // Wait for the request task to complete outside of the lock - return await requestTask; - } - - /// - /// Dequeues an item from the queue or waits asynchronously - /// until an item is available. - /// - /// - public T Dequeue() - { - return Dequeue(CancellationToken.None); - } - - /// - /// Dequeues an item from the queue or waits asynchronously - /// until an item is available. The wait can be cancelled - /// using the given CancellationToken. - /// - /// - /// A CancellationToken with which a dequeue wait can be cancelled. - /// - /// - public T Dequeue(CancellationToken cancellationToken) - { - TaskCompletionSource requestTask; - using (queueLock.Lock(cancellationToken)) - { - if (this.itemQueue.Count > 0) - { - T item = this.itemQueue.Dequeue(); - this.IsEmpty = this.itemQueue.Count == 0; - - return item; - } - - requestTask = new TaskCompletionSource(); - this.requestQueue.Enqueue(requestTask); - - if (cancellationToken.CanBeCanceled) - { - cancellationToken.Register(() => requestTask.TrySetCanceled()); - } - } - - return requestTask.Task.GetAwaiter().GetResult(); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Utility/AsyncUtils.cs b/src/PowerShellEditorServices/Utility/AsyncUtils.cs deleted file mode 100644 index 8da21b942..000000000 --- a/src/PowerShellEditorServices/Utility/AsyncUtils.cs +++ /dev/null @@ -1,25 +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.Threading; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Provides utility methods for common asynchronous operations. - /// - internal static class AsyncUtils - { - /// - /// Creates a with an handle initial and - /// max count of one. - /// - /// A simple single handle . - internal static SemaphoreSlim CreateSimpleLockingSemaphore() - { - return new SemaphoreSlim(initialCount: 1, maxCount: 1); - } - } -} diff --git a/src/PowerShellEditorServices/Utility/ExecutionTimer.cs b/src/PowerShellEditorServices/Utility/ExecutionTimer.cs deleted file mode 100644 index 2634eb6eb..000000000 --- a/src/PowerShellEditorServices/Utility/ExecutionTimer.cs +++ /dev/null @@ -1,105 +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.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Simple timer to be used with `using` to time executions. - /// - /// - /// An example showing how ExecutionTimer is intended to be used - /// - /// using (ExecutionTimer.Start(logger, "Execution of MyMethod completed.")) - /// { - /// MyMethod(various, arguments); - /// } - /// - /// This will print a message like "Execution of MyMethod completed. [50ms]" to the logs. - /// - public struct ExecutionTimer : IDisposable - { - private static readonly ObjectPool s_stopwatchPool = new ObjectPool(); - - private Stopwatch _stopwatch; - - private readonly ILogger _logger; - - private readonly string _message; - - private readonly string _callerMemberName; - - private readonly string _callerFilePath; - - private readonly int _callerLineNumber; - - /// - /// Create a new execution timer and start it. - /// - /// The logger to log the execution timer message in. - /// The message to prefix the execution time with. - /// The name of the calling method or property. - /// The path to the source file of the caller. - /// The line where the timer is called. - /// A new, started execution timer. - public static ExecutionTimer Start( - ILogger logger, - string message, - [CallerMemberName] string callerMemberName = null, - [CallerFilePath] string callerFilePath = null, - [CallerLineNumber] int callerLineNumber = -1) - { - var timer = new ExecutionTimer(logger, message, callerMemberName, callerFilePath, callerLineNumber); - timer._stopwatch.Start(); - return timer; - } - - internal ExecutionTimer( - ILogger logger, - string message, - string callerMemberName, - string callerFilePath, - int callerLineNumber) - { - _logger = logger; - _message = message; - _callerMemberName = callerMemberName; - _callerFilePath = callerFilePath; - _callerLineNumber = callerLineNumber; - _stopwatch = s_stopwatchPool.Rent(); - } - - /// - /// Dispose of the execution timer by stopping the stopwatch and then printing - /// the elapsed time in the logs. - /// - public void Dispose() - { - _stopwatch.Stop(); - - string logMessage = new StringBuilder() - .Append(_message) - .Append(" [") - .Append(_stopwatch.ElapsedMilliseconds) - .Append("ms]") - .ToString(); - - _stopwatch.Reset(); - - s_stopwatchPool.Return(_stopwatch); - - _logger.Write( - LogLevel.Verbose, - logMessage, - callerName: _callerMemberName, - callerSourceFile: _callerFilePath, - callerLineNumber: _callerLineNumber); - } - } -} diff --git a/src/PowerShellEditorServices/Utility/Extensions.cs b/src/PowerShellEditorServices/Utility/Extensions.cs deleted file mode 100644 index 08ffea60c..000000000 --- a/src/PowerShellEditorServices/Utility/Extensions.cs +++ /dev/null @@ -1,154 +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.Linq; -using System.Collections.Generic; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - internal static class ObjectExtensions - { - /// - /// Extension to evaluate an object's ToString() method in an exception safe way. This will - /// extension method will not throw. - /// - /// The object on which to call ToString() - /// The ToString() return value or a suitable error message is that throws. - public static string SafeToString(this object obj) - { - string str; - - try - { - str = obj.ToString(); - } - catch (Exception ex) - { - str = $""; - } - - return str; - } - - /// - /// Get the maximum of the elements from the given enumerable. - /// - /// Type of object for which the enumerable is defined. - /// An enumerable object of type T - /// A comparer for ordering elements of type T. The comparer should handle null values. - /// An object of type T. If the enumerable is empty or has all null elements, then the method returns null. - public static T MaxElement(this IEnumerable elements, Func comparer) where T:class - { - if (elements == null) - { - throw new ArgumentNullException(nameof(elements)); - } - - if (comparer == null) - { - throw new ArgumentNullException(nameof(comparer)); - } - - if (!elements.Any()) - { - return null; - } - - var maxElement = elements.First(); - foreach(var element in elements.Skip(1)) - { - if (element != null && comparer(element, maxElement) > 0) - { - maxElement = element; - } - } - - return maxElement; - } - - /// - /// Get the minimum of the elements from the given enumerable. - /// - /// Type of object for which the enumerable is defined. - /// An enumerable object of type T - /// A comparer for ordering elements of type T. The comparer should handle null values. - /// An object of type T. If the enumerable is empty or has all null elements, then the method returns null. - public static T MinElement(this IEnumerable elements, Func comparer) where T : class - { - return MaxElement(elements, (elementX, elementY) => -1 * comparer(elementX, elementY)); - } - - /// - /// Compare extents with respect to their widths. - /// - /// Width of an extent is defined as the difference between its EndOffset and StartOffest properties. - /// - /// Extent of type IScriptExtent. - /// Extent of type IScriptExtent. - /// 0 if extentX and extentY are equal in width. 1 if width of extent X is greater than that of extent Y. Otherwise, -1. - public static int ExtentWidthComparer(this IScriptExtent extentX, IScriptExtent extentY) - { - - if (extentX == null && extentY == null) - { - return 0; - } - - if (extentX != null && extentY == null) - { - return 1; - } - - if (extentX == null) - { - return -1; - } - - var extentWidthX = extentX.EndOffset - extentX.StartOffset; - var extentWidthY = extentY.EndOffset - extentY.StartOffset; - if (extentWidthX > extentWidthY) - { - return 1; - } - else if (extentWidthX < extentWidthY) - { - return -1; - } - else - { - return 0; - } - } - - /// - /// Check if the given coordinates are wholly contained in the instance's extent. - /// - /// Extent of type IScriptExtent. - /// 1-based line number. - /// 1-based column number - /// True if the coordinates are wholly contained in the instance's extent, otherwise, false. - public static bool Contains(this IScriptExtent scriptExtent, int line, int column) - { - if (scriptExtent.StartLineNumber > line || scriptExtent.EndLineNumber < line) - { - return false; - } - - if (scriptExtent.StartLineNumber == line) - { - return scriptExtent.StartColumnNumber <= column; - } - - if (scriptExtent.EndLineNumber == line) - { - return scriptExtent.EndColumnNumber >= column; - } - - return true; - } - } -} diff --git a/src/PowerShellEditorServices/Utility/Logging.cs b/src/PowerShellEditorServices/Utility/Logging.cs deleted file mode 100644 index 4813789d3..000000000 --- a/src/PowerShellEditorServices/Utility/Logging.cs +++ /dev/null @@ -1,288 +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.Collections.Generic; -using Serilog; -using Serilog.Events; -using Serilog.Sinks.File; -using Serilog.Sinks.Async; -using System.Runtime.CompilerServices; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Defines the level indicators for log messages. - /// - public enum LogLevel - { - /// - /// Indicates a diagnostic log message. - /// - Diagnostic, - - /// - /// Indicates a verbose log message. - /// - Verbose, - - /// - /// Indicates a normal, non-verbose log message. - /// - Normal, - - /// - /// Indicates a warning message. - /// - Warning, - - /// - /// Indicates an error message. - /// - Error - } - - /// - /// Provides logging for EditorServices - /// - public interface ILogger : IDisposable - { - /// - /// The minimum log level that this logger instance is configured to log at. - /// - LogLevel MinimumConfiguredLogLevel { get; } - - /// - /// Write a message with the given severity to the logs. - /// - /// The severity level of the log message. - /// The log message itself. - /// The name of the calling method. - /// The name of the source file of the caller. - /// The line number where the log is being called. - void Write( - LogLevel logLevel, - string logMessage, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0); - - /// - /// Log an exception in the logs. - /// - /// The error message of the exception to be logged. - /// The exception itself that has been thrown. - /// The name of the method in which the ILogger is being called. - /// The name of the source file in which the ILogger is being called. - /// The line number in the file where the ILogger is being called. - void WriteException( - string errorMessage, - Exception exception, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0); - - /// - /// Log an exception that has been handled cleanly or is otherwise not expected to cause problems in the logs. - /// - /// The error message of the exception to be logged. - /// The exception itself that has been thrown. - /// The name of the method in which the ILogger is being called. - /// The name of the source file in which the ILogger is being called. - /// The line number in the file where the ILogger is being called. - void WriteHandledException( - string errorMessage, - Exception exception, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0); - } - - - /// - /// Manages logging and logger constructor for EditorServices. - /// - public static class Logging - { - /// - /// Builder class for configuring and creating logger instances. - /// - public class Builder - { - /// - /// The level at which to log. - /// - private LogLevel _logLevel; - - /// - /// Paths at which to create log files. - /// - private Dictionary _filePaths; - - /// - /// Whether or not to send logging to the console. - /// - private bool _useConsole; - - /// - /// The log level to use when logging to the console. - /// - private LogLevel? _consoleLogLevel; - - /// - /// Constructs An ILogger implementation builder instance with default configurations: - /// No log files, not logging to console, log level normal. - /// - public Builder() - { - _logLevel = Utility.LogLevel.Normal; - _filePaths = new Dictionary(); - _useConsole = false; - } - - /// - /// The severity level of the messages to log. Not setting this makes the log level default to "Normal". - /// - /// The severity level of the messages to log. - /// the ILogger builder for reuse. - public Builder LogLevel(LogLevel logLevel) - { - _logLevel = logLevel; - return this; - } - - /// - /// Add a path to output a log file to. - /// - /// The path ofethe file to log to. - /// - /// The minimum log level for this file, null defaults to the configured global level. - /// Note that setting a more verbose level than the global configuration won't work -- - /// messages are filtered by the global configuration before they hit file-specific filters. - /// - /// the ILogger builder for reuse. - public Builder AddLogFile(string filePath, LogLevel? logLevel = null) - { - _filePaths.Add(filePath, logLevel); - return this; - } - - /// - /// Configure the ILogger to send log messages to the console. - /// - /// The minimum log level for console logging. - /// the ILogger builder for reuse. - public Builder AddConsoleLogging(LogLevel? logLevel = null) - { - _useConsole = true; - _consoleLogLevel = logLevel; - return this; - } - - /// - /// Take the log configuration and use it to create An ILogger implementation. - /// - /// The constructed logger. - public ILogger Build() - { - var configuration = new LoggerConfiguration() - .MinimumLevel.Is(ConvertLogLevel(_logLevel)); - - if (_useConsole) - { - configuration = configuration.WriteTo.Console( - restrictedToMinimumLevel: ConvertLogLevel(_consoleLogLevel ?? _logLevel), - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Message}{NewLine}"); - } - - foreach (KeyValuePair logFile in _filePaths) - { - configuration = configuration.WriteTo.Async(a => a.File(logFile.Key, - restrictedToMinimumLevel: ConvertLogLevel(logFile.Value ?? _logLevel), - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Message}{NewLine}") - ); - } - - return new PsesLogger(configuration.CreateLogger(), _logLevel); - } - } - - /// - /// A do-nothing logger that simply discards messages. - /// - public static ILogger NullLogger - { - get - { - return s_nullLogger ?? (s_nullLogger = CreateLogger().Build()); - } - } - - private static ILogger s_nullLogger; - - /// - /// Contruct An ILogger implementation with the applied configuration. - /// - /// The constructed logger. - public static Builder CreateLogger() - { - return new Builder(); - } - - /// - /// Convert an EditorServices log level to a Serilog log level. - /// - /// The EditorServices log level. - /// The Serilog LogEventLevel corresponding to the EditorServices log level. - private static LogEventLevel ConvertLogLevel(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Diagnostic: - return LogEventLevel.Verbose; - - case LogLevel.Verbose: - return LogEventLevel.Debug; - - case LogLevel.Normal: - return LogEventLevel.Information; - - case LogLevel.Warning: - return LogEventLevel.Warning; - - case LogLevel.Error: - return LogEventLevel.Error; - } - - throw new ArgumentException($"Unknown LogLevel: '{logLevel}')", nameof(logLevel)); - } - } - - /// - /// Extension method class for the ILogger class. - /// - public static class ILoggerExtensions - { - /// - /// Log the amount of time an execution takes. The intended usage is to call this method in the - /// header of a `using` block to time the interior of the block. - /// - /// The ILogger to log the execution time with. - /// The message to log about what has been executed. - /// The name of the member calling this method. - /// The file where this method has been called. - /// The line number where this method has been called. - /// - public static ExecutionTimer LogExecutionTime( - this ILogger logger, - string message, - [CallerMemberName] string callerMemberName = null, - [CallerFilePath] string callerFilePath = null, - [CallerLineNumber] int callerLineNumber = -1) - { - return ExecutionTimer.Start(logger, message, callerMemberName, callerFilePath, callerLineNumber); - } - } -} diff --git a/src/PowerShellEditorServices/Utility/ObjectPool.cs b/src/PowerShellEditorServices/Utility/ObjectPool.cs deleted file mode 100644 index 1a372f02e..000000000 --- a/src/PowerShellEditorServices/Utility/ObjectPool.cs +++ /dev/null @@ -1,29 +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.Collections.Concurrent; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// A basic implementation of the object pool pattern. - /// - internal class ObjectPool - where T : new() - { - private ConcurrentBag _pool = new ConcurrentBag(); - - /// - /// Get an instance of an object, either new or from the pool depending on availability. - /// - public T Rent() => _pool.TryTake(out var obj) ? obj : new T(); - - /// - /// Return an object to the pool. - /// - /// The object to return to the pool. - public void Return(T obj) => _pool.Add(obj); - } -} diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs deleted file mode 100644 index 6b62122ec..000000000 --- a/src/PowerShellEditorServices/Utility/PathUtils.cs +++ /dev/null @@ -1,49 +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.IO; -using System.Runtime.InteropServices; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Utility to help handling paths across different platforms. - /// - /// - /// Some constants were copied from the internal System.Management.Automation.StringLiterals class. - /// - internal static class PathUtils - { - /// - /// The default path separator used by the base implementation of the providers. - /// - /// Porting note: IO.Path.DirectorySeparatorChar is correct for all platforms. On Windows, - /// it is '\', and on Linux, it is '/', as expected. - /// - internal static readonly char DefaultPathSeparator = Path.DirectorySeparatorChar; - internal static readonly string DefaultPathSeparatorString = DefaultPathSeparator.ToString(); - - /// - /// The alternate path separator used by the base implementation of the providers. - /// - /// Porting note: we do not use .NET's AlternatePathSeparatorChar here because it correctly - /// states that both the default and alternate are '/' on Linux. However, for PowerShell to - /// be "slash agnostic", we need to use the assumption that a '\' is the alternate path - /// separator on Linux. - /// - internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\'; - internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString(); - - /// - /// Converts all alternate path separators to the current platform's main path separators. - /// - /// The path to normalize. - /// The normalized path. - public static string NormalizePathSeparators(string path) - { - return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); - } - } -} diff --git a/src/PowerShellEditorServices/Utility/PsesLogger.cs b/src/PowerShellEditorServices/Utility/PsesLogger.cs deleted file mode 100644 index de257f556..000000000 --- a/src/PowerShellEditorServices/Utility/PsesLogger.cs +++ /dev/null @@ -1,221 +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.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using Serilog.Core; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// An ILogger implementation object for EditorServices, acts as an adapter to Serilog. - /// - public class PsesLogger : ILogger - { - /// - /// The standard log template for all log entries. - /// - private static readonly string s_logMessageTemplate = - "[{LogLevelName:l}] tid:{ThreadId} in '{CallerName:l}' {CallerSourceFile:l}: line {CallerLineNumber}{IndentedLogMsg:l}"; - - /// - /// The name of the ERROR log level. - /// - private static readonly string ErrorLevelName = LogLevel.Error.ToString().ToUpper(); - - /// - /// The name of the WARNING log level. - /// - private static readonly string WarningLevelName = LogLevel.Warning.ToString().ToUpper(); - - /// - /// The internal Serilog logger to log to. - /// - private readonly Logger _logger; - - /// - /// Construct a new logger around a Serilog ILogger. - /// - /// The Serilog logger to use internally. - /// The minimum severity level the logger is configured to log messages at. - internal PsesLogger(Logger logger, LogLevel minimumLogLevel) - { - _logger = logger; - MinimumConfiguredLogLevel = minimumLogLevel; - } - - /// - /// The minimum log level that this logger is configured to log at. - /// - public LogLevel MinimumConfiguredLogLevel { get; } - - /// - /// Write a message with the given severity to the logs. - /// - /// The severity level of the log message. - /// The log message itself. - /// The name of the calling method. - /// The name of the source file of the caller. - /// The line number where the log is being called. - public void Write( - LogLevel logLevel, - string logMessage, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0) - { - Write(logLevel, new StringBuilder(logMessage), callerName, callerSourceFile, callerLineNumber); - } - - /// - /// Write a message with the given severity to the logs. Takes a StringBuilder to allow for minimal allocation. - /// - /// The severity level of the log message. - /// The log message itself in StringBuilder form for manipulation. - /// The name of the calling method. - /// The name of the source file of the caller. - /// The line number where the log is being called. - private void Write( - LogLevel logLevel, - StringBuilder logMessage, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0) - { - string indentedLogMsg = IndentMsg(logMessage); - string logLevelName = logLevel.ToString().ToUpper(); - - int threadId = Thread.CurrentThread.ManagedThreadId; - - switch (logLevel) - { - case LogLevel.Diagnostic: - _logger.Verbose(s_logMessageTemplate, logLevelName, threadId, callerName, callerSourceFile, callerLineNumber, indentedLogMsg); - return; - case LogLevel.Verbose: - _logger.Debug(s_logMessageTemplate, logLevelName, threadId, callerName, callerSourceFile, callerLineNumber, indentedLogMsg); - return; - case LogLevel.Normal: - _logger.Information(s_logMessageTemplate, logLevelName, threadId, callerName, callerSourceFile, callerLineNumber, indentedLogMsg); - return; - case LogLevel.Warning: - _logger.Warning(s_logMessageTemplate, logLevelName, threadId, callerName, callerSourceFile, callerLineNumber, indentedLogMsg); - return; - case LogLevel.Error: - _logger.Error(s_logMessageTemplate, logLevelName, threadId, callerName, callerSourceFile, callerLineNumber, indentedLogMsg); - return; - } - } - - /// - /// Log an exception in the logs. - /// - /// The error message of the exception to be logged. - /// The exception itself that has been thrown. - /// The name of the method in which the ILogger is being called. - /// The name of the source file in which the ILogger is being called. - /// The line number in the file where the ILogger is being called. - public void WriteException( - string errorMessage, - Exception exception, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0) - { - StringBuilder body = FormatExceptionMessage("Exception", errorMessage, exception); - Write(LogLevel.Error, body, callerName, callerSourceFile, callerLineNumber); - } - - /// - /// Log an exception that has been handled cleanly or is otherwise not expected to cause problems in the logs. - /// - /// The error message of the exception to be logged. - /// The exception itself that has been thrown. - /// The name of the method in which the ILogger is being called. - /// The name of the source file in which the ILogger is being called. - /// The line number in the file where the ILogger is being called. - public void WriteHandledException( - string errorMessage, - Exception exception, - [CallerMemberName] string callerName = null, - [CallerFilePath] string callerSourceFile = null, - [CallerLineNumber] int callerLineNumber = 0) - { - StringBuilder body = FormatExceptionMessage("Handled exception", errorMessage, exception); - Write(LogLevel.Warning, body, callerName, callerSourceFile, callerLineNumber); - } - - /// - /// Utility function to indent a log message by one level. - /// - /// Log message string builder to transform. - /// The indented log message string. - private static string IndentMsg(StringBuilder logMessageBuilder) - { - return logMessageBuilder - .Replace(Environment.NewLine, s_indentedPrefix) - .Insert(0, s_indentedPrefix) - .AppendLine() - .ToString(); - } - - /// - /// Creates a prettified log message from an exception. - /// - /// The user-readable tag for this exception entry. - /// The user-readable short description of the error. - /// The exception object itself. Must not be null. - /// An indented, formatted string of the body. - private static StringBuilder FormatExceptionMessage( - string messagePrelude, - string errorMessage, - Exception exception) - { - var sb = new StringBuilder() - .Append(messagePrelude).Append(": ").Append(errorMessage).Append(Environment.NewLine) - .Append(Environment.NewLine) - .Append(exception.ToString()); - - return sb; - } - - /// - /// A newline followed by a single indentation prefix. - /// - private static readonly string s_indentedPrefix = Environment.NewLine + " "; - - #region IDisposable Support - private bool _disposedValue = false; // To detect redundant calls - - /// - /// Internal disposer. - /// - /// Whether or not the object is being disposed. - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _logger.Dispose(); - } - - _disposedValue = true; - } - } - - /// - /// Dispose of this object, using the Dispose pattern. - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - } - #endregion - } -} diff --git a/src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs b/src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs deleted file mode 100644 index 14f91e9b5..000000000 --- a/src/PowerShellEditorServices/Utility/ThreadSynchronizationContext.cs +++ /dev/null @@ -1,100 +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.Collections.Concurrent; -using System.Threading; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Provides a SynchronizationContext implementation that can be used - /// in console applications or any thread which doesn't have its - /// own SynchronizationContext. - /// - public class ThreadSynchronizationContext : SynchronizationContext - { - #region Private Fields - - private ILogger logger; - private BlockingCollection> requestQueue = - new BlockingCollection>(); - - #endregion - - #region Constructors - - /// - /// - /// - /// An ILogger implementation used for writing log messages. - public ThreadSynchronizationContext(ILogger logger) - { - this.logger = logger; - } - - #endregion - - #region Public Methods - - /// - /// Posts a request for execution to the SynchronizationContext. - /// This will be executed on the SynchronizationContext's thread. - /// - /// - /// The callback to be invoked on the SynchronizationContext's thread. - /// - /// - /// A state object to pass along to the callback when executed through - /// the SynchronizationContext. - /// - public override void Post(SendOrPostCallback callback, object state) - { - if (!this.requestQueue.IsAddingCompleted) - { - // Add the request to the queue - this.requestQueue.Add( - new Tuple( - callback, state)); - } - else - { - this.logger.Write( - LogLevel.Verbose, - "Attempted to post message to synchronization context after it's already completed"); - } - } - - #endregion - - #region Public Methods - - /// - /// Starts the SynchronizationContext message loop on the current thread. - /// - public void RunLoopOnCurrentThread() - { - Tuple request; - - while (this.requestQueue.TryTake(out request, Timeout.Infinite)) - { - // Invoke the request's callback - request.Item1(request.Item2); - } - } - - /// - /// Ends the SynchronizationContext message loop. - /// - public void EndLoop() - { - // Tell the blocking queue that we're done - this.requestQueue.CompleteAdding(); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Utility/Utils.cs b/src/PowerShellEditorServices/Utility/Utils.cs deleted file mode 100644 index cbac80891..000000000 --- a/src/PowerShellEditorServices/Utility/Utils.cs +++ /dev/null @@ -1,55 +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.Reflection; -using System.Runtime.InteropServices; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// General purpose common utilities to prevent reimplementation. - /// - internal static class Utils - { - /// - /// True if we are running on .NET Core, false otherwise. - /// - public static bool IsNetCore { get; } = RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.Ordinal); - - /// - /// Get's the Version of PowerShell being used. - /// - public static Version PSVersion { get; } = PowerShellReflectionUtils.PSVersion; - - /// - /// True if we are running in Windows PowerShell, false otherwise. - /// - public static bool IsPS5 { get; } = PSVersion.Major == 5; - - /// - /// True if we are running in PowerShell Core 6, false otherwise. - /// - public static bool IsPS6 { get; } = PSVersion.Major == 6; - - /// - /// True if we are running in PowerShell 7, false otherwise. - /// - public static bool IsPS7 { get; } = PSVersion.Major == 7; - } - - internal static class PowerShellReflectionUtils - { - - private static readonly Assembly s_psRuntimeAssembly = typeof(System.Management.Automation.Runspaces.Runspace).Assembly; - private static readonly PropertyInfo s_psVersionProperty = s_psRuntimeAssembly.GetType("System.Management.Automation.PSVersionInfo") - .GetProperty("PSVersion", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - /// - /// Get's the Version of PowerShell being used. - /// - public static Version PSVersion { get; } = s_psVersionProperty.GetValue(null) as Version; - } -} diff --git a/src/PowerShellEditorServices/Utility/Validate.cs b/src/PowerShellEditorServices/Utility/Validate.cs deleted file mode 100644 index 19aefe2c7..000000000 --- a/src/PowerShellEditorServices/Utility/Validate.cs +++ /dev/null @@ -1,143 +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.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices.Utility -{ - /// - /// Provides common validation methods to simplify method - /// parameter checks. - /// - public static class Validate - { - /// - /// Throws ArgumentNullException if value is null. - /// - /// The name of the parameter being validated. - /// The value of the parameter being validated. - public static void IsNotNull(string parameterName, object valueToCheck) - { - if (valueToCheck == null) - { - throw new ArgumentNullException(parameterName); - } - } - - /// - /// Throws ArgumentOutOfRangeException if the value is outside - /// of the given lower and upper limits. - /// - /// The name of the parameter being validated. - /// The value of the parameter being validated. - /// The lower limit which the value should not be less than. - /// The upper limit which the value should not be greater than. - public static void IsWithinRange( - string parameterName, - int valueToCheck, - int lowerLimit, - int upperLimit) - { - // TODO: Debug assert here if lowerLimit >= upperLimit - - if (valueToCheck < lowerLimit || valueToCheck > upperLimit) - { - throw new ArgumentOutOfRangeException( - parameterName, - valueToCheck, - string.Format( - "Value is not between {0} and {1}", - lowerLimit, - upperLimit)); - } - } - - /// - /// Throws ArgumentOutOfRangeException if the value is greater than or equal - /// to the given upper limit. - /// - /// The name of the parameter being validated. - /// The value of the parameter being validated. - /// The upper limit which the value should be less than. - public static void IsLessThan( - string parameterName, - int valueToCheck, - int upperLimit) - { - if (valueToCheck >= upperLimit) - { - throw new ArgumentOutOfRangeException( - parameterName, - valueToCheck, - string.Format( - "Value is greater than or equal to {0}", - upperLimit)); - } - } - - /// - /// Throws ArgumentOutOfRangeException if the value is less than or equal - /// to the given lower limit. - /// - /// The name of the parameter being validated. - /// The value of the parameter being validated. - /// The lower limit which the value should be greater than. - public static void IsGreaterThan( - string parameterName, - int valueToCheck, - int lowerLimit) - { - if (valueToCheck < lowerLimit) - { - throw new ArgumentOutOfRangeException( - parameterName, - valueToCheck, - string.Format( - "Value is less than or equal to {0}", - lowerLimit)); - } - } - - /// - /// Throws ArgumentException if the value is equal to the undesired value. - /// - /// The type of value to be validated. - /// The name of the parameter being validated. - /// The value that valueToCheck should not equal. - /// The value of the parameter being validated. - public static void IsNotEqual( - string parameterName, - TValue valueToCheck, - TValue undesiredValue) - { - if (EqualityComparer.Default.Equals(valueToCheck, undesiredValue)) - { - throw new ArgumentException( - string.Format( - "The given value '{0}' should not equal '{1}'", - valueToCheck, - undesiredValue), - parameterName); - } - } - - /// - /// Throws ArgumentException if the value is null, an empty string, - /// or a string containing only whitespace. - /// - /// The name of the parameter being validated. - /// The value of the parameter being validated. - public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck) - { - if (string.IsNullOrWhiteSpace(valueToCheck)) - { - throw new ArgumentException( - "Parameter contains a null, empty, or whitespace string.", - parameterName); - } - } - } -} diff --git a/src/PowerShellEditorServices/Workspace/BufferPosition.cs b/src/PowerShellEditorServices/Workspace/BufferPosition.cs deleted file mode 100644 index effdd7660..000000000 --- a/src/PowerShellEditorServices/Workspace/BufferPosition.cs +++ /dev/null @@ -1,111 +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.Diagnostics; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about a position in a file buffer. All - /// positions are expressed in 1-based positions (i.e. the - /// first line and column in the file is position 1,1). - /// - [DebuggerDisplay("Position = {Line}:{Column}")] - public class BufferPosition - { - #region Properties - - /// - /// Provides an instance that represents a position that has not been set. - /// - public static readonly BufferPosition None = new BufferPosition(-1, -1); - - /// - /// Gets the line number of the position in the buffer. - /// - public int Line { get; private set; } - - /// - /// Gets the column number of the position in the buffer. - /// - public int Column { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the BufferPosition class. - /// - /// The line number of the position. - /// The column number of the position. - public BufferPosition(int line, int column) - { - this.Line = line; - this.Column = column; - } - - #endregion - - #region Public Methods - - /// - /// Compares two instances of the BufferPosition class. - /// - /// The object to which this instance will be compared. - /// True if the positions are equal, false otherwise. - public override bool Equals(object obj) - { - if (!(obj is BufferPosition)) - { - return false; - } - - BufferPosition other = (BufferPosition)obj; - - return - this.Line == other.Line && - this.Column == other.Column; - } - - /// - /// Calculates a unique hash code that represents this instance. - /// - /// A hash code representing this instance. - public override int GetHashCode() - { - return this.Line.GetHashCode() ^ this.Column.GetHashCode(); - } - - /// - /// Compares two positions to check if one is greater than the other. - /// - /// The first position to compare. - /// The second position to compare. - /// True if positionOne is greater than positionTwo. - public static bool operator >(BufferPosition positionOne, BufferPosition positionTwo) - { - return - (positionOne != null && positionTwo == null) || - (positionOne.Line > positionTwo.Line) || - (positionOne.Line == positionTwo.Line && - positionOne.Column > positionTwo.Column); - } - - /// - /// Compares two positions to check if one is less than the other. - /// - /// The first position to compare. - /// The second position to compare. - /// True if positionOne is less than positionTwo. - public static bool operator <(BufferPosition positionOne, BufferPosition positionTwo) - { - return positionTwo > positionOne; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Workspace/BufferRange.cs b/src/PowerShellEditorServices/Workspace/BufferRange.cs deleted file mode 100644 index 147eed042..000000000 --- a/src/PowerShellEditorServices/Workspace/BufferRange.cs +++ /dev/null @@ -1,123 +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.Diagnostics; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides details about a range between two positions in - /// a file buffer. - /// - [DebuggerDisplay("Start = {Start.Line}:{Start.Column}, End = {End.Line}:{End.Column}")] - public class BufferRange - { - #region Properties - - /// - /// Provides an instance that represents a range that has not been set. - /// - public static readonly BufferRange None = new BufferRange(0, 0, 0, 0); - - /// - /// Gets the start position of the range in the buffer. - /// - public BufferPosition Start { get; private set; } - - /// - /// Gets the end position of the range in the buffer. - /// - public BufferPosition End { get; private set; } - - /// - /// Returns true if the current range is non-zero, i.e. - /// contains valid start and end positions. - /// - public bool HasRange - { - get - { - return this.Equals(BufferRange.None); - } - } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the BufferRange class. - /// - /// The start position of the range. - /// The end position of the range. - public BufferRange(BufferPosition start, BufferPosition end) - { - if (start > end) - { - throw new ArgumentException( - string.Format( - "Start position ({0}, {1}) must come before or be equal to the end position ({2}, {3}).", - start.Line, start.Column, - end.Line, end.Column)); - } - - this.Start = start; - this.End = end; - } - - /// - /// Creates a new instance of the BufferRange class. - /// - /// The 1-based starting line number of the range. - /// The 1-based starting column number of the range. - /// The 1-based ending line number of the range. - /// The 1-based ending column number of the range. - public BufferRange( - int startLine, - int startColumn, - int endLine, - int endColumn) - { - this.Start = new BufferPosition(startLine, startColumn); - this.End = new BufferPosition(endLine, endColumn); - } - - #endregion - - #region Public Methods - - /// - /// Compares two instances of the BufferRange class. - /// - /// The object to which this instance will be compared. - /// True if the ranges are equal, false otherwise. - public override bool Equals(object obj) - { - if (!(obj is BufferRange)) - { - return false; - } - - BufferRange other = (BufferRange)obj; - - return - this.Start.Equals(other.Start) && - this.End.Equals(other.End); - } - - /// - /// Calculates a unique hash code that represents this instance. - /// - /// A hash code representing this instance. - public override int GetHashCode() - { - return this.Start.GetHashCode() ^ this.End.GetHashCode(); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Workspace/FileChange.cs b/src/PowerShellEditorServices/Workspace/FileChange.cs deleted file mode 100644 index 79f6925ea..000000000 --- a/src/PowerShellEditorServices/Workspace/FileChange.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 -{ - /// - /// Contains details relating to a content change in an open file. - /// - public class FileChange - { - /// - /// The string which is to be inserted in the file. - /// - public string InsertString { get; set; } - - /// - /// The 1-based line number where the change starts. - /// - public int Line { get; set; } - - /// - /// The 1-based column offset where the change starts. - /// - public int Offset { get; set; } - - /// - /// The 1-based line number where the change ends. - /// - public int EndLine { get; set; } - - /// - /// The 1-based column offset where the change ends. - /// - public int EndOffset { get; set; } - - /// - /// Indicates that the InsertString is an overwrite - /// of the content, and all stale content and metadata - /// should be discarded. - /// - public bool IsReload { get; set; } - } -} diff --git a/src/PowerShellEditorServices/Workspace/FilePosition.cs b/src/PowerShellEditorServices/Workspace/FilePosition.cs deleted file mode 100644 index a7c9036c7..000000000 --- a/src/PowerShellEditorServices/Workspace/FilePosition.cs +++ /dev/null @@ -1,109 +0,0 @@ -// -// 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 -{ - /// - /// Provides details and operations for a buffer position in a - /// specific file. - /// - public class FilePosition : BufferPosition - { - #region Private Fields - - private ScriptFile scriptFile; - - #endregion - - #region Constructors - - /// - /// Creates a new FilePosition instance for the 1-based line and - /// column numbers in the specified file. - /// - /// The ScriptFile in which the position is located. - /// The 1-based line number in the file. - /// The 1-based column number in the file. - public FilePosition( - ScriptFile scriptFile, - int line, - int column) - : base(line, column) - { - this.scriptFile = scriptFile; - } - - /// - /// Creates a new FilePosition instance for the specified file by - /// copying the specified BufferPosition - /// - /// The ScriptFile in which the position is located. - /// The original BufferPosition from which the line and column will be copied. - public FilePosition( - ScriptFile scriptFile, - BufferPosition copiedPosition) - : this(scriptFile, copiedPosition.Line, copiedPosition.Column) - { - scriptFile.ValidatePosition(copiedPosition); - } - - #endregion - - #region Public Methods - - /// - /// Gets a FilePosition relative to this position by adding the - /// provided line and column offset relative to the contents of - /// the current file. - /// - /// The line offset to add to this position. - /// The column offset to add to this position. - /// A new FilePosition instance for the calculated position. - public FilePosition AddOffset(int lineOffset, int columnOffset) - { - return this.scriptFile.CalculatePosition( - this, - lineOffset, - columnOffset); - } - - /// - /// Gets a FilePosition for the line and column position - /// of the beginning of the current line after any initial - /// whitespace for indentation. - /// - /// A new FilePosition instance for the calculated position. - public FilePosition GetLineStart() - { - string scriptLine = scriptFile.FileLines[this.Line - 1]; - - int lineStartColumn = 1; - for (int i = 0; i < scriptLine.Length; i++) - { - if (!char.IsWhiteSpace(scriptLine[i])) - { - lineStartColumn = i + 1; - break; - } - } - - return new FilePosition(this.scriptFile, this.Line, lineStartColumn); - } - - /// - /// Gets a FilePosition for the line and column position - /// of the end of the current line. - /// - /// A new FilePosition instance for the calculated position. - public FilePosition GetLineEnd() - { - string scriptLine = scriptFile.FileLines[this.Line - 1]; - return new FilePosition(this.scriptFile, this.Line, scriptLine.Length + 1); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs deleted file mode 100644 index 958778240..000000000 --- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs +++ /dev/null @@ -1,679 +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.Collections.Generic; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Runtime.InteropServices; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Contains the details and contents of an open script file. - /// - public class ScriptFile - { - #region Private Fields - - private static readonly string[] s_newlines = new[] - { - "\r\n", - "\n" - }; - - private Version powerShellVersion; - - #endregion - - #region Properties - - /// - /// Gets a unique string that identifies this file. At this time, - /// this property returns a normalized version of the value stored - /// in the FilePath property. - /// - public string Id - { - get { return this.FilePath.ToLower(); } - } - - /// - /// Gets the path at which this file resides. - /// - public string FilePath { get; private set; } - - /// - /// Gets the path which the editor client uses to identify this file. - /// - public string ClientFilePath { get; private set; } - - /// - /// Gets the file path in LSP DocumentUri form. The ClientPath property must not be null. - /// - public string DocumentUri - { - get - { - return this.ClientFilePath == null - ? string.Empty - : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); - } - } - - /// - /// Gets or sets a boolean that determines whether - /// semantic analysis should be enabled for this file. - /// For internal use only. - /// - internal bool IsAnalysisEnabled { get; set; } - - /// - /// Gets a boolean that determines whether this file is - /// in-memory or not (either unsaved or non-file content). - /// - public bool IsInMemory { get; private set; } - - /// - /// Gets a string containing the full contents of the file. - /// - public string Contents - { - get - { - return string.Join(Environment.NewLine, this.FileLines); - } - } - - /// - /// Gets a BufferRange that represents the entire content - /// range of the file. - /// - public BufferRange FileRange { get; private set; } - - /// - /// Gets the list of syntax markers found by parsing this - /// file's contents. - /// - public List DiagnosticMarkers - { - get; - private set; - } - - /// - /// Gets the list of strings for each line of the file. - /// - internal List FileLines - { - get; - private set; - } - - /// - /// Gets the ScriptBlockAst representing the parsed script contents. - /// - public ScriptBlockAst ScriptAst - { - get; - private set; - } - - /// - /// Gets the array of Tokens representing the parsed script contents. - /// - public Token[] ScriptTokens - { - get; - private set; - } - - /// - /// Gets the array of filepaths dot sourced in this ScriptFile - /// - public string[] ReferencedFiles - { - get; - private set; - } - - #endregion - - #region Constructors - - /// - /// Creates a new ScriptFile instance by reading file contents from - /// the given TextReader. - /// - /// The path at which the script file resides. - /// The path which the client uses to identify the file. - /// The TextReader to use for reading the file's contents. - /// The version of PowerShell for which the script is being parsed. - public ScriptFile( - string filePath, - string clientFilePath, - TextReader textReader, - Version powerShellVersion) - { - this.FilePath = filePath; - this.ClientFilePath = clientFilePath; - this.IsAnalysisEnabled = true; - this.IsInMemory = Workspace.IsPathInMemory(filePath); - this.powerShellVersion = powerShellVersion; - - // SetFileContents() calls ParseFileContents() which initializes the rest of the properties. - this.SetFileContents(textReader.ReadToEnd()); - } - - /// - /// Creates a new ScriptFile instance with the specified file contents. - /// - /// The path at which the script file resides. - /// The path which the client uses to identify the file. - /// The initial contents of the script file. - /// The version of PowerShell for which the script is being parsed. - public ScriptFile( - string filePath, - string clientFilePath, - string initialBuffer, - Version powerShellVersion) - : this( - filePath, - clientFilePath, - new StringReader(initialBuffer), - powerShellVersion) - { - } - - /// - /// Creates a new ScriptFile instance with the specified filepath. - /// - /// The path at which the script file resides. - /// The path which the client uses to identify the file. - /// The version of PowerShell for which the script is being parsed. - public ScriptFile( - string filePath, - string clientFilePath, - Version powerShellVersion) - : this( - filePath, - clientFilePath, - File.ReadAllText(filePath), - powerShellVersion) - { - } - - #endregion - - #region Public Methods - - /// - /// Get the lines in a string. - /// - /// Input string to be split up into lines. - /// The lines in the string. - [Obsolete("This method is not designed for public exposure and will be retired in later versions of EditorServices")] - public static IList GetLines(string text) - { - return GetLinesInternal(text); - } - - /// - /// Get the lines in a string. - /// - /// Input string to be split up into lines. - /// The lines in the string. - internal static List GetLinesInternal(string text) - { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } - - return new List(text.Split(s_newlines, StringSplitOptions.None)); - } - - /// - /// Deterines whether the supplied path indicates the file is an "untitled:Unitled-X" - /// which has not been saved to file. - /// - /// The path to check. - /// True if the path is an untitled file, false otherwise. - public static bool IsUntitledPath(string path) - { - Validate.IsNotNull(nameof(path), path); - - return path.ToLower().StartsWith("untitled:"); - } - - /// - /// Gets a line from the file's contents. - /// - /// The 1-based line number in the file. - /// The complete line at the given line number. - public string GetLine(int lineNumber) - { - Validate.IsWithinRange( - "lineNumber", lineNumber, - 1, this.FileLines.Count + 1); - - return this.FileLines[lineNumber - 1]; - } - - /// - /// Gets a range of lines from the file's contents. - /// - /// The buffer range from which lines will be extracted. - /// An array of strings from the specified range of the file. - public string[] GetLinesInRange(BufferRange bufferRange) - { - this.ValidatePosition(bufferRange.Start); - this.ValidatePosition(bufferRange.End); - - List linesInRange = new List(); - - int startLine = bufferRange.Start.Line, - endLine = bufferRange.End.Line; - - for (int line = startLine; line <= endLine; line++) - { - string currentLine = this.FileLines[line - 1]; - int startColumn = - line == startLine - ? bufferRange.Start.Column - : 1; - int endColumn = - line == endLine - ? bufferRange.End.Column - : currentLine.Length + 1; - - currentLine = - currentLine.Substring( - startColumn - 1, - endColumn - startColumn); - - linesInRange.Add(currentLine); - } - - return linesInRange.ToArray(); - } - - /// - /// Throws ArgumentOutOfRangeException if the given position is outside - /// of the file's buffer extents. - /// - /// The position in the buffer to be validated. - public void ValidatePosition(BufferPosition bufferPosition) - { - this.ValidatePosition( - bufferPosition.Line, - bufferPosition.Column); - } - - /// - /// Throws ArgumentOutOfRangeException if the given position is outside - /// of the file's buffer extents. - /// - /// The 1-based line to be validated. - /// The 1-based column to be validated. - public void ValidatePosition(int line, int column) - { - int maxLine = this.FileLines.Count; - if (line < 1 || line > maxLine) - { - throw new ArgumentOutOfRangeException($"Position {line}:{column} is outside of the line range of 1 to {maxLine}."); - } - - // The maximum column is either **one past** the length of the string - // or 1 if the string is empty. - string lineString = this.FileLines[line - 1]; - int maxColumn = lineString.Length > 0 ? lineString.Length + 1 : 1; - - if (column < 1 || column > maxColumn) - { - throw new ArgumentOutOfRangeException($"Position {line}:{column} is outside of the column range of 1 to {maxColumn}."); - } - } - - - /// - /// Defunct ValidatePosition method call. The isInsertion parameter is ignored. - /// - /// - /// - /// - [Obsolete("Use ValidatePosition(int, int) instead")] - public void ValidatePosition(int line, int column, bool isInsertion) - { - ValidatePosition(line, column); - } - - /// - /// Applies the provided FileChange to the file's contents - /// - /// The FileChange to apply to the file's contents. - public void ApplyChange(FileChange fileChange) - { - // Break up the change lines - string[] changeLines = fileChange.InsertString.Split('\n'); - - if (fileChange.IsReload) - { - this.FileLines.Clear(); - foreach (var changeLine in changeLines) - { - this.FileLines.Add(changeLine); - } - } - else - { - // VSCode sometimes likes to give the change start line as (FileLines.Count + 1). - // This used to crash EditorServices, but we now treat it as an append. - // See https://github.com/PowerShell/vscode-powershell/issues/1283 - if (fileChange.Line == this.FileLines.Count + 1) - { - foreach (string addedLine in changeLines) - { - string finalLine = addedLine.TrimEnd('\r'); - this.FileLines.Add(finalLine); - } - } - // Similarly, when lines are deleted from the end of the file, - // VSCode likes to give the end line as (FileLines.Count + 1). - else if (fileChange.EndLine == this.FileLines.Count + 1 && String.Empty.Equals(fileChange.InsertString)) - { - int lineIndex = fileChange.Line - 1; - this.FileLines.RemoveRange(lineIndex, this.FileLines.Count - lineIndex); - } - // Otherwise, the change needs to go between existing content - else - { - this.ValidatePosition(fileChange.Line, fileChange.Offset); - this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset); - - // Get the first fragment of the first line - string firstLineFragment = - this.FileLines[fileChange.Line - 1] - .Substring(0, fileChange.Offset - 1); - - // Get the last fragment of the last line - string endLine = this.FileLines[fileChange.EndLine - 1]; - string lastLineFragment = - endLine.Substring( - fileChange.EndOffset - 1, - (this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1); - - // Remove the old lines - for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++) - { - this.FileLines.RemoveAt(fileChange.Line - 1); - } - - // Build and insert the new lines - int currentLineNumber = fileChange.Line; - for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++) - { - // Since we split the lines above using \n, make sure to - // trim the ending \r's off as well. - string finalLine = changeLines[changeIndex].TrimEnd('\r'); - - // Should we add first or last line fragments? - if (changeIndex == 0) - { - // Append the first line fragment - finalLine = firstLineFragment + finalLine; - } - if (changeIndex == changeLines.Length - 1) - { - // Append the last line fragment - finalLine = finalLine + lastLineFragment; - } - - this.FileLines.Insert(currentLineNumber - 1, finalLine); - currentLineNumber++; - } - } - } - - // Parse the script again to be up-to-date - this.ParseFileContents(); - } - - /// - /// Calculates the zero-based character offset of a given - /// line and column position in the file. - /// - /// The 1-based line number from which the offset is calculated. - /// The 1-based column number from which the offset is calculated. - /// The zero-based offset for the given file position. - public int GetOffsetAtPosition(int lineNumber, int columnNumber) - { - Validate.IsWithinRange("lineNumber", lineNumber, 1, this.FileLines.Count); - Validate.IsGreaterThan("columnNumber", columnNumber, 0); - - int offset = 0; - - for (int i = 0; i < lineNumber; i++) - { - if (i == lineNumber - 1) - { - // Subtract 1 to account for 1-based column numbering - offset += columnNumber - 1; - } - else - { - // Add an offset to account for the current platform's newline characters - offset += this.FileLines[i].Length + Environment.NewLine.Length; - } - } - - return offset; - } - - /// - /// Calculates a FilePosition relative to a starting BufferPosition - /// using the given 1-based line and column offset. - /// - /// The original BufferPosition from which an new position should be calculated. - /// The 1-based line offset added to the original position in this file. - /// The 1-based column offset added to the original position in this file. - /// A new FilePosition instance with the resulting line and column number. - public FilePosition CalculatePosition( - BufferPosition originalPosition, - int lineOffset, - int columnOffset) - { - int newLine = originalPosition.Line + lineOffset, - newColumn = originalPosition.Column + columnOffset; - - this.ValidatePosition(newLine, newColumn); - - string scriptLine = this.FileLines[newLine - 1]; - newColumn = Math.Min(scriptLine.Length + 1, newColumn); - - return new FilePosition(this, newLine, newColumn); - } - - /// - /// Calculates the 1-based line and column number position based - /// on the given buffer offset. - /// - /// The buffer offset to convert. - /// A new BufferPosition containing the position of the offset. - public BufferPosition GetPositionAtOffset(int bufferOffset) - { - BufferRange bufferRange = - GetRangeBetweenOffsets( - bufferOffset, bufferOffset); - - return bufferRange.Start; - } - - /// - /// Calculates the 1-based line and column number range based on - /// the given start and end buffer offsets. - /// - /// The start offset of the range. - /// The end offset of the range. - /// A new BufferRange containing the positions in the offset range. - public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset) - { - bool foundStart = false; - int currentOffset = 0; - int searchedOffset = startOffset; - - BufferPosition startPosition = new BufferPosition(0, 0); - BufferPosition endPosition = startPosition; - - int line = 0; - while (line < this.FileLines.Count) - { - if (searchedOffset <= currentOffset + this.FileLines[line].Length) - { - int column = searchedOffset - currentOffset; - - // Have we already found the start position? - if (foundStart) - { - // Assign the end position and end the search - endPosition = new BufferPosition(line + 1, column + 1); - break; - } - else - { - startPosition = new BufferPosition(line + 1, column + 1); - - // Do we only need to find the start position? - if (startOffset == endOffset) - { - endPosition = startPosition; - break; - } - else - { - // Since the end offset can be on the same line, - // skip the line increment and continue searching - // for the end position - foundStart = true; - searchedOffset = endOffset; - continue; - } - } - } - - // Increase the current offset and include newline length - currentOffset += this.FileLines[line].Length + Environment.NewLine.Length; - line++; - } - - return new BufferRange(startPosition, endPosition); - } - - #endregion - - #region Private Methods - - private void SetFileContents(string fileContents) - { - // Split the file contents into lines and trim - // any carriage returns from the strings. - this.FileLines = GetLinesInternal(fileContents); - - // Parse the contents to get syntax tree and errors - this.ParseFileContents(); - } - - /// - /// Parses the current file contents to get the AST, tokens, - /// and parse errors. - /// - private void ParseFileContents() - { - ParseError[] parseErrors = null; - - // First, get the updated file range - int lineCount = this.FileLines.Count; - if (lineCount > 0) - { - this.FileRange = - new BufferRange( - new BufferPosition(1, 1), - new BufferPosition( - lineCount + 1, - this.FileLines[lineCount - 1].Length + 1)); - } - else - { - this.FileRange = BufferRange.None; - } - - try - { - Token[] scriptTokens; - - // This overload appeared with Windows 10 Update 1 - if (this.powerShellVersion.Major >= 6 || - (this.powerShellVersion.Major == 5 && this.powerShellVersion.Build >= 10586)) - { - // Include the file path so that module relative - // paths are evaluated correctly - this.ScriptAst = - Parser.ParseInput( - this.Contents, - this.FilePath, - out scriptTokens, - out parseErrors); - } - else - { - this.ScriptAst = - Parser.ParseInput( - this.Contents, - out scriptTokens, - out parseErrors); - } - - this.ScriptTokens = scriptTokens; - } - catch (RuntimeException ex) - { - var parseError = - new ParseError( - null, - ex.ErrorRecord.FullyQualifiedErrorId, - ex.Message); - - parseErrors = new[] { parseError }; - this.ScriptTokens = new Token[0]; - this.ScriptAst = null; - } - - // Translate parse errors into syntax markers - this.DiagnosticMarkers = - parseErrors - .Select(ScriptFileMarker.FromParseError) - .ToList(); - - // Untitled files have no directory - // Discussed in https://github.com/PowerShell/PowerShellEditorServices/pull/815. - // Rather than working hard to enable things for untitled files like a phantom directory, - // users should save the file. - if (IsUntitledPath(this.FilePath)) - { - // Need to initialize the ReferencedFiles property to an empty array. - this.ReferencedFiles = new string[0]; - return; - } - - // Get all dot sourced referenced files and store them - this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath)); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Workspace/ScriptFileMarker.cs b/src/PowerShellEditorServices/Workspace/ScriptFileMarker.cs deleted file mode 100644 index 9eb73cb6c..000000000 --- a/src/PowerShellEditorServices/Workspace/ScriptFileMarker.cs +++ /dev/null @@ -1,189 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Management.Automation; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Contains details for a code correction which can be applied from a ScriptFileMarker. - /// - public class MarkerCorrection - { - /// - /// Gets or sets the display name of the code correction. - /// - public string Name { get; set; } - - /// - /// Gets or sets the list of ScriptRegions that define the edits to be made by the correction. - /// - public ScriptRegion[] Edits { get; set; } - } - - /// - /// Defines the message level of a script file marker. - /// - public enum ScriptFileMarkerLevel - { - ///  -        /// Information: This warning is trivial, but may be useful. They are recommended by PowerShell best practice. -        ///  -        Information = 0, -        ///  -        /// WARNING: This warning may cause a problem or does not follow PowerShell's recommended guidelines. -        ///  -        Warning = 1, -        ///  -        /// ERROR: This warning is likely to cause a problem or does not follow PowerShell's required guidelines. -        ///  -        Error = 2, -        ///  -        /// ERROR: This diagnostic is caused by an actual parsing error, and is generated only by the engine. -        ///  -        ParseError = 3 - }; - - /// - /// Contains details about a marker that should be displayed - /// for the a script file. The marker information could come - /// from syntax parsing or semantic analysis of the script. - /// - public class ScriptFileMarker - { - #region Properties - - /// - /// Gets or sets the marker's message string. - /// - public string Message { get; set; } - - /// - /// Gets or sets the ruleName associated with this marker. - /// - public string RuleName { get; set; } - - /// - /// Gets or sets the marker's message level. - /// - public ScriptFileMarkerLevel Level { get; set; } - - /// - /// Gets or sets the ScriptRegion where the marker should appear. - /// - public ScriptRegion ScriptRegion { get; set; } - - /// - /// Gets or sets an optional code correction that can be applied based on this marker. - /// - public MarkerCorrection Correction { get; set; } - - /// - /// Gets or sets the name of the marker's source like "PowerShell" - /// or "PSScriptAnalyzer". - /// - public string Source { get; set; } - - #endregion - - #region Public Methods - - internal static ScriptFileMarker FromParseError( - ParseError parseError) - { - Validate.IsNotNull("parseError", parseError); - - return new ScriptFileMarker - { - Message = parseError.Message, - Level = ScriptFileMarkerLevel.Error, - ScriptRegion = ScriptRegion.Create(parseError.Extent), - Source = "PowerShell" - }; - } - private static string GetIfExistsString(PSObject psobj, string memberName) - { - if (psobj.Members.Match(memberName).Count > 0) - { - return psobj.Members[memberName].Value != null ? (string)psobj.Members[memberName].Value : ""; - } - else - { - return ""; - } - } - - internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) - { - Validate.IsNotNull("psObject", psObject); - MarkerCorrection correction = null; - - // make sure psobject is of type DiagnosticRecord - if (!psObject.TypeNames.Contains( - "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord", - StringComparer.OrdinalIgnoreCase)) - { - throw new ArgumentException("Input PSObject must of DiagnosticRecord type."); - } - - // casting psobject to dynamic allows us to access - // the diagnostic record's properties directly i.e. . - // without having to go through PSObject's Members property. - var diagnosticRecord = psObject as dynamic; - - if (diagnosticRecord.SuggestedCorrections != null) - { - var suggestedCorrections = diagnosticRecord.SuggestedCorrections as dynamic; - List editRegions = new List(); - string correctionMessage = null; - foreach (var suggestedCorrection in suggestedCorrections) - { - editRegions.Add(new ScriptRegion - { - File = diagnosticRecord.ScriptPath, - Text = suggestedCorrection.Text, - StartLineNumber = suggestedCorrection.StartLineNumber, - StartColumnNumber = suggestedCorrection.StartColumnNumber, - EndLineNumber = suggestedCorrection.EndLineNumber, - EndColumnNumber = suggestedCorrection.EndColumnNumber - }); - correctionMessage = suggestedCorrection.Description; - } - - correction = new MarkerCorrection - { - Name = correctionMessage == null ? diagnosticRecord.Message : correctionMessage, - Edits = editRegions.ToArray() - }; - } - - string severity = diagnosticRecord.Severity.ToString(); - if (!Enum.TryParse(severity, out ScriptFileMarkerLevel level)) - { - throw new ArgumentException( - $"The provided DiagnosticSeverity value '{severity}' is unknown.", - "diagnosticSeverity"); - } - - return new ScriptFileMarker - { - Message = $"{diagnosticRecord.Message as string}", - RuleName = $"{diagnosticRecord.RuleName as string}", - Level = level, - ScriptRegion = ScriptRegion.Create(diagnosticRecord.Extent as IScriptExtent), - Correction = correction, - Source = "PSScriptAnalyzer" - }; - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Workspace/ScriptRegion.cs b/src/PowerShellEditorServices/Workspace/ScriptRegion.cs deleted file mode 100644 index 5717b1382..000000000 --- a/src/PowerShellEditorServices/Workspace/ScriptRegion.cs +++ /dev/null @@ -1,112 +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.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Contains details about a specific region of text in script file. - /// - public sealed class ScriptRegion : IScriptExtent - { - #region Properties - - /// - /// Gets the file path of the script file in which this region is contained. - /// - public string File { get; set; } - - /// - /// Gets or sets the text that is contained within the region. - /// - public string Text { get; set; } - - /// - /// Gets or sets the starting line number of the region. - /// - public int StartLineNumber { get; set; } - - /// - /// Gets or sets the starting column number of the region. - /// - public int StartColumnNumber { get; set; } - - /// - /// Gets or sets the starting file offset of the region. - /// - public int StartOffset { get; set; } - - /// - /// Gets or sets the ending line number of the region. - /// - public int EndLineNumber { get; set; } - - /// - /// Gets or sets the ending column number of the region. - /// - public int EndColumnNumber { get; set; } - - /// - /// Gets or sets the ending file offset of the region. - /// - public int EndOffset { get; set; } - - /// - /// Gets the starting IScriptPosition in the script. - /// (Currently unimplemented.) - /// - IScriptPosition IScriptExtent.StartScriptPosition => throw new NotImplementedException(); - - /// - /// Gets the ending IScriptPosition in the script. - /// (Currently unimplemented.) - /// - IScriptPosition IScriptExtent.EndScriptPosition => throw new NotImplementedException(); - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the ScriptRegion class from an - /// instance of an IScriptExtent implementation. - /// - /// - /// The IScriptExtent to copy into the ScriptRegion. - /// - /// - /// A new ScriptRegion instance with the same details as the IScriptExtent. - /// - public static ScriptRegion Create(IScriptExtent scriptExtent) - { - // IScriptExtent throws an ArgumentOutOfRange exception if Text is null - string scriptExtentText; - try - { - scriptExtentText = scriptExtent.Text; - } - catch (ArgumentOutOfRangeException) - { - scriptExtentText = string.Empty; - } - - return new ScriptRegion - { - File = scriptExtent.File, - Text = scriptExtentText, - StartLineNumber = scriptExtent.StartLineNumber, - StartColumnNumber = scriptExtent.StartColumnNumber, - StartOffset = scriptExtent.StartOffset, - EndLineNumber = scriptExtent.EndLineNumber, - EndColumnNumber = scriptExtent.EndColumnNumber, - EndOffset = scriptExtent.EndOffset - }; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs deleted file mode 100644 index f6a51b22f..000000000 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ /dev/null @@ -1,716 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.Linq; -using System.IO; -using System.Security; -using System.Text; -using System.Runtime.InteropServices; -using Microsoft.Extensions.FileSystemGlobbing; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Manages a "workspace" of script files that are open for a particular - /// editing session. Also helps to navigate references between ScriptFiles. - /// - public class Workspace - { - #region Private Fields - - // List of all file extensions considered PowerShell files in the .Net Core Framework. - private static readonly string[] s_psFileExtensionsCoreFramework = - { - ".ps1", - ".psm1", - ".psd1" - }; - - // .Net Core doesn't appear to use the same three letter pattern matching rule although the docs - // suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'. - // ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_ - private static readonly string[] s_psFileExtensionsFullFramework = - { - ".ps1", - ".psm1", - ".psd1", - ".ps1xml" - }; - - // An array of globs which includes everything. - private static readonly string[] s_psIncludeAllGlob = new [] - { - "**/*" - }; - - private static readonly string[] s_supportedUriSchemes = new[] - { - "file", - "untitled", - "inmemory" - }; - - private ILogger logger; - private Version powerShellVersion; - private Dictionary workspaceFiles = new Dictionary(); - - #endregion - - #region Properties - - /// - /// Gets or sets the root path of the workspace. - /// - public string WorkspacePath { get; set; } - - /// - /// Gets or sets the default list of file globs to exclude during workspace searches. - /// - public List ExcludeFilesGlob { get; set; } - - /// - /// Gets or sets whether the workspace should follow symlinks in search operations. - /// - public bool FollowSymlinks { get; set; } - - #endregion - - #region Constructors - - /// - /// Creates a new instance of the Workspace class. - /// - /// The version of PowerShell for which scripts will be parsed. - /// An ILogger implementation used for writing log messages. - public Workspace(Version powerShellVersion, ILogger logger) - { - this.powerShellVersion = powerShellVersion; - this.logger = logger; - this.ExcludeFilesGlob = new List(); - this.FollowSymlinks = true; - } - - #endregion - - #region Public Methods - - /// - /// Creates a new ScriptFile instance which is identified by the given file - /// path and initially contains the given buffer contents. - /// - /// The file path for which a buffer will be retrieved. - /// The initial buffer contents if there is not an existing ScriptFile for this path. - /// A ScriptFile instance for the specified path. - public ScriptFile CreateScriptFileFromFileBuffer(string filePath, string initialBuffer) - { - Validate.IsNotNullOrEmptyString("filePath", filePath); - - // Resolve the full file path - string resolvedFilePath = this.ResolveFilePath(filePath); - string keyName = resolvedFilePath.ToLower(); - - ScriptFile scriptFile = - new ScriptFile( - resolvedFilePath, - filePath, - initialBuffer, - this.powerShellVersion); - - this.workspaceFiles[keyName] = scriptFile; - - this.logger.Write(LogLevel.Verbose, "Opened file as in-memory buffer: " + resolvedFilePath); - - return scriptFile; - } - - /// - /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. - /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using - /// instead. - /// - /// The file path at which the script resides. - /// - /// is not found. - /// - /// - /// contains a null or empty string. - /// - public ScriptFile GetFile(string filePath) - { - Validate.IsNotNullOrEmptyString("filePath", filePath); - - // Resolve the full file path - string resolvedFilePath = this.ResolveFilePath(filePath); - string keyName = resolvedFilePath.ToLower(); - - // Make sure the file isn't already loaded into the workspace - ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) - { - // This method allows FileNotFoundException to bubble up - // if the file isn't found. - using (FileStream fileStream = new FileStream(resolvedFilePath, FileMode.Open, FileAccess.Read)) - using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8)) - { - scriptFile = - new ScriptFile( - resolvedFilePath, - filePath, - streamReader, - this.powerShellVersion); - - this.workspaceFiles.Add(keyName, scriptFile); - } - - this.logger.Write(LogLevel.Verbose, "Opened file on disk: " + resolvedFilePath); - } - - return scriptFile; - } - - /// - /// Tries to get an open file in the workspace. Returns true if it succeeds, false otherwise. - /// - /// The file path at which the script resides. - /// The out parameter that will contain the ScriptFile object. - public bool TryGetFile(string filePath, out ScriptFile scriptFile) - { - var fileUri = new Uri(filePath); - - switch (fileUri.Scheme) - { - // List supported schemes here - case "file": - case "untitled": - break; - - default: - scriptFile = null; - return false; - } - - try - { - if (filePath.Contains(":/") // Quick heuristic to determine if we might have a URI - && !s_supportedUriSchemes.Contains(new Uri(filePath).Scheme)) - { - scriptFile = null; - return false; - } - } - catch - { - // If something goes wrong trying to check for URIs, just proceed to normal logic - } - - try - { - scriptFile = GetFile(filePath); - return true; - } - catch (Exception e) when ( - e is NotSupportedException || - e is FileNotFoundException || - e is DirectoryNotFoundException || - e is PathTooLongException || - e is IOException || - e is SecurityException || - e is UnauthorizedAccessException) - { - this.logger.WriteHandledException($"Failed to get file for {nameof(filePath)}: '{filePath}'", e); - scriptFile = null; - return false; - } - } - - /// - /// Gets a new ScriptFile instance which is identified by the given file path. - /// - /// The file path for which a buffer will be retrieved. - /// A ScriptFile instance if there is a buffer for the path, null otherwise. - public ScriptFile GetFileBuffer(string filePath) - { - return this.GetFileBuffer(filePath, null); - } - - /// - /// Gets a new ScriptFile instance which is identified by the given file - /// path and initially contains the given buffer contents. - /// - /// The file path for which a buffer will be retrieved. - /// The initial buffer contents if there is not an existing ScriptFile for this path. - /// A ScriptFile instance for the specified path. - public ScriptFile GetFileBuffer(string filePath, string initialBuffer) - { - Validate.IsNotNullOrEmptyString("filePath", filePath); - - // Resolve the full file path - string resolvedFilePath = this.ResolveFilePath(filePath); - string keyName = resolvedFilePath.ToLower(); - - // Make sure the file isn't already loaded into the workspace - ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile) && initialBuffer != null) - { - scriptFile = - new ScriptFile( - resolvedFilePath, - filePath, - initialBuffer, - this.powerShellVersion); - - this.workspaceFiles.Add(keyName, scriptFile); - - this.logger.Write(LogLevel.Verbose, "Opened file as in-memory buffer: " + resolvedFilePath); - } - - return scriptFile; - } - - /// - /// Gets an array of all opened ScriptFiles in the workspace. - /// - /// An array of all opened ScriptFiles in the workspace. - public ScriptFile[] GetOpenedFiles() - { - return workspaceFiles.Values.ToArray(); - } - - /// - /// Closes a currently open script file with the given file path. - /// - /// The file path at which the script resides. - public void CloseFile(ScriptFile scriptFile) - { - Validate.IsNotNull("scriptFile", scriptFile); - - this.workspaceFiles.Remove(scriptFile.Id); - } - - /// - /// Gets all file references by recursively searching - /// through referenced files in a scriptfile - /// - /// Contains the details and contents of an open script file - /// A scriptfile array where the first file - /// in the array is the "root file" of the search - public ScriptFile[] ExpandScriptReferences(ScriptFile scriptFile) - { - Dictionary referencedScriptFiles = new Dictionary(); - List expandedReferences = new List(); - - // add original file so it's not searched for, then find all file references - referencedScriptFiles.Add(scriptFile.Id, scriptFile); - RecursivelyFindReferences(scriptFile, referencedScriptFiles); - - // remove original file from referened file and add it as the first element of the - // expanded referenced list to maintain order so the original file is always first in the list - referencedScriptFiles.Remove(scriptFile.Id); - expandedReferences.Add(scriptFile); - - if (referencedScriptFiles.Count > 0) - { - expandedReferences.AddRange(referencedScriptFiles.Values); - } - - return expandedReferences.ToArray(); - } - - /// - /// Gets the workspace-relative path of the given file path. - /// - /// The original full file path. - /// A relative file path - public string GetRelativePath(string filePath) - { - string resolvedPath = filePath; - - if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(this.WorkspacePath)) - { - Uri workspaceUri = new Uri(this.WorkspacePath); - Uri fileUri = new Uri(filePath); - - resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString(); - - // Convert the directory separators if necessary - if (System.IO.Path.DirectorySeparatorChar == '\\') - { - resolvedPath = resolvedPath.Replace('/', '\\'); - } - } - - return resolvedPath; - } - - /// - /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values. - /// - /// An enumerator over the PowerShell files found in the workspace. - public IEnumerable EnumeratePSFiles() - { - return EnumeratePSFiles( - ExcludeFilesGlob.ToArray(), - s_psIncludeAllGlob, - maxDepth: 64, - ignoreReparsePoints: !FollowSymlinks - ); - } - - /// - /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner. - /// - /// An enumerator over the PowerShell files found in the workspace. - public IEnumerable EnumeratePSFiles( - string[] excludeGlobs, - string[] includeGlobs, - int maxDepth, - bool ignoreReparsePoints - ) - { - if (WorkspacePath == null || !Directory.Exists(WorkspacePath)) - { - yield break; - } - - var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher(); - foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } - foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } - - var fsFactory = new WorkspaceFileSystemWrapperFactory( - WorkspacePath, - maxDepth, - Utils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, - ignoreReparsePoints, - logger - ); - var fileMatchResult = matcher.Execute(fsFactory.RootDirectory); - foreach (FilePatternMatch item in fileMatchResult.Files) - { - yield return Path.Combine(WorkspacePath, item.Path); - } - } - - #endregion - - #region Private Methods - /// - /// Recusrively searches through referencedFiles in scriptFiles - /// and builds a Dictonary of the file references - /// - /// Details an contents of "root" script file - /// A Dictionary of referenced script files - private void RecursivelyFindReferences( - ScriptFile scriptFile, - Dictionary referencedScriptFiles) - { - // Get the base path of the current script for use in resolving relative paths - string baseFilePath = - GetBaseFilePath( - scriptFile.FilePath); - - foreach (string referencedFileName in scriptFile.ReferencedFiles) - { - string resolvedScriptPath = - this.ResolveRelativeScriptPath( - baseFilePath, - referencedFileName); - - // If there was an error resolving the string, skip this reference - if (resolvedScriptPath == null) - { - continue; - } - - this.logger.Write( - LogLevel.Verbose, - string.Format( - "Resolved relative path '{0}' to '{1}'", - referencedFileName, - resolvedScriptPath)); - - // Get the referenced file if it's not already in referencedScriptFiles - if (this.TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) - { - // Normalize the resolved script path and add it to the - // referenced files list if it isn't there already - resolvedScriptPath = resolvedScriptPath.ToLower(); - if (!referencedScriptFiles.ContainsKey(resolvedScriptPath)) - { - referencedScriptFiles.Add(resolvedScriptPath, referencedFile); - RecursivelyFindReferences(referencedFile, referencedScriptFiles); - } - } - } - } - - internal string ResolveFilePath(string filePath) - { - if (!IsPathInMemory(filePath)) - { - if (filePath.StartsWith(@"file://")) - { - filePath = Workspace.UnescapeDriveColon(filePath); - // Client sent the path in URI format, extract the local path - filePath = new Uri(filePath).LocalPath; - } - - // Clients could specify paths with escaped space, [ and ] characters which .NET APIs - // will not handle. These paths will get appropriately escaped just before being passed - // into the PowerShell engine. - filePath = PowerShellContext.UnescapeWildcardEscapedPath(filePath); - - // Get the absolute file path - filePath = Path.GetFullPath(filePath); - } - - this.logger.Write(LogLevel.Verbose, "Resolved path: " + filePath); - - return filePath; - } - - internal static bool IsPathInMemory(string filePath) - { - bool isInMemory = false; - - // In cases where a "virtual" file is displayed in the editor, - // we need to treat the file differently than one that exists - // on disk. A virtual file could be something like a diff - // view of the current file or an untitled file. - try - { - // File system absoulute paths will have a URI scheme of file:. - // Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile. - var uri = new Uri(filePath); - isInMemory = !uri.IsFile; - } - catch (UriFormatException) - { - // Relative file paths cause a UriFormatException. - // In this case, fallback to using Path.GetFullPath(). - try - { - Path.GetFullPath(filePath); - } - catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException) - { - isInMemory = true; - } - catch (PathTooLongException) - { - // If we ever get here, it should be an actual file so, not in memory - } - } - - return isInMemory; - } - - private string GetBaseFilePath(string filePath) - { - if (IsPathInMemory(filePath)) - { - // If the file is in memory, use the workspace path - return this.WorkspacePath; - } - - if (!Path.IsPathRooted(filePath)) - { - // TODO: Assert instead? - throw new InvalidOperationException( - string.Format( - "Must provide a full path for originalScriptPath: {0}", - filePath)); - } - - // Get the directory of the file path - return Path.GetDirectoryName(filePath); - } - - internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath) - { - string combinedPath = null; - Exception resolveException = null; - - try - { - // If the path is already absolute there's no need to resolve it relatively - // to the baseFilePath. - if (Path.IsPathRooted(relativePath)) - { - return relativePath; - } - - // Get the directory of the original script file, combine it - // with the given path and then resolve the absolute file path. - combinedPath = - Path.GetFullPath( - Path.Combine( - baseFilePath, - relativePath)); - } - catch (NotSupportedException e) - { - // Occurs if the path is incorrectly formatted for any reason. One - // instance where this occurred is when a user had curly double-quote - // characters in their source instead of normal double-quotes. - resolveException = e; - } - catch (ArgumentException e) - { - // Occurs if the path contains invalid characters, specifically those - // listed in System.IO.Path.InvalidPathChars. - resolveException = e; - } - - if (resolveException != null) - { - this.logger.Write( - LogLevel.Error, - $"Could not resolve relative script path\r\n" + - $" baseFilePath = {baseFilePath}\r\n " + - $" relativePath = {relativePath}\r\n\r\n" + - $"{resolveException.ToString()}"); - } - - return combinedPath; - } - - /// - /// Takes a file-scheme URI with an escaped colon after the drive letter and unescapes only the colon. - /// VSCode sends escaped colons after drive letters, but System.Uri expects unescaped. - /// - /// The fully-escaped file-scheme URI string. - /// A file-scheme URI string with the drive colon unescaped. - private static string UnescapeDriveColon(string fileUri) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return fileUri; - } - - // Check here that we have something like "file:///C%3A/" as a prefix (caller must check the file:// part) - if (!(fileUri[7] == '/' && - char.IsLetter(fileUri[8]) && - fileUri[9] == '%' && - fileUri[10] == '3' && - fileUri[11] == 'A' && - fileUri[12] == '/')) - { - return fileUri; - } - - var sb = new StringBuilder(fileUri.Length - 2); // We lost "%3A" and gained ":", so length - 2 - sb.Append("file:///"); - sb.Append(fileUri[8]); // The drive letter - sb.Append(':'); - sb.Append(fileUri.Substring(12)); // The rest of the URI after the colon - - return sb.ToString(); - } - - /// - /// Converts a file system path into a DocumentUri required by Language Server Protocol. - /// - /// - /// When sending a document path to a LSP client, the path must be provided as a - /// DocumentUri in order to features like the Problems window or peek definition - /// to be able to open the specified file. - /// - /// - /// A file system path. Note: if the path is already a DocumentUri, it will be returned unmodified. - /// - /// The file system path encoded as a DocumentUri. - public static string ConvertPathToDocumentUri(string path) - { - const string fileUriPrefix = "file:"; - const string untitledUriPrefix = "untitled:"; - - // If path is already in document uri form, there is nothing to convert. - if (path.StartsWith(untitledUriPrefix, StringComparison.Ordinal) || - path.StartsWith(fileUriPrefix, StringComparison.Ordinal)) - { - return path; - } - - string escapedPath = Uri.EscapeDataString(path); - - // Max capacity of the StringBuilder will be the current escapedPath length - // plus extra chars for file:///. - var docUriStrBld = new StringBuilder(escapedPath.Length + fileUriPrefix.Length + 3); - docUriStrBld.Append(fileUriPrefix).Append("//"); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // VSCode file URIs on Windows need the drive letter to be lowercase. Search the - // original path for colon since a char search (no string culture involved) is - // faster than a string search. If found, then lowercase the associated drive letter. - if (path.Contains(':')) - { - // A valid, drive-letter based path converted to URI form needs to be prefixed - // with a / to indicate the path is an absolute path. - docUriStrBld.Append("/"); - int prefixLen = docUriStrBld.Length; - - docUriStrBld.Append(escapedPath); - - // Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \. - docUriStrBld.Replace("%5C", "/"); - - // Find the first colon after the "file:///" prefix, skipping the first char after - // the prefix since a Windows path cannot start with a colon. End the check at - // less than docUriStrBld.Length - 2 since we need to look-ahead two characters. - for (int i = prefixLen + 1; i < docUriStrBld.Length - 2; i++) - { - if ((docUriStrBld[i] == '%') && (docUriStrBld[i + 1] == '3') && (docUriStrBld[i + 2] == 'A')) - { - int driveLetterIndex = i - 1; - char driveLetter = char.ToLowerInvariant(docUriStrBld[driveLetterIndex]); - docUriStrBld.Replace(docUriStrBld[driveLetterIndex], driveLetter, driveLetterIndex, 1); - break; - } - } - } - else - { - // This is a Windows path without a drive specifier, must be either a relative or UNC path. - int prefixLen = docUriStrBld.Length; - - docUriStrBld.Append(escapedPath); - - // Uri.EscapeDataString goes a bit far, encoding \ chars. Also, VSCode wants / instead of \. - docUriStrBld.Replace("%5C", "/"); - - // The proper URI form for a UNC path is file://server/share. In the case of a UNC - // path, remove the path's leading // because the file:// prefix already provides it. - if ((docUriStrBld.Length > prefixLen + 1) && - (docUriStrBld[prefixLen] == '/') && - (docUriStrBld[prefixLen + 1] == '/')) - { - docUriStrBld.Remove(prefixLen, 2); - } - } - } - else - { - // On non-Windows systems, append the escapedPath and undo the over-aggressive - // escaping of / done by Uri.EscapeDataString. - docUriStrBld.Append(escapedPath).Replace("%2F", "/"); - } - - if (!Utils.IsNetCore) - { - // ' is not encoded by Uri.EscapeDataString in Windows PowerShell 5.x. - // This is apparently a difference between .NET Framework and .NET Core. - docUriStrBld.Replace("'", "%27"); - } - - return docUriStrBld.ToString(); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Workspace/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices/Workspace/WorkspaceFileSystemWrapper.cs deleted file mode 100644 index d90d0b4ec..000000000 --- a/src/PowerShellEditorServices/Workspace/WorkspaceFileSystemWrapper.cs +++ /dev/null @@ -1,381 +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 Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; -using System.Security; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; - -namespace Microsoft.PowerShell.EditorServices -{ - - /// - /// A FileSystem wrapper class which only returns files and directories that the consumer is interested in, - /// with a maximum recursion depth and silently ignores most file system errors. Typically this is used by the - /// Microsoft.Extensions.FileSystemGlobbing library. - /// - public class WorkspaceFileSystemWrapperFactory - { - private readonly DirectoryInfoBase _rootDirectory; - private readonly string[] _allowedExtensions; - private readonly bool _ignoreReparsePoints; - - /// - /// Gets the maximum depth of the directories that will be searched - /// - internal int MaxRecursionDepth { get; } - - /// - /// Gets the logging facility - /// - internal ILogger Logger { get; } - - /// - /// Gets the directory where the factory is rooted. Only files and directories at this level, or deeper, will be visible - /// by the wrapper - /// - public DirectoryInfoBase RootDirectory - { - get { return _rootDirectory; } - } - - /// - /// Creates a new FileWrapper Factory - /// - /// The path to the root directory for the factory. - /// The maximum directory depth. - /// An array of file extensions that will be visible from the factory. For example [".ps1", ".psm1"] - /// Whether objects which are Reparse Points should be ignored. https://docs.microsoft.com/en-us/windows/desktop/fileio/reparse-points - /// An ILogger implementation used for writing log messages. - public WorkspaceFileSystemWrapperFactory(String rootPath, int recursionDepthLimit, string[] allowedExtensions, bool ignoreReparsePoints, ILogger logger) - { - MaxRecursionDepth = recursionDepthLimit; - _rootDirectory = new WorkspaceFileSystemDirectoryWrapper(this, new DirectoryInfo(rootPath), 0); - _allowedExtensions = allowedExtensions; - _ignoreReparsePoints = ignoreReparsePoints; - Logger = logger; - } - - /// - /// Creates a wrapped object from . - /// - internal DirectoryInfoBase CreateDirectoryInfoWrapper(DirectoryInfo dirInfo, int depth) => - new WorkspaceFileSystemDirectoryWrapper(this, dirInfo, depth >= 0 ? depth : 0); - - /// - /// Creates a wrapped object from . - /// - internal FileInfoBase CreateFileInfoWrapper(FileInfo fileInfo, int depth) => - new WorkspaceFileSystemFileInfoWrapper(this, fileInfo, depth >= 0 ? depth : 0); - - /// - /// Enumerates all objects in the specified directory and ignores most errors - /// - internal IEnumerable SafeEnumerateFileSystemInfos(DirectoryInfo dirInfo) - { - // Find the subdirectories - string[] subDirs; - try - { - subDirs = Directory.GetDirectories(dirInfo.FullName, "*", SearchOption.TopDirectoryOnly); - } - catch (DirectoryNotFoundException e) - { - Logger.WriteHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to it being an invalid path", - e); - - yield break; - } - catch (PathTooLongException e) - { - Logger.WriteHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to the path being too long", - e); - - yield break; - } - catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) - { - Logger.WriteHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to the path not being accessible", - e); - - yield break; - } - catch (Exception e) - { - Logger.WriteHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to an exception", - e); - - yield break; - } - foreach (string dirPath in subDirs) - { - var subDirInfo = new DirectoryInfo(dirPath); - if (_ignoreReparsePoints && (subDirInfo.Attributes & FileAttributes.ReparsePoint) != 0) { continue; } - yield return subDirInfo; - } - - // Find the files - string[] filePaths; - try - { - filePaths = Directory.GetFiles(dirInfo.FullName, "*", SearchOption.TopDirectoryOnly); - } - catch (DirectoryNotFoundException e) - { - Logger.WriteHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to it being an invalid path", - e); - - yield break; - } - catch (PathTooLongException e) - { - Logger.WriteHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to the path being too long", - e); - - yield break; - } - catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) - { - Logger.WriteHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to the path not being accessible", - e); - - yield break; - } - catch (Exception e) - { - Logger.WriteHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to an exception", - e); - - yield break; - } - foreach (string filePath in filePaths) - { - var fileInfo = new FileInfo(filePath); - if (_allowedExtensions == null || _allowedExtensions.Length == 0) { yield return fileInfo; continue; } - if (_ignoreReparsePoints && (fileInfo.Attributes & FileAttributes.ReparsePoint) != 0) { continue; } - foreach (string extension in _allowedExtensions) - { - if (fileInfo.Extension == extension) { yield return fileInfo; break; } - } - } - } - } - - /// - /// Wraps an instance of and provides implementation of - /// . - /// Based on https://github.com/aspnet/Extensions/blob/c087cadf1dfdbd2b8785ef764e5ef58a1a7e5ed0/src/FileSystemGlobbing/src/Abstractions/DirectoryInfoWrapper.cs - /// - public class WorkspaceFileSystemDirectoryWrapper : DirectoryInfoBase - { - private readonly DirectoryInfo _concreteDirectoryInfo; - private readonly bool _isParentPath; - private readonly WorkspaceFileSystemWrapperFactory _fsWrapperFactory; - private readonly int _depth; - - /// - /// Initializes an instance of . - /// - public WorkspaceFileSystemDirectoryWrapper(WorkspaceFileSystemWrapperFactory factory, DirectoryInfo directoryInfo, int depth) - { - _concreteDirectoryInfo = directoryInfo; - _isParentPath = (depth == 0); - _fsWrapperFactory = factory; - _depth = depth; - } - - /// - public override IEnumerable EnumerateFileSystemInfos() - { - if (!_concreteDirectoryInfo.Exists || _depth >= _fsWrapperFactory.MaxRecursionDepth) { yield break; } - foreach (FileSystemInfo fileSystemInfo in _fsWrapperFactory.SafeEnumerateFileSystemInfos(_concreteDirectoryInfo)) - { - switch (fileSystemInfo) - { - case DirectoryInfo dirInfo: - yield return _fsWrapperFactory.CreateDirectoryInfoWrapper(dirInfo, _depth + 1); - break; - case FileInfo fileInfo: - yield return _fsWrapperFactory.CreateFileInfoWrapper(fileInfo, _depth); - break; - default: - // We should NEVER get here, but if we do just continue on - break; - } - } - } - - /// - /// Returns an instance of that represents a subdirectory. - /// - /// - /// If equals '..', this returns the parent directory. - /// - /// The directory name. - /// The directory - public override DirectoryInfoBase GetDirectory(string name) - { - bool isParentPath = string.Equals(name, "..", StringComparison.Ordinal); - - if (isParentPath) { return ParentDirectory; } - - var dirs = _concreteDirectoryInfo.GetDirectories(name); - - if (dirs.Length == 1) { return _fsWrapperFactory.CreateDirectoryInfoWrapper(dirs[0], _depth + 1); } - if (dirs.Length == 0) { return null; } - // This shouldn't happen. The parameter name isn't supposed to contain wild card. - throw new InvalidOperationException( - string.Format( - System.Globalization.CultureInfo.CurrentCulture, - "More than one sub directories are found under {0} with name {1}.", - _concreteDirectoryInfo.FullName, name)); - } - - /// - public override FileInfoBase GetFile(string name) => _fsWrapperFactory.CreateFileInfoWrapper(new FileInfo(Path.Combine(_concreteDirectoryInfo.FullName, name)), _depth); - - /// - public override string Name => _isParentPath ? ".." : _concreteDirectoryInfo.Name; - - /// - /// Returns the full path to the directory. - /// - public override string FullName => _concreteDirectoryInfo.FullName; - - /// - /// Safely calculates the parent of this directory, swallowing most errors. - /// - private DirectoryInfoBase SafeParentDirectory() - { - try - { - return _fsWrapperFactory.CreateDirectoryInfoWrapper(_concreteDirectoryInfo.Parent, _depth - 1); - } - catch (DirectoryNotFoundException e) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to it being an invalid path", - e); - } - catch (PathTooLongException e) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to the path being too long", - e); - } - catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to the path not being accessible", - e); - } - catch (Exception e) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to an exception", - e); - } - return null; - } - - /// - /// Returns the parent directory. (Overrides ). - /// - public override DirectoryInfoBase ParentDirectory - { - get - { - return SafeParentDirectory(); - } - } - } - - /// - /// Wraps an instance of to provide implementation of . - /// - public class WorkspaceFileSystemFileInfoWrapper : FileInfoBase - { - private readonly FileInfo _concreteFileInfo; - private readonly WorkspaceFileSystemWrapperFactory _fsWrapperFactory; - private readonly int _depth; - - /// - /// Initializes instance of to wrap the specified object . - /// - public WorkspaceFileSystemFileInfoWrapper(WorkspaceFileSystemWrapperFactory factory, FileInfo fileInfo, int depth) - { - _fsWrapperFactory = factory; - _concreteFileInfo = fileInfo; - _depth = depth; - } - - /// - /// The file name. (Overrides ). - /// - public override string Name => _concreteFileInfo.Name; - - /// - /// The full path of the file. (Overrides ). - /// - public override string FullName => _concreteFileInfo.FullName; - - /// - /// Safely calculates the parent of this file, swallowing most errors. - /// - private DirectoryInfoBase SafeParentDirectory() - { - try - { - return _fsWrapperFactory.CreateDirectoryInfoWrapper(_concreteFileInfo.Directory, _depth); - } - catch (DirectoryNotFoundException e) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to it being an invalid path", - e); - } - catch (PathTooLongException e) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to the path being too long", - e); - } - catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to the path not being accessible", - e); - } - catch (Exception e) - { - _fsWrapperFactory.Logger.WriteHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to an exception", - e); - } - return null; - } - - /// - /// The directory containing the file. (Overrides ). - /// - public override DirectoryInfoBase ParentDirectory - { - get - { - return SafeParentDirectory(); - } - } - } -} From 96fb6c2ea979e8b6d7d78eb9b77f17bbe40fcd86 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Tue, 1 Oct 2019 15:38:24 -0700 Subject: [PATCH 42/47] Remove engine from files and namespaces (#1048) --- .gitignore | 2 +- PowerShellEditorServices.build.ps1 | 12 +++++----- PowerShellEditorServices.sln | 2 +- docs/api/index.md | 2 +- docs/guide/extensions.md | 12 +++++----- .../Commands/Private/BuiltInCommands.ps1 | 4 ++-- .../Commands/Public/CmdletInterface.ps1 | 2 +- .../Commands/Public/Import-EditorCommand.ps1 | 8 +++---- .../PowerShellEditorServices.psm1 | 24 +++++++++---------- module/docs/Import-EditorCommand.md | 6 ++--- .../Cmdlets/VSCodeHtmlContentViewCommands.cs | 2 +- .../PowerShellEditorServices.VSCode.csproj | 2 +- .../.vscode/launch.json | 0 .../Hosting/EditorServicesHost.cs | 6 ++--- .../Hosting/HostDetails.cs | 2 +- .../Hosting/ProfilePaths.cs | 2 +- .../Hosting/PsesLogLevel.cs | 2 +- .../Logging/LoggerExtensions.cs | 2 +- .../PowerShellEditorServices.csproj} | 4 ++-- .../Server/NamedPipePsesLanguageServer.cs | 4 ++-- .../Server/PsesDebugServer.cs | 8 +++---- .../Server/PsesLanguageServer.cs | 10 ++++---- .../Server/StdioPsesLanguageServer.cs | 6 ++--- .../Services/Analysis/AnalysisService.cs | 4 ++-- .../Services/CodeLens/CodeLensData.cs | 2 +- .../Services/CodeLens/ICodeLensProvider.cs | 4 ++-- .../Services/CodeLens/ICodeLenses.cs | 4 ++-- .../CodeLens/PesterCodeLensProvider.cs | 8 +++---- .../CodeLens/ReferencesCodeLensProvider.cs | 9 ++++--- .../DebugAdapter/DebugEventHandlerService.cs | 6 ++--- .../Services/DebugAdapter/DebugService.cs | 10 ++++---- .../DebugAdapter/DebugStateService.cs | 2 +- .../Debugging/BreakpointDetails.cs | 2 +- .../Debugging/BreakpointDetailsBase.cs | 8 +++---- .../Debugging/CommandBreakpointDetails.cs | 2 +- .../Debugging/DebuggerStoppedEventArgs.cs | 4 ++-- .../InvalidPowerShellExpressionException.cs | 4 ++-- .../Debugging/StackFrameDetails.cs | 8 +++---- .../Debugging/VariableContainerDetails.cs | 2 +- .../DebugAdapter/Debugging/VariableDetails.cs | 2 +- .../Debugging/VariableDetailsBase.cs | 2 +- .../DebugAdapter/Debugging/VariableScope.cs | 2 +- .../Handlers/BreakpointHandlers.cs | 10 ++++---- .../Handlers/ConfigurationDoneHandler.cs | 8 +++---- .../Handlers/DebugEvaluateHandler.cs | 6 ++--- .../Handlers/DebuggerActionHandlers.cs | 4 ++-- .../Handlers/DisconnectHandler.cs | 10 ++++---- .../Handlers/InitializeHandler.cs | 4 ++-- .../Handlers/LaunchAndAttachHandler.cs | 6 ++--- .../DebugAdapter/Handlers/ScopesHandler.cs | 6 ++--- .../Handlers/SetVariableHandler.cs | 6 ++--- .../DebugAdapter/Handlers/SourceHandler.cs | 2 +- .../Handlers/StackTraceHandler.cs | 6 ++--- .../DebugAdapter/Handlers/ThreadsHandler.cs | 2 +- .../DebugAdapter/Handlers/VariablesHandler.cs | 6 ++--- .../Console/ChoiceDetails.cs | 4 ++-- .../Console/ChoicePromptHandler.cs | 2 +- .../Console/CollectionFieldDetails.cs | 2 +- .../Console/ConsoleChoicePromptHandler.cs | 2 +- .../Console/ConsoleInputPromptHandler.cs | 2 +- .../PowerShellContext/Console/ConsoleProxy.cs | 2 +- .../Console/ConsoleReadLine.cs | 2 +- .../Console/CredentialFieldDetails.cs | 2 +- .../PowerShellContext/Console/FieldDetails.cs | 2 +- .../Console/IConsoleOperations.cs | 2 +- .../Console/InputPromptHandler.cs | 2 +- .../Console/PromptHandler.cs | 2 +- .../Console/TerminalChoicePromptHandler.cs | 2 +- .../Console/TerminalInputPromptHandler.cs | 2 +- .../Console/UnixConsoleOperations.cs | 2 +- .../Console/WindowsConsoleOperations.cs | 2 +- .../EditorOperationsService.cs | 8 +++---- .../PowerShellContext/ExtensionService.cs | 4 ++-- .../Extensions/EditorCommand.cs | 2 +- .../Extensions/EditorCommandAttribute.cs | 2 +- .../Extensions/EditorContext.cs | 4 ++-- .../Extensions/EditorObject.cs | 2 +- .../Extensions/EditorRequests.cs | 2 +- .../Extensions/EditorWindow.cs | 2 +- .../Extensions/EditorWorkspace.cs | 2 +- .../Extensions/FileContext.cs | 4 ++-- .../Extensions/IEditorOperations.cs | 4 ++-- .../Handlers/EvaluateHandler.cs | 4 ++-- .../Handlers/ExpandAliasHandler.cs | 4 ++-- .../Handlers/GetCommandHandler.cs | 4 ++-- .../Handlers/GetCommentHelpHandler.cs | 6 ++--- .../Handlers/GetVersionHandler.cs | 2 +- .../Handlers/IEvaluateHandler.cs | 2 +- .../Handlers/IGetCommentHelpHandler.cs | 2 +- .../Handlers/IGetPSHostProcessesHandler.cs | 2 +- .../Handlers/IGetRunspaceHandler.cs | 2 +- .../Handlers/IGetVersionHandler.cs | 4 ++-- .../IInvokeExtensionCommandHandler.cs | 2 +- .../Handlers/ITemplateHandlers.cs | 2 +- .../Handlers/InvokeExtensionCommandHandler.cs | 6 ++--- .../PSHostProcessAndRunspaceHandlers.cs | 4 ++-- .../Handlers/ShowHelpHandler.cs | 4 ++-- .../Handlers/TemplateHandlers.cs | 8 +++---- .../PowerShellContextService.cs | 10 ++++---- .../RemoteFileManagerService.cs | 6 ++--- .../Capabilities/DscBreakpointCapability.cs | 6 ++--- .../Session/ExecutionOptions.cs | 2 +- .../Session/ExecutionStatus.cs | 2 +- .../ExecutionStatusChangedEventArgs.cs | 4 ++-- .../Session/ExecutionTarget.cs | 2 +- .../Session/Host/EditorServicesPSHost.cs | 4 ++-- .../Host/EditorServicesPSHostUserInterface.cs | 4 ++-- .../Session/Host/IHostInput.cs | 2 +- .../Session/Host/IHostOutput.cs | 2 +- .../Session/Host/PromptEvents.cs | 2 +- .../Session/Host/PromptHandlers.cs | 2 +- .../Host/ProtocolPSHostUserInterface.cs | 2 +- .../Host/SimplePSHostRawUserInterface.cs | 2 +- .../Host/TerminalPSHostRawUserInterface.cs | 2 +- .../Host/TerminalPSHostUserInterface.cs | 2 +- .../Session/IPromptContext.cs | 2 +- .../Session/IRunspaceCapability.cs | 2 +- .../Session/IVersionSpecificOperations.cs | 2 +- .../Session/InvocationEventQueue.cs | 2 +- .../Session/LegacyReadLineContext.cs | 2 +- .../PowerShellContext/Session/OutputType.cs | 4 ++-- .../Session/OutputWrittenEventArgs.cs | 2 +- .../Session/PSReadLinePromptContext.cs | 2 +- .../Session/PSReadLineProxy.cs | 2 +- .../Session/PipelineExecutionRequest.cs | 2 +- .../Session/PowerShell5Operations.cs | 2 +- .../Session/PowerShellContextState.cs | 2 +- .../Session/PowerShellExecutionResult.cs | 2 +- .../Session/PowerShellVersionDetails.cs | 2 +- .../Session/ProgressDetails.cs | 2 +- .../PowerShellContext/Session/PromptNest.cs | 2 +- .../Session/PromptNestFrame.cs | 2 +- .../Session/PromptNestFrameType.cs | 2 +- .../Session/RunspaceChangedEventArgs.cs | 2 +- .../Session/RunspaceDetails.cs | 2 +- .../Session/RunspaceHandle.cs | 2 +- .../Session/SessionDetails.cs | 2 +- .../Session/SessionStateChangedEventArgs.cs | 2 +- .../Session/ThreadController.cs | 2 +- .../PowerShellContext/TemplateService.cs | 6 ++--- .../Utilities/CommandHelpers.cs | 2 +- .../Symbols/IDocumentSymbolProvider.cs | 4 ++-- .../Services/Symbols/IDocumentSymbols.cs | 4 ++-- .../Symbols/ParameterSetSignatures.cs | 4 ++-- .../Symbols/PesterDocumentSymbolProvider.cs | 4 ++-- .../Symbols/PsdDocumentSymbolProvider.cs | 4 ++-- .../Symbols/ScriptDocumentSymbolProvider.cs | 4 ++-- .../Services/Symbols/ScriptExtent.cs | 2 +- .../Services/Symbols/SymbolDetails.cs | 4 ++-- .../Services/Symbols/SymbolReference.cs | 4 ++-- .../Services/Symbols/SymbolType.cs | 2 +- .../Services/Symbols/SymbolsService.cs | 10 ++++---- .../Services/Symbols/Vistors/AstOperations.cs | 4 ++-- .../Symbols/Vistors/FindCommandVisitor.cs | 12 +++++----- .../Symbols/Vistors/FindDeclarationVisitor.cs | 2 +- .../Symbols/Vistors/FindDotSourcedVisitor.cs | 2 +- .../Symbols/Vistors/FindReferencesVisitor.cs | 2 +- .../Symbols/Vistors/FindSymbolVisitor.cs | 2 +- .../Symbols/Vistors/FindSymbolsVisitor.cs | 2 +- .../Symbols/Vistors/FindSymbolsVisitor2.cs | 2 +- .../Services/TextDocument/BufferPosition.cs | 2 +- .../Services/TextDocument/BufferRange.cs | 2 +- .../TextDocument/CompletionResults.cs | 2 +- .../Services/TextDocument/FileChange.cs | 2 +- .../Services/TextDocument/FilePosition.cs | 2 +- .../Services/TextDocument/FoldingReference.cs | 2 +- .../Handlers/CodeActionHandler.cs | 6 ++--- .../TextDocument/Handlers/CodeLensHandlers.cs | 10 ++++---- .../Handlers/CompletionHandler.cs | 10 ++++---- .../Handlers/DefinitionHandler.cs | 8 +++---- .../Handlers/DocumentHighlightHandler.cs | 10 ++++---- .../Handlers/DocumentSymbolHandler.cs | 10 ++++---- .../Handlers/FoldingRangeHandler.cs | 6 ++--- .../Handlers/FormattingHandlers.cs | 4 ++-- .../TextDocument/Handlers/HoverHandler.cs | 8 +++---- .../Handlers/ReferencesHandler.cs | 8 +++---- .../Handlers/SignatureHelpHandler.cs | 8 +++---- .../Handlers/TextDocumentHandler.cs | 6 ++--- .../Services/TextDocument/ScriptFile.cs | 4 ++-- .../Services/TextDocument/ScriptFileMarker.cs | 2 +- .../Services/TextDocument/ScriptRegion.cs | 2 +- .../Services/TextDocument/TokenOperations.cs | 2 +- .../Workspace/ConfigurationService.cs | 4 ++-- .../Handlers/ConfigurationHandler.cs | 6 ++--- .../Handlers/WorkspaceSymbolsHandler.cs | 8 +++---- .../Workspace/LanguageServerSettings.cs | 4 ++-- .../Workspace/WorkspaceFileSystemWrapper.cs | 4 ++-- .../Services/Workspace/WorkspaceService.cs | 6 ++--- .../Utility/AsyncLock.cs | 0 .../Utility/AsyncQueue.cs | 0 .../Utility/AsyncUtils.cs | 0 .../Utility/Extensions.cs | 0 .../Utility/IScriptExtentExtensions.cs | 2 +- .../Utility/LspDebugUtils.cs | 2 +- .../Utility/PathUtils.cs | 0 .../Utility/Validate.cs | 0 .../Utility/VersionUtils.cs | 0 .../LanguageServerProtocolMessageTests.cs | 2 +- .../PowerShellEditorServices.Test.E2E.csproj | 2 +- 199 files changed, 378 insertions(+), 379 deletions(-) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/.vscode/launch.json (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Hosting/EditorServicesHost.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Hosting/HostDetails.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Hosting/ProfilePaths.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Hosting/PsesLogLevel.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Logging/LoggerExtensions.cs (94%) rename src/{PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj => PowerShellEditorServices/PowerShellEditorServices.csproj} (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Server/NamedPipePsesLanguageServer.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Server/PsesDebugServer.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Server/PsesLanguageServer.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Server/StdioPsesLanguageServer.cs (90%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Analysis/AnalysisService.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/CodeLens/CodeLensData.cs (86%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/CodeLens/ICodeLensProvider.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/CodeLens/ICodeLenses.cs (88%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/CodeLens/PesterCodeLensProvider.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/CodeLens/ReferencesCodeLensProvider.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/DebugEventHandlerService.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/DebugService.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/DebugStateService.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/BreakpointDetails.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs (84%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs (90%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/StackFrameDetails.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/VariableContainerDetails.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/VariableDetails.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/VariableDetailsBase.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Debugging/VariableScope.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/BreakpointHandlers.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/DisconnectHandler.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/InitializeHandler.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/ScopesHandler.cs (87%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/SetVariableHandler.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/SourceHandler.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/StackTraceHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/ThreadsHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/DebugAdapter/Handlers/VariablesHandler.cs (89%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/ChoiceDetails.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/ChoicePromptHandler.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/CollectionFieldDetails.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/ConsoleProxy.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/ConsoleReadLine.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/CredentialFieldDetails.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/FieldDetails.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/IConsoleOperations.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/InputPromptHandler.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/PromptHandler.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/UnixConsoleOperations.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Console/WindowsConsoleOperations.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/EditorOperationsService.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/ExtensionService.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorCommand.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorContext.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorObject.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorRequests.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorWindow.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/EditorWorkspace.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/FileContext.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Extensions/IEditorOperations.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/EvaluateHandler.cs (90%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/GetCommandHandler.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/GetVersionHandler.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/IEvaluateHandler.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/IGetVersionHandler.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/ITemplateHandlers.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs (88%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/ShowHelpHandler.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Handlers/TemplateHandlers.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/PowerShellContextService.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/RemoteFileManagerService.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/ExecutionOptions.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/ExecutionStatus.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/ExecutionTarget.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/IHostInput.cs (89%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/IHostOutput.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/PromptEvents.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/PromptHandlers.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/IPromptContext.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/IRunspaceCapability.cs (78%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/IVersionSpecificOperations.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/InvocationEventQueue.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/LegacyReadLineContext.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/OutputType.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PSReadLinePromptContext.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PSReadLineProxy.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PipelineExecutionRequest.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PowerShell5Operations.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PowerShellContextState.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PowerShellExecutionResult.cs (92%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PowerShellVersionDetails.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/ProgressDetails.cs (91%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PromptNest.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PromptNestFrame.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/PromptNestFrameType.cs (80%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/RunspaceDetails.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/RunspaceHandle.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/SessionDetails.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Session/ThreadController.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/TemplateService.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/PowerShellContext/Utilities/CommandHelpers.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/IDocumentSymbolProvider.cs (84%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/IDocumentSymbols.cs (88%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/ParameterSetSignatures.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/PesterDocumentSymbolProvider.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/PsdDocumentSymbolProvider.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/ScriptDocumentSymbolProvider.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/ScriptExtent.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/SymbolDetails.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/SymbolReference.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/SymbolType.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/SymbolsService.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/AstOperations.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindCommandVisitor.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindDeclarationVisitor.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindDotSourcedVisitor.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindReferencesVisitor.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindSymbolVisitor.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindSymbolsVisitor.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Symbols/Vistors/FindSymbolsVisitor2.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/BufferPosition.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/BufferRange.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/CompletionResults.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/FileChange.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/FilePosition.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/FoldingReference.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/CodeActionHandler.cs (96%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/CodeLensHandlers.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/CompletionHandler.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/DefinitionHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/DocumentHighlightHandler.cs (90%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/DocumentSymbolHandler.cs (95%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/FoldingRangeHandler.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/FormattingHandlers.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/HoverHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/ReferencesHandler.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/SignatureHelpHandler.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/Handlers/TextDocumentHandler.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/ScriptFile.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/ScriptFileMarker.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/ScriptRegion.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/TextDocument/TokenOperations.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Workspace/ConfigurationService.cs (74%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Workspace/Handlers/ConfigurationHandler.cs (97%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs (94%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Workspace/LanguageServerSettings.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Workspace/WorkspaceFileSystemWrapper.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Services/Workspace/WorkspaceService.cs (99%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/AsyncLock.cs (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/AsyncQueue.cs (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/AsyncUtils.cs (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/Extensions.cs (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/IScriptExtentExtensions.cs (93%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/LspDebugUtils.cs (98%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/PathUtils.cs (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/Validate.cs (100%) rename src/{PowerShellEditorServices.Engine => PowerShellEditorServices}/Utility/VersionUtils.cs (100%) diff --git a/.gitignore b/.gitignore index be64ce05c..8d18bc259 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ docs/metadata/ *.zip # Generated build info file -src/PowerShellEditorServices.Engine/Hosting/BuildInfo.cs +src/PowerShellEditorServices/Hosting/BuildInfo.cs # quickbuild.exe /VersionGeneratingLogs/ diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 9c4f77faa..aa32c403e 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -27,7 +27,7 @@ $script:ModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices/bin/" $script:VSCodeModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices.VSCode/bin/" $script:WindowsPowerShellFrameworkTarget = 'net461' $script:NetFrameworkPlatformId = 'win' -$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices.Engine", "Hosting", "BuildInfo.cs") +$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices", "Hosting", "BuildInfo.cs") $script:PSCoreModulePath = $null @@ -50,7 +50,7 @@ Schema is: #> $script:RequiredBuildAssets = @{ $script:ModuleBinPath = @{ - 'PowerShellEditorServices.Engine' = @( + 'PowerShellEditorServices' = @( 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', 'publish/Microsoft.Extensions.DependencyInjection.dll', 'publish/Microsoft.Extensions.FileSystemGlobbing.dll', @@ -58,8 +58,8 @@ $script:RequiredBuildAssets = @{ 'publish/Microsoft.Extensions.Logging.dll', 'publish/Microsoft.Extensions.Options.dll', 'publish/Microsoft.Extensions.Primitives.dll', - 'publish/Microsoft.PowerShell.EditorServices.Engine.dll', - 'publish/Microsoft.PowerShell.EditorServices.Engine.pdb', + 'publish/Microsoft.PowerShell.EditorServices.dll', + 'publish/Microsoft.PowerShell.EditorServices.pdb', 'publish/Newtonsoft.Json.dll', 'publish/OmniSharp.Extensions.JsonRpc.dll', 'publish/OmniSharp.Extensions.LanguageProtocol.dll', @@ -304,7 +304,7 @@ task CreateBuildInfo -Before Build { [string]$buildTime = [datetime]::Now.ToString("s", [System.Globalization.CultureInfo]::InvariantCulture) $buildInfoContents = @" -namespace Microsoft.PowerShell.EditorServices.Engine.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { public static class BuildInfo { @@ -319,7 +319,7 @@ namespace Microsoft.PowerShell.EditorServices.Engine.Hosting } task Build { - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Engine\PowerShellEditorServices.Engine.csproj -f $script:TargetPlatform } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f $script:TargetPlatform } exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } } diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index 91156418d..30c954422 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -22,7 +22,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.VSCode", "src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj", "{3B38E8DA-8BFF-4264-AF16-47929E6398A3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Engine", "src\PowerShellEditorServices.Engine\PowerShellEditorServices.Engine.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.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 diff --git a/docs/api/index.md b/docs/api/index.md index 94c6a5d4a..c53ce056c 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -20,7 +20,7 @@ the PowerShell debugger. Use the @Microsoft.PowerShell.EditorServices.Console.ConsoleService to provide interactive console support in the user's editor. -Use the @Microsoft.PowerShell.EditorServices.Engine.Services.ExtensionService to allow +Use the @Microsoft.PowerShell.EditorServices.Services.ExtensionService to allow the user to extend the host editor with new capabilities using PowerShell code. The core of all the services is the @Microsoft.PowerShell.EditorServices.PowerShellContext diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 66953aa68..b37c032a6 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -9,7 +9,7 @@ uses PowerShell Editor Services. ### Introducing `$psEditor` The entry point for the PowerShell Editor Services extensibility model is the `$psEditor` -object of the type @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject. For +object of the type @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorObject. For those familiar with the PowerShell ISE's `$psISE` object, the `$psEditor` object is very similar. The primary difference is that this model has been generalized to work against any editor which leverages PowerShell Editor Services for its PowerShell editing experience. @@ -19,7 +19,7 @@ any editor which leverages PowerShell Editor Services for its PowerShell editing > please file an issue on our GitHub page. This object gives access to all of the high-level services in the current -editing session. For example, the @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorObject.Workspace +editing session. For example, the @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorObject.Workspace property gives access to the editor's workspace, allowing you to create or open files in the editor. @@ -79,17 +79,17 @@ Register-EditorCommand ` -ScriptBlock { Write-Output "My command's script block was invoked!" } ``` -### The @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext parameter +### The @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext parameter Your function, cmdlet, or ScriptBlock can optionally accept a single parameter -of type @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext which provides +of type @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext which provides information about the state of the host editor at the time your command was invoked. With this object you can easily perform operations like manipulatin the state of the user's active editor buffer or changing the current selection. The usual convention is that a `$context` parameter is added to your editor command's function. For now it is recommended that you fully specify the -type of the @Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext object +type of the @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext object so that you get full IntelliSense on your context parameter. Here is an example of using the `$context` parameter: @@ -99,7 +99,7 @@ Register-EditorCommand ` -Name "MyModule.MyEditorCommandWithContext" ` -DisplayName "My command with context usage" ` -ScriptBlock { - param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context) + param([Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext]$context) Write-Output "The user's cursor is on line $($context.CursorPosition.Line)!" } ``` diff --git a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 b/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 index e808cc16a..e56e44af7 100644 --- a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 +++ b/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 @@ -3,7 +3,7 @@ Register-EditorCommand ` -DisplayName 'Open Editor Profile' ` -SuppressOutput ` -ScriptBlock { - param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context) + param([Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext]$context) If (!(Test-Path -Path $Profile)) { New-Item -Path $Profile -ItemType File } $psEditor.Workspace.OpenFile($Profile) } @@ -13,7 +13,7 @@ Register-EditorCommand ` -DisplayName 'Open Profile from List (Current User)' ` -SuppressOutput ` -ScriptBlock { - param([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]$context) + param([Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext]$context) $Current = Split-Path -Path $profile -Leaf $List = @($Current,'Microsoft.VSCode_profile.ps1','Microsoft.PowerShell_profile.ps1','Microsoft.PowerShellISE_profile.ps1','Profile.ps1') | Select-Object -Unique diff --git a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 index efd5b481e..9018680d9 100644 --- a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 +++ b/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 @@ -47,7 +47,7 @@ function Register-EditorCommand { $commandArgs += $Function } - $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand -ArgumentList $commandArgs + $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommand -ArgumentList $commandArgs if ($psEditor.RegisterCommand($editorCommand)) { Write-Verbose "Registered new command '$Name'" diff --git a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 b/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 index 466d9368e..e57823a3f 100644 --- a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 +++ b/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 @@ -7,7 +7,7 @@ function Import-EditorCommand { <# .EXTERNALHELP ..\PowerShellEditorServices.Commands-help.xml #> - [OutputType([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand])] + [OutputType([Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommand])] [CmdletBinding(DefaultParameterSetName='ByCommand')] param( [Parameter(Position=0, @@ -75,7 +75,7 @@ function Import-EditorCommand { $commands = $Command | Get-Command -ErrorAction SilentlyContinue } } - $attributeType = [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommandAttribute] + $attributeType = [Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommandAttribute] foreach ($aCommand in $commands) { # Get the attribute from our command to get name info. $details = $aCommand.ScriptBlock.Attributes | Where-Object TypeId -eq $attributeType @@ -99,7 +99,7 @@ function Import-EditorCommand { } # Check for a context parameter. $contextParameter = $aCommand.Parameters.Values | - Where-Object ParameterType -eq ([Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorContext]) + Where-Object ParameterType -eq ([Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext]) # If one is found then add a named argument. Otherwise call the command directly. if ($contextParameter) { @@ -109,7 +109,7 @@ function Import-EditorCommand { $scriptBlock = [scriptblock]::Create($aCommand.Name) } - $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand @( + $editorCommand = New-Object Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommand @( <# commandName: #> $details.Name, <# displayName: #> $details.DisplayName, <# suppressOutput: #> $details.SuppressOutput, diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index 79a5dbd23..b37b6ee65 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -10,7 +10,7 @@ if ($PSEdition -eq 'Desktop') { Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.Principal.Windows.dll" } -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.Engine.dll" +Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.dll" function Start-EditorServicesHost { [CmdletBinding()] @@ -91,13 +91,13 @@ function Start-EditorServicesHost { $editorServicesHost = $null $hostDetails = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.HostDetails @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.HostDetails @( $HostName, $HostProfileId, (Microsoft.PowerShell.Utility\New-Object System.Version @($HostVersion))) $editorServicesHost = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServicesHost @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServicesHost @( $hostDetails, $BundledModulesPath, $EnableConsoleRepl.IsPresent, @@ -108,7 +108,7 @@ function Start-EditorServicesHost { # Build the profile paths using the root paths of the current $profile variable $profilePaths = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.ProfilePaths @( + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.ProfilePaths @( $hostDetails.ProfileId, [System.IO.Path]::GetDirectoryName($profile.AllUsersAllHosts), [System.IO.Path]::GetDirectoryName($profile.CurrentUserAllHosts)) @@ -116,32 +116,32 @@ function Start-EditorServicesHost { $editorServicesHost.StartLogging($LogPath, $LogLevel); $languageServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportConfig + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportConfig $debugServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportConfig + Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportConfig switch ($PSCmdlet.ParameterSetName) { "Stdio" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::Stdio - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::Stdio + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::Stdio + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::Stdio break } "NamedPipe" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe $languageServiceConfig.InOutPipeName = "$LanguageServiceNamedPipe" if ($DebugServiceNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe $debugServiceConfig.InOutPipeName = "$DebugServiceNamedPipe" } break } "NamedPipeSimplex" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe $languageServiceConfig.InPipeName = $LanguageServiceInNamedPipe $languageServiceConfig.OutPipeName = $LanguageServiceOutNamedPipe if ($DebugServiceInNamedPipe -and $DebugServiceOutNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Engine.Hosting.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe $debugServiceConfig.InPipeName = $DebugServiceInNamedPipe $debugServiceConfig.OutPipeName = $DebugServiceOutNamedPipe } diff --git a/module/docs/Import-EditorCommand.md b/module/docs/Import-EditorCommand.md index b04487595..cf90e8c55 100644 --- a/module/docs/Import-EditorCommand.md +++ b/module/docs/Import-EditorCommand.md @@ -30,7 +30,7 @@ The Import-EditorCommand function will search the specified module for functions Alternatively, you can specify command info objects (like those from the Get-Command cmdlet) to be processed directly. -To tag a command as an editor command, attach the attribute 'Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommandAttribute' to the function like you would with 'CmdletBindingAttribute'. The attribute accepts the named parameters 'Name', 'DisplayName', and 'SuppressOutput'. +To tag a command as an editor command, attach the attribute 'Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommandAttribute' to the function like you would with 'CmdletBindingAttribute'. The attribute accepts the named parameters 'Name', 'DisplayName', and 'SuppressOutput'. ## EXAMPLES @@ -55,7 +55,7 @@ Registers all editor commands that contain "Editor" in the name and return all s ```powershell function Invoke-MyEditorCommand { [CmdletBinding()] - [Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand(DisplayName='My Command', SuppressOutput)] + [Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommand(DisplayName='My Command', SuppressOutput)] param() end { ConvertTo-ScriptExtent -Offset 0 | Set-ScriptExtent -Text 'My Command!' @@ -145,7 +145,7 @@ You can pass commands to register as editor commands. ## OUTPUTS -### Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext.EditorCommand +### Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorCommand If the "PassThru" parameter is specified editor commands that were successfully registered will be returned. This function does not output to the pipeline otherwise. diff --git a/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs b/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs index d26162743..de767821c 100644 --- a/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs +++ b/src/PowerShellEditorServices.VSCode/Cmdlets/VSCodeHtmlContentViewCommands.cs @@ -7,7 +7,7 @@ using System.Management.Automation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.VSCode.CustomViews; using OmniSharp.Extensions.LanguageServer.Protocol.Server; diff --git a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj index 54a39becd..e1138cbe3 100644 --- a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj +++ b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj @@ -15,7 +15,7 @@ - + All diff --git a/src/PowerShellEditorServices.Engine/.vscode/launch.json b/src/PowerShellEditorServices/.vscode/launch.json similarity index 100% rename from src/PowerShellEditorServices.Engine/.vscode/launch.json rename to src/PowerShellEditorServices/.vscode/launch.json diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs rename to src/PowerShellEditorServices/Hosting/EditorServicesHost.cs index b92d9d487..18d5197bd 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs @@ -18,12 +18,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Server; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Server; +using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Utility; using Serilog; -namespace Microsoft.PowerShell.EditorServices.Engine.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { public enum EditorServicesHostStatus { diff --git a/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs b/src/PowerShellEditorServices/Hosting/HostDetails.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs rename to src/PowerShellEditorServices/Hosting/HostDetails.cs index ffd829087..cdbe96f94 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/HostDetails.cs +++ b/src/PowerShellEditorServices/Hosting/HostDetails.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { /// /// Contains details about the current host application (most diff --git a/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs b/src/PowerShellEditorServices/Hosting/ProfilePaths.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs rename to src/PowerShellEditorServices/Hosting/ProfilePaths.cs index 4f5e17dce..53eb714f1 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/ProfilePaths.cs +++ b/src/PowerShellEditorServices/Hosting/ProfilePaths.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; -namespace Microsoft.PowerShell.EditorServices.Engine.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { /// /// Provides profile path resolution behavior relative to the name diff --git a/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs b/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs rename to src/PowerShellEditorServices/Hosting/PsesLogLevel.cs index 4438fdd02..d1b85077d 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/PsesLogLevel.cs +++ b/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { public enum PsesLogLevel { diff --git a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs b/src/PowerShellEditorServices/Logging/LoggerExtensions.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs rename to src/PowerShellEditorServices/Logging/LoggerExtensions.cs index 785a72d83..036f978a6 100644 --- a/src/PowerShellEditorServices.Engine/Logging/LoggerExtensions.cs +++ b/src/PowerShellEditorServices/Logging/LoggerExtensions.cs @@ -7,7 +7,7 @@ using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Logging +namespace Microsoft.PowerShell.EditorServices.Logging { internal static class LoggerExtensions { diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj similarity index 92% rename from src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj rename to src/PowerShellEditorServices/PowerShellEditorServices.csproj index 3a51e5b13..18b163544 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -3,10 +3,10 @@ - PowerShell Editor Services Engine + PowerShell Editor Services Provides common PowerShell editor capabilities as a .NET library. netstandard2.0 - Microsoft.PowerShell.EditorServices.Engine + Microsoft.PowerShell.EditorServices Latest diff --git a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs rename to src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs index 4742bbe09..864373a30 100644 --- a/src/PowerShellEditorServices.Engine/Server/NamedPipePsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs @@ -11,10 +11,10 @@ using System.Security.AccessControl; using System.Security.Principal; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Hosting; +using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Server +namespace Microsoft.PowerShell.EditorServices.Server { internal class NamedPipePsesLanguageServer : PsesLanguageServer { diff --git a/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs rename to src/PowerShellEditorServices/Server/PsesDebugServer.cs index 1088cd145..b3bf2edba 100644 --- a/src/PowerShellEditorServices.Engine/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -8,13 +8,13 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Handlers; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Server +namespace Microsoft.PowerShell.EditorServices.Server { public class PsesDebugServer : IDisposable { @@ -50,7 +50,7 @@ public async Task StartAsync(IServiceProvider languageServerServiceProvider) .AddSingleton() .AddSingleton() .AddSingleton(); - + options .WithInput(_inputStream) .WithOutput(_outputStream); diff --git a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs rename to src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 7da6fc1e3..64bb10faa 100644 --- a/src/PowerShellEditorServices.Engine/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -12,13 +12,13 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Handlers; -using Microsoft.PowerShell.EditorServices.Engine.Hosting; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.LanguageServer.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Server +namespace Microsoft.PowerShell.EditorServices.Server { internal abstract class PsesLanguageServer { diff --git a/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs b/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs similarity index 90% rename from src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs rename to src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs index 0da4d961e..a2c699b5e 100644 --- a/src/PowerShellEditorServices.Engine/Server/StdioPsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs @@ -7,9 +7,9 @@ using System.IO; using System.Management.Automation.Host; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Hosting; +using Microsoft.PowerShell.EditorServices.Hosting; -namespace Microsoft.PowerShell.EditorServices.Engine.Server +namespace Microsoft.PowerShell.EditorServices.Server { internal class StdioPsesLanguageServer : PsesLanguageServer { @@ -31,7 +31,7 @@ internal StdioPsesLanguageServer( internalHost, profilePaths) { - + } protected override (Stream input, Stream output) GetInputOutputStreams() diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs rename to src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs index d36e497d2..89cd6bcb8 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs @@ -16,9 +16,9 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; using System.Collections.Concurrent; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Provides a high-level service for performing semantic analysis diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs b/src/PowerShellEditorServices/Services/CodeLens/CodeLensData.cs similarity index 86% rename from src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs rename to src/PowerShellEditorServices/Services/CodeLens/CodeLensData.cs index 8768e2555..15fff728d 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/CodeLensData.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses +namespace Microsoft.PowerShell.EditorServices.CodeLenses { /// /// Represents data expected back in an LSP CodeLens response. diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs rename to src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs index 06921c67c..f42525a19 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ICodeLensProvider.cs @@ -3,10 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses +namespace Microsoft.PowerShell.EditorServices.CodeLenses { /// /// Specifies the contract for a Code Lens provider. diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs b/src/PowerShellEditorServices/Services/CodeLens/ICodeLenses.cs similarity index 88% rename from src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs rename to src/PowerShellEditorServices/Services/CodeLens/ICodeLenses.cs index bafd13bbc..fd6e1eadf 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ICodeLenses.cs @@ -4,10 +4,10 @@ // using System.Collections.Generic; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses +namespace Microsoft.PowerShell.EditorServices.CodeLenses { /// /// Specifies the contract for an implementation of diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs rename to src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs index d2b7daaf4..d927dd90e 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs @@ -4,13 +4,13 @@ // using System.Collections.Generic; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses +namespace Microsoft.PowerShell.EditorServices.CodeLenses { internal class PesterCodeLensProvider : ICodeLensProvider { diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs rename to src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs index 2c1862d59..98af40532 100644 --- a/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -6,15 +6,14 @@ using System; using System.Collections.Generic; using System.Text; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.CodeLenses +namespace Microsoft.PowerShell.EditorServices.CodeLenses { /// /// Provides the "reference" code lens by extracting document symbols. diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs index e4410f439..da867637e 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugEventHandlerService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs @@ -5,13 +5,13 @@ using System.Management.Automation; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { internal class DebugEventHandlerService { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 64889ae5c..c8d566b12 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -14,12 +14,12 @@ using Microsoft.PowerShell.EditorServices.Utility; using System.Threading; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Provides a high-level service for interacting with the diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs index fd2ec00cb..c5b755358 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/DebugStateService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { internal class DebugStateService { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs index fb479dfb7..4e4ee6340 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetails.cs @@ -7,7 +7,7 @@ using System.Management.Automation; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Provides details about a breakpoint that is set in the diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs similarity index 84% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs index 3393bd007..061939836 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/BreakpointDetailsBase.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Provides details about a breakpoint that is set in the @@ -12,13 +12,13 @@ namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter public abstract class BreakpointDetailsBase { /// - /// Gets or sets a boolean indicator that if true, breakpoint could be set - /// (but not necessarily at the desired location). + /// Gets or sets a boolean indicator that if true, breakpoint could be set + /// (but not necessarily at the desired location). /// public bool Verified { get; set; } /// - /// Gets or set an optional message about the state of the breakpoint. This is shown to the user + /// Gets or set an optional message about the state of the breakpoint. This is shown to the user /// and can be used to explain why a breakpoint could not be verified. /// public string Message { get; set; } diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs index d181f3c92..2b7c65def 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/CommandBreakpointDetails.cs @@ -7,7 +7,7 @@ using System.Management.Automation; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Provides details about a command breakpoint that is set in the PowerShell debugger. diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs index 9b478afb0..26ea3519a 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/DebuggerStoppedEventArgs.cs @@ -3,11 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using System.Management.Automation; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Provides event arguments for the DebugService.DebuggerStopped event. diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs similarity index 90% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs index a708778f9..82597bdd4 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/InvalidPowerShellExpressionException.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Represents the exception that is thrown when an invalid expression is provided to the DebugService's SetVariable method. @@ -16,7 +16,7 @@ public class InvalidPowerShellExpressionException : Exception /// Initializes a new instance of the SetVariableExpressionException class. /// /// Message indicating why the expression is invalid. - public InvalidPowerShellExpressionException(string message) + public InvalidPowerShellExpressionException(string message) : base(message) { } diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs index c58f53623..f99476980 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs @@ -5,7 +5,7 @@ using System.Management.Automation; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Contains details pertaining to a single stack frame in @@ -56,7 +56,7 @@ public class StackFrameDetails public int? EndColumnNumber { get; internal set; } /// - /// Gets a boolean value indicating whether or not the stack frame is executing + /// Gets a boolean value indicating whether or not the stack frame is executing /// in script external to the current workspace root. /// public bool IsExternalCode { get; internal set; } @@ -106,9 +106,9 @@ static internal StackFrameDetails Create( string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath; int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0); - // TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add + // TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add // settings to control this feature. - //if (workspaceRootPath != null && + //if (workspaceRootPath != null && // invocationInfo != null && // !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase)) //{ diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs index 28d2df551..75d264039 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableContainerDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Container for variables that is not itself a variable per se. However given how diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs index 9d3e375b0..5e181e954 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Contains details pertaining to a variable in the current diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs index 0eb8c32ab..357ebbea3 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableDetailsBase.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetailsBase.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Defines the common details between a variable and a variable container such as a scope diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableScope.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableScope.cs index 411388951..2e4a013e9 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Debugging/VariableScope.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableScope.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter { /// /// Contains details pertaining to a variable scope in the current diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index 8b0d0962c..8b65c4eea 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -9,15 +9,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class SetFunctionBreakpointsHandler : ISetFunctionBreakpointsHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 354d723f4..03cdf7fde 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -6,14 +6,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ConfigurationDoneHandler : IConfigurationDoneHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs index a35133eb9..246621ce7 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs @@ -7,10 +7,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class DebugEvaluateHandler : IEvaluateHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs index 2ae5c932c..21c520881 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs @@ -8,11 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ContinueHandler : IContinueHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs index 7c5711e3f..61df26447 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/DisconnectHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs @@ -7,13 +7,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Server; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Server; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class DisconnectHandler : IDisconnectHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs index 0876fdff1..77861d443 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/InitializeHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs @@ -6,10 +6,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class InitializeHandler : IInitializeHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 77e62588a..1772d4a13 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -10,13 +10,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("launch")] interface IPsesLaunchHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs similarity index 87% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs index 22a33154c..02c989611 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ScopesHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs @@ -7,13 +7,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ScopesHandler : IScopesHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs index 542b3f36e..dbd4e0939 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SetVariableHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SetVariableHandler.cs @@ -9,14 +9,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class SetVariableHandler : ISetVariableHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SourceHandler.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SourceHandler.cs index 168682ca0..0b41345b3 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/SourceHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/SourceHandler.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class SourceHandler : ISourceHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs index 9b2263b24..57061c317 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/StackTraceHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs @@ -8,13 +8,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class StackTraceHandler : IStackTraceHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ThreadsHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ThreadsHandler.cs index 06c14c44c..800494a2a 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/ThreadsHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ThreadsHandler.cs @@ -8,7 +8,7 @@ using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ThreadsHandler : IThreadsHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs similarity index 89% rename from src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs rename to src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs index cf8003302..5854d7a58 100644 --- a/src/PowerShellEditorServices.Engine/Services/DebugAdapter/Handlers/VariablesHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs @@ -8,13 +8,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class VariablesHandler : IVariablesHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoiceDetails.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoiceDetails.cs index ece5bd4ae..d93d0daca 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoiceDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoiceDetails.cs @@ -6,11 +6,11 @@ using System; using System.Management.Automation.Host; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Contains the details about a choice that should be displayed - /// to the user. This class is meant to be serializable to the + /// to the user. This class is meant to be serializable to the /// user's UI. /// public class ChoiceDetails diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoicePromptHandler.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoicePromptHandler.cs index 9f9557920..ecfe1ffde 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ChoicePromptHandler.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Indicates the style of prompt to be displayed. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/CollectionFieldDetails.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/CollectionFieldDetails.cs index e9a6fb425..bfcf4cec8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CollectionFieldDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/CollectionFieldDetails.cs @@ -6,7 +6,7 @@ using System; using System.Collections; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Contains the details of an colleciton input field shown diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs index 976ff4533..bf209aeb0 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleChoicePromptHandler.cs @@ -6,7 +6,7 @@ using System.Linq; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a standard implementation of ChoicePromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs index be274122c..1c9187b4f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleInputPromptHandler.cs @@ -6,7 +6,7 @@ using System; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a standard implementation of InputPromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleProxy.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleProxy.cs index 00c2e13e6..cda8997fe 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleProxy.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleProxy.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides asynchronous implementations of the API's as well as diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleReadLine.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleReadLine.cs index 9f5648309..0c306944e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/ConsoleReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/ConsoleReadLine.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using System; using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/CredentialFieldDetails.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/CredentialFieldDetails.cs index edcbc2fd6..5c27a70f6 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/CredentialFieldDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/CredentialFieldDetails.cs @@ -7,7 +7,7 @@ using System.Management.Automation; using System.Security; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Contains the details of a PSCredential field shown diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/FieldDetails.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/FieldDetails.cs index 88274be04..3fe573659 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/FieldDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/FieldDetails.cs @@ -11,7 +11,7 @@ using System.Management.Automation.Host; using System.Reflection; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Contains the details of an input field shown from an diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/IConsoleOperations.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/IConsoleOperations.cs index c124c5cf4..2bfeb883c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/IConsoleOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/IConsoleOperations.cs @@ -7,7 +7,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides platform specific console utilities. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/InputPromptHandler.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/InputPromptHandler.cs index 442266a92..ed95b9120 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/InputPromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/InputPromptHandler.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a base implementation for IPromptHandler classes diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/PromptHandler.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/PromptHandler.cs index 1bcdec99e..97e8c4c6a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/PromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/PromptHandler.cs @@ -6,7 +6,7 @@ using System; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Defines an abstract base class for prompt handler implementations. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs index 9cc303bf4..f49c6ca39 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalChoicePromptHandler.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a standard implementation of ChoicePromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs index 245a036c2..dacb51f32 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/TerminalInputPromptHandler.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a standard implementation of InputPromptHandler diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs index 9d715561f..7df7b186e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/UnixConsoleOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/UnixConsoleOperations.cs @@ -9,7 +9,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using UnixConsoleEcho; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class UnixConsoleOperations : IConsoleOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Console/WindowsConsoleOperations.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Console/WindowsConsoleOperations.cs index c1118b584..69c6ac91e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Console/WindowsConsoleOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Console/WindowsConsoleOperations.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class WindowsConsoleOperations : IConsoleOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs index 5d42a56d6..1d6ddd247 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs @@ -3,14 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Engine.Handlers; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { internal class EditorOperationsService : IEditorOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs index 983d8354a..4399a5eb9 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/ExtensionService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs @@ -3,14 +3,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System; using System.Collections.Generic; using System.Management.Automation; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Provides a high-level service which enables PowerShell scripts diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorCommand.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorCommand.cs index 193878f2b..7f24d04a2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommand.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorCommand.cs @@ -5,7 +5,7 @@ using System.Management.Automation; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides details about a command that has been registered diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs index c43becca3..59aa71df3 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorCommandAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides an attribute that can be used to target PowerShell diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorContext.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorContext.cs index c2a1d0a95..144c7db53 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorContext.cs @@ -6,9 +6,9 @@ using System; using System.Linq; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides context for the host editor at the time of creation. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorObject.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorObject.cs index 3522debb4..e873cbe0d 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorObject.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorObject.cs @@ -6,7 +6,7 @@ using System; using System.Reflection; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides the entry point of the extensibility API, inserted into diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorRequests.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorRequests.cs index a4c293ced..bd639f249 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorRequests.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorRequests.cs @@ -5,7 +5,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { public class ExtensionCommandAddedNotification { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorWindow.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorWindow.cs index d69649510..f21a574ea 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWindow.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorWindow.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a PowerShell-facing API which allows scripts to diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorWorkspace.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorWorkspace.cs index 32670d74b..974d166e7 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/EditorWorkspace.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a PowerShell-facing API which allows scripts to diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/FileContext.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/FileContext.cs index 6ed9a64f2..43023de24 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/FileContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/FileContext.cs @@ -6,9 +6,9 @@ using System; using System.IO; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides context for a file that is open in the editor. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/IEditorOperations.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Extensions/IEditorOperations.cs index 2435930cd..15a167203 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Extensions/IEditorOperations.cs @@ -4,9 +4,9 @@ // using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides an interface that must be implemented by an editor diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/EvaluateHandler.cs similarity index 90% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/EvaluateHandler.cs index 04a8c01ab..f0579667c 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/EvaluateHandler.cs @@ -6,9 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class EvaluateHandler : IEvaluateHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs index 1f9677c46..207b3c33a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ExpandAliasHandler.cs @@ -8,11 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/expandAlias")] public interface IExpandAliasHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommandHandler.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommandHandler.cs index 0cffb060a..56c39f5b8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommandHandler.cs @@ -8,11 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/getCommand")] public interface IGetCommandHandler : IJsonRpcRequestHandler> { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs index e0f5289c7..1f79582a1 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs @@ -10,10 +10,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class GetCommentHelpHandler : IGetCommentHelpHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs index 03c73b7d4..ad8fd62d1 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class GetVersionHandler : IGetVersionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IEvaluateHandler.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IEvaluateHandler.cs index 90a7e18ee..b2836af9a 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IEvaluateHandler.cs @@ -6,7 +6,7 @@ using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("evaluate")] public interface IEvaluateHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs index 142d78e8f..599e13df5 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs @@ -7,7 +7,7 @@ using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/getCommentHelp")] public interface IGetCommentHelpHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs index 61b59623e..88df2a429 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetPSHostProcessesHandler.cs @@ -6,7 +6,7 @@ using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/getPSHostProcesses")] public interface IGetPSHostProcessesHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs index 039f60563..77237dee1 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetRunspaceHandler.cs @@ -6,7 +6,7 @@ using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/getRunspace")] public interface IGetRunspaceHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetVersionHandler.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetVersionHandler.cs index afaa4f4ab..1810435a2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IGetVersionHandler.cs @@ -3,11 +3,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/getVersion")] public interface IGetVersionHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs index 465bf1a5b..42e8b72ed 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/IInvokeExtensionCommandHandler.cs @@ -7,7 +7,7 @@ using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/invokeExtensionCommand")] public interface IInvokeExtensionCommandHandler : IJsonRpcNotificationHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ITemplateHandlers.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ITemplateHandlers.cs index 13f2cf4e3..798f0c9fe 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ITemplateHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ITemplateHandlers.cs @@ -6,7 +6,7 @@ using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/getProjectTemplates")] public interface IGetProjectTemplatesHandler : IJsonRpcRequestHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs similarity index 88% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs index 51d0b2e6a..355a3d3db 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/InvokeExtensionCommandHandler.cs @@ -6,11 +6,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.Embedded.MediatR; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class InvokeExtensionCommandHandler : IInvokeExtensionCommandHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs index 5337ffb6f..9f82983ad 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/PSHostProcessAndRunspaceHandlers.cs @@ -9,9 +9,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ShowHelpHandler.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ShowHelpHandler.cs index 348c1893b..897d6f4ae 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/ShowHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/ShowHelpHandler.cs @@ -7,11 +7,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.JsonRpc; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/showHelp")] public interface IShowHelpHandler : IJsonRpcNotificationHandler { } diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/TemplateHandlers.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Handlers/TemplateHandlers.cs index 59e4e16cb..210ff3ca3 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/TemplateHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/TemplateHandlers.cs @@ -7,10 +7,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class TemplateHandlers : IGetProjectTemplatesHandler, INewProjectFromTemplateHandler { @@ -41,7 +41,7 @@ await _templateService.GetAvailableTemplatesAsync( Templates = availableTemplates }; } - + return new GetProjectTemplatesResponse { NeedsModuleInstall = true, diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 3afac2843..c0effd3be 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -19,13 +19,13 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { using System.Management.Automation; - using Microsoft.PowerShell.EditorServices.Engine.Handlers; - using Microsoft.PowerShell.EditorServices.Engine.Hosting; - using Microsoft.PowerShell.EditorServices.Engine.Logging; - using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; + using Microsoft.PowerShell.EditorServices.Handlers; + using Microsoft.PowerShell.EditorServices.Hosting; + using Microsoft.PowerShell.EditorServices.Logging; + using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; /// /// Manages the lifetime and usage of a PowerShell session. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/RemoteFileManagerService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/RemoteFileManagerService.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/RemoteFileManagerService.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/RemoteFileManagerService.cs index cc32755cc..cca85a199 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/RemoteFileManagerService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/RemoteFileManagerService.cs @@ -4,8 +4,8 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -17,7 +17,7 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Manages files that are accessed from a remote PowerShell session. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs index 76a5c4d80..64e459ecb 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs @@ -6,11 +6,11 @@ using System.Linq; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using Microsoft.Extensions.Logging; - using Microsoft.PowerShell.EditorServices.Engine.Logging; - using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; + using Microsoft.PowerShell.EditorServices.Logging; + using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionOptions.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionOptions.cs index fa2fd0f1e..fba9736e0 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionOptions.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionOptions.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Defines options for the execution of a command. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatus.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatus.cs index daf6e499b..66caef00e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatus.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatus.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Enumerates the possible execution results that can occur after diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs index ffb4fb5de..f9abb1cbb 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionStatusChangedEventArgs.cs @@ -3,10 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// - /// Contains details about an executed + /// Contains details about an executed /// public class ExecutionStatusChangedEventArgs { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionTarget.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionTarget.cs index 79c8a860c..19b365f76 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ExecutionTarget.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ExecutionTarget.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Represents the different API's available for executing commands. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs index 43ab96e41..39604452b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs @@ -4,13 +4,13 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Hosting; +using Microsoft.PowerShell.EditorServices.Hosting; using System; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides an implementation of the PSHost class for the diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs index 8d4007725..685f812a8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHostUserInterface.cs @@ -15,9 +15,9 @@ using System.Threading; using System.Globalization; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides an implementation of the PSHostUserInterface class diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostInput.cs similarity index 89% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostInput.cs index d22da8cf9..cdce37129 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostInput.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostInput.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides methods for integrating with the host's input system. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostOutput.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostOutput.cs index 53c72b573..9b5565f37 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/IHostOutput.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/IHostOutput.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a simplified interface for writing output to a diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptEvents.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptEvents.cs index 99bfb6035..967830d9d 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptEvents.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptEvents.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { public class ShowChoicePromptRequest { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs index b433354fd..98d4905b5 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/PromptHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs index d3e79f54c..6053245c2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs index 4d13abd5f..701f48851 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/SimplePSHostRawUserInterface.cs @@ -7,7 +7,7 @@ using System.Management.Automation.Host; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides an simple implementation of the PSHostRawUserInterface class. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs index 02f11057c..c1a45fcb1 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostRawUserInterface.cs @@ -9,7 +9,7 @@ using System.Management.Automation.Host; using System.Threading; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides an implementation of the PSHostRawUserInterface class diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs index b57fcaec7..93e8173ad 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/TerminalPSHostUserInterface.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IPromptContext.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/IPromptContext.cs index d14611cee..2d1785670 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IPromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IPromptContext.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides methods for interacting with implementations of ReadLine. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IRunspaceCapability.cs similarity index 78% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/IRunspaceCapability.cs index a79d15c6b..466d80e3b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IRunspaceCapability.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IRunspaceCapability.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal interface IRunspaceCapability { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IVersionSpecificOperations.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/IVersionSpecificOperations.cs index d02c65df5..2b9fb1f6b 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/IVersionSpecificOperations.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/IVersionSpecificOperations.cs @@ -8,7 +8,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal interface IVersionSpecificOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/InvocationEventQueue.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/InvocationEventQueue.cs index 0c41bdc8b..b28f0e2d2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/InvocationEventQueue.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/InvocationEventQueue.cs @@ -12,7 +12,7 @@ using System.Threading; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/LegacyReadLineContext.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/LegacyReadLineContext.cs index 508ddb9c0..8f116a201 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/LegacyReadLineContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/LegacyReadLineContext.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class LegacyReadLineContext : IPromptContext { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputType.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputType.cs index 384d60820..92b5ef540 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputType.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputType.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Enumerates the types of output lines that will be sent @@ -12,7 +12,7 @@ namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext public enum OutputType { /// - /// A normal output line, usually written with the or Write-Host or + /// A normal output line, usually written with the or Write-Host or /// Write-Output cmdlets. /// Normal, diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs index 264576ad6..284dc5dc5 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/OutputWrittenEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides details about output that has been written to the diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index ce3c427f8..26b3cc5a7 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -10,7 +10,7 @@ using System; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using System.Management.Automation; using Microsoft.Extensions.Logging; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLineProxy.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLineProxy.cs index 548a0a2c6..c15f4c719 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PSReadLineProxy.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLineProxy.cs @@ -7,7 +7,7 @@ using System.Reflection; using Microsoft.Extensions.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class PSReadLineProxy { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PipelineExecutionRequest.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PipelineExecutionRequest.cs index dc9bb8962..21af0280f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PipelineExecutionRequest.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PipelineExecutionRequest.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal interface IPipelineExecutionRequest { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShell5Operations.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShell5Operations.cs index e6764f65f..2e7fedcf8 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShell5Operations.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShell5Operations.cs @@ -9,7 +9,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class PowerShell5Operations : IVersionSpecificOperations { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellContextState.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellContextState.cs index d5c46dcaa..c2742e270 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellContextState.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellContextState.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Enumerates the possible states for a PowerShellContext. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellExecutionResult.cs similarity index 92% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellExecutionResult.cs index f9261bbf9..1be771187 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellExecutionResult.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellExecutionResult.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Enumerates the possible execution results that can occur after diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellVersionDetails.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellVersionDetails.cs index 682c83228..6e1a1a32e 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PowerShellVersionDetails.cs @@ -8,7 +8,7 @@ using System.Collections; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Defines the possible enumeration values for the PowerShell process architecture. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProgressDetails.cs similarity index 91% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ProgressDetails.cs index f6afc5829..fc7fb2047 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ProgressDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProgressDetails.cs @@ -5,7 +5,7 @@ using System.Management.Automation; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides details about the progress of a particular activity. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNest.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNest.cs index 7150d46f6..380c4bb81 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNest.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNest.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using System; using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrame.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrame.cs index 05fbbe5fa..2d6a1a473 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrame.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrame.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { using System.Management.Automation; diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrameType.cs similarity index 80% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrameType.cs index 4f360edab..ec4d13977 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/PromptNestFrameType.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PromptNestFrameType.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { [Flags] internal enum PromptNestFrameType diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs index f1e558c89..77512c9c1 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceChangedEventArgs.cs @@ -5,7 +5,7 @@ using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Defines the set of actions that will cause the runspace to be changed. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceDetails.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceDetails.cs index 94dfee7a1..54f38cf34 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceDetails.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Specifies the possible types of a runspace. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceHandle.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceHandle.cs index 70b643b0c..650cce9d2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/RunspaceHandle.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/RunspaceHandle.cs @@ -7,7 +7,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Runspaces; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides a handle to the runspace that is managed by diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionDetails.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionDetails.cs index 8da235340..d30722d9f 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionDetails.cs @@ -8,7 +8,7 @@ using System.Collections; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides details about the current PowerShell session. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs index 9cbcf7765..70c40a7f2 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/SessionStateChangedEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides details about a change in state of a PowerShellContext. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ThreadController.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ThreadController.cs index fead192b6..4066d5117 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Session/ThreadController.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ThreadController.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides the ability to route PowerShell command invocations to a specific thread. diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/TemplateService.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/TemplateService.cs index fff0b60ba..ed0846481 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/TemplateService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/TemplateService.cs @@ -4,15 +4,15 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Handlers; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Linq; using System.Management.Automation; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Provides a service for listing PowerShell project templates and creating diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs index 4682d0dd4..ced8732f7 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Utilities/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides utility methods for working with PowerShell commands. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs similarity index 84% rename from src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs rename to src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs index 604917c0d..efb5af5d8 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs @@ -4,9 +4,9 @@ // using System.Collections.Generic; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Specifies the contract for a document symbols provider. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs b/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbols.cs similarity index 88% rename from src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs rename to src/PowerShellEditorServices/Services/Symbols/IDocumentSymbols.cs index 23c14d850..b1cd09c1b 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs +++ b/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbols.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Specifies the contract for an implementation of diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs b/src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs rename to src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs index 3fe325355..df61a0179 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/ParameterSetSignatures.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs @@ -6,9 +6,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Management.Automation; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// A class for containing the commandName, the command's diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs rename to src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs index 8d1425db2..1f754a235 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides an IDocumentSymbolProvider implementation for diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs rename to src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs index c5cb6099b..3a9d5b2a9 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides an IDocumentSymbolProvider implementation for diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs rename to src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs index 63416c516..0ad1f20ea 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides an IDocumentSymbolProvider implementation for diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs b/src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs rename to src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs index 00001f7dd..6abff2626 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs @@ -6,7 +6,7 @@ using System; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides a default IScriptExtent implementation diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs rename to src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs index d8866b024..a8272d375 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs @@ -6,9 +6,9 @@ using System.Diagnostics; using System.Management.Automation; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides detailed information for a given symbol. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs rename to src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs index 963ea8b81..02a808c2d 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs @@ -5,9 +5,9 @@ using System.Diagnostics; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// A class that holds the type, name, script extent, and source line of a symbol diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs rename to src/PowerShellEditorServices/Services/Symbols/SymbolType.cs index 9344e5ef3..29352e24b 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// A way to define symbols on a higher level diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs rename to src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs index 1bde8b9ff..f3c9c53b0 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs @@ -14,13 +14,13 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Provides a high-level service for performing code completion and diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs index 86179305e..4dc284340 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs @@ -13,10 +13,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides common operations for the syntax tree of a parsed script. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindCommandVisitor.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindCommandVisitor.cs index dcc45eb3b..6d033188b 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindCommandVisitor.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// The vistior used to find the commandAst of a specific location in an AST @@ -37,7 +37,7 @@ public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) if (currentLine.Length >= trueEndColumnNumber) { // Get the text left in the line after the command's extent - string remainingLine = + string remainingLine = currentLine.Substring( commandAst.Extent.EndColumnNumber); @@ -48,8 +48,8 @@ public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) // just after the last character in the command string or script line. int preTrimLength = remainingLine.Length; int postTrimLength = remainingLine.TrimStart().Length; - trueEndColumnNumber = - commandAst.Extent.EndColumnNumber + + trueEndColumnNumber = + commandAst.Extent.EndColumnNumber + (preTrimLength - postTrimLength) + 1; } @@ -70,12 +70,12 @@ public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) } /// - /// Is the position of the given location is in the range of the start + /// Is the position of the given location is in the range of the start /// of the first element to the character before the second element /// /// The script extent of the first element of the command ast /// The script extent of the second element of the command ast - /// True if the given position is in the range of the start of + /// True if the given position is in the range of the start of /// the first element to the character before the second element private bool IsPositionInExtent(IScriptExtent firstExtent, IScriptExtent secondExtent) { diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDeclarationVisitor.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindDeclarationVisitor.cs index e159af4c8..91f3671ff 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDeclarationVisitor.cs @@ -6,7 +6,7 @@ using System; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// The visitor used to find the definition of a symbol diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDotSourcedVisitor.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindDotSourcedVisitor.cs index 6caec1e49..11443e327 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDotSourcedVisitor.cs @@ -8,7 +8,7 @@ using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// The vistor used to find the dont sourced files in an AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindReferencesVisitor.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindReferencesVisitor.cs index 5a12dd366..7ca206dae 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindReferencesVisitor.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// The visitor used to find the references of a symbol in a script's AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolVisitor.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolVisitor.cs index 41eb8dc59..6d1629869 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolVisitor.cs @@ -5,7 +5,7 @@ using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// The visitor used to find the the symbol at a specfic location in the AST diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor.cs index 95808be91..c7f36544e 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// The visitor used to find all the symbols (function and class defs) in the AST. diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs rename to src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs index 2aedd1228..44f1560d4 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Symbols +namespace Microsoft.PowerShell.EditorServices.Services.Symbols { // TODO: Restore this when we figure out how to support multiple // PS versions in the new PSES-as-a-module world (issue #276) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs b/src/PowerShellEditorServices/Services/TextDocument/BufferPosition.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs rename to src/PowerShellEditorServices/Services/TextDocument/BufferPosition.cs index c7e6bef79..1cdf6cf67 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferPosition.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/BufferPosition.cs @@ -5,7 +5,7 @@ using System.Diagnostics; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Provides details about a position in a file buffer. All diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs b/src/PowerShellEditorServices/Services/TextDocument/BufferRange.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs rename to src/PowerShellEditorServices/Services/TextDocument/BufferRange.cs index 986f3702e..dbc577b05 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/BufferRange.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/BufferRange.cs @@ -6,7 +6,7 @@ using System; using System.Diagnostics; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Provides details about a range between two positions in diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs b/src/PowerShellEditorServices/Services/TextDocument/CompletionResults.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs rename to src/PowerShellEditorServices/Services/TextDocument/CompletionResults.cs index 308386d50..e267eb0a1 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/CompletionResults.cs @@ -10,7 +10,7 @@ using System.Text.RegularExpressions; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Provides the results of a single code completion request. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs b/src/PowerShellEditorServices/Services/TextDocument/FileChange.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs rename to src/PowerShellEditorServices/Services/TextDocument/FileChange.cs index f465f0826..c407ae881 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/FileChange.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/FileChange.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Contains details relating to a content change in an open file. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs b/src/PowerShellEditorServices/Services/TextDocument/FilePosition.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs rename to src/PowerShellEditorServices/Services/TextDocument/FilePosition.cs index 5b95905dd..8b0e4e970 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/FilePosition.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/FilePosition.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Provides details and operations for a buffer position in a diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs b/src/PowerShellEditorServices/Services/TextDocument/FoldingReference.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs rename to src/PowerShellEditorServices/Services/TextDocument/FoldingReference.cs index e0d5d7ac8..477c27d88 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/FoldingReference.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/FoldingReference.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// A class that holds the information for a foldable region of text in a document diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs similarity index 96% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs index 522e3ba1d..ade90f2b6 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -8,14 +8,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class CodeActionHandler : ICodeActionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs index 46d96cacd..e87b7595f 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -10,16 +10,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.CodeLenses; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.CodeLenses; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index da14ce998..1f2f2e09a 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -9,16 +9,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.PowerShellContext; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class CompletionHandler : ICompletionHandler, ICompletionResolveHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs index cd35f8ce1..64033c564 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs @@ -7,15 +7,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class DefinitionHandler : IDefinitionHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs similarity index 90% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs index ea1a2e11a..2790f8409 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -4,10 +4,10 @@ // using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Engine.Utility; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -16,7 +16,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class DocumentHighlightHandler : IDocumentHighlightHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs similarity index 95% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index ed354fb04..9e3ccfd78 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -11,16 +11,16 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class DocumentSymbolHandler : IDocumentSymbolHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs index 54e8dfbca..dbb554fb0 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FoldingRangeHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FoldingRangeHandler.cs @@ -7,13 +7,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class FoldingRangeHandler : IFoldingRangeHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs index bc93390c0..90243b9d4 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -6,12 +6,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class DocumentFormattingHandler : IDocumentFormattingHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs index cbc978694..8a107ed42 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs @@ -7,14 +7,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class HoverHandler : IHoverHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs index c5f73a41b..c72ce141e 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -7,15 +7,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { class ReferencesHandler : IReferencesHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs index 102e71410..eac0303ab 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -7,14 +7,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class SignatureHelpHandler : ISignatureHelpHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index d3b70c43a..375034533 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -8,8 +8,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; @@ -17,7 +17,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { class TextDocumentHandler : ITextDocumentSyncHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs rename to src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index 41563ec25..5b1b58bd5 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -9,10 +9,10 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Contains the details and contents of an open script file. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs rename to src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs index 0269c2d3d..9609b2cae 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs @@ -10,7 +10,7 @@ using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Utility; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Contains details for a code correction which can be applied from a ScriptFileMarker. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs rename to src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs index 0ffee20b2..f18fcd6c4 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs @@ -6,7 +6,7 @@ using System; using System.Management.Automation.Language; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// /// Contains details about a specific region of text in script file. diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs rename to src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs index 8a3985573..4b3c7f6ab 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/TokenOperations.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs @@ -8,7 +8,7 @@ using System.Text.RegularExpressions; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument +namespace Microsoft.PowerShell.EditorServices.Services.TextDocument { /// diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs b/src/PowerShellEditorServices/Services/Workspace/ConfigurationService.cs similarity index 74% rename from src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs rename to src/PowerShellEditorServices/Services/Workspace/ConfigurationService.cs index 2351adda1..e9d54359a 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/ConfigurationService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/ConfigurationService.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Engine.Services.Configuration; +using Microsoft.PowerShell.EditorServices.Services.Configuration; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { public class ConfigurationService { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs similarity index 97% rename from src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs rename to src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs index 5d2fc49d8..ae97d43ad 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -8,14 +8,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Configuration; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Configuration; using OmniSharp.Extensions.Embedded.MediatR; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ConfigurationHandler : IDidChangeConfigurationHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs rename to src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index d05a02dea..da1ba6f21 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -9,15 +9,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Services; -using Microsoft.PowerShell.EditorServices.Engine.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -namespace Microsoft.PowerShell.EditorServices.Engine.Handlers +namespace Microsoft.PowerShell.EditorServices.Handlers { public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs b/src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs rename to src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs index 894df1d65..2f70d5fe6 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs @@ -10,9 +10,9 @@ using System.Reflection; using System.Security; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Configuration +namespace Microsoft.PowerShell.EditorServices.Services.Configuration { public class LanguageServerSettings { diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs rename to src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs index 6b53b10df..89335db09 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs @@ -9,9 +9,9 @@ using System.Security; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Engine.Logging; +using Microsoft.PowerShell.EditorServices.Logging; -namespace Microsoft.PowerShell.EditorServices.Engine.Services.Workspace +namespace Microsoft.PowerShell.EditorServices.Services.Workspace { /// diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs similarity index 99% rename from src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs rename to src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index f8beeba70..cae7e36a7 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -13,10 +13,10 @@ using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Utility; -using Microsoft.PowerShell.EditorServices.Engine.Services.Workspace; -using Microsoft.PowerShell.EditorServices.Engine.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Services.Workspace; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Engine.Services +namespace Microsoft.PowerShell.EditorServices.Services { /// /// Manages a "workspace" of script files that are open for a particular diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs b/src/PowerShellEditorServices/Utility/AsyncLock.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/AsyncLock.cs rename to src/PowerShellEditorServices/Utility/AsyncLock.cs diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs b/src/PowerShellEditorServices/Utility/AsyncQueue.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/AsyncQueue.cs rename to src/PowerShellEditorServices/Utility/AsyncQueue.cs diff --git a/src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs b/src/PowerShellEditorServices/Utility/AsyncUtils.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/AsyncUtils.cs rename to src/PowerShellEditorServices/Utility/AsyncUtils.cs diff --git a/src/PowerShellEditorServices.Engine/Utility/Extensions.cs b/src/PowerShellEditorServices/Utility/Extensions.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/Extensions.cs rename to src/PowerShellEditorServices/Utility/Extensions.cs diff --git a/src/PowerShellEditorServices.Engine/Utility/IScriptExtentExtensions.cs b/src/PowerShellEditorServices/Utility/IScriptExtentExtensions.cs similarity index 93% rename from src/PowerShellEditorServices.Engine/Utility/IScriptExtentExtensions.cs rename to src/PowerShellEditorServices/Utility/IScriptExtentExtensions.cs index db7148df0..c48735c14 100644 --- a/src/PowerShellEditorServices.Engine/Utility/IScriptExtentExtensions.cs +++ b/src/PowerShellEditorServices/Utility/IScriptExtentExtensions.cs @@ -6,7 +6,7 @@ using System.Management.Automation.Language; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace Microsoft.PowerShell.EditorServices.Engine.Utility +namespace Microsoft.PowerShell.EditorServices.Utility { internal static class IScriptExtentExtensions { diff --git a/src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs similarity index 98% rename from src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs rename to src/PowerShellEditorServices/Utility/LspDebugUtils.cs index 914b7e2da..4423252ac 100644 --- a/src/PowerShellEditorServices.Engine/Utility/LspDebugUtils.cs +++ b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs @@ -1,5 +1,5 @@ using System; -using Microsoft.PowerShell.EditorServices.Engine.Services.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using OmniSharp.Extensions.DebugAdapter.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Utility diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/PathUtils.cs rename to src/PowerShellEditorServices/Utility/PathUtils.cs diff --git a/src/PowerShellEditorServices.Engine/Utility/Validate.cs b/src/PowerShellEditorServices/Utility/Validate.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/Validate.cs rename to src/PowerShellEditorServices/Utility/Validate.cs diff --git a/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs b/src/PowerShellEditorServices/Utility/VersionUtils.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs rename to src/PowerShellEditorServices/Utility/VersionUtils.cs diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 8654832a6..776b84a40 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -11,7 +11,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Engine.Handlers; +using Microsoft.PowerShell.EditorServices.Handlers; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Models; diff --git a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj index a2b3878f8..f2e1e0732 100644 --- a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj +++ b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj @@ -16,7 +16,7 @@ - + From 0ed893a0555f02a014a536c5ca60b0e0a7d739b8 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 3 Oct 2019 13:33:29 -0700 Subject: [PATCH 43/47] apply apt state for PS7 (#1051) --- .../Services/PowerShellContext/PowerShellContextService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index c0effd3be..a9bc845d7 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -39,7 +39,7 @@ public class PowerShellContextService : IDisposable, IHostSupportsInteractiveSes static PowerShellContextService() { // PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection - if (!VersionUtils.IsNetCore) + if (!VersionUtils.IsNetCore || VersionUtils.IsPS7) { MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); From 40aadf783f5300e2475b12ee9764ddf38f8a3082 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 3 Oct 2019 14:08:52 -0700 Subject: [PATCH 44/47] delete buildinfo --- src/PowerShellEditorServices/Hosting/BuildInfo.cs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/PowerShellEditorServices/Hosting/BuildInfo.cs diff --git a/src/PowerShellEditorServices/Hosting/BuildInfo.cs b/src/PowerShellEditorServices/Hosting/BuildInfo.cs deleted file mode 100644 index 020786ae0..000000000 --- a/src/PowerShellEditorServices/Hosting/BuildInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - public static class BuildInfo - { - public const string BuildVersion = ""; - public const string BuildOrigin = ""; - public static readonly System.DateTime? BuildTime = System.DateTime.Parse("2019-10-03T13:14:51"); - } -} From 1477dae1120d15e33a5cbe20442f9af83be92ae2 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 3 Oct 2019 14:09:28 -0700 Subject: [PATCH 45/47] implement powerShell/startDebugger (#1049) * implement powerShell/startDebugger * add line Co-Authored-By: Patrick Meinecke --- .../Server/PsesDebugServer.cs | 11 +++++++++- .../Services/DebugAdapter/DebugService.cs | 2 +- .../Handlers/ConfigurationDoneHandler.cs | 15 ++++++++++--- .../PowerShellContextService.cs | 22 +++++++++++++++++-- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index b3bf2edba..860dbc845 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -24,6 +24,8 @@ public class PsesDebugServer : IDisposable private IJsonRpcServer _jsonRpcServer; + private PowerShellContextService _powerShellContextService; + public PsesDebugServer( ILoggerFactory factory, Stream inputStream, @@ -42,8 +44,14 @@ public async Task StartAsync(IServiceProvider languageServerServiceProvider) options.Reciever = new DapReciever(); options.LoggerFactory = _loggerFactory; ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); + + // We need to let the PowerShell Context Service know that we are in a debug session + // so that it doesn't send the powerShell/startDebugger message. + _powerShellContextService = languageServerServiceProvider.GetService(); + _powerShellContextService.IsDebugServerActive = true; + options.Services = new ServiceCollection() - .AddSingleton(languageServerServiceProvider.GetService()) + .AddSingleton(_powerShellContextService) .AddSingleton(languageServerServiceProvider.GetService()) .AddSingleton(languageServerServiceProvider.GetService()) .AddSingleton(this) @@ -85,6 +93,7 @@ public async Task StartAsync(IServiceProvider languageServerServiceProvider) public void Dispose() { + _powerShellContextService.IsDebugServerActive = false; _jsonRpcServer.Dispose(); } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index c8d566b12..dbde29254 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -1191,7 +1191,7 @@ private string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLengt /// public event EventHandler DebuggerStopped; - private async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) + internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) { bool noScriptName = false; string localScriptPath = e.InvocationInfo.ScriptName; diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 03cdf7fde..addf829a1 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -73,9 +73,18 @@ public Task Handle(ConfigurationDoneArguments request if (_debugService.IsDebuggerStopped) { - // If this is an interactive session and there's a pending breakpoint, - // send that information along to the debugger client - _debugEventHandlerService.TriggerDebuggerStopped(_debugService.CurrentDebuggerStoppedEventArgs); + if (_debugService.CurrentDebuggerStoppedEventArgs != null) + { + // If this is an interactive session and there's a pending breakpoint, + // send that information along to the debugger client + _debugEventHandlerService.TriggerDebuggerStopped(_debugService.CurrentDebuggerStoppedEventArgs); + } + else + { + // If this is an interactive session and there's a pending breakpoint that has not been propagated through + // the debug service, fire the debug service's OnDebuggerStop event. + _debugService.OnDebuggerStopAsync(null, _powerShellContextService.CurrentDebuggerStopEventArgs); + } } } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index a9bc845d7..dfe550c18 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -137,6 +137,10 @@ public RunspaceDetails CurrentRunspace /// public string InitialWorkingDirectory { get; private set; } + internal bool IsDebugServerActive { get; set; } + + internal DebuggerStopEventArgs CurrentDebuggerStopEventArgs { get; private set; } + #endregion #region Constructors @@ -2296,6 +2300,15 @@ private void StartCommandLoopOnRunspaceAvailable() private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) { + // We maintain the current stop event args so that we can use it in the DebugServer to fire the "stopped" event + // when the DebugServer is fully started. + CurrentDebuggerStopEventArgs = e; + + if (!IsDebugServerActive) + { + _languageServer.SendNotification("powerShell/startDebugger"); + } + if (CurrentRunspace.Context == RunspaceContext.Original) { StartCommandLoopOnRunspaceAvailable(); @@ -2361,6 +2374,9 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) e.ResumeAction = localDebuggerStoppedTask.GetAwaiter().GetResult(); this.logger.LogTrace("Received debugger resume action " + e.ResumeAction.ToString()); + // Since we are no longer at a breakpoint, we set this to null. + CurrentDebuggerStopEventArgs = null; + // Notify listeners that the debugger has resumed this.DebuggerResumed?.Invoke(this, e.ResumeAction); @@ -2398,6 +2414,9 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) this.PromptNest, this.CurrentRunspace.Runspace)) { + // Since we are no longer at a breakpoint, we set this to null. + CurrentDebuggerStopEventArgs = null; + if (this.CurrentRunspace.Context == RunspaceContext.DebuggedRunspace) { // Notify listeners that the debugger has resumed @@ -2436,8 +2455,7 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) private void ConfigureRunspaceCapabilities(RunspaceDetails runspaceDetails) { - // TODO: Bring this back - //DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); + DscBreakpointCapability.CheckForCapability(this.CurrentRunspace, this, this.logger); } private void PushRunspace(RunspaceDetails newRunspaceDetails) From 4cccc9651712899a36535b7855028148946174bb Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 3 Oct 2019 14:10:36 -0700 Subject: [PATCH 46/47] Enable alias corrections (#1053) --- .../Workspace/LanguageServerSettings.cs | 97 +++++++++++-------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs b/src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs index 2f70d5fe6..dbdefc9de 100644 --- a/src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices/Services/Workspace/LanguageServerSettings.cs @@ -198,6 +198,7 @@ public CodeFormattingSettings(CodeFormattingSettings codeFormattingSettings) } } + public bool AutoCorrectAliases { get; set; } public CodeFormattingPreset Preset { get; set; } public bool OpenBraceOnSameLine { get; set; } public bool NewLineAfterOpenBrace { get; set; } @@ -257,50 +258,62 @@ public Hashtable GetPSSASettingsHashtable( private Hashtable GetCustomPSSASettingsHashtable(int tabSize, bool insertSpaces) { - return new Hashtable + var ruleConfigurations = new Hashtable { - {"IncludeRules", new string[] { - "PSPlaceCloseBrace", - "PSPlaceOpenBrace", - "PSUseConsistentWhitespace", - "PSUseConsistentIndentation", - "PSAlignAssignmentStatement" + { "PSPlaceOpenBrace", new Hashtable { + { "Enable", true }, + { "OnSameLine", OpenBraceOnSameLine }, + { "NewLineAfter", NewLineAfterOpenBrace }, + { "IgnoreOneLineBlock", IgnoreOneLineBlock } }}, - {"Rules", new Hashtable { - {"PSPlaceOpenBrace", new Hashtable { - {"Enable", true}, - {"OnSameLine", OpenBraceOnSameLine}, - {"NewLineAfter", NewLineAfterOpenBrace}, - {"IgnoreOneLineBlock", IgnoreOneLineBlock} - }}, - {"PSPlaceCloseBrace", new Hashtable { - {"Enable", true}, - {"NewLineAfter", NewLineAfterCloseBrace}, - {"IgnoreOneLineBlock", IgnoreOneLineBlock} - }}, - {"PSUseConsistentIndentation", new Hashtable { - {"Enable", true}, - {"IndentationSize", tabSize}, - {"PipelineIndentation", PipelineIndentationStyle }, - {"Kind", insertSpaces ? "space" : "tab"} - }}, - {"PSUseConsistentWhitespace", new Hashtable { - {"Enable", true}, - {"CheckOpenBrace", WhitespaceBeforeOpenBrace}, - {"CheckOpenParen", WhitespaceBeforeOpenParen}, - {"CheckOperator", WhitespaceAroundOperator}, - {"CheckSeparator", WhitespaceAfterSeparator}, - {"CheckInnerBrace", WhitespaceInsideBrace}, - {"CheckPipe", WhitespaceAroundPipe}, - }}, - {"PSAlignAssignmentStatement", new Hashtable { - {"Enable", true}, - {"CheckHashtable", AlignPropertyValuePairs} - }}, - {"PSUseCorrectCasing", new Hashtable { - {"Enable", UseCorrectCasing} - }}, - }} + { "PSPlaceCloseBrace", new Hashtable { + { "Enable", true }, + { "NewLineAfter", NewLineAfterCloseBrace }, + { "IgnoreOneLineBlock", IgnoreOneLineBlock } + }}, + { "PSUseConsistentIndentation", new Hashtable { + { "Enable", true }, + { "IndentationSize", tabSize }, + { "PipelineIndentation", PipelineIndentationStyle }, + { "Kind", insertSpaces ? "space" : "tab" } + }}, + { "PSUseConsistentWhitespace", new Hashtable { + { "Enable", true }, + { "CheckOpenBrace", WhitespaceBeforeOpenBrace }, + { "CheckOpenParen", WhitespaceBeforeOpenParen }, + { "CheckOperator", WhitespaceAroundOperator }, + { "CheckSeparator", WhitespaceAfterSeparator }, + { "CheckInnerBrace", WhitespaceInsideBrace }, + { "CheckPipe", WhitespaceAroundPipe }, + }}, + { "PSAlignAssignmentStatement", new Hashtable { + { "Enable", true }, + { "CheckHashtable", AlignPropertyValuePairs } + }}, + { "PSUseCorrectCasing", new Hashtable { + { "Enable", UseCorrectCasing } + }}, + }; + + if (AutoCorrectAliases) + { + // Empty hashtable required to activate the rule, + // since PSAvoidUsingCmdletAliases inherits from IScriptRule and not ConfigurableRule + ruleConfigurations.Add("PSAvoidUsingCmdletAliases", new Hashtable()); + } + + return new Hashtable() + { + { "IncludeRules", new string[] { + "PSPlaceCloseBrace", + "PSPlaceOpenBrace", + "PSUseConsistentWhitespace", + "PSUseConsistentIndentation", + "PSAlignAssignmentStatement" + }}, + { + "Rules", ruleConfigurations + } }; } } From e9f9bcd136ab525e62cbf466c6d0d4a36eded993 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 3 Oct 2019 14:33:48 -0700 Subject: [PATCH 47/47] Codacy comments --- .../Hosting/EditorServicesHost.cs | 13 ------------- .../Services/Analysis/AnalysisService.cs | 2 +- .../Services/CodeLens/ReferencesCodeLensProvider.cs | 5 ++--- .../TextDocument/Handlers/CodeActionHandler.cs | 2 +- .../TextDocument/Handlers/CodeLensHandlers.cs | 6 +++--- .../TextDocument/Handlers/DocumentSymbolHandler.cs | 4 ++-- .../TextDocument/Handlers/FormattingHandlers.cs | 2 +- 7 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs index 18d5197bd..0f80f0920 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs @@ -236,13 +236,6 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - // Uncomment to debug language service - // while (!System.Diagnostics.Debugger.IsAttached) - // { - // System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); - // Thread.Sleep(2000); - // } - _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); switch (config.TransportType) @@ -296,12 +289,6 @@ public void StartDebugService( ProfilePaths profilePaths, bool useExistingSession) { - //while (System.Diagnostics.Debugger.IsAttached) - //{ - // System.Console.WriteLine($"{Process.GetCurrentProcess().Id}"); - // Thread.Sleep(2000); - //} - _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); switch (config.TransportType) diff --git a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs index 89cd6bcb8..a9ce58fe2 100644 --- a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs @@ -868,7 +868,7 @@ private void PublishScriptDiagnostics( } - var uriBuilder = new UriBuilder() + var uriBuilder = new UriBuilder { Scheme = Uri.UriSchemeFile, Path = scriptFile.FilePath, diff --git a/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs index 98af40532..5d1a55ae6 100644 --- a/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -61,8 +61,7 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) { if (sym.SymbolType == SymbolType.Function) { - //acc.Add(new CodeLens(this, scriptFile, sym.ScriptRegion)); - acc.Add(new CodeLens() + acc.Add(new CodeLens { Data = JToken.FromObject(new { @@ -122,7 +121,7 @@ public CodeLens ResolveCodeLens(CodeLens codeLens, ScriptFile scriptFile) referenceLocations = acc.ToArray(); } - return new CodeLens() + return new CodeLens { Data = codeLens.Data, Range = codeLens.Range, diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs index ade90f2b6..80180ca61 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -36,7 +36,7 @@ public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService { _logger = factory.CreateLogger(); _analysisService = analysisService; - _registrationOptions = new CodeActionRegistrationOptions() + _registrationOptions = new CodeActionRegistrationOptions { DocumentSelector = new DocumentSelector(new DocumentFilter() { Language = "powershell" }), CodeActionKinds = s_supportedCodeActions diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs index e87b7595f..a4c9d2ca8 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -24,7 +24,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler { private readonly DocumentSelector _documentSelector = new DocumentSelector( - new DocumentFilter() + new DocumentFilter { Language = "powershell" } @@ -52,7 +52,7 @@ public CodeLensHandlers(ILoggerFactory factory, SymbolsService symbolsService, W CodeLensRegistrationOptions IRegistration.GetRegistrationOptions() { - return new CodeLensRegistrationOptions() + return new CodeLensRegistrationOptions { DocumentSelector = _documentSelector, ResolveProvider = true @@ -71,7 +71,7 @@ public Task Handle(CodeLensParams request, CancellationToken public TextDocumentRegistrationOptions GetRegistrationOptions() { - return new TextDocumentRegistrationOptions() + return new TextDocumentRegistrationOptions { DocumentSelector = _documentSelector, }; diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index 9e3ccfd78..52ba0660b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -25,7 +25,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers public class DocumentSymbolHandler : IDocumentSymbolHandler { private readonly DocumentSelector _documentSelector = new DocumentSelector( - new DocumentFilter() + new DocumentFilter { Language = "powershell" } @@ -53,7 +53,7 @@ public DocumentSymbolHandler(ILoggerFactory factory, ConfigurationService config public TextDocumentRegistrationOptions GetRegistrationOptions() { - return new TextDocumentRegistrationOptions() + return new TextDocumentRegistrationOptions { DocumentSelector = _documentSelector, }; diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs index 90243b9d4..aabdeb615 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -52,7 +52,7 @@ public async Task Handle(DocumentFormattingParams request, Ca request.Options.InsertSpaces); - // TODO raise an error event in case format returns null; + // TODO raise an error event in case format returns null string formattedScript; Range editRange; var extent = scriptFile.ScriptAst.Extent;