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/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/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/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
new file mode 100644
index 000000000..9512b7918
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs
@@ -0,0 +1,345 @@
+//
+// 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.Linq;
+using System.Management.Automation;
+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;
+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 _serviceCollection;
+
+ private readonly HostDetails _hostDetails;
+
+ private ILanguageServer _languageServer;
+
+ private readonly Extensions.Logging.ILogger _logger;
+
+ private readonly ILoggerFactory _factory;
+
+ #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);
+
+ _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;
+ */
+
+#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, PsesLogLevel 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: {_hostDetails.Name}
+ Version: {_hostDetails.Version}
+ ProfileId: {_hostDetails.ProfileId}
+ Arch: {osArch}
+
+ Operating system details:
+
+ Version: {osVersion}
+ Arch: {osArch}
+
+ Build information:
+
+ Version: {BuildInfo.BuildVersion}
+ Origin: {BuildInfo.BuildOrigin}
+ Date: {buildTime}
+";
+
+ _logger.LogInformation(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)
+ {
+ while (System.Diagnostics.Debugger.IsAttached)
+ {
+ 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());
+ }
+ );
+
+ _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection)
+ {
+ NamedPipeName = config.InOutPipeName ?? config.InPipeName,
+ OutNamedPipeName = config.OutPipeName,
+ LoggerFactory = _factory,
+ MinimumLogLevel = LogLevel.Trace,
+ }
+ .BuildLanguageServer();
+
+ _logger.LogInformation("Starting language server");
+
+ 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));
+ }
+
+ ///
+ /// 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
+ }
+
+ ///
+ /// 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!
+ _languageServer.WaitForShutdown().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
+ _logger.LogError($"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/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
new file mode 100644
index 000000000..07605468c
--- /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 WaitForShutdown();
+ }
+}
diff --git a/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs
new file mode 100644
index 000000000..0b5801b36
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/Interface/ILanguageServerBuilder.cs
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 000000000..73a6d8186
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs
@@ -0,0 +1,165 @@
+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;
+using OmniSharp.Extensions.LanguageServer.Server;
+using PowerShellEditorServices.Engine.Services.Handlers;
+
+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 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 => {
+ NamedPipeServerStream namedPipe = CreateNamedPipe(
+ _configuration.NamedPipeName,
+ _configuration.OutNamedPipeName,
+ out NamedPipeServerStream outNamedPipe);
+
+ namedPipe.WaitForConnection();
+ if (outNamedPipe != null)
+ {
+ outNamedPipe.WaitForConnection();
+ }
+
+ options.Input = namedPipe;
+ options.Output = outNamedPipe ?? namedPipe;
+
+ options.LoggerFactory = _configuration.LoggerFactory;
+ options.MinimumLogLevel = _configuration.MinimumLogLevel;
+ options.Services = _configuration.Services;
+ options
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler()
+ .WithHandler();
+ });
+
+ _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
new file mode 100644
index 000000000..801f74037
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServerBuilder.cs
@@ -0,0 +1,37 @@
+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 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/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
new file mode 100644
index 000000000..c0fb4a3df
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+ PowerShell Editor Services Engine
+ Provides common PowerShell editor capabilities as a .NET library.
+ netstandard2.0
+ Microsoft.PowerShell.EditorServices.Engine
+ Latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs
new file mode 100644
index 000000000..992fb65ab
--- /dev/null
+++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs
@@ -0,0 +1,946 @@
+//
+// 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;
+using OmniSharp.Extensions.LanguageServer.Protocol.Models;
+using OmniSharp.Extensions.LanguageServer.Protocol.Server;
+using System.Threading;
+
+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];
+
+ private Dictionary> codeActionsPerFile =
+ new Dictionary>();
+
+ private static CancellationTokenSource s_existingRequestCancellation;
+
+ ///
+ /// 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;
+
+ private readonly ILanguageServer _languageServer;
+ private readonly ConfigurationService _configurationService;
+
+ #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,
+ ILanguageServer languageServer,
+ ConfigurationService configurationService,
+ ILogger logger,
+ PSModuleInfo pssaModuleInfo = null)
+ {
+ _analysisRunspacePool = analysisRunspacePool;
+ SettingsPath = pssaSettingsPath;
+ ActiveRules = activeRules.ToArray();
+ _languageServer = languageServer;
+ _configurationService = configurationService;
+ _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(ConfigurationService configurationService, ILanguageServer languageServer, ILogger logger)
+ {
+ string settingsPath = configurationService.CurrentSettings.ScriptAnalysis.SettingsPath;
+ 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,
+ languageServer,
+ configurationService,
+ 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