diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d1fcfc4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin +obj +project.lock.json \ No newline at end of file diff --git a/README.md b/README.md index 61e90e7a..e38a335b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # sqltoolsservice -SQL Tools API service repo. +SQL Tools Service host diff --git a/ServiceHost/.vscode/launch.json b/ServiceHost/.vscode/launch.json new file mode 100644 index 00000000..51cdc900 --- /dev/null +++ b/ServiceHost/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp1.0/servicehost.dll", + "args": [], + "cwd": "${workspaceRoot}", + "externalConsole": true, + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": 16900 + } + ] +} \ No newline at end of file diff --git a/ServiceHost/.vscode/tasks.json b/ServiceHost/.vscode/tasks.json new file mode 100644 index 00000000..67d6eb75 --- /dev/null +++ b/ServiceHost/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "0.1.0", + "command": "dotnet", + "isShellCommand": true, + "args": [], + "tasks": [ + { + "taskName": "build", + "args": [], + "isBuildCommand": true, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/ServiceHost/Client/DebugAdapterClientBase.cs b/ServiceHost/Client/DebugAdapterClientBase.cs new file mode 100644 index 00000000..5e6a1758 --- /dev/null +++ b/ServiceHost/Client/DebugAdapterClientBase.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. +// + +using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + public class DebugAdapterClient : ProtocolEndpoint + { + public DebugAdapterClient(ChannelBase clientChannel) + : base(clientChannel, MessageProtocolType.DebugAdapter) + { + } + + public async Task LaunchScript(string scriptFilePath) + { + await this.SendRequest( + LaunchRequest.Type, + new LaunchRequestArguments { + Program = scriptFilePath + }); + + await this.SendRequest(ConfigurationDoneRequest.Type, null); + } + + protected override Task OnStart() + { + return Task.FromResult(true); + } + + protected override Task OnConnect() + { + // Initialize the debug adapter + return this.SendRequest( + InitializeRequest.Type, + new InitializeRequestArguments + { + LinesStartAt1 = true + }); + } + } +} + diff --git a/ServiceHost/Client/LanguageClientBase.cs b/ServiceHost/Client/LanguageClientBase.cs new file mode 100644 index 00000000..665383d0 --- /dev/null +++ b/ServiceHost/Client/LanguageClientBase.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 Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + /// + /// Provides a base implementation for language server clients. + /// + public abstract class LanguageClientBase : ProtocolEndpoint + { + /// + /// 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) + : base(clientChannel, MessageProtocolType.LanguageServer) + { + } + + protected override Task OnStart() + { + // Initialize the implementation class + return this.Initialize(); + } + + protected override async Task OnStop() + { + // First, notify the language server that we're stopping + var response = await this.SendRequest(ShutdownRequest.Type, new object()); + await this.SendEvent(ExitNotification.Type, new object()); + } + + protected abstract Task Initialize(); + } +} + diff --git a/ServiceHost/Client/LanguageServiceClient.cs b/ServiceHost/Client/LanguageServiceClient.cs new file mode 100644 index 00000000..67d769ed --- /dev/null +++ b/ServiceHost/Client/LanguageServiceClient.cs @@ -0,0 +1,121 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + public class LanguageServiceClient : LanguageClientBase + { + private Dictionary cachedDiagnostics = + new Dictionary(); + + public LanguageServiceClient(ChannelBase clientChannel) + : base(clientChannel) + { + } + + protected override Task Initialize() + { + // Add handlers for common events + this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEvent); + + return Task.FromResult(true); + } + + protected override Task OnConnect() + { + // Send the 'initialize' request and wait for the response + var initializeRequest = new InitializeRequest + { + RootPath = "", + Capabilities = new ClientCapabilities() + }; + + return this.SendRequest( + InitializeRequest.Type, + initializeRequest); + } + + #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 HandlePublishDiagnosticsEvent( + 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/ServiceHost/DebugAdapter/AttachRequest.cs b/ServiceHost/DebugAdapter/AttachRequest.cs new file mode 100644 index 00000000..4b8faa9c --- /dev/null +++ b/ServiceHost/DebugAdapter/AttachRequest.cs @@ -0,0 +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 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 Address { get; set; } + + public int Port { get; set; } + } +} diff --git a/ServiceHost/DebugAdapter/Breakpoint.cs b/ServiceHost/DebugAdapter/Breakpoint.cs new file mode 100644 index 00000000..db04972c --- /dev/null +++ b/ServiceHost/DebugAdapter/Breakpoint.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 Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class Breakpoint + { + /// + /// 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 string Source { get; set; } + + public int? Line { get; set; } + + public int? Column { get; set; } + + private Breakpoint() + { + } + +#if false + public static Breakpoint Create( + BreakpointDetails breakpointDetails) + { + //Validate.IsNotNull(nameof(breakpointDetails), breakpointDetails); + + return new Breakpoint + { + Verified = breakpointDetails.Verified, + Message = breakpointDetails.Message, + Source = 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 + }; + } +#endif + + 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 = source, + Line = sourceBreakpoint.Line, + Column = sourceBreakpoint.Column + }; + } + } +} diff --git a/ServiceHost/DebugAdapter/ConfigurationDoneRequest.cs b/ServiceHost/DebugAdapter/ConfigurationDoneRequest.cs new file mode 100644 index 00000000..ef5e0c8e --- /dev/null +++ b/ServiceHost/DebugAdapter/ConfigurationDoneRequest.cs @@ -0,0 +1,16 @@ +// +// 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/ServiceHost/DebugAdapter/ContinueRequest.cs b/ServiceHost/DebugAdapter/ContinueRequest.cs new file mode 100644 index 00000000..5ee5b2ec --- /dev/null +++ b/ServiceHost/DebugAdapter/ContinueRequest.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. +// + +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/ServiceHost/DebugAdapter/DisconnectRequest.cs b/ServiceHost/DebugAdapter/DisconnectRequest.cs new file mode 100644 index 00000000..7f7036b0 --- /dev/null +++ b/ServiceHost/DebugAdapter/DisconnectRequest.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. +// + +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/ServiceHost/DebugAdapter/EvaluateRequest.cs b/ServiceHost/DebugAdapter/EvaluateRequest.cs new file mode 100644 index 00000000..b079f59a --- /dev/null +++ b/ServiceHost/DebugAdapter/EvaluateRequest.cs @@ -0,0 +1,53 @@ +// +// 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/ServiceHost/DebugAdapter/ExitedEvent.cs b/ServiceHost/DebugAdapter/ExitedEvent.cs new file mode 100644 index 00000000..8a218973 --- /dev/null +++ b/ServiceHost/DebugAdapter/ExitedEvent.cs @@ -0,0 +1,22 @@ +// +// 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 + EventType Type = + EventType.Create("exited"); + } + + public class ExitedEventBody + { + public int ExitCode { get; set; } + } +} + diff --git a/ServiceHost/DebugAdapter/InitializeRequest.cs b/ServiceHost/DebugAdapter/InitializeRequest.cs new file mode 100644 index 00000000..16c99544 --- /dev/null +++ b/ServiceHost/DebugAdapter/InitializeRequest.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 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 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 a (side effect free) evaluate request for data hovers. + /// + public bool SupportsEvaluateForHovers { get; set; } + } +} diff --git a/ServiceHost/DebugAdapter/InitializedEvent.cs b/ServiceHost/DebugAdapter/InitializedEvent.cs new file mode 100644 index 00000000..68ed6680 --- /dev/null +++ b/ServiceHost/DebugAdapter/InitializedEvent.cs @@ -0,0 +1,16 @@ +// +// 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 + EventType Type = + EventType.Create("initialized"); + } +} diff --git a/ServiceHost/DebugAdapter/LaunchRequest.cs b/ServiceHost/DebugAdapter/LaunchRequest.cs new file mode 100644 index 00000000..5fa1cb10 --- /dev/null +++ b/ServiceHost/DebugAdapter/LaunchRequest.cs @@ -0,0 +1,66 @@ +// +// 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 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 program to debug. + /// + public string Program { 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 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/ServiceHost/DebugAdapter/NextRequest.cs b/ServiceHost/DebugAdapter/NextRequest.cs new file mode 100644 index 00000000..5cb2e6ac --- /dev/null +++ b/ServiceHost/DebugAdapter/NextRequest.cs @@ -0,0 +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 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/ServiceHost/DebugAdapter/OutputEvent.cs b/ServiceHost/DebugAdapter/OutputEvent.cs new file mode 100644 index 00000000..a9cfee8c --- /dev/null +++ b/ServiceHost/DebugAdapter/OutputEvent.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class OutputEvent + { + public static readonly + EventType Type = + EventType.Create("output"); + } + + public class OutputEventBody + { + public string Category { get; set; } + + public string Output { get; set; } + } +} + diff --git a/ServiceHost/DebugAdapter/PauseRequest.cs b/ServiceHost/DebugAdapter/PauseRequest.cs new file mode 100644 index 00000000..86ceee4f --- /dev/null +++ b/ServiceHost/DebugAdapter/PauseRequest.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. +// + +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/ServiceHost/DebugAdapter/Scope.cs b/ServiceHost/DebugAdapter/Scope.cs new file mode 100644 index 00000000..8384e6c3 --- /dev/null +++ b/ServiceHost/DebugAdapter/Scope.cs @@ -0,0 +1,40 @@ +// +// 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; } + +#if false + 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) + }; + } +#endif + } +} + diff --git a/ServiceHost/DebugAdapter/ScopesRequest.cs b/ServiceHost/DebugAdapter/ScopesRequest.cs new file mode 100644 index 00000000..044f5bf7 --- /dev/null +++ b/ServiceHost/DebugAdapter/ScopesRequest.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.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/ServiceHost/DebugAdapter/SetBreakpointsRequest.cs b/ServiceHost/DebugAdapter/SetBreakpointsRequest.cs new file mode 100644 index 00000000..a4993291 --- /dev/null +++ b/ServiceHost/DebugAdapter/SetBreakpointsRequest.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 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 class SetBreakpointsResponseBody + { + public Breakpoint[] Breakpoints { get; set; } + } +} diff --git a/ServiceHost/DebugAdapter/SetExceptionBreakpointsRequest.cs b/ServiceHost/DebugAdapter/SetExceptionBreakpointsRequest.cs new file mode 100644 index 00000000..a586f27b --- /dev/null +++ b/ServiceHost/DebugAdapter/SetExceptionBreakpointsRequest.cs @@ -0,0 +1,31 @@ +// +// 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/ServiceHost/DebugAdapter/SetFunctionBreakpointsRequest.cs b/ServiceHost/DebugAdapter/SetFunctionBreakpointsRequest.cs new file mode 100644 index 00000000..434c6dd5 --- /dev/null +++ b/ServiceHost/DebugAdapter/SetFunctionBreakpointsRequest.cs @@ -0,0 +1,31 @@ +// +// 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; } + } +} diff --git a/ServiceHost/DebugAdapter/Source.cs b/ServiceHost/DebugAdapter/Source.cs new file mode 100644 index 00000000..5a71ea1e --- /dev/null +++ b/ServiceHost/DebugAdapter/Source.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.Protocol.DebugAdapter +{ + public class Source + { + public string Name { get; set; } + + public string Path { get; set; } + + public int? SourceReference { get; set; } + } +} + diff --git a/ServiceHost/DebugAdapter/SourceRequest.cs b/ServiceHost/DebugAdapter/SourceRequest.cs new file mode 100644 index 00000000..9d185af2 --- /dev/null +++ b/ServiceHost/DebugAdapter/SourceRequest.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 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/ServiceHost/DebugAdapter/StackFrame.cs b/ServiceHost/DebugAdapter/StackFrame.cs new file mode 100644 index 00000000..cfa2f6a9 --- /dev/null +++ b/ServiceHost/DebugAdapter/StackFrame.cs @@ -0,0 +1,53 @@ +// +// 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 + { + public int Id { get; set; } + + public string Name { get; set; } + + public Source Source { get; set; } + + public int Line { get; set; } + + public int Column { get; set; } + + // /** An identifier for the stack frame. */ + //id: number; + ///** The name of the stack frame, typically a method name */ + //name: string; + ///** The source of the frame. */ + //source: Source; + ///** The line within the file of the frame. */ + //line: number; + ///** The column within the line. */ + //column: number; + ///** All arguments and variables declared in this stackframe. */ + //scopes: Scope[]; + +#if false + public static StackFrame Create( + StackFrameDetails stackFrame, + int id) + { + return new StackFrame + { + Id = id, + Name = stackFrame.FunctionName, + Line = stackFrame.LineNumber, + Column = stackFrame.ColumnNumber, + Source = new Source + { + Path = stackFrame.ScriptPath + } + }; + } +#endif + } +} + diff --git a/ServiceHost/DebugAdapter/StackTraceRequest.cs b/ServiceHost/DebugAdapter/StackTraceRequest.cs new file mode 100644 index 00000000..06929ba4 --- /dev/null +++ b/ServiceHost/DebugAdapter/StackTraceRequest.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.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 + { + public int ThreadId { get; private set; } + + /// + /// Gets the maximum number of frames to return. If levels is not specified or 0, all frames are returned. + /// + public int Levels { get; private set; } + } + + public class StackTraceResponseBody + { + public StackFrame[] StackFrames { get; set; } + } +} diff --git a/ServiceHost/DebugAdapter/StartedEvent.cs b/ServiceHost/DebugAdapter/StartedEvent.cs new file mode 100644 index 00000000..f08f504b --- /dev/null +++ b/ServiceHost/DebugAdapter/StartedEvent.cs @@ -0,0 +1,16 @@ +// +// 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 + EventType Type = + EventType.Create("started"); + } +} diff --git a/ServiceHost/DebugAdapter/StepInRequest.cs b/ServiceHost/DebugAdapter/StepInRequest.cs new file mode 100644 index 00000000..cfd8f764 --- /dev/null +++ b/ServiceHost/DebugAdapter/StepInRequest.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. +// + +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/ServiceHost/DebugAdapter/StepOutRequest.cs b/ServiceHost/DebugAdapter/StepOutRequest.cs new file mode 100644 index 00000000..5b86f3f7 --- /dev/null +++ b/ServiceHost/DebugAdapter/StepOutRequest.cs @@ -0,0 +1,16 @@ +// +// 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/ServiceHost/DebugAdapter/StoppedEvent.cs b/ServiceHost/DebugAdapter/StoppedEvent.cs new file mode 100644 index 00000000..b0850ddb --- /dev/null +++ b/ServiceHost/DebugAdapter/StoppedEvent.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class StoppedEvent + { + public static readonly + EventType Type = + EventType.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; } + + public int Line { get; set; } + + public int Column { get; set; } + + /// + /// Gets or sets additional information such as an error message. + /// + public string Text { get; set; } + } +} + diff --git a/ServiceHost/DebugAdapter/TerminatedEvent.cs b/ServiceHost/DebugAdapter/TerminatedEvent.cs new file mode 100644 index 00000000..2756899f --- /dev/null +++ b/ServiceHost/DebugAdapter/TerminatedEvent.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. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +{ + public class TerminatedEvent + { + public static readonly + EventType Type = + EventType.Create("terminated"); + } +} + diff --git a/ServiceHost/DebugAdapter/Thread.cs b/ServiceHost/DebugAdapter/Thread.cs new file mode 100644 index 00000000..35a8a139 --- /dev/null +++ b/ServiceHost/DebugAdapter/Thread.cs @@ -0,0 +1,15 @@ +// +// 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/ServiceHost/DebugAdapter/ThreadsRequest.cs b/ServiceHost/DebugAdapter/ThreadsRequest.cs new file mode 100644 index 00000000..e72fcc96 --- /dev/null +++ b/ServiceHost/DebugAdapter/ThreadsRequest.cs @@ -0,0 +1,22 @@ +// +// 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/ServiceHost/DebugAdapter/Variable.cs b/ServiceHost/DebugAdapter/Variable.cs new file mode 100644 index 00000000..7cd03176 --- /dev/null +++ b/ServiceHost/DebugAdapter/Variable.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. +// + +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; } + + // /** 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; } + +#if false + public static Variable Create(VariableDetailsBase variable) + { + return new Variable + { + Name = variable.Name, + Value = variable.ValueString ?? string.Empty, + VariablesReference = + variable.IsExpandable ? + variable.Id : 0 + }; + } +#endif + } +} + diff --git a/ServiceHost/DebugAdapter/VariablesRequest.cs b/ServiceHost/DebugAdapter/VariablesRequest.cs new file mode 100644 index 00000000..40dbaabb --- /dev/null +++ b/ServiceHost/DebugAdapter/VariablesRequest.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.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/ServiceHost/LanguageServer/ClientCapabilities.cs b/ServiceHost/LanguageServer/ClientCapabilities.cs new file mode 100644 index 00000000..2bebdeb6 --- /dev/null +++ b/ServiceHost/LanguageServer/ClientCapabilities.cs @@ -0,0 +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.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 + { + } +} + diff --git a/ServiceHost/LanguageServer/Completion.cs b/ServiceHost/LanguageServer/Completion.cs new file mode 100644 index 00000000..cf0f1559 --- /dev/null +++ b/ServiceHost/LanguageServer/Completion.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 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"); + } + + 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 + } + + [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 TextEdit TextEdit { 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; } + } +} + diff --git a/ServiceHost/LanguageServer/Configuration.cs b/ServiceHost/LanguageServer/Configuration.cs new file mode 100644 index 00000000..d4f57781 --- /dev/null +++ b/ServiceHost/LanguageServer/Configuration.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class DidChangeConfigurationNotification + { + public static readonly + EventType> Type = + EventType>.Create("workspace/didChangeConfiguration"); + } + + public class DidChangeConfigurationParams + { + public TConfig Settings { get; set; } + } +} diff --git a/ServiceHost/LanguageServer/Definition.cs b/ServiceHost/LanguageServer/Definition.cs new file mode 100644 index 00000000..37241dd6 --- /dev/null +++ b/ServiceHost/LanguageServer/Definition.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. +// + +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/ServiceHost/LanguageServer/Diagnostics.cs b/ServiceHost/LanguageServer/Diagnostics.cs new file mode 100644 index 00000000..116b77c3 --- /dev/null +++ b/ServiceHost/LanguageServer/Diagnostics.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 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 + EventType Type = + EventType.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; } + } +} + diff --git a/ServiceHost/LanguageServer/DocumentHighlight.cs b/ServiceHost/LanguageServer/DocumentHighlight.cs new file mode 100644 index 00000000..c169acc4 --- /dev/null +++ b/ServiceHost/LanguageServer/DocumentHighlight.cs @@ -0,0 +1,31 @@ +// +// 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/ServiceHost/LanguageServer/EditorCommands.cs b/ServiceHost/LanguageServer/EditorCommands.cs new file mode 100644 index 00000000..cdd12813 --- /dev/null +++ b/ServiceHost/LanguageServer/EditorCommands.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class ExtensionCommandAddedNotification + { + public static readonly + EventType Type = + EventType.Create("powerShell/extensionCommandAdded"); + + public string Name { get; set; } + + public string DisplayName { get; set; } + } + + public class ExtensionCommandUpdatedNotification + { + public static readonly + EventType Type = + EventType.Create("powerShell/extensionCommandUpdated"); + + public string Name { get; set; } + } + + public class ExtensionCommandRemovedNotification + { + public static readonly + EventType Type = + EventType.Create("powerShell/extensionCommandRemoved"); + + public string Name { get; set; } + } + + public class ClientEditorContext + { + 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 OpenFileRequest + { + public static readonly + RequestType Type = + RequestType.Create("editor/openFile"); + } +} + diff --git a/ServiceHost/LanguageServer/ExpandAliasRequest.cs b/ServiceHost/LanguageServer/ExpandAliasRequest.cs new file mode 100644 index 00000000..cd79c92f --- /dev/null +++ b/ServiceHost/LanguageServer/ExpandAliasRequest.cs @@ -0,0 +1,16 @@ +// +// 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/ServiceHost/LanguageServer/FindModuleRequest.cs b/ServiceHost/LanguageServer/FindModuleRequest.cs new file mode 100644 index 00000000..12ac6cf9 --- /dev/null +++ b/ServiceHost/LanguageServer/FindModuleRequest.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class FindModuleRequest + { + public static readonly + RequestType, object> Type = + RequestType, object>.Create("powerShell/findModule"); + } + + + public class PSModuleMessage + { + public string Name { get; set; } + public string Description { get; set; } + } +} diff --git a/ServiceHost/LanguageServer/Hover.cs b/ServiceHost/LanguageServer/Hover.cs new file mode 100644 index 00000000..2af60e51 --- /dev/null +++ b/ServiceHost/LanguageServer/Hover.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. +// + +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/ServiceHost/LanguageServer/Initialize.cs b/ServiceHost/LanguageServer/Initialize.cs new file mode 100644 index 00000000..40a9d1a2 --- /dev/null +++ b/ServiceHost/LanguageServer/Initialize.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. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class InitializeRequest + { + public static readonly + RequestType Type = + RequestType.Create("initialize"); + + /// + /// Gets or sets the root path of the editor's open workspace. + /// If null it is assumed that a file was opened without having + /// a workspace open. + /// + public string RootPath { get; set; } + + /// + /// Gets or sets the capabilities provided by the client (editor). + /// + public ClientCapabilities Capabilities { get; set; } + } + + 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/ServiceHost/LanguageServer/InstallModuleRequest.cs b/ServiceHost/LanguageServer/InstallModuleRequest.cs new file mode 100644 index 00000000..2be47ce9 --- /dev/null +++ b/ServiceHost/LanguageServer/InstallModuleRequest.cs @@ -0,0 +1,16 @@ +// +// 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/ServiceHost/LanguageServer/References.cs b/ServiceHost/LanguageServer/References.cs new file mode 100644 index 00000000..a55ebebf --- /dev/null +++ b/ServiceHost/LanguageServer/References.cs @@ -0,0 +1,27 @@ +// +// 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 : TextDocumentPosition + { + public ReferencesContext Context { get; set; } + } + + public class ReferencesContext + { + public bool IncludeDeclaration { get; set; } + } +} + diff --git a/ServiceHost/LanguageServer/ServerCapabilities.cs b/ServiceHost/LanguageServer/ServerCapabilities.cs new file mode 100644 index 00000000..34ba312f --- /dev/null +++ b/ServiceHost/LanguageServer/ServerCapabilities.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. +// + +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; } + } + + /// + /// 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/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs b/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs new file mode 100644 index 00000000..49c571fa --- /dev/null +++ b/ServiceHost/LanguageServer/ShowOnlineHelpRequest.cs @@ -0,0 +1,16 @@ +// +// 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 ShowOnlineHelpRequest + { + public static readonly + RequestType Type = + RequestType.Create("powerShell/showOnlineHelp"); + } +} diff --git a/ServiceHost/LanguageServer/Shutdown.cs b/ServiceHost/LanguageServer/Shutdown.cs new file mode 100644 index 00000000..51971901 --- /dev/null +++ b/ServiceHost/LanguageServer/Shutdown.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 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 + RequestType Type = + RequestType.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 + EventType Type = + EventType.Create("exit"); + } +} + diff --git a/ServiceHost/LanguageServer/SignatureHelp.cs b/ServiceHost/LanguageServer/SignatureHelp.cs new file mode 100644 index 00000000..6387461d --- /dev/null +++ b/ServiceHost/LanguageServer/SignatureHelp.cs @@ -0,0 +1,42 @@ +// +// 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 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/ServiceHost/LanguageServer/TextDocument.cs b/ServiceHost/LanguageServer/TextDocument.cs new file mode 100644 index 00000000..fd2ee358 --- /dev/null +++ b/ServiceHost/LanguageServer/TextDocument.cs @@ -0,0 +1,149 @@ +// +// 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 +{ + /// + /// 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; } + } + + /// + /// Defines a position in a text document. + /// + [DebuggerDisplay("TextDocumentPosition = {Position.Line}:{Position.Character}")] + public class TextDocumentPosition : TextDocumentIdentifier + { + /// + /// Gets or sets the position in the document. + /// + public Position Position { get; set; } + } + + public class DidOpenTextDocumentNotification : TextDocumentIdentifier + { + public static readonly + EventType Type = + EventType.Create("textDocument/didOpen"); + + /// + /// Gets or sets the full content of the opened document. + /// + public string Text { get; set; } + } + + public class DidCloseTextDocumentNotification + { + public static readonly + EventType Type = + EventType.Create("textDocument/didClose"); + } + + public class DidChangeTextDocumentNotification + { + public static readonly + EventType Type = + EventType.Create("textDocument/didChange"); + } + + public class DidChangeTextDocumentParams : TextDocumentIdentifier + { + /// + /// 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 struct 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/ServiceHost/LanguageServer/WorkspaceSymbols.cs b/ServiceHost/LanguageServer/WorkspaceSymbols.cs new file mode 100644 index 00000000..405d6e98 --- /dev/null +++ b/ServiceHost/LanguageServer/WorkspaceSymbols.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 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"); + } + + public class WorkspaceSymbolRequest + { + public static readonly + RequestType Type = + RequestType.Create("workspace/symbol"); + } + + public class WorkspaceSymbolParams + { + public string Query { get; set;} + } +} + diff --git a/ServiceHost/MessageProtocol/Channel/ChannelBase.cs b/ServiceHost/MessageProtocol/Channel/ChannelBase.cs new file mode 100644 index 00000000..54de0d9c --- /dev/null +++ b/ServiceHost/MessageProtocol/Channel/ChannelBase.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 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 a boolean that is true if the channel is connected or false if not. + /// + public bool IsConnected { get; protected set; } + + /// + /// 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); + } + + /// + /// Returns a Task that allows the consumer of the ChannelBase + /// implementation to wait until a connection has been made to + /// the opposite endpoint whether it's a client or server. + /// + /// A Task to be awaited until a connection is made. + public abstract Task WaitForConnection(); + + /// + /// 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/ServiceHost/MessageProtocol/Channel/NamedPipeClientChannel.cs b/ServiceHost/MessageProtocol/Channel/NamedPipeClientChannel.cs new file mode 100644 index 00000000..27f00f67 --- /dev/null +++ b/ServiceHost/MessageProtocol/Channel/NamedPipeClientChannel.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. +// + +#if false + +using System; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + public class NamedPipeClientChannel : ChannelBase + { + private string pipeName; + private bool isClientConnected; + private NamedPipeClientStream pipeClient; + + public NamedPipeClientChannel(string pipeName) + { + this.pipeName = pipeName; + } + + public override async Task WaitForConnection() + { +#if NanoServer + await this.pipeClient.ConnectAsync(); +#else + this.IsConnected = false; + + while (!this.IsConnected) + { + try + { + // Wait for 500 milliseconds so that we don't tie up the thread + this.pipeClient.Connect(500); + this.IsConnected = this.pipeClient.IsConnected; + } + catch (TimeoutException) + { + // Connect timed out, wait and try again + await Task.Delay(1000); + continue; + } + } +#endif + + // If we've reached this point, we're connected + this.IsConnected = true; + } + + protected override void Initialize(IMessageSerializer messageSerializer) + { + this.pipeClient = + new NamedPipeClientStream( + ".", + this.pipeName, + PipeDirection.InOut, + PipeOptions.Asynchronous); + + this.MessageReader = + new MessageReader( + this.pipeClient, + messageSerializer); + + this.MessageWriter = + new MessageWriter( + this.pipeClient, + messageSerializer); + } + + protected override void Shutdown() + { + if (this.pipeClient != null) + { + this.pipeClient.Dispose(); + } + } + } +} + +#endif \ No newline at end of file diff --git a/ServiceHost/MessageProtocol/Channel/NamedPipeServerChannel.cs b/ServiceHost/MessageProtocol/Channel/NamedPipeServerChannel.cs new file mode 100644 index 00000000..d4cf730c --- /dev/null +++ b/ServiceHost/MessageProtocol/Channel/NamedPipeServerChannel.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#if false +using System; +using System.IO.Pipes; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + public class NamedPipeServerChannel : ChannelBase + { + private string pipeName; + private NamedPipeServerStream pipeServer; + + public NamedPipeServerChannel(string pipeName) + { + this.pipeName = pipeName; + } + + public override async Task WaitForConnection() + { +#if NanoServer + await this.pipeServer.WaitForConnectionAsync(); +#else + await Task.Factory.FromAsync(this.pipeServer.BeginWaitForConnection, this.pipeServer.EndWaitForConnection, null); +#endif + + this.IsConnected = true; + } + + protected override void Initialize(IMessageSerializer messageSerializer) + { + this.pipeServer = + new NamedPipeServerStream( + pipeName, + PipeDirection.InOut, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous); + + this.MessageReader = + new MessageReader( + this.pipeServer, + messageSerializer); + + this.MessageWriter = + new MessageWriter( + this.pipeServer, + messageSerializer); + } + + protected override void Shutdown() + { + if (this.pipeServer != null) + { + this.pipeServer.Dispose(); + } + } + } +} + +#endif diff --git a/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs b/ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs new file mode 100644 index 00000000..a6be8cc8 --- /dev/null +++ b/ServiceHost/MessageProtocol/Channel/StdioClientChannel.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; +using System.Diagnostics; +using System.IO; +using System.Text; +using System; +using System.Threading.Tasks; + +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 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, + params string[] serverProcessArguments) + { + 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.MessageWriter = + new MessageWriter( + this.outputStream, + messageSerializer); + + this.IsConnected = true; + } + + public override Task WaitForConnection() + { + // We're always connected immediately in the stdio channel + return Task.FromResult(true); + } + + 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/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs b/ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs new file mode 100644 index 00000000..bfec284e --- /dev/null +++ b/ServiceHost/MessageProtocol/Channel/StdioServerChannel.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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; +using System.IO; +using System.Text; +using System; +using System.Threading.Tasks; + +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 Stream inputStream; + private Stream outputStream; + + protected override void Initialize(IMessageSerializer messageSerializer) + { +#if !NanoServer + // Ensure that the console is using UTF-8 encoding + System.Console.InputEncoding = Encoding.UTF8; + System.Console.OutputEncoding = Encoding.UTF8; +#endif + + // 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.MessageWriter = + new MessageWriter( + this.outputStream, + messageSerializer); + + this.IsConnected = true; + } + + public override Task WaitForConnection() + { + // We're always connected immediately in the stdio channel + return Task.FromResult(true); + } + + protected override void Shutdown() + { + // No default implementation needed, streams will be + // disposed on process shutdown. + } + } +} diff --git a/ServiceHost/MessageProtocol/Constants.cs b/ServiceHost/MessageProtocol/Constants.cs new file mode 100644 index 00000000..d443dc12 --- /dev/null +++ b/ServiceHost/MessageProtocol/Constants.cs @@ -0,0 +1,31 @@ +// +// 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/ServiceHost/MessageProtocol/EventContext.cs b/ServiceHost/MessageProtocol/EventContext.cs new file mode 100644 index 00000000..e4f4051d --- /dev/null +++ b/ServiceHost/MessageProtocol/EventContext.cs @@ -0,0 +1,34 @@ +// +// 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 SendEvent( + EventType eventType, + TParams eventParams) + { + await this.messageWriter.WriteEvent( + eventType, + eventParams); + } + } +} + diff --git a/ServiceHost/MessageProtocol/EventType.cs b/ServiceHost/MessageProtocol/EventType.cs new file mode 100644 index 00000000..1b2fe189 --- /dev/null +++ b/ServiceHost/MessageProtocol/EventType.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. +// + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol +{ + /// + /// Defines an event type with a particular method name. + /// + /// The parameter type for this event. + public class EventType + { + /// + /// Gets the method name for the event type. + /// + public string MethodName { get; private set; } + + /// + /// 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 EventType Create(string methodName) + { + return new EventType() + { + MethodName = methodName + }; + } + } +} + diff --git a/ServiceHost/MessageProtocol/IMessageSender.cs b/ServiceHost/MessageProtocol/IMessageSender.cs new file mode 100644 index 00000000..80474357 --- /dev/null +++ b/ServiceHost/MessageProtocol/IMessageSender.cs @@ -0,0 +1,22 @@ +// +// 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 +{ + internal interface IMessageSender + { + Task SendEvent( + EventType eventType, + TParams eventParams); + + Task SendRequest( + RequestType requestType, + TParams requestParams, + bool waitForResponse); + } +} + diff --git a/ServiceHost/MessageProtocol/IMessageSerializer.cs b/ServiceHost/MessageProtocol/IMessageSerializer.cs new file mode 100644 index 00000000..eafd8f20 --- /dev/null +++ b/ServiceHost/MessageProtocol/IMessageSerializer.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 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/ServiceHost/MessageProtocol/Message.cs b/ServiceHost/MessageProtocol/Message.cs new file mode 100644 index 00000000..0d304cc0 --- /dev/null +++ b/ServiceHost/MessageProtocol/Message.cs @@ -0,0 +1,136 @@ +// +// 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/ServiceHost/MessageProtocol/MessageDispatcher.cs b/ServiceHost/MessageProtocol/MessageDispatcher.cs new file mode 100644 index 00000000..d7b62fcd --- /dev/null +++ b/ServiceHost/MessageProtocol/MessageDispatcher.cs @@ -0,0 +1,325 @@ +// +// 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 + { + #region Fields + + private ChannelBase protocolChannel; + // private AsyncQueue messagesToWrite; + private AsyncContextThread messageLoopThread; + + private Dictionary> requestHandlers = + new Dictionary>(); + + private Dictionary> eventHandlers = + new Dictionary>(); + + private Action responseHandler; + + private CancellationTokenSource messageLoopCancellationToken = + new CancellationTokenSource(); + + #endregion + + #region Properties + + public SynchronizationContext SynchronizationContext { get; private set; } + + public 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 MessageReader MessageReader { get; private set; } + + protected MessageWriter MessageWriter { get; private set; } + + + #endregion + + #region Constructors + + public MessageDispatcher(ChannelBase protocolChannel) + { + this.protocolChannel = protocolChannel; + this.MessageReader = protocolChannel.MessageReader; + this.MessageWriter = protocolChannel.MessageWriter; + } + + #endregion + + #region Public Methods + + public void Start() + { + // 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.ListenForMessages(this.messageLoopCancellationToken.Token)) + .ContinueWith(this.OnListenTaskCompleted); + } + + public void Stop() + { + // Stop the message loop thread + if (this.messageLoopThread != null) + { + this.messageLoopCancellationToken.Cancel(); + this.messageLoopThread.Stop(); + } + } + + public void SetRequestHandler( + RequestType requestType, + Func, Task> requestHandler) + { + this.SetRequestHandler( + requestType, + requestHandler, + false); + } + + public void SetRequestHandler( + RequestType requestType, + Func, Task> requestHandler, + bool overrideExisting) + { + if (overrideExisting) + { + // Remove the existing handler so a new one can be set + this.requestHandlers.Remove(requestType.MethodName); + } + + this.requestHandlers.Add( + requestType.MethodName, + (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 SetEventHandler( + EventType eventType, + Func eventHandler) + { + this.SetEventHandler( + eventType, + eventHandler, + false); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler, + bool overrideExisting) + { + if (overrideExisting) + { + // Remove the existing handler so a new one can be set + this.eventHandlers.Remove(eventType.MethodName); + } + + this.eventHandlers.Add( + eventType.MethodName, + (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); + }); + } + + public void SetResponseHandler(Action responseHandler) + { + this.responseHandler = responseHandler; + } + + #endregion + + #region Events + + public event EventHandler UnhandledException; + + protected void OnUnhandledException(Exception unhandledException) + { + if (this.UnhandledException != null) + { + this.UnhandledException(this, unhandledException); + } + } + + #endregion + + #region Private Methods + + private async Task ListenForMessages(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.MessageReader.ReadMessage(); + } + 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 (EndOfStreamException) + { + // The stream has ended, end the message loop + break; + } + catch (Exception e) + { + var b = e.Message; + newMessage = null; + } + + // 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) + { + // Process the message + await this.DispatchMessage( + newMessage, + this.MessageWriter); + } + } + } + + protected async Task DispatchMessage( + 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 + } + } + else if (messageToDispatch.MessageType == MessageType.Response) + { + if (this.responseHandler != null) + { + this.responseHandler(messageToDispatch); + } + } + 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 + } + } + else + { + // TODO: Return message not supported + } + + 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; + } + } + } + } + + private void OnListenTaskCompleted(Task listenTask) + { + if (listenTask.IsFaulted) + { + this.OnUnhandledException(listenTask.Exception); + } + else if (listenTask.IsCompleted || listenTask.IsCanceled) + { + // TODO: Dispose of anything? + } + } + + #endregion + } +} + diff --git a/ServiceHost/MessageProtocol/MessageParseException.cs b/ServiceHost/MessageProtocol/MessageParseException.cs new file mode 100644 index 00000000..2155bb14 --- /dev/null +++ b/ServiceHost/MessageProtocol/MessageParseException.cs @@ -0,0 +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; + +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/ServiceHost/MessageProtocol/MessageProtocolType.cs b/ServiceHost/MessageProtocol/MessageProtocolType.cs new file mode 100644 index 00000000..7aa3d6c8 --- /dev/null +++ b/ServiceHost/MessageProtocol/MessageProtocolType.cs @@ -0,0 +1,23 @@ +// +// 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/ServiceHost/MessageProtocol/MessageReader.cs b/ServiceHost/MessageProtocol/MessageReader.cs new file mode 100644 index 00000000..32ec9387 --- /dev/null +++ b/ServiceHost/MessageProtocol/MessageReader.cs @@ -0,0 +1,262 @@ +// +// 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 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, + Encoding messageEncoding = null) + { + Validate.IsNotNull("streamReader", inputStream); + Validate.IsNotNull("messageSerializer", messageSerializer); + + 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 ReadMessage() + { + string messageContent = null; + + // Do we need to read more data or can we process the existing buffer? + while (!this.needsMoreData || await this.ReadNextChunk()) + { + // 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); + + // Load the message + Logger.Write( + LogLevel.Verbose, + string.Format( + "READ MESSAGE:\r\n\r\n{0}", + messageObject.ToString(Formatting.Indented))); + + // Return the parsed message + return this.messageSerializer.DeserializeMessage(messageObject); + } + + #endregion + + #region Private Methods + + private async Task ReadNextChunk() + { + // 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/ServiceHost/MessageProtocol/MessageWriter.cs b/ServiceHost/MessageProtocol/MessageWriter.cs new file mode 100644 index 00000000..28fbe1eb --- /dev/null +++ b/ServiceHost/MessageProtocol/MessageWriter.cs @@ -0,0 +1,141 @@ +// +// 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 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) + { + Validate.IsNotNull("streamWriter", outputStream); + Validate.IsNotNull("messageSerializer", messageSerializer); + + this.outputStream = outputStream; + this.messageSerializer = messageSerializer; + } + + #endregion + + #region Public Methods + + // TODO: This method should be made protected or private + + public async Task WriteMessage(Message messageToWrite) + { + Validate.IsNotNull("messageToWrite", messageToWrite); + + // Serialize the message + JObject messageObject = + this.messageSerializer.SerializeMessage( + messageToWrite); + + // Log the JSON representation of the message + Logger.Write( + LogLevel.Verbose, + string.Format( + "WRITE MESSAGE:\r\n\r\n{0}", + JsonConvert.SerializeObject( + messageObject, + Formatting.Indented, + Constants.JsonSerializerSettings))); + + 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()) + { + // Send the message + await this.outputStream.WriteAsync(headerBytes, 0, headerBytes.Length); + await this.outputStream.WriteAsync(messageBytes, 0, messageBytes.Length); + await this.outputStream.FlushAsync(); + } + } + + public async Task WriteRequest( + RequestType requestType, + TParams requestParams, + int requestId) + { + // Allow null content + JToken contentObject = + requestParams != null ? + JToken.FromObject(requestParams, contentSerializer) : + null; + + await this.WriteMessage( + Message.Request( + requestId.ToString(), + requestType.MethodName, + contentObject)); + } + + public async Task WriteResponse(TResult resultContent, string method, string requestId) + { + // Allow null content + JToken contentObject = + resultContent != null ? + JToken.FromObject(resultContent, contentSerializer) : + null; + + await this.WriteMessage( + Message.Response( + requestId, + method, + contentObject)); + } + + public async Task WriteEvent(EventType eventType, TParams eventParams) + { + // Allow null content + JToken contentObject = + eventParams != null ? + JToken.FromObject(eventParams, contentSerializer) : + null; + + await this.WriteMessage( + Message.Event( + eventType.MethodName, + contentObject)); + } + + #endregion + } +} diff --git a/ServiceHost/MessageProtocol/ProtocolEndpoint.cs b/ServiceHost/MessageProtocol/ProtocolEndpoint.cs new file mode 100644 index 00000000..ae033191 --- /dev/null +++ b/ServiceHost/MessageProtocol/ProtocolEndpoint.cs @@ -0,0 +1,313 @@ +// +// 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 System; +using System.Collections.Generic; +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 bool isStarted; + private int currentMessageId; + private ChannelBase protocolChannel; + private MessageProtocolType messageProtocolType; + private TaskCompletionSource endpointExitedTask; + private SynchronizationContext originalSynchronizationContext; + + private Dictionary> pendingRequests = + new Dictionary>(); + + /// + /// Gets the MessageDispatcher which allows registration of + /// handlers for requests, responses, and events that are + /// transmitted through the channel. + /// + protected MessageDispatcher MessageDispatcher { get; 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, + MessageProtocolType messageProtocolType) + { + this.protocolChannel = protocolChannel; + this.messageProtocolType = messageProtocolType; + this.originalSynchronizationContext = SynchronizationContext.Current; + } + + /// + /// Starts the language server client and sends the Initialize method. + /// + /// A Task that can be awaited for initialization to complete. + public async Task Start() + { + if (!this.isStarted) + { + // Start the provided protocol channel + this.protocolChannel.Start(this.messageProtocolType); + + // Start the message dispatcher + this.MessageDispatcher = new MessageDispatcher(this.protocolChannel); + + // Set the handler for any message responses that come back + this.MessageDispatcher.SetResponseHandler(this.HandleResponse); + + // Listen for unhandled exceptions from the dispatcher + this.MessageDispatcher.UnhandledException += MessageDispatcher_UnhandledException; + + // Notify implementation about endpoint start + await this.OnStart(); + + // Wait for connection and notify the implementor + // NOTE: This task is not meant to be awaited. + Task waitTask = + this.protocolChannel + .WaitForConnection() + .ContinueWith( + async (t) => + { + // Start the MessageDispatcher + this.MessageDispatcher.Start(); + await this.OnConnect(); + }); + + // Endpoint is now started + this.isStarted = true; + } + } + + public void WaitForExit() + { + this.endpointExitedTask = new TaskCompletionSource(); + this.endpointExitedTask.Task.Wait(); + } + + public async Task Stop() + { + if (this.isStarted) + { + // Make sure no future calls try to stop the endpoint during shutdown + this.isStarted = false; + + // Stop the implementation first + await this.OnStop(); + + // Stop the dispatcher and channel + this.MessageDispatcher.Stop(); + this.protocolChannel.Stop(); + + // Notify anyone waiting for exit + if (this.endpointExitedTask != null) + { + this.endpointExitedTask.SetResult(true); + } + } + } + + #region Message Sending + + /// + /// Sends a request to the server + /// + /// + /// + /// + /// + /// + public Task SendRequest( + RequestType requestType, + TParams requestParams) + { + return this.SendRequest(requestType, requestParams, true); + } + + public async Task SendRequest( + RequestType requestType, + TParams requestParams, + bool waitForResponse) + { + if (!this.protocolChannel.IsConnected) + { + throw new InvalidOperationException("SendRequest called when ProtocolChannel was not yet connected"); + } + + this.currentMessageId++; + + TaskCompletionSource responseTask = null; + + if (waitForResponse) + { + responseTask = new TaskCompletionSource(); + this.pendingRequests.Add( + this.currentMessageId.ToString(), + responseTask); + } + + await this.protocolChannel.MessageWriter.WriteRequest( + 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 SendEvent( + EventType eventType, + TParams eventParams) + { + if (!this.protocolChannel.IsConnected) + { + throw new InvalidOperationException("SendEvent called when ProtocolChannel was not yet connected"); + } + + // 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.MessageDispatcher.InMessageLoopThread) + { + TaskCompletionSource writeTask = new TaskCompletionSource(); + + this.MessageDispatcher.SynchronizationContext.Post( + async (obj) => + { + await this.protocolChannel.MessageWriter.WriteEvent( + eventType, + eventParams); + + writeTask.SetResult(true); + }, null); + + return writeTask.Task; + } + else + { + return this.protocolChannel.MessageWriter.WriteEvent( + eventType, + eventParams); + } + } + + #endregion + + #region Message Handling + + public void SetRequestHandler( + RequestType requestType, + Func, Task> requestHandler) + { + this.MessageDispatcher.SetRequestHandler( + requestType, + requestHandler); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler) + { + this.MessageDispatcher.SetEventHandler( + eventType, + eventHandler, + false); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler, + bool overrideExisting) + { + this.MessageDispatcher.SetEventHandler( + eventType, + eventHandler, + overrideExisting); + } + + private void HandleResponse(Message responseMessage) + { + TaskCompletionSource pendingRequestTask = null; + + if (this.pendingRequests.TryGetValue(responseMessage.Id, out pendingRequestTask)) + { + pendingRequestTask.SetResult(responseMessage); + this.pendingRequests.Remove(responseMessage.Id); + } + } + + #endregion + + #region Subclass Lifetime Methods + + protected virtual Task OnStart() + { + return Task.FromResult(true); + } + + protected virtual Task OnConnect() + { + return Task.FromResult(true); + } + + protected virtual Task OnStop() + { + return Task.FromResult(true); + } + + #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 + } +} + diff --git a/ServiceHost/MessageProtocol/RequestContext.cs b/ServiceHost/MessageProtocol/RequestContext.cs new file mode 100644 index 00000000..a578f466 --- /dev/null +++ b/ServiceHost/MessageProtocol/RequestContext.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 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 SendResult(TResult resultDetails) + { + await this.messageWriter.WriteResponse( + resultDetails, + requestMessage.Method, + requestMessage.Id); + } + + public async Task SendEvent(EventType eventType, TParams eventParams) + { + await this.messageWriter.WriteEvent( + eventType, + eventParams); + } + + public async Task SendError(object errorDetails) + { + await this.messageWriter.WriteMessage( + Message.ResponseError( + requestMessage.Id, + requestMessage.Method, + JToken.FromObject(errorDetails))); + } + } +} + diff --git a/ServiceHost/MessageProtocol/RequestType.cs b/ServiceHost/MessageProtocol/RequestType.cs new file mode 100644 index 00000000..e5d0cea0 --- /dev/null +++ b/ServiceHost/MessageProtocol/RequestType.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.Diagnostics; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol +{ + [DebuggerDisplay("RequestType MethodName = {MethodName}")] + public class RequestType + { + public string MethodName { get; private set; } + + public static RequestType Create(string typeName) + { + return new RequestType() + { + MethodName = typeName + }; + } + } +} + diff --git a/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs b/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs new file mode 100644 index 00000000..8f736ab0 --- /dev/null +++ b/ServiceHost/MessageProtocol/Serializers/JsonRpcMessageSerializer.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 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/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs b/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs new file mode 100644 index 00000000..3a1e41e6 --- /dev/null +++ b/ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs @@ -0,0 +1,115 @@ +// +// 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) + { + messageObject.Add("type", JToken.FromObject("response")); + messageObject.Add("request_seq", JToken.FromObject(message.Id)); + 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/ServiceHost/Messages/PromptEvents.cs b/ServiceHost/Messages/PromptEvents.cs new file mode 100644 index 00000000..29dcb53c --- /dev/null +++ b/ServiceHost/Messages/PromptEvents.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. +// + +#if false +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 string Caption { get; set; } + + public string Message { get; set; } + + public ChoiceDetails[] Choices { get; set; } + + public int DefaultChoice { get; set; } + } + + public class ShowChoicePromptResponse + { + public bool PromptCancelled { get; set; } + + public string ChosenItem { 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; } + } +} + +#endif \ No newline at end of file diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs new file mode 100644 index 00000000..32298019 --- /dev/null +++ b/ServiceHost/Program.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.PowerShell.EditorServices.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.SqlTools.ServiceHost +{ + class Program + { + static void Main(string[] args) + { + var hostDetails = new HostDetails("name", "profileId", new Version(1,0)); + var profilePaths = new ProfilePaths("hostProfileId", "baseAllUsersPath", "baseCurrentUserPath"); + var languageServer = new LanguageServer(hostDetails, profilePaths); + + languageServer.Start().Wait(); + + languageServer.WaitForExit(); + } + } +} diff --git a/ServiceHost/Properties/AssemblyInfo.cs b/ServiceHost/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..3234ac73 --- /dev/null +++ b/ServiceHost/Properties/AssemblyInfo.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.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerShell Editor Services Host Protocol Library")] +[assembly: AssemblyDescription("Provides message types and client/server APIs for the PowerShell Editor Services JSON protocol.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("PowerShell Editor Services")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("78caf6c3-5955-4b15-a302-2bd6b7871d5b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] +[assembly: AssemblyInformationalVersion("0.0.0.0")] + +[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Test.Protocol")] \ No newline at end of file diff --git a/ServiceHost/Server/DebugAdapter.cs b/ServiceHost/Server/DebugAdapter.cs new file mode 100644 index 00000000..1b63dd13 --- /dev/null +++ b/ServiceHost/Server/DebugAdapter.cs @@ -0,0 +1,584 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#if false + +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.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public class DebugAdapter : DebugAdapterBase + { + private EditorSession editorSession; + private OutputDebouncer outputDebouncer; + private bool isConfigurationDoneRequestComplete; + private bool isLaunchRequestComplete; + private bool noDebug; + private string scriptPathToLaunch; + private string arguments; + + public DebugAdapter(HostDetails hostDetails, ProfilePaths profilePaths) + : this(hostDetails, profilePaths, new StdioServerChannel()) + { + } + + public DebugAdapter(HostDetails hostDetails, ProfilePaths profilePaths, ChannelBase serverChannel) + : base(serverChannel) + { + this.editorSession = new EditorSession(); + this.editorSession.StartSession(hostDetails, profilePaths); + this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped; + this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; + + // Set up the output debouncer to throttle output event writes + this.outputDebouncer = new OutputDebouncer(this); + } + + protected override void Initialize() + { + // Register all supported message types + + this.SetRequestHandler(LaunchRequest.Type, this.HandleLaunchRequest); + this.SetRequestHandler(AttachRequest.Type, this.HandleAttachRequest); + this.SetRequestHandler(ConfigurationDoneRequest.Type, this.HandleConfigurationDoneRequest); + this.SetRequestHandler(DisconnectRequest.Type, this.HandleDisconnectRequest); + + this.SetRequestHandler(SetBreakpointsRequest.Type, this.HandleSetBreakpointsRequest); + this.SetRequestHandler(SetExceptionBreakpointsRequest.Type, this.HandleSetExceptionBreakpointsRequest); + this.SetRequestHandler(SetFunctionBreakpointsRequest.Type, this.HandleSetFunctionBreakpointsRequest); + + this.SetRequestHandler(ContinueRequest.Type, this.HandleContinueRequest); + this.SetRequestHandler(NextRequest.Type, this.HandleNextRequest); + this.SetRequestHandler(StepInRequest.Type, this.HandleStepInRequest); + this.SetRequestHandler(StepOutRequest.Type, this.HandleStepOutRequest); + this.SetRequestHandler(PauseRequest.Type, this.HandlePauseRequest); + + this.SetRequestHandler(ThreadsRequest.Type, this.HandleThreadsRequest); + this.SetRequestHandler(StackTraceRequest.Type, this.HandleStackTraceRequest); + this.SetRequestHandler(ScopesRequest.Type, this.HandleScopesRequest); + this.SetRequestHandler(VariablesRequest.Type, this.HandleVariablesRequest); + this.SetRequestHandler(SourceRequest.Type, this.HandleSourceRequest); + this.SetRequestHandler(EvaluateRequest.Type, this.HandleEvaluateRequest); + } + + protected Task LaunchScript(RequestContext requestContext) + { + return editorSession.PowerShellContext + .ExecuteScriptAtPath(this.scriptPathToLaunch, this.arguments) + .ContinueWith( + async (t) => { + Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); + + await requestContext.SendEvent( + TerminatedEvent.Type, + null); + + // Stop the server + await this.Stop(); + + // Notify that the session has ended + this.OnSessionEnded(); + }); + } + + protected override void Shutdown() + { + // Make sure remaining output is flushed before exiting + this.outputDebouncer.Flush().Wait(); + + Logger.Write(LogLevel.Normal, "Debug adapter is shutting down..."); + + if (this.editorSession != null) + { + this.editorSession.Dispose(); + this.editorSession = null; + } + } + + #region Built-in Message Handlers + + protected async Task HandleConfigurationDoneRequest( + object args, + RequestContext requestContext) + { + // The order of debug protocol messages apparently isn't as guaranteed as we might like. + // Need to be able to handle the case where we get the configurationDone request after the + // launch request. + if (this.isLaunchRequestComplete) + { + this.LaunchScript(requestContext); + } + + this.isConfigurationDoneRequestComplete = true; + + await requestContext.SendResult(null); + } + + protected async Task HandleLaunchRequest( + LaunchRequestArguments launchParams, + RequestContext requestContext) + { + // Set the working directory for the PowerShell runspace to the cwd passed in via launch.json. + // In case that is null, use the the folder of the script to be executed. If the resulting + // working dir path is a file path then extract the directory and use that. + string workingDir = launchParams.Cwd ?? launchParams.Program; + workingDir = PowerShellContext.UnescapePath(workingDir); + try + { + if ((File.GetAttributes(workingDir) & FileAttributes.Directory) != FileAttributes.Directory) + { + workingDir = Path.GetDirectoryName(workingDir); + } + } + catch (Exception ex) + { + Logger.Write(LogLevel.Error, "cwd path is invalid: " + ex.Message); + +#if NanoServer + workingDir = AppContext.BaseDirectory; +#else + workingDir = Environment.CurrentDirectory; +#endif + } + + editorSession.PowerShellContext.SetWorkingDirectory(workingDir); + Logger.Write(LogLevel.Verbose, "Working dir 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); + } + + // We may not actually launch the script in response to this + // request unless it comes after the configurationDone request. + // If the launch request comes first, then stash the launch + // params so that the subsequent configurationDone request handler + // can launch the script. + this.noDebug = launchParams.NoDebug; + this.scriptPathToLaunch = launchParams.Program; + this.arguments = arguments; + + // The order of debug protocol messages apparently isn't as guaranteed as we might like. + // Need to be able to handle the case where we get the launch request after the + // configurationDone request. + if (this.isConfigurationDoneRequestComplete) + { + this.LaunchScript(requestContext); + } + + this.isLaunchRequestComplete = true; + + await requestContext.SendResult(null); + } + + protected Task HandleAttachRequest( + AttachRequestArguments attachParams, + RequestContext requestContext) + { + // TODO: Implement this once we support attaching to processes + throw new NotImplementedException(); + } + + protected Task HandleDisconnectRequest( + object disconnectParams, + RequestContext requestContext) + { + EventHandler handler = null; + + handler = + async (o, e) => + { + if (e.NewSessionState == PowerShellContextState.Ready) + { + await requestContext.SendResult(null); + editorSession.PowerShellContext.SessionStateChanged -= handler; + + // Stop the server + this.Stop(); + } + }; + + editorSession.PowerShellContext.SessionStateChanged += handler; + editorSession.PowerShellContext.AbortExecution(); + + return Task.FromResult(true); + } + + protected async Task HandleSetBreakpointsRequest( + SetBreakpointsRequestArguments setBreakpointsParams, + RequestContext requestContext) + { + ScriptFile scriptFile; + + // Fix for issue #195 - user can change name of file outside of VSCode in which case + // VSCode sends breakpoint requests with the original filename that doesn't exist anymore. + try + { + scriptFile = editorSession.Workspace.GetFile(setBreakpointsParams.Source.Path); + } + catch (FileNotFoundException) + { + Logger.Write( + LogLevel.Warning, + $"Attempted to set breakpoints on a non-existing file: {setBreakpointsParams.Source.Path}"); + + string message = this.noDebug ? string.Empty : "Source does not exist, breakpoint not set."; + + var srcBreakpoints = setBreakpointsParams.Breakpoints + .Select(srcBkpt => Protocol.DebugAdapter.Breakpoint.Create( + srcBkpt, setBreakpointsParams.Source.Path, message, verified: this.noDebug)); + + // Return non-verified breakpoint message. + await requestContext.SendResult( + new SetBreakpointsResponseBody { + Breakpoints = srcBreakpoints.ToArray() + }); + + return; + } + + 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); + } + + // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. + BreakpointDetails[] updatedBreakpointDetails = breakpointDetails; + if (!this.noDebug) + { + updatedBreakpointDetails = + await editorSession.DebugService.SetLineBreakpoints( + scriptFile, + breakpointDetails); + } + + await requestContext.SendResult( + new SetBreakpointsResponseBody { + Breakpoints = + updatedBreakpointDetails + .Select(Protocol.DebugAdapter.Breakpoint.Create) + .ToArray() + }); + } + + protected async Task HandleSetFunctionBreakpointsRequest( + 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); + } + + // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. + CommandBreakpointDetails[] updatedBreakpointDetails = breakpointDetails; + if (!this.noDebug) + { + updatedBreakpointDetails = + await editorSession.DebugService.SetCommandBreakpoints( + breakpointDetails); + } + + await requestContext.SendResult( + new SetBreakpointsResponseBody { + Breakpoints = + updatedBreakpointDetails + .Select(Protocol.DebugAdapter.Breakpoint.Create) + .ToArray() + }); + } + + protected async Task HandleSetExceptionBreakpointsRequest( + SetExceptionBreakpointsRequestArguments setExceptionBreakpointsParams, + RequestContext requestContext) + { + // TODO: Handle this appropriately + + await requestContext.SendResult(null); + } + + protected async Task HandleContinueRequest( + object continueParams, + RequestContext requestContext) + { + editorSession.DebugService.Continue(); + + await requestContext.SendResult(null); + } + + protected async Task HandleNextRequest( + object nextParams, + RequestContext requestContext) + { + editorSession.DebugService.StepOver(); + + await requestContext.SendResult(null); + } + + protected Task HandlePauseRequest( + object pauseParams, + RequestContext requestContext) + { + try + { + editorSession.DebugService.Break(); + } + catch (NotSupportedException e) + { + return requestContext.SendError(e.Message); + } + + // This request is responded to by sending the "stopped" event + return Task.FromResult(true); + } + + protected async Task HandleStepInRequest( + object stepInParams, + RequestContext requestContext) + { + editorSession.DebugService.StepIn(); + + await requestContext.SendResult(null); + } + + protected async Task HandleStepOutRequest( + object stepOutParams, + RequestContext requestContext) + { + editorSession.DebugService.StepOut(); + + await requestContext.SendResult(null); + } + + protected async Task HandleThreadsRequest( + object threadsParams, + RequestContext requestContext) + { + await requestContext.SendResult( + new ThreadsResponseBody + { + Threads = new Thread[] + { + // TODO: What do I do with these? + new Thread + { + Id = 1, + Name = "Main Thread" + } + } + }); + } + + protected async Task HandleStackTraceRequest( + StackTraceRequestArguments stackTraceParams, + RequestContext requestContext) + { + StackFrameDetails[] stackFrames = + editorSession.DebugService.GetStackFrames(); + + List newStackFrames = new List(); + + for (int i = 0; i < stackFrames.Length; 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.SendResult( + new StackTraceResponseBody + { + StackFrames = newStackFrames.ToArray() + }); + } + + protected async Task HandleScopesRequest( + ScopesRequestArguments scopesParams, + RequestContext requestContext) + { + VariableScope[] variableScopes = + editorSession.DebugService.GetVariableScopes( + scopesParams.FrameId); + + await requestContext.SendResult( + new ScopesResponseBody + { + Scopes = + variableScopes + .Select(Scope.Create) + .ToArray() + }); + } + + protected async Task HandleVariablesRequest( + 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.SendResult(variablesResponse); + } + + protected Task HandleSourceRequest( + 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 HandleEvaluateRequest( + EvaluateRequestArguments evaluateParams, + RequestContext requestContext) + { + string valueString = null; + int variableId = 0; + + bool isFromRepl = + string.Equals( + evaluateParams.Context, + "repl", + StringComparison.CurrentCultureIgnoreCase); + + if (isFromRepl) + { + // Send the input through the console service + editorSession.ConsoleService.ExecuteCommand( + evaluateParams.Expression, + false); + } + else + { + VariableDetails result = + await editorSession.DebugService.EvaluateExpression( + evaluateParams.Expression, + evaluateParams.FrameId, + isFromRepl); + + if (result != null) + { + valueString = result.ValueString; + variableId = + result.IsExpandable ? + result.Id : 0; + } + } + + await requestContext.SendResult( + new EvaluateResponseBody + { + Result = valueString, + VariablesReference = variableId + }); + } + + #endregion + + #region Events + + public event EventHandler SessionEnded; + + protected virtual void OnSessionEnded() + { + this.SessionEnded?.Invoke(this, null); + } + + #endregion + + #region Event Handlers + + async void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) + { + // Flush pending output before sending the event + await this.outputDebouncer.Flush(); + + // 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.Breakpoints.Count > 0) + { + debuggerStoppedReason = + e.Breakpoints[0] is CommandBreakpoint + ? "function breakpoint" + : "breakpoint"; + } + + await this.SendEvent( + StoppedEvent.Type, + new StoppedEventBody + { + Source = new Source + { + Path = e.InvocationInfo.ScriptName, + }, + Line = e.InvocationInfo.ScriptLineNumber, + Column = e.InvocationInfo.OffsetInLine, + ThreadId = 1, // TODO: Change this based on context + Reason = debuggerStoppedReason + }); + } + + async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) + { + // Queue the output for writing + await this.outputDebouncer.Invoke(e); + } + + #endregion + } +} + +#endif \ No newline at end of file diff --git a/ServiceHost/Server/DebugAdapterBase.cs b/ServiceHost/Server/DebugAdapterBase.cs new file mode 100644 index 00000000..f896c119 --- /dev/null +++ b/ServiceHost/Server/DebugAdapterBase.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. +// +#if false +using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public abstract class DebugAdapterBase : ProtocolEndpoint + { + public DebugAdapterBase(ChannelBase serverChannel) + : base (serverChannel, MessageProtocolType.DebugAdapter) + { + } + + /// + /// Overridden by the subclass to provide initialization + /// logic after the server channel is started. + /// + protected abstract void Initialize(); + + /// + /// Can be overridden by the subclass to provide shutdown + /// logic before the server exits. + /// + protected virtual void Shutdown() + { + // No default implementation yet. + } + + protected override Task OnStart() + { + // Register handlers for server lifetime messages + this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); + + // Initialize the implementation class + this.Initialize(); + + return Task.FromResult(true); + } + + protected override Task OnStop() + { + this.Shutdown(); + + return Task.FromResult(true); + } + + private async Task HandleInitializeRequest( + object shutdownParams, + RequestContext requestContext) + { + // Now send the Initialize response to continue setup + await requestContext.SendResult( + new InitializeResponseBody { + SupportsConfigurationDoneRequest = true, + SupportsConditionalBreakpoints = true, + SupportsFunctionBreakpoints = true + }); + + // Send the Initialized event so that we get breakpoints + await requestContext.SendEvent( + InitializedEvent.Type, + null); + } + } +} + +#endif \ No newline at end of file diff --git a/ServiceHost/Server/LanguageServer.cs b/ServiceHost/Server/LanguageServer.cs new file mode 100644 index 00000000..1e0dd925 --- /dev/null +++ b/ServiceHost/Server/LanguageServer.cs @@ -0,0 +1,1262 @@ +// +// 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.Protocol.LanguageServer; +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.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public class LanguageServer : LanguageServerBase + { + // private static CancellationTokenSource existingRequestCancellation; + + // private bool profilesLoaded; + // private EditorSession editorSession; + // private OutputDebouncer outputDebouncer; + // private LanguageServerEditorOperations editorOperations; + // private LanguageServerSettings currentSettings = new LanguageServerSettings(); + + /// + /// Provides details about the host application. + /// + public LanguageServer(HostDetails hostDetails, ProfilePaths profilePaths) + : this(hostDetails, profilePaths, new StdioServerChannel()) + { + } + + /// + /// Provides details about the host application. + /// + public LanguageServer(HostDetails hostDetails, ProfilePaths profilePaths, ChannelBase serverChannel) + : base(serverChannel) + { +#if false + this.editorSession = new EditorSession(); + this.editorSession.StartSession(hostDetails, profilePaths); + this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten; + + // Attach to ExtensionService events + this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAdded; + this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdated; + this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemoved; + + // Create the IEditorOperations implementation + this.editorOperations = + new LanguageServerEditorOperations( + this.editorSession, + this); + + // Always send console prompts through the UI in the language service + // TODO: This will change later once we have a general REPL available + // in VS Code. + this.editorSession.ConsoleService.PushPromptHandlerContext( + new ProtocolPromptHandlerContext( + this, + this.editorSession.ConsoleService)); + + // Set up the output debouncer to throttle output event writes + this.outputDebouncer = new OutputDebouncer(this); +#endif + } + +protected async Task HandleInitializeRequest( + InitializeRequest initializeParams, + RequestContext requestContext) + { + // Grab the workspace path from the parameters + //editorSession.Workspace.WorkspacePath = initializeParams.RootPath; + + await requestContext.SendResult( + new InitializeResult + { + Capabilities = new ServerCapabilities + { + TextDocumentSync = TextDocumentSyncKind.Incremental, + DefinitionProvider = true, + ReferencesProvider = true, + DocumentHighlightProvider = true, + DocumentSymbolProvider = true, + WorkspaceSymbolProvider = true, + HoverProvider = true, + CompletionProvider = new CompletionOptions + { + ResolveProvider = true, + TriggerCharacters = new string[] { ".", "-", ":", "\\" } + }, + SignatureHelpProvider = new SignatureHelpOptions + { + TriggerCharacters = new string[] { " " } // TODO: Other characters here? + } + } + }); + } + + protected override void Initialize() + { + // Register all supported message types + + this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); + + // this.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); + // this.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); + // this.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); + // this.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); + + // this.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest); + // this.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest); + // this.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequest); + // this.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequest); + // this.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest); + // this.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); + // this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); + // this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); + // this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + + // this.SetRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest); + // this.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequest); + + // this.SetRequestHandler(FindModuleRequest.Type, this.HandleFindModuleRequest); + // this.SetRequestHandler(InstallModuleRequest.Type, this.HandleInstallModuleRequest); + + // this.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequest); + + // this.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); + +#if false + // Initialize the extension service + // TODO: This should be made awaited once Initialize is async! + this.editorSession.ExtensionService.Initialize( + this.editorOperations).Wait(); +#endif + } + +#if false + protected override async Task Shutdown() + { + // Make sure remaining output is flushed before exiting + await this.outputDebouncer.Flush(); + + //Logger.Write(LogLevel.Normal, "Language service is shutting down..."); + + if (this.editorSession != null) + { + this.editorSession.Dispose(); + this.editorSession = null; + } + } + + #region Built-in Message Handlers + + protected async Task HandleInitializeRequest( + InitializeRequest initializeParams, + RequestContext requestContext) + { + // Grab the workspace path from the parameters + //editorSession.Workspace.WorkspacePath = initializeParams.RootPath; + + await requestContext.SendResult( + new InitializeResult + { + Capabilities = new ServerCapabilities + { + TextDocumentSync = TextDocumentSyncKind.Incremental, + DefinitionProvider = true, + ReferencesProvider = true, + DocumentHighlightProvider = true, + DocumentSymbolProvider = true, + WorkspaceSymbolProvider = true, + HoverProvider = true, + CompletionProvider = new CompletionOptions + { + ResolveProvider = true, + TriggerCharacters = new string[] { ".", "-", ":", "\\" } + }, + SignatureHelpProvider = new SignatureHelpOptions + { + TriggerCharacters = new string[] { " " } // TODO: Other characters here? + } + } + }); + } + + protected async Task HandleShowOnlineHelpRequest( + string helpParams, + RequestContext requestContext) + { + if (helpParams == null) { helpParams = "get-help"; } + + var psCommand = new PSCommand(); + psCommand.AddCommand("Get-Help"); + psCommand.AddArgument(helpParams); + psCommand.AddParameter("Online"); + + await editorSession.PowerShellContext.ExecuteCommand(psCommand); + + await requestContext.SendResult(null); + } + + private async Task HandleInstallModuleRequest( + string moduleName, + RequestContext requestContext + ) + { + var script = string.Format("Install-Module -Name {0} -Scope CurrentUser", moduleName); + + var executeTask = + editorSession.PowerShellContext.ExecuteScriptString( + script, + true, + true).ConfigureAwait(false); + + await requestContext.SendResult(null); + } + + private Task HandleInvokeExtensionCommandRequest( + 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.InvokeCommand( + commandDetails.Name, + editorContext); + + commandTask.ContinueWith(t => + { + return requestContext.SendResult(null); + }); + + return Task.FromResult(true); + } + + private async Task HandleExpandAliasRequest( + 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 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.ExecuteCommand(psCommand); + + psCommand = new PSCommand(); + psCommand.AddCommand("__Expand-Alias").AddArgument(content); + var result = await this.editorSession.PowerShellContext.ExecuteCommand(psCommand); + + await requestContext.SendResult(result.First().ToString()); + } + + private async Task HandleFindModuleRequest( + object param, + RequestContext requestContext) + { + var psCommand = new PSCommand(); + psCommand.AddScript("Find-Module | Select Name, Description"); + + var modules = await editorSession.PowerShellContext.ExecuteCommand(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.SendResult(moduleList); + } + + protected Task HandleDidOpenTextDocumentNotification( + DidOpenTextDocumentNotification openParams, + EventContext eventContext) + { + ScriptFile openedFile = + editorSession.Workspace.GetFileBuffer( + openParams.Uri, + openParams.Text); + + // TODO: Get all recently edited files in the workspace + this.RunScriptDiagnostics( + new ScriptFile[] { openedFile }, + editorSession, + eventContext); + + Logger.Write(LogLevel.Verbose, "Finished opening document."); + + return Task.FromResult(true); + } + + protected Task HandleDidCloseTextDocumentNotification( + TextDocumentIdentifier closeParams, + EventContext eventContext) + { + // Find and close the file in the current session + var fileToClose = editorSession.Workspace.GetFile(closeParams.Uri); + + if (fileToClose != null) + { + editorSession.Workspace.CloseFile(fileToClose); + } + + Logger.Write(LogLevel.Verbose, "Finished closing document."); + + return Task.FromResult(true); + } + + protected Task HandleDidChangeTextDocumentNotification( + 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.Uri); + + changedFile.ApplyChange( + GetFileChangeDetails( + textChange.Range.Value, + textChange.Text)); + + changedFiles.Add(changedFile); + } + + // TODO: Get all recently edited files in the workspace + this.RunScriptDiagnostics( + changedFiles.ToArray(), + editorSession, + eventContext); + + return Task.FromResult(true); + } + + protected async Task HandleDidChangeConfigurationNotification( + DidChangeConfigurationParams configChangeParams, + EventContext eventContext) + { + bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; + bool oldScriptAnalysisEnabled = + this.currentSettings.ScriptAnalysis.Enable.HasValue; + string oldScriptAnalysisSettingsPath = + this.currentSettings.ScriptAnalysis.SettingsPath; + + this.currentSettings.Update( + configChangeParams.Settings.Powershell, + this.editorSession.Workspace.WorkspacePath); + + if (!this.profilesLoaded && + this.currentSettings.EnableProfileLoading && + oldLoadProfiles != this.currentSettings.EnableProfileLoading) + { + await this.editorSession.PowerShellContext.LoadHostProfiles(); + this.profilesLoaded = 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)) + { + this.editorSession.RestartAnalysisService(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) + { + ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0]; + + foreach (var scriptFile in editorSession.Workspace.GetOpenedFiles()) + { + await PublishScriptDiagnostics( + scriptFile, + emptyAnalysisDiagnostics, + eventContext); + } + } + + // If script analysis is enabled and the settings file changed get new diagnostic records. + if (this.currentSettings.ScriptAnalysis.Enable.Value && settingsPathChanged) + { + await this.RunScriptDiagnostics( + this.editorSession.Workspace.GetOpenedFiles(), + this.editorSession, + eventContext); + } + } + } + + protected async Task HandleDefinitionRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentPosition.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.GetDefinitionOfSymbol( + scriptFile, + foundSymbol, + editorSession.Workspace); + + if (definition != null) + { + definitionLocations.Add( + new Location + { + Uri = new Uri(definition.FoundDefinition.FilePath).AbsoluteUri, + Range = GetRangeFromScriptRegion(definition.FoundDefinition.ScriptRegion) + }); + } + } + + await requestContext.SendResult(definitionLocations.ToArray()); + } + + protected async Task HandleReferencesRequest( + ReferencesParams referencesParams, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + referencesParams.Uri); + + SymbolReference foundSymbol = + editorSession.LanguageService.FindSymbolAtLocation( + scriptFile, + referencesParams.Position.Line + 1, + referencesParams.Position.Character + 1); + + FindReferencesResult referencesResult = + await editorSession.LanguageService.FindReferencesOfSymbol( + foundSymbol, + editorSession.Workspace.ExpandScriptReferences(scriptFile)); + + Location[] referenceLocations = null; + + if (referencesResult != null) + { + referenceLocations = + referencesResult + .FoundReferences + .Select(r => + { + return new Location + { + Uri = new Uri(r.FilePath).AbsoluteUri, + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }; + }) + .ToArray(); + } + else + { + referenceLocations = new Location[0]; + } + + await requestContext.SendResult(referenceLocations); + } + + protected async Task HandleCompletionRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) + { + int cursorLine = textDocumentPosition.Position.Line + 1; + int cursorColumn = textDocumentPosition.Position.Character + 1; + + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentPosition.Uri); + + CompletionResults completionResults = + await editorSession.LanguageService.GetCompletionsInFile( + scriptFile, + cursorLine, + cursorColumn); + + CompletionItem[] completionItems = null; + + if (completionResults != null) + { + int sortIndex = 1; + completionItems = + completionResults + .Completions + .Select( + c => CreateCompletionItem( + c, + completionResults.ReplacedRange, + sortIndex++)) + .ToArray(); + } + else + { + completionItems = new CompletionItem[0]; + } + + await requestContext.SendResult(completionItems); + } + + protected async Task HandleCompletionResolveRequest( + CompletionItem completionItem, + RequestContext requestContext) + { + if (completionItem.Kind == CompletionItemKind.Function) + { + // Get the documentation for the function + CommandInfo commandInfo = + await CommandHelpers.GetCommandInfo( + completionItem.Label, + this.editorSession.PowerShellContext); + + completionItem.Documentation = + await CommandHelpers.GetCommandSynopsis( + commandInfo, + this.editorSession.PowerShellContext); + } + + // Send back the updated CompletionItem + await requestContext.SendResult(completionItem); + } + + protected async Task HandleSignatureHelpRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentPosition.Uri); + + ParameterSetSignatures parameterSets = + await editorSession.LanguageService.FindParameterSetsInFile( + scriptFile, + textDocumentPosition.Position.Line + 1, + textDocumentPosition.Position.Character + 1); + + SignatureInformation[] signatures = null; + int? activeParameter = null; + int? activeSignature = 0; + + if (parameterSets != null) + { + signatures = + parameterSets + .Signatures + .Select(s => + { + return new SignatureInformation + { + Label = parameterSets.CommandName + " " + s.SignatureText, + Documentation = null, + Parameters = + s.Parameters + .Select(CreateParameterInfo) + .ToArray() + }; + }) + .ToArray(); + } + else + { + signatures = new SignatureInformation[0]; + } + + await requestContext.SendResult( + new SignatureHelp + { + Signatures = signatures, + ActiveParameter = activeParameter, + ActiveSignature = activeSignature + }); + } + + protected async Task HandleDocumentHighlightRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentPosition.Uri); + + FindOccurrencesResult occurrencesResult = + editorSession.LanguageService.FindOccurrencesInFile( + scriptFile, + textDocumentPosition.Position.Line + 1, + textDocumentPosition.Position.Character + 1); + + DocumentHighlight[] documentHighlights = null; + + if (occurrencesResult != null) + { + documentHighlights = + occurrencesResult + .FoundOccurrences + .Select(o => + { + return new DocumentHighlight + { + Kind = DocumentHighlightKind.Write, // TODO: Which symbol types are writable? + Range = GetRangeFromScriptRegion(o.ScriptRegion) + }; + }) + .ToArray(); + } + else + { + documentHighlights = new DocumentHighlight[0]; + } + + await requestContext.SendResult(documentHighlights); + } + + protected async Task HandleHoverRequest( + TextDocumentPosition textDocumentPosition, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentPosition.Uri); + + SymbolDetails symbolDetails = + await editorSession + .LanguageService + .FindSymbolDetailsAtLocation( + scriptFile, + textDocumentPosition.Position.Line + 1, + textDocumentPosition.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.SendResult( + new Hover + { + Contents = symbolInfo.ToArray(), + Range = symbolRange + }); + } + + protected async Task HandleDocumentSymbolRequest( + TextDocumentIdentifier textDocumentIdentifier, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentIdentifier.Uri); + + FindOccurrencesResult foundSymbols = + editorSession.LanguageService.FindSymbolsInFile( + scriptFile); + + SymbolInformation[] symbols = null; + + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + if (foundSymbols != null) + { + symbols = + foundSymbols + .FoundOccurrences + .Select(r => + { + return new SymbolInformation + { + ContainerName = containerName, + Kind = GetSymbolKind(r.SymbolType), + Location = new Location + { + Uri = new Uri(r.FilePath).AbsolutePath, + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = GetDecoratedSymbolName(r) + }; + }) + .ToArray(); + } + else + { + symbols = new SymbolInformation[0]; + } + + await requestContext.SendResult(symbols); + } + + + private SymbolKind GetSymbolKind(SymbolType symbolType) + { + switch (symbolType) + { + case SymbolType.Configuration: + case SymbolType.Function: + case SymbolType.Workflow: + return SymbolKind.Function; + + default: + return SymbolKind.Variable; + } + } + + + private 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 HandleWorkspaceSymbolRequest( + 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) + { + var matchedSymbols = + foundSymbols + .FoundOccurrences + .Where(r => IsQueryMatch(workspaceSymbolParams.Query, r.SymbolName)) + .Select(r => + { + return new SymbolInformation + { + ContainerName = containerName, + Kind = r.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, + Location = new Location + { + Uri = new Uri(r.FilePath).AbsoluteUri, + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = GetDecoratedSymbolName(r) + }; + }); + + symbols.AddRange(matchedSymbols); + } + } + + await requestContext.SendResult(symbols.ToArray()); + } + + private bool IsQueryMatch(string query, string symbolName) + { + return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + + protected Task HandleEvaluateRequest( + 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.ExecuteScriptString( + evaluateParams.Expression, + true, + 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.SendResult( + new DebugAdapterMessages.EvaluateResponseBody + { + Result = "", + VariablesReference = 0 + }); + }); + + return Task.FromResult(true); + } + + #endregion + + #region Event Handlers + + private async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) + { + // Queue the output for writing + await this.outputDebouncer.Invoke(e); + } + + private async void ExtensionService_ExtensionAdded(object sender, EditorCommand e) + { + await this.SendEvent( + ExtensionCommandAddedNotification.Type, + new ExtensionCommandAddedNotification + { + Name = e.Name, + DisplayName = e.DisplayName + }); + } + + private async void ExtensionService_ExtensionUpdated(object sender, EditorCommand e) + { + await this.SendEvent( + ExtensionCommandUpdatedNotification.Type, + new ExtensionCommandUpdatedNotification + { + Name = e.Name, + }); + } + + private async void ExtensionService_ExtensionRemoved(object sender, EditorCommand e) + { + await this.SendEvent( + ExtensionCommandRemovedNotification.Type, + new ExtensionCommandRemovedNotification + { + Name = e.Name, + }); + } + + + #endregion +#endif + #region Helper Methods + + 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 FileChange GetFileChangeDetails(Range changeRange, string insertString) + { + // The protocol's positions are zero-based so add 1 to all offsets + + 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 + }; + } + +#if false + private Task RunScriptDiagnostics( + ScriptFile[] filesToAnalyze, + EditorSession editorSession, + EventContext eventContext) + { + if (!this.currentSettings.ScriptAnalysis.Enable.Value) + { + // If the user has disabled script analysis, skip it entirely + return Task.FromResult(true); + } + + // If there's an existing task, attempt to cancel it + try + { + if (existingRequestCancellation != null) + { + // Try to cancel the request + existingRequestCancellation.Cancel(); + + // If cancellation didn't throw an exception, + // clean up the existing token + existingRequestCancellation.Dispose(); + existingRequestCancellation = null; + } + } + catch (Exception e) + { + // TODO: Catch a more specific exception! + Logger.Write( + LogLevel.Error, + string.Format( + "Exception while cancelling analysis task:\n\n{0}", + e.ToString())); + + TaskCompletionSource cancelTask = new TaskCompletionSource(); + cancelTask.SetCanceled(); + return cancelTask.Task; + } + + // 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? + existingRequestCancellation = new CancellationTokenSource(); + Task.Factory.StartNew( + () => + DelayThenInvokeDiagnostics( + 750, + filesToAnalyze, + editorSession, + eventContext, + existingRequestCancellation.Token), + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); + + return Task.FromResult(true); + } + + private static async Task DelayThenInvokeDiagnostics( + int delayMilliseconds, + ScriptFile[] filesToAnalyze, + EditorSession editorSession, + EventContext eventContext, + 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 + 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) + { + ScriptFileMarker[] semanticMarkers = null; + if (editorSession.AnalysisService != null) + { + Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath); + + semanticMarkers = + editorSession.AnalysisService.GetSemanticMarkers( + scriptFile); + + Logger.Write(LogLevel.Verbose, "Analysis complete."); + } + else + { + // Semantic markers aren't available if the AnalysisService + // isn't available + semanticMarkers = new ScriptFileMarker[0]; + } + + var allMarkers = scriptFile.SyntaxMarkers.Concat(semanticMarkers); + + await PublishScriptDiagnostics( + scriptFile, + semanticMarkers, + eventContext); + } + } + + + private static async Task PublishScriptDiagnostics( + ScriptFile scriptFile, + ScriptFileMarker[] semanticMarkers, + EventContext eventContext) + { + var allMarkers = scriptFile.SyntaxMarkers.Concat(semanticMarkers); + + // Always send syntax and semantic errors. We want to + // make sure no out-of-date markers are being displayed. + await eventContext.SendEvent( + PublishDiagnosticsNotification.Type, + new PublishDiagnosticsNotification + { + Uri = scriptFile.ClientFilePath, + Diagnostics = + allMarkers + .Select(GetDiagnosticFromMarker) + .ToArray() + }); + } + + private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) + { + return new Diagnostic + { + Severity = MapDiagnosticSeverity(scriptFileMarker.Level), + Message = scriptFileMarker.Message, + Range = new Range + { + // TODO: What offsets should I use? + 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.Method: + return CompletionItemKind.Method; + + case CompletionType.Variable: + case CompletionType.ParameterName: + return CompletionItemKind.Variable; + + case CompletionType.Path: + return CompletionItemKind.File; + + default: + return CompletionItemKind.Text; + } + } + + private static CompletionItem CreateCompletionItem( + CompletionDetails completionDetails, + BufferRange completionRange, + int sortIndex) + { + string detailString = null; + string documentationString = null; + + 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; + } + } + } + + // We want a special "sort order" for parameters that is not lexicographical. + // Fortunately, PowerShell returns parameters in the preferred sort order by + // default (with common params at the end). We just need to make sure the default + // order also be the lexicographical order which we do by prefixig the ListItemText + // with a leading 0's four digit index. This would not sort correctly for a list + // > 999 parameters but surely we won't have so many items in the "parameter name" + // completion list. Technically we don't need the ListItemText at all but it may come + // in handy during debug. + var sortText = (completionDetails.CompletionType == CompletionType.ParameterName) + ? $"{sortIndex:D3}{completionDetails.ListItemText}" + : null; + + return new CompletionItem + { + InsertText = completionDetails.CompletionText, + Label = completionDetails.ListItemText, + Kind = MapCompletionKind(completionDetails.CompletionType), + Detail = detailString, + Documentation = documentationString, + SortText = sortText, + TextEdit = new TextEdit + { + NewText = completionDetails.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 + } + } + } + }; + } +#endif + + 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; + } + } + +#if false + private static ParameterInformation CreateParameterInfo(ParameterInfo parameterInfo) + { + return new ParameterInformation + { + Label = parameterInfo.Name, + Documentation = string.Empty + }; + } +#endif + + #endregion + } +} + diff --git a/ServiceHost/Server/LanguageServerBase.cs b/ServiceHost/Server/LanguageServerBase.cs new file mode 100644 index 00000000..f9a9219c --- /dev/null +++ b/ServiceHost/Server/LanguageServerBase.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 Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public abstract class LanguageServerBase : ProtocolEndpoint + { + private bool isStarted; + private ChannelBase serverChannel; + private TaskCompletionSource serverExitedTask; + + public LanguageServerBase(ChannelBase serverChannel) : + base(serverChannel, MessageProtocolType.LanguageServer) + { + this.serverChannel = serverChannel; + } + + protected override Task OnStart() + { + // Register handlers for server lifetime messages + this.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); + this.SetEventHandler(ExitNotification.Type, this.HandleExitNotification); + + // Initialize the implementation class + this.Initialize(); + + return Task.FromResult(true); + } + + protected override async Task OnStop() + { + await this.Shutdown(); + } + + /// + /// Overridden by the subclass to provide initialization + /// logic after the server channel is started. + /// + protected abstract void Initialize(); + + /// + /// Can be overridden by the subclass to provide shutdown + /// logic before the server exits. Subclasses do not need + /// to invoke or return the value of the base implementation. + /// + protected virtual Task Shutdown() + { + // No default implementation yet. + return Task.FromResult(true); + } + + private async Task HandleShutdownRequest( + object shutdownParams, + RequestContext requestContext) + { + // Allow the implementor to shut down gracefully + await this.Shutdown(); + + await requestContext.SendResult(new object()); + } + + private async Task HandleExitNotification( + object exitParams, + EventContext eventContext) + { + // Stop the server channel + await this.Stop(); + + // Notify any waiter that the server has exited + if (this.serverExitedTask != null) + { + this.serverExitedTask.SetResult(true); + } + } + } +} + diff --git a/ServiceHost/Server/LanguageServerEditorOperations.cs b/ServiceHost/Server/LanguageServerEditorOperations.cs new file mode 100644 index 00000000..6ae28caf --- /dev/null +++ b/ServiceHost/Server/LanguageServerEditorOperations.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +#if false +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 EditorSession editorSession; + private IMessageSender messageSender; + + public LanguageServerEditorOperations( + EditorSession editorSession, + IMessageSender messageSender) + { + this.editorSession = editorSession; + this.messageSender = messageSender; + } + + public async Task GetEditorContext() + { + ClientEditorContext clientContext = + await this.messageSender.SendRequest( + GetEditorContextRequest.Type, + new GetEditorContextRequest(), + true); + + return this.ConvertClientEditorContext(clientContext); + } + + public async Task InsertText(string filePath, string text, BufferRange insertRange) + { + await this.messageSender.SendRequest( + 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 SetSelection(BufferRange selectionRange) + { + return this.messageSender.SendRequest( + 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) + { + return + new EditorContext( + this, + this.editorSession.Workspace.GetFile(clientContext.CurrentFilePath), + 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)); + } + + public Task OpenFile(string filePath) + { + return + this.messageSender.SendRequest( + OpenFileRequest.Type, + filePath, + true); + } + } +} +#endif \ No newline at end of file diff --git a/ServiceHost/Server/LanguageServerSettings.cs b/ServiceHost/Server/LanguageServerSettings.cs new file mode 100644 index 00000000..cb90268c --- /dev/null +++ b/ServiceHost/Server/LanguageServerSettings.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.IO; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public class LanguageServerSettings + { + public bool EnableProfileLoading { get; set; } + + public ScriptAnalysisSettings ScriptAnalysis { get; set; } + + public LanguageServerSettings() + { + this.ScriptAnalysis = new ScriptAnalysisSettings(); + } + + public void Update(LanguageServerSettings settings, string workspaceRootPath) + { + if (settings != null) + { + this.EnableProfileLoading = settings.EnableProfileLoading; + this.ScriptAnalysis.Update(settings.ScriptAnalysis, workspaceRootPath); + } + } + } + + 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) + { + if (settings != null) + { + this.Enable = settings.Enable; + + string settingsPath = settings.SettingsPath; + + 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; + } + } + } + + 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; } + } +} diff --git a/ServiceHost/Server/OutputDebouncer.cs b/ServiceHost/Server/OutputDebouncer.cs new file mode 100644 index 00000000..e65c83b1 --- /dev/null +++ b/ServiceHost/Server/OutputDebouncer.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 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. + /// + internal 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 OnInvoke(OutputWrittenEventArgs output) + { + bool outputIsError = output.OutputType == OutputType.Error; + + if (this.currentOutputIsError != outputIsError) + { + if (this.currentOutputString != null) + { + // Flush the output + await this.OnFlush(); + } + + 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 OnFlush() + { + // Only flush output if there is some to flush + if (this.currentOutputString != null) + { + // Send an event for the current output + await this.messageSender.SendEvent( + 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/ServiceHost/Server/PromptHandlers.cs b/ServiceHost/Server/PromptHandlers.cs new file mode 100644 index 00000000..3d48a3e0 --- /dev/null +++ b/ServiceHost/Server/PromptHandlers.cs @@ -0,0 +1,190 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +#if false +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; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + internal class ProtocolPromptHandlerContext : IPromptHandlerContext + { + private IMessageSender messageSender; + private ConsoleService consoleService; + + public ProtocolPromptHandlerContext( + IMessageSender messageSender, + ConsoleService consoleService) + { + this.messageSender = messageSender; + this.consoleService = consoleService; + } + + public ChoicePromptHandler GetChoicePromptHandler() + { + return new ProtocolChoicePromptHandler( + this.messageSender, + this.consoleService); + } + + public InputPromptHandler GetInputPromptHandler() + { + return new ProtocolInputPromptHandler( + this.messageSender, + this.consoleService); + } + } + + internal class ProtocolChoicePromptHandler : ChoicePromptHandler + { + private IMessageSender messageSender; + private ConsoleService consoleService; + + public ProtocolChoicePromptHandler( + IMessageSender messageSender, + ConsoleService consoleService) + { + this.messageSender = messageSender; + this.consoleService = consoleService; + } + + protected override void ShowPrompt(PromptStyle promptStyle) + { + messageSender + .SendRequest( + ShowChoicePromptRequest.Type, + new ShowChoicePromptRequest + { + Caption = this.Caption, + Message = this.Message, + Choices = this.Choices, + DefaultChoice = this.DefaultChoice + }, true) + .ContinueWith(HandlePromptResponse) + .ConfigureAwait(false); + } + + private void HandlePromptResponse( + Task responseTask) + { + if (responseTask.IsCompleted) + { + ShowChoicePromptResponse response = responseTask.Result; + + if (!response.PromptCancelled) + { + this.consoleService.ReceivePromptResponse( + response.ChosenItem, + false); + } + else + { + // Cancel the current prompt + this.consoleService.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.consoleService.SendControlC(); + } + } + } + + internal class ProtocolInputPromptHandler : ConsoleInputPromptHandler + { + private IMessageSender messageSender; + private ConsoleService consoleService; + + public ProtocolInputPromptHandler( + IMessageSender messageSender, + ConsoleService consoleService) + : base(consoleService) + { + this.messageSender = messageSender; + this.consoleService = consoleService; + } + + protected override void ShowErrorMessage(Exception e) + { + // Use default behavior for writing the error message + base.ShowErrorMessage(e); + } + + protected override void ShowPromptMessage(string caption, string message) + { + // Use default behavior for writing the prompt message + base.ShowPromptMessage(caption, message); + } + + protected override void ShowFieldPrompt(FieldDetails fieldDetails) + { + // Write the prompt to the console first so that there's a record + // of it occurring + base.ShowFieldPrompt(fieldDetails); + + messageSender + .SendRequest( + ShowInputPromptRequest.Type, + new ShowInputPromptRequest + { + Name = fieldDetails.Name, + Label = fieldDetails.Label + }, true) + .ContinueWith(HandlePromptResponse) + .ConfigureAwait(false); + } + + private void HandlePromptResponse( + Task responseTask) + { + if (responseTask.IsCompleted) + { + ShowInputPromptResponse response = responseTask.Result; + + if (!response.PromptCancelled) + { + this.consoleService.ReceivePromptResponse( + response.ResponseText, + true); + } + else + { + // Cancel the current prompt + this.consoleService.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.consoleService.SendControlC(); + } + } + } +} + +#endif \ No newline at end of file diff --git a/ServiceHost/Session/EditorSession.cs b/ServiceHost/Session/EditorSession.cs new file mode 100644 index 00000000..2d4dea38 --- /dev/null +++ b/ServiceHost/Session/EditorSession.cs @@ -0,0 +1,159 @@ +// +// 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.Extensions; +using Microsoft.PowerShell.EditorServices.Session; +// 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 + { + public void StartSession(HostDetails hostDetails, ProfilePaths profilePaths) + { + } +#if false + #region Properties + + /// + /// 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 ConsoleService instance for this session. + /// + public ConsoleService ConsoleService { get; private set; } + + /// + /// Gets the ExtensionService instance for this session. + /// + public ExtensionService ExtensionService { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Starts the session using the provided IConsoleHost implementation + /// for the ConsoleService. + /// + /// + /// Provides details about the host application. + /// + /// + /// An object containing the profile paths for the session. + /// + public void StartSession(HostDetails hostDetails, ProfilePaths profilePaths) + { + // Initialize all services + this.PowerShellContext = new PowerShellContext(hostDetails, profilePaths); + this.LanguageService = new LanguageService(this.PowerShellContext); + this.DebugService = new DebugService(this.PowerShellContext); + this.ConsoleService = new ConsoleService(this.PowerShellContext); + this.ExtensionService = new ExtensionService(this.PowerShellContext); + + this.InstantiateAnalysisService(); + + // Create a workspace to contain open files + this.Workspace = new Workspace(this.PowerShellContext.PowerShellVersion); + } + + /// + /// Restarts the AnalysisService so it can be configured with a new settings file. + /// + /// Path to the settings file. + public void RestartAnalysisService(string settingsPath) + { + this.AnalysisService?.Dispose(); + InstantiateAnalysisService(settingsPath); + } + + internal void InstantiateAnalysisService(string settingsPath = null) + { + // Only enable the AnalysisService if the machine has PowerShell + // v5 installed. Script Analyzer works on earlier PowerShell + // versions but our hard dependency on their binaries complicates + // the deployment and assembly loading since we would have to + // conditionally load the binaries for v3/v4 support. This problem + // will be solved in the future by using Script Analyzer as a + // module rather than an assembly dependency. + if (this.PowerShellContext.PowerShellVersion.Major >= 5) + { + // AnalysisService will throw FileNotFoundException if + // Script Analyzer binaries are not included. + try + { + this.AnalysisService = new AnalysisService(this.PowerShellContext.ConsoleHost, settingsPath); + } + catch (FileNotFoundException) + { + Logger.Write( + LogLevel.Warning, + "Script Analyzer binaries not found, AnalysisService will be disabled."); + } + } + else + { + Logger.Write( + LogLevel.Normal, + "Script Analyzer cannot be loaded due to unsupported PowerShell version " + + this.PowerShellContext.PowerShellVersion.ToString()); + } + } + + #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 +#endif + } +} diff --git a/ServiceHost/Session/HostDetails.cs b/ServiceHost/Session/HostDetails.cs new file mode 100644 index 00000000..a20bc8cf --- /dev/null +++ b/ServiceHost/Session/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.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/ServiceHost/Session/OutputType.cs b/ServiceHost/Session/OutputType.cs new file mode 100644 index 00000000..ad67f689 --- /dev/null +++ b/ServiceHost/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/ServiceHost/Session/OutputWrittenEventArgs.cs b/ServiceHost/Session/OutputWrittenEventArgs.cs new file mode 100644 index 00000000..b1408a99 --- /dev/null +++ b/ServiceHost/Session/OutputWrittenEventArgs.cs @@ -0,0 +1,65 @@ +// +// 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/ServiceHost/Session/ProfilePaths.cs b/ServiceHost/Session/ProfilePaths.cs new file mode 100644 index 00000000..d1ec5557 --- /dev/null +++ b/ServiceHost/Session/ProfilePaths.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.Collections.Generic; +using System.IO; +using System.Linq; + +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/ServiceHost/Utility/AsyncContext.cs b/ServiceHost/Utility/AsyncContext.cs new file mode 100644 index 00000000..421ca3d9 --- /dev/null +++ b/ServiceHost/Utility/AsyncContext.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. +// + +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. + /// + public static void Start(Func asyncMainFunc) + { + // 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(); + 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/ServiceHost/Utility/AsyncContextThread.cs b/ServiceHost/Utility/AsyncContextThread.cs new file mode 100644 index 00000000..92629437 --- /dev/null +++ b/ServiceHost/Utility/AsyncContextThread.cs @@ -0,0 +1,85 @@ +// +// 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. + /// + /// + /// A Task which can be used to monitor the thread for completion. + /// + public Task Run(Func taskReturningFunc) + { + // 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); + }, + this.threadCancellationToken.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + return this.threadTask; + } + + /// + /// Stops the thread task. + /// + public void Stop() + { + this.threadCancellationToken.Cancel(); + } + + #endregion + } +} + diff --git a/ServiceHost/Utility/AsyncDebouncer.cs b/ServiceHost/Utility/AsyncDebouncer.cs new file mode 100644 index 00000000..f8bbd242 --- /dev/null +++ b/ServiceHost/Utility/AsyncDebouncer.cs @@ -0,0 +1,169 @@ +// +// 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 Invoke(TInvokeArgs invokeArgument) + { + using (await this.asyncLock.LockAsync()) + { + // Invoke the implementor + await this.OnInvoke(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 Flush() + { + using (await this.asyncLock.LockAsync()) + { + // Cancel the current timer + this.CancelTimer(); + + // Flush the current output + await this.OnFlush(); + } + } + + #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 OnInvoke(TInvokeArgs invokeArgument); + + /// + /// Implemented by the subclass to complete the current operation. + /// + /// A Task to be awaited for the operation to complete. + protected abstract Task OnFlush(); + + #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.Flush(); + } + 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/ServiceHost/Utility/AsyncLock.cs b/ServiceHost/Utility/AsyncLock.cs new file mode 100644 index 00000000..eee894d9 --- /dev/null +++ b/ServiceHost/Utility/AsyncLock.cs @@ -0,0 +1,103 @@ +// +// 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); + } + + #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/ServiceHost/Utility/AsyncQueue.cs b/ServiceHost/Utility/AsyncQueue.cs new file mode 100644 index 00000000..98c00dc8 --- /dev/null +++ b/ServiceHost/Utility/AsyncQueue.cs @@ -0,0 +1,155 @@ +// +// 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; + } + } + + /// + /// 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; + } + + #endregion + } +} + diff --git a/ServiceHost/Utility/Extensions.cs b/ServiceHost/Utility/Extensions.cs new file mode 100644 index 00000000..432b0c83 --- /dev/null +++ b/ServiceHost/Utility/Extensions.cs @@ -0,0 +1,34 @@ +// +// 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.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; + } + } +} diff --git a/ServiceHost/Utility/Logger.cs b/ServiceHost/Utility/Logger.cs new file mode 100644 index 00000000..fbdef40f --- /dev/null +++ b/ServiceHost/Utility/Logger.cs @@ -0,0 +1,229 @@ +// +// 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.CompilerServices; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Utility +{ + /// + /// Defines the level indicators for log messages. + /// + public enum LogLevel + { + /// + /// 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 a simple logging interface. May be replaced with a + /// more robust solution at a later date. + /// + public static class Logger + { + private static LogWriter logWriter; + + /// + /// Initializes the Logger for the current session. + /// + /// + /// Optional. Specifies the path at which log messages will be written. + /// + /// + /// Optional. Specifies the minimum log message level to write to the log file. + /// + public static void Initialize( + string logFilePath = "EditorServices.log", + LogLevel minimumLogLevel = LogLevel.Normal) + { + if (logWriter != null) + { + logWriter.Dispose(); + } + + // TODO: Parameterize this + logWriter = + new LogWriter( + minimumLogLevel, + logFilePath, + true); + } + + /// + /// Closes the Logger. + /// + public static void Close() + { + if (logWriter != null) + { + logWriter.Dispose(); + } + } + + /// + /// Writes a message to the log file. + /// + /// The level at which the message will be written. + /// The message text to be written. + /// The name of the calling method. + /// The source file path where the calling method exists. + /// The line number of the calling method. + public static void Write( + LogLevel logLevel, + string logMessage, + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerSourceFile = null, + [CallerLineNumber] int callerLineNumber = 0) + { + if (logWriter != null) + { + logWriter.Write( + logLevel, + logMessage, + callerName, + callerSourceFile, + callerLineNumber); + } + } + } + + internal class LogWriter : IDisposable + { + private TextWriter textWriter; + private LogLevel minimumLogLevel = LogLevel.Verbose; + + public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisting) + { + this.minimumLogLevel = minimumLogLevel; + + // Ensure that we have a usable log file path +// if (!Path.IsPathRooted(logFilePath)) +// { +// logFilePath = +// Path.Combine( +// #if NanoServer +// AppContext.BaseDirectory, +// #else +// AppDomain.CurrentDomain.BaseDirectory, +// #endif +// logFilePath); +// } + +// if (!this.TryOpenLogFile(logFilePath, deleteExisting)) +// { +// // If the log file couldn't be opened at this location, +// // try opening it in a more reliable path +// this.TryOpenLogFile( +// Path.Combine( +// #if NanoServer +// Environment.GetEnvironmentVariable("TEMP"), +// #else +// Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), +// #endif +// Path.GetFileName(logFilePath)), +// deleteExisting); +// } + } + + public void Write( + LogLevel logLevel, + string logMessage, + string callerName = null, + string callerSourceFile = null, + int callerLineNumber = 0) + { + if (this.textWriter != null && + logLevel >= this.minimumLogLevel) + { + // Print the timestamp and log level + this.textWriter.WriteLine( + "{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n", + DateTime.Now, + logLevel.ToString().ToUpper(), + callerName, + callerLineNumber, + callerSourceFile); + + // Print out indented message lines + foreach (var messageLine in logMessage.Split('\n')) + { + this.textWriter.WriteLine(" " + messageLine.TrimEnd()); + } + + // Finish with a newline and flush the writer + this.textWriter.WriteLine(); + this.textWriter.Flush(); + } + } + + public void Dispose() + { + if (this.textWriter != null) + { + this.textWriter.Flush(); + this.textWriter.Dispose(); + this.textWriter = null; + } + } + + private bool TryOpenLogFile( + string logFilePath, + bool deleteExisting) + { + try + { + // Make sure the log directory exists + Directory.CreateDirectory( + Path.GetDirectoryName( + logFilePath)); + + // Open the log file for writing with UTF8 encoding + this.textWriter = + new StreamWriter( + new FileStream( + logFilePath, + deleteExisting ? + FileMode.Create : + FileMode.Append), + Encoding.UTF8); + + return true; + } + catch (Exception e) + { + if (e is UnauthorizedAccessException || + e is IOException) + { + // This exception is thrown when we can't open the file + // at the path in logFilePath. Return false to indicate + // that the log file couldn't be created. + return false; + } + + // Unexpected exception, rethrow it + throw; + } + } + } +} diff --git a/ServiceHost/Utility/ThreadSynchronizationContext.cs b/ServiceHost/Utility/ThreadSynchronizationContext.cs new file mode 100644 index 00000000..03b57bee --- /dev/null +++ b/ServiceHost/Utility/ThreadSynchronizationContext.cs @@ -0,0 +1,77 @@ +// +// 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 BlockingCollection> requestQueue = + new BlockingCollection>(); + + #endregion + + #region Constructors + + /// + /// 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) + { + // Add the request to the queue + this.requestQueue.Add( + new Tuple( + callback, state)); + } + + #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/ServiceHost/Utility/Validate.cs b/ServiceHost/Utility/Validate.cs new file mode 100644 index 00000000..19aefe2c --- /dev/null +++ b/ServiceHost/Utility/Validate.cs @@ -0,0 +1,143 @@ +// +// 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/ServiceHost/Workspace/BufferPosition.cs b/ServiceHost/Workspace/BufferPosition.cs new file mode 100644 index 00000000..effdd766 --- /dev/null +++ b/ServiceHost/Workspace/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/ServiceHost/Workspace/BufferRange.cs b/ServiceHost/Workspace/BufferRange.cs new file mode 100644 index 00000000..147eed04 --- /dev/null +++ b/ServiceHost/Workspace/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/ServiceHost/Workspace/FileChange.cs b/ServiceHost/Workspace/FileChange.cs new file mode 100644 index 00000000..3bec7982 --- /dev/null +++ b/ServiceHost/Workspace/FileChange.cs @@ -0,0 +1,38 @@ +// +// 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; } + } +} diff --git a/ServiceHost/Workspace/FilePosition.cs b/ServiceHost/Workspace/FilePosition.cs new file mode 100644 index 00000000..d216b1e5 --- /dev/null +++ b/ServiceHost/Workspace/FilePosition.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. +// + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides details and operations for a buffer position in a + /// specific file. + /// + public class FilePosition : BufferPosition + { + public FilePosition( + ScriptFile scriptFile, + int line, + int column) + : base(line, column) + { + } +#if false + #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 +#endif + } +} + diff --git a/ServiceHost/Workspace/ScriptFile.cs b/ServiceHost/Workspace/ScriptFile.cs new file mode 100644 index 00000000..4623ed2a --- /dev/null +++ b/ServiceHost/Workspace/ScriptFile.cs @@ -0,0 +1,558 @@ +// +// 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.Linq; +//using System.Management.Automation; +//using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains the details and contents of an open script file. + /// + public class ScriptFile + { +#if false + #region Private Fields + + private Token[] scriptTokens; + 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 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("\r\n", 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 ScriptFileMarker[] SyntaxMarkers + { + get; + private set; + } + + /// + /// Gets the list of strings for each line of the file. + /// + internal IList 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 { return this.scriptTokens; } + } + + /// + /// 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; + + 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 = filePath; + this.ClientFilePath = clientFilePath; + this.IsAnalysisEnabled = true; + this.powerShellVersion = powerShellVersion; + + this.SetFileContents(initialBuffer); + } + + #endregion + + #region Public Methods + + /// + /// 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) + { + if (line < 1 || line > this.FileLines.Count + 1) + { + throw new ArgumentOutOfRangeException("Position is outside of file line range."); + } + + // 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( + string.Format( + "Position is outside of column range for line {0}.", + line)); + } + } + + /// + /// Applies the provided FileChange to the file's contents + /// + /// The FileChange to apply to the file's contents. + public void ApplyChange(FileChange fileChange) + { + this.ValidatePosition(fileChange.Line, fileChange.Offset); + this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset); + + // Break up the change lines + string[] changeLines = fileChange.InsertString.Split('\n'); + + // 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 = + fileContents + .Split('\n') + .Select(line => line.TrimEnd('\r')) + .ToList(); + + // 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 + { +#if PowerShellv5r2 + // This overload appeared with Windows 10 Update 1 + if (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 this.scriptTokens, + out parseErrors); + } + else + { + this.ScriptAst = + Parser.ParseInput( + this.Contents, + out this.scriptTokens, + out parseErrors); + } +#else + this.ScriptAst = + Parser.ParseInput( + this.Contents, + out this.scriptTokens, + out parseErrors); +#endif + } + 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.SyntaxMarkers = + parseErrors + .Select(ScriptFileMarker.FromParseError) + .ToArray(); + + //Get all dot sourced referenced files and store them + this.ReferencedFiles = + AstOperations.FindDotSourcedIncludes(this.ScriptAst); + } + +#endregion +#endif + } +} diff --git a/ServiceHost/Workspace/ScriptFileMarker.cs b/ServiceHost/Workspace/ScriptFileMarker.cs new file mode 100644 index 00000000..838b17eb --- /dev/null +++ b/ServiceHost/Workspace/ScriptFileMarker.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 Microsoft.PowerShell.EditorServices.Utility; +using System; +//using System.Management.Automation.Language; + +#if ScriptAnalyzer +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +#endif + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Defines the message level of a script file marker. + /// + public enum ScriptFileMarkerLevel + { + /// + /// The marker represents an informational message. + /// + Information = 0, + + /// + /// The marker represents a warning message. + /// + Warning, + + /// + /// The marker represents an error message. + /// + Error + }; + + /// + /// 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 marker's message level. + /// + public ScriptFileMarkerLevel Level { get; set; } + + /// + /// Gets or sets the ScriptRegion where the marker should appear. + /// + public ScriptRegion ScriptRegion { get; set; } + + #endregion + + #region Public Methods + +#if false + internal static ScriptFileMarker FromParseError( + ParseError parseError) + { + Validate.IsNotNull("parseError", parseError); + + return new ScriptFileMarker + { + Message = parseError.Message, + Level = ScriptFileMarkerLevel.Error, + ScriptRegion = ScriptRegion.Create(parseError.Extent) + }; + } +#endif + +#if ScriptAnalyzer + internal static ScriptFileMarker FromDiagnosticRecord( + DiagnosticRecord diagnosticRecord) + { + Validate.IsNotNull("diagnosticRecord", diagnosticRecord); + + return new ScriptFileMarker + { + Message = diagnosticRecord.Message, + Level = GetMarkerLevelFromDiagnosticSeverity(diagnosticRecord.Severity), + ScriptRegion = ScriptRegion.Create(diagnosticRecord.Extent) + }; + } + + private static ScriptFileMarkerLevel GetMarkerLevelFromDiagnosticSeverity( + DiagnosticSeverity diagnosticSeverity) + { + switch (diagnosticSeverity) + { + case DiagnosticSeverity.Information: + return ScriptFileMarkerLevel.Information; + case DiagnosticSeverity.Warning: + return ScriptFileMarkerLevel.Warning; + case DiagnosticSeverity.Error: + return ScriptFileMarkerLevel.Error; + default: + throw new ArgumentException( + string.Format( + "The provided DiagnosticSeverity value '{0}' is unknown.", + diagnosticSeverity), + "diagnosticSeverity"); + } + } +#endif + + #endregion + } +} + diff --git a/ServiceHost/Workspace/ScriptRegion.cs b/ServiceHost/Workspace/ScriptRegion.cs new file mode 100644 index 00000000..e7662372 --- /dev/null +++ b/ServiceHost/Workspace/ScriptRegion.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.Language; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Contains details about a specific region of text in script file. + /// + public sealed class ScriptRegion + { + #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; } + + #endregion + + #region Constructors + +#if false + /// + /// 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) + { + return new ScriptRegion + { + File = scriptExtent.File, + Text = scriptExtent.Text, + StartLineNumber = scriptExtent.StartLineNumber, + StartColumnNumber = scriptExtent.StartColumnNumber, + StartOffset = scriptExtent.StartOffset, + EndLineNumber = scriptExtent.EndLineNumber, + EndColumnNumber = scriptExtent.EndColumnNumber, + EndOffset = scriptExtent.EndOffset + }; + } +#endif + #endregion + } +} diff --git a/ServiceHost/Workspace/Workspace.cs b/ServiceHost/Workspace/Workspace.cs new file mode 100644 index 00000000..e51eacf7 --- /dev/null +++ b/ServiceHost/Workspace/Workspace.cs @@ -0,0 +1,307 @@ +// +// 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.Text; + +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 + { +#if false + #region Private Fields + + 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; } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of the Workspace class. + /// + /// The version of PowerShell for which scripts will be parsed. + public Workspace(Version powerShellVersion) + { + this.powerShellVersion = powerShellVersion; + } + + #endregion + + #region Public Methods + + /// + /// Gets an open file in the workspace. If the file isn't open but + /// exists on the filesystem, load and return it. + /// + /// 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); + } + + Logger.Write(LogLevel.Verbose, "Opened file on disk: " + resolvedFilePath); + } + + return scriptFile; + } + + /// + /// Gets a new ScriptFile instance which is identified by the given file + /// path and initially contains the given buffer contents. + /// + /// + /// + /// + 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)) + { + scriptFile = + new ScriptFile( + resolvedFilePath, + filePath, + initialBuffer, + this.powerShellVersion); + + this.workspaceFiles.Add(keyName, scriptFile); + + 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(); + } + + #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); + + ScriptFile referencedFile; + foreach (string referencedFileName in scriptFile.ReferencedFiles) + { + string resolvedScriptPath = + this.ResolveRelativeScriptPath( + baseFilePath, + referencedFileName); + + // Make sure file exists before trying to get the file + if (File.Exists(resolvedScriptPath)) + { + // Get the referenced file if it's not already in referencedScriptFiles + referencedFile = this.GetFile(resolvedScriptPath); + + // 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); + } + } + } + } + + private string ResolveFilePath(string filePath) + { + if (!IsPathInMemory(filePath)) + { + if (filePath.StartsWith(@"file://")) + { + // Client sent the path in URI format, extract the local path and trim + // any extraneous slashes + Uri fileUri = new Uri(filePath); + filePath = fileUri.LocalPath.TrimStart('/'); + } + + // Some clients send paths with UNIX-style slashes, replace those if necessary + filePath = filePath.Replace('/', '\\'); + + // 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.UnescapePath(filePath); + + // Get the absolute file path + filePath = Path.GetFullPath(filePath); + } + + Logger.Write(LogLevel.Verbose, "Resolved path: " + filePath); + + return filePath; + } + + internal static bool IsPathInMemory(string filePath) + { + // When viewing PowerShell files in the Git diff viewer, VS Code + // sends the contents of the file at HEAD with a URI that starts + // with 'inmemory'. Untitled files which have been marked of + // type PowerShell have a path starting with 'untitled'. + return + filePath.StartsWith("inmemory") || + filePath.StartsWith("untitled"); + } + + 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); + } + + private string ResolveRelativeScriptPath(string baseFilePath, string relativePath) + { + 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. + string combinedPath = + Path.GetFullPath( + Path.Combine( + baseFilePath, + relativePath)); + + return combinedPath; + } + + #endregion +#endif + } +} diff --git a/ServiceHost/project.json b/ServiceHost/project.json new file mode 100644 index 00000000..28565355 --- /dev/null +++ b/ServiceHost/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "debugType": "portable", + "emitEntryPoint": true + }, + "dependencies": { + "Newtonsoft.Json": "9.0.1", + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + "imports": "dnxcore50" + } + } +}