From 721525e33df91e37000c7d92d18b1e3566f9d908 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Wed, 10 Jul 2019 22:39:01 -0700 Subject: [PATCH 1/4] wip --- Microsoft.DotNet.Try.Client/package-lock.json | 44 ++--- .../ExecuteRequestHandler.cs | 23 +-- Microsoft.DotNet.Try.js/package-lock.json | 3 +- .../Kernel/CSharpReplTests.cs | 2 +- .../Kernel/CodeSubmissionProcessorTests.cs | 2 +- .../Kernel/KernelCommandPipelineTests.cs | 34 +++- .../Kernel/PackageReferenceTests.cs | 37 +++++ WorkspaceServer/Kernel/AddNuGetPackage.cs | 17 ++ WorkspaceServer/Kernel/CSharpRepl.cs | 156 +++++++++--------- .../Kernel/CSharpReplExtensions.cs | 69 ++++++++ .../Kernel/CodeSubmissionEvaluated.cs | 2 +- .../Kernel/CodeSubmissionEvaluationFailed.cs | 23 +-- .../Kernel/CodeSubmissionProcessors.cs | 52 +++--- .../Kernel/CodeSubmissionReceived.cs | 6 +- .../Kernel/CompleteCodeSubmissionReceived.cs | 2 +- WorkspaceServer/Kernel/CompletionReceived.cs | 2 +- WorkspaceServer/Kernel/CompositeKernel.cs | 40 ++++- WorkspaceServer/Kernel/DiagnosticsReceived.cs | 8 +- .../Kernel/DocumentationReceived.cs | 8 +- WorkspaceServer/Kernel/IKernel.cs | 3 +- WorkspaceServer/Kernel/IKernelCommand.cs | 25 --- .../Kernel/IKernelCommandResult.cs | 2 +- WorkspaceServer/Kernel/IKernelEvent.cs | 61 +------ .../IncompleteCodeSubmissionReceived.cs | 6 +- WorkspaceServer/Kernel/KernelBase.cs | 37 +++-- WorkspaceServer/Kernel/KernelCommandBase.cs | 9 + .../Kernel/KernelCommandContext.cs | 13 +- .../Kernel/KernelCommandPipeline.cs | 30 +++- .../KernelCommandPipelineContinuation.cs | 11 ++ .../Kernel/KernelCommandPipelineMiddleware.cs | 3 +- WorkspaceServer/Kernel/KernelCommandResult.cs | 15 +- WorkspaceServer/Kernel/KernelEventBase.cs | 21 +++ .../Kernel/NoSuitableKernelException.cs | 11 ++ .../{PackageAdded.cs => NuGetPackageAdded.cs} | 8 +- .../Kernel/NugetPackageReference.cs | 50 ++++++ WorkspaceServer/Kernel/RequestCompletion.cs | 9 + WorkspaceServer/Kernel/RequestDiagnostics.cs | 9 + .../Kernel/RequestDocumentation.cs | 9 + .../Kernel/RequestSignatureHelp.cs | 9 + WorkspaceServer/Kernel/SendStandardInput.cs | 9 + .../Kernel/SignatureHelpReceived.cs | 5 +- .../Kernel/StandardErrorReceived.cs | 10 +- .../Kernel/StandardInputReceived.cs | 12 +- .../Kernel/StandardOutputReceived.cs | 10 +- WorkspaceServer/Kernel/Started.cs | 5 - WorkspaceServer/Kernel/Stopped.cs | 5 - WorkspaceServer/Kernel/ValueProduced.cs | 8 +- 47 files changed, 566 insertions(+), 369 deletions(-) create mode 100644 WorkspaceServer.Tests/Kernel/PackageReferenceTests.cs create mode 100644 WorkspaceServer/Kernel/AddNuGetPackage.cs create mode 100644 WorkspaceServer/Kernel/CSharpReplExtensions.cs create mode 100644 WorkspaceServer/Kernel/KernelCommandBase.cs create mode 100644 WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs create mode 100644 WorkspaceServer/Kernel/KernelEventBase.cs create mode 100644 WorkspaceServer/Kernel/NoSuitableKernelException.cs rename WorkspaceServer/Kernel/{PackageAdded.cs => NuGetPackageAdded.cs} (56%) create mode 100644 WorkspaceServer/Kernel/NugetPackageReference.cs create mode 100644 WorkspaceServer/Kernel/RequestCompletion.cs create mode 100644 WorkspaceServer/Kernel/RequestDiagnostics.cs create mode 100644 WorkspaceServer/Kernel/RequestDocumentation.cs create mode 100644 WorkspaceServer/Kernel/RequestSignatureHelp.cs create mode 100644 WorkspaceServer/Kernel/SendStandardInput.cs diff --git a/Microsoft.DotNet.Try.Client/package-lock.json b/Microsoft.DotNet.Try.Client/package-lock.json index ae6558cdc..12fe1595c 100644 --- a/Microsoft.DotNet.Try.Client/package-lock.json +++ b/Microsoft.DotNet.Try.Client/package-lock.json @@ -4560,8 +4560,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4582,14 +4581,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4604,20 +4601,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4734,8 +4728,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4747,7 +4740,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4762,7 +4754,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4770,14 +4761,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4796,7 +4785,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4877,8 +4865,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4890,7 +4877,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4976,8 +4962,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5013,7 +4998,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5033,7 +5017,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5077,14 +5060,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -9743,8 +9724,7 @@ "version": "1.2.0", "resolved": "https://msazure.pkgs.visualstudio.com/_packaging/MLS/npm/registry/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs b/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs index 975ed862d..b04dcb0d7 100644 --- a/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs +++ b/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs @@ -18,7 +18,7 @@ public class ExecuteRequestHandler : IDisposable { private readonly IKernel _kernel; private readonly RenderingEngine _renderingEngine; - private readonly ConcurrentDictionary _openRequests = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _openRequests = new ConcurrentDictionary(); private int _executionCount; private readonly CodeSubmissionProcessors _processors; private readonly CompositeDisposable _disposables = new CompositeDisposable(); @@ -74,14 +74,16 @@ public async Task Handle(JupyterRequestContext context) { var command = new SubmitCode(executeRequest.Code, "csharp"); command = await _processors.ProcessAsync(command); - var id = command.Id; + + var id = Guid.NewGuid(); + var transient = new Dictionary { { "display_id", id.ToString() } }; var openRequest = new OpenRequest(context, executeRequest, executionCount, id, transient); - _openRequests[id] = openRequest; + _openRequests[command] = openRequest; var kernelResult = await _kernel.SendAsync(command); - openRequest.AddDisposable(kernelResult.Events.Subscribe(OnKernelResultEvent)); + openRequest.AddDisposable(kernelResult.KernelEvents.Subscribe(OnKernelResultEvent)); } catch (Exception e) { @@ -118,7 +120,6 @@ public async Task Handle(JupyterRequestContext context) context.RequestHandlerStatus.SetAsIdle(); } } - void OnKernelResultEvent(IKernelEvent value) { @@ -142,9 +143,9 @@ void OnKernelResultEvent(IKernelEvent value) } } - private static void OnCodeSubmissionEvaluatedFailed(CodeSubmissionEvaluationFailed codeSubmissionEvaluationFailed, ConcurrentDictionary openRequests) + private static void OnCodeSubmissionEvaluatedFailed(CodeSubmissionEvaluationFailed codeSubmissionEvaluationFailed, ConcurrentDictionary openRequests) { - var openRequest = openRequests[codeSubmissionEvaluationFailed.ParentId]; + var openRequest = openRequests[codeSubmissionEvaluationFailed.Command]; var errorContent = new Error( eName: "Unhandled Exception", @@ -181,9 +182,9 @@ private static void OnCodeSubmissionEvaluatedFailed(CodeSubmissionEvaluationFail } private static void OnValueProduced(ValueProduced valueProduced, - ConcurrentDictionary openRequests, RenderingEngine renderingEngine) + ConcurrentDictionary openRequests, RenderingEngine renderingEngine) { - var openRequest = openRequests[valueProduced.ParentId]; + var openRequest = openRequests[valueProduced.Command]; try { var rendering = renderingEngine.Render(valueProduced.Value); @@ -232,9 +233,9 @@ private static void OnValueProduced(ValueProduced valueProduced, } private static void OnCodeSubmissionEvaluated(CodeSubmissionEvaluated codeSubmissionEvaluated, - ConcurrentDictionary openRequests) + ConcurrentDictionary openRequests) { - var openRequest = openRequests[codeSubmissionEvaluated.ParentId]; + var openRequest = openRequests[codeSubmissionEvaluated.Command]; // reply ok var executeReplyPayload = new ExecuteReplyOk(executionCount: openRequest.ExecutionCount); diff --git a/Microsoft.DotNet.Try.js/package-lock.json b/Microsoft.DotNet.Try.js/package-lock.json index 32e72fbf3..fb8dadc16 100644 --- a/Microsoft.DotNet.Try.js/package-lock.json +++ b/Microsoft.DotNet.Try.js/package-lock.json @@ -5700,8 +5700,7 @@ "version": "1.2.0", "resolved": "https://msazure.pkgs.visualstudio.com/_packaging/MLS/npm/registry/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs b/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs index 4552e42c0..1efb64528 100644 --- a/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs +++ b/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs @@ -55,7 +55,7 @@ public async Task it_returns_exceptions_thrown_in_user_code() .Should() .BeOfType() .Which - .Error + .Exception .Should() .BeOfType(); } diff --git a/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs b/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs index cb06eee01..408b2177d 100644 --- a/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs +++ b/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs @@ -10,7 +10,7 @@ using Xunit; namespace WorkspaceServer.Tests.Kernel -{ +{ public class CodeSubmissionProcessorTests { private readonly CodeSubmissionProcessors _processors; diff --git a/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs b/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs index bebb957e2..b60c4b5df 100644 --- a/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs +++ b/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Linq; +using System.Reactive.Linq; using System.Threading.Tasks; using FluentAssertions; using WorkspaceServer.Kernel; @@ -23,21 +25,41 @@ public void When_SubmitCode_command_adds_packages_to_fsharp_kernel_then_PackageA throw new NotImplementedException(); } - [Fact(Skip = "WIP")] + [Fact] public async Task When_SubmitCode_command_adds_packages_to_csharp_kernel_then_the_submission_is_not_passed_to_csharpScript() { - var kernel = new CompositeKernel(new[] { new CSharpRepl() }); + var kernel = new CompositeKernel(new[] { new CSharpRepl().UseNugetDirective() }); var command = new SubmitCode("#r \"nuget:PocketLogger, 1.2.3\" \nvar a = new List();", "csharp"); await kernel.SendAsync(command); + command.Code.Should().Be("var a = new List();"); } - [Fact(Skip = "WIP")] - public void When_SubmitCode_command_adds_packages_to_csharp_kernel_then_PackageAdded_event_is_raised() + [Fact] + public async Task When_SubmitCode_command_adds_packages_to_csharp_kernel_then_PackageAdded_event_is_raised() { - throw new NotImplementedException(); + var kernel = new CompositeKernel(new[] { new CSharpRepl().UseNugetDirective() }); + + var command = new SubmitCode("#r \"nuget:PocketLogger, 1.2.3\" \nvar a = new List();", "csharp"); + + var result = await kernel.SendAsync(command); + + var events = result.KernelEvents + .ToEnumerable() + .ToArray(); + + events + .Should() + .ContainSingle(e => e is NuGetPackageAdded); + + events.OfType() + .Single() + .Command + .As() + .NugetReference + .Should() + .BeEquivalentTo(new NugetPackageReference("PocketLogger", "1.2.3")); } } - } \ No newline at end of file diff --git a/WorkspaceServer.Tests/Kernel/PackageReferenceTests.cs b/WorkspaceServer.Tests/Kernel/PackageReferenceTests.cs new file mode 100644 index 000000000..7f26ee61d --- /dev/null +++ b/WorkspaceServer.Tests/Kernel/PackageReferenceTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using FluentAssertions; +using WorkspaceServer.Kernel; +using Xunit; + +namespace WorkspaceServer.Tests.Kernel +{ + public class PackageReferenceTests + { + [Theory] + [InlineData("nuget:PocketLogger")] + [InlineData("nuget:PocketLogger,1.2.3")] + [InlineData("nuget:PocketLogger, 1.2.3")] + [InlineData("nuget:PocketLogger, 1.2.3-beta")] + public void Nuget_package_reference_correctly_parses_package_name(string value) + { + NugetPackageReference.TryParse(value, out var reference); + + reference.PackageName.Should().Be("PocketLogger"); + } + + [Theory] + [InlineData("nuget:PocketLogger", "")] + [InlineData("nuget:PocketLogger,1.2.3", "1.2.3")] + [InlineData("nuget:PocketLogger, 1.2.3", "1.2.3")] + [InlineData("nuget:PocketLogger, 1.2.3-beta", "1.2.3-beta")] + public void Nuget_package_reference_correctly_parses_package_version(string value, string expectedVersion) + { + NugetPackageReference.TryParse(value, out var reference); + + reference.PackageVersion.Should().Be(expectedVersion); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/AddNuGetPackage.cs b/WorkspaceServer/Kernel/AddNuGetPackage.cs new file mode 100644 index 000000000..f489323e6 --- /dev/null +++ b/WorkspaceServer/Kernel/AddNuGetPackage.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace WorkspaceServer.Kernel +{ + public class AddNuGetPackage : KernelCommandBase + { + public AddNuGetPackage(NugetPackageReference nugetReference) + { + NugetReference = nugetReference ?? throw new ArgumentNullException(nameof(nugetReference)); + } + + public NugetPackageReference NugetReference { get; } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CSharpRepl.cs b/WorkspaceServer/Kernel/CSharpRepl.cs index de91c40e2..534b39e9a 100644 --- a/WorkspaceServer/Kernel/CSharpRepl.cs +++ b/WorkspaceServer/Kernel/CSharpRepl.cs @@ -26,17 +26,9 @@ public class CSharpRepl : KernelBase private StringBuilder _inputBuffer = new StringBuilder(); - public CSharpRepl() { SetupScriptOptions(); - SetupPipeline(); - } - - private void SetupPipeline() - { - - } private void SetupScriptOptions() @@ -55,39 +47,92 @@ private void SetupScriptOptions() typeof(Task<>).GetTypeInfo().Assembly); } - private async Task HandleCodeSubmission(SubmitCode codeSubmission, KernelCommandContext context) + private (bool shouldExecute, string completeSubmission) IsBufferACompleteSubmission(string input) + { + _inputBuffer.AppendLine(input); + + var code = _inputBuffer.ToString(); + var syntaxTree = SyntaxFactory.ParseSyntaxTree(code, ParseOptions); + + if (!SyntaxFactory.IsCompleteSubmission(syntaxTree)) + { + return (false, code); + } + + _inputBuffer = new StringBuilder(); + return (true, code); + } + + protected internal override async Task HandleAsync( + IKernelCommand command, + KernelCommandContext context) + { + switch (command) + { + case SubmitCode submitCode: + if (submitCode.Language == "csharp") + { + await HandleSubmitCode(submitCode, context); + } + + break; + + case AddNuGetPackage addPackage: + + await HandleAddNugetPackage(addPackage, context); + + break; + } + } + + private async Task HandleAddNugetPackage( + AddNuGetPackage addPackage, + KernelCommandContext context) + { + // FIX: (HandleAddNugetPackage) + + + + + + } + + private async Task HandleSubmitCode( + SubmitCode codeSubmission, + KernelCommandContext context) { - var commandResult = new KernelCommandResult(); - commandResult.RelayEventsOn(PublishEvent); + var commandResult = new KernelCommandResult(PublishEvent); context.Result = commandResult; - commandResult.OnNext(new CodeSubmissionReceived(codeSubmission.Id, codeSubmission.Code)); + commandResult.OnNext(new CodeSubmissionReceived( + codeSubmission.Code, + codeSubmission)); - var (shouldExecute, code) = ComputeFullSubmission(codeSubmission.Code); + var (shouldExecute, code) = IsBufferACompleteSubmission(codeSubmission.Code); if (shouldExecute) { - commandResult.OnNext(new CompleteCodeSubmissionReceived(codeSubmission.Id)); + commandResult.OnNext(new CompleteCodeSubmissionReceived(codeSubmission)); Exception exception = null; try { if (_scriptState == null) { _scriptState = await CSharpScript.RunAsync( - code, - ScriptOptions, - cancellationToken: context.CancellationToken); + code, + ScriptOptions, + cancellationToken: context.CancellationToken); } else { _scriptState = await _scriptState.ContinueWithAsync( - code, - ScriptOptions, - e => - { - exception = e; - return true; - }, - context.CancellationToken); + code, + ScriptOptions, + e => + { + exception = e; + return true; + }, + context.CancellationToken); } } catch (Exception e) @@ -95,74 +140,35 @@ private async Task HandleCodeSubmission(SubmitCode codeSubmission, KernelCommand exception = e; } - var hasReturnValue = _scriptState != null && (bool)_hasReturnValueMethod.Invoke(_scriptState.Script, null); + var hasReturnValue = _scriptState != null && + (bool) _hasReturnValueMethod.Invoke(_scriptState.Script, null); if (hasReturnValue) { - commandResult.OnNext(new ValueProduced(codeSubmission.Id, _scriptState.ReturnValue)); + commandResult.OnNext(new ValueProduced(_scriptState.ReturnValue, codeSubmission)); } + if (exception != null) { - var diagnostics = _scriptState?.Script?.GetDiagnostics() ?? Enumerable.Empty(); - if (diagnostics.Any()) - { - var message = string.Join("\n", diagnostics.Select(d => d.GetMessage())); + var message = string.Join("\n", (_scriptState?.Script?.GetDiagnostics() ?? + Enumerable.Empty()).Select(d => d.GetMessage())); + + commandResult.OnNext(new CodeSubmissionEvaluationFailed(exception, message, codeSubmission)); + - commandResult.OnNext(new CodeSubmissionEvaluationFailed(codeSubmission.Id, exception, message)); - } - else - { - commandResult.OnNext(new CodeSubmissionEvaluationFailed(codeSubmission.Id, exception)); - - } commandResult.OnError(exception); } else { - commandResult.OnNext(new CodeSubmissionEvaluated(codeSubmission.Id)); + commandResult.OnNext(new CodeSubmissionEvaluated(codeSubmission)); commandResult.OnCompleted(); } } else { - commandResult.OnNext(new IncompleteCodeSubmissionReceived(codeSubmission.Id)); + commandResult.OnNext(new IncompleteCodeSubmissionReceived(codeSubmission)); commandResult.OnCompleted(); } } - - private (bool shouldExecute, string completeSubmission) ComputeFullSubmission(string input) - { - _inputBuffer.AppendLine(input); - - var code = _inputBuffer.ToString(); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(code, ParseOptions); - - if (!SyntaxFactory.IsCompleteSubmission(syntaxTree)) - { - return (false, code); - } - - _inputBuffer = new StringBuilder(); - return (true, code); - } - - protected internal override Task HandleAsync(KernelCommandContext context) - { - switch (context.Command) - { - case SubmitCode submitCode: - if (submitCode.Language == "csharp") - { - return HandleCodeSubmission(submitCode, context); - } - else - { - return Task.CompletedTask; - } - - default: - return Task.CompletedTask; - } - } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CSharpReplExtensions.cs b/WorkspaceServer/Kernel/CSharpReplExtensions.cs new file mode 100644 index 000000000..8eaebe7cd --- /dev/null +++ b/WorkspaceServer/Kernel/CSharpReplExtensions.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation and contributors. 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.CommandLine; + +namespace WorkspaceServer.Kernel +{ + public static class CSharpReplExtensions + { + public static CSharpRepl UseNugetDirective(this CSharpRepl repl) + { + var packageRefArg = new Argument((SymbolResult result, out NugetPackageReference reference) => + NugetPackageReference.TryParse(result.Token.Value, out reference)) + { + Name = "package" + }; + + var parser = new Command("#r") + { + packageRefArg + }; + + repl.Pipeline.AddMiddleware(async (command, context, next) => + { + switch (command) + { + case SubmitCode submitCode: + + var lines = new Queue( + submitCode.Code.Split(new[] { "\r\n", "\n" }, + StringSplitOptions.None)); + + var unhandledLines = new List(); + + while (lines.Count > 0) + { + var currentLine = lines.Dequeue(); + var parseResult = parser.Parse(currentLine); + + if (parseResult.Errors.Count == 0) + { + var nugetReference = parseResult.FindResultFor(packageRefArg).GetValueOrDefault(); + + var addNuGetPackage = new AddNuGetPackage(nugetReference); + + await repl.SendAsync( + addNuGetPackage, + context.CancellationToken); + } + else + { + unhandledLines.Add(currentLine); + } + } + + submitCode.Code = string.Join("\n", unhandledLines); + + break; + } + + await next(command, context); + }); + + return repl; + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CodeSubmissionEvaluated.cs b/WorkspaceServer/Kernel/CodeSubmissionEvaluated.cs index 3e480b9fd..ed6bc0a81 100644 --- a/WorkspaceServer/Kernel/CodeSubmissionEvaluated.cs +++ b/WorkspaceServer/Kernel/CodeSubmissionEvaluated.cs @@ -7,7 +7,7 @@ namespace WorkspaceServer.Kernel { public class CodeSubmissionEvaluated : KernelEventBase { - public CodeSubmissionEvaluated(Guid parentId) : base(parentId) + public CodeSubmissionEvaluated(IKernelCommand command) : base(command) { } } diff --git a/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs b/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs index ef9b1a2a0..bf675507d 100644 --- a/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs +++ b/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs @@ -1,5 +1,4 @@ - -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -8,15 +7,19 @@ namespace WorkspaceServer.Kernel { public class CodeSubmissionEvaluationFailed : KernelEventBase { - public object Error { get; } - public string Message { get; } - - public CodeSubmissionEvaluationFailed(Guid parentId, object error, string message = null): base(parentId) + public CodeSubmissionEvaluationFailed( + Exception exception, + string message, + SubmitCode submitCode) : base(submitCode) { - Error = error; - Message = string.IsNullOrWhiteSpace(message) - ? error is Exception exception ? exception.Message : error.ToString() - : message; + Exception = exception; + Message = string.IsNullOrWhiteSpace(message) + ? exception.Message + : message; } + + public Exception Exception { get; } + + public string Message { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs b/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs index da6276e4a..e6bf4f90a 100644 --- a/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs +++ b/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs @@ -31,40 +31,34 @@ public void Add(ICodeSubmissionProcessor processor) _parser = new CommandLineBuilder(_rootCommand).Build(); } - public async Task ProcessAsync(SubmitCode codeSubmission) + public async Task ProcessAsync(SubmitCode submitCode) { - try + var lines = new Queue(submitCode.Code.Split(new[] { "\r\n", "\n" }, + StringSplitOptions.None)); + var unhandledLines = new Queue(); + while (lines.Count > 0) { - var lines = new Queue(codeSubmission.Code.Split(new[] {"\r\n", "\n"}, - StringSplitOptions.None)); - var unhandledLines = new Queue(); - while (lines.Count > 0) - { - var currentLine = lines.Dequeue(); - var result = _parser.Parse(currentLine); + var currentLine = lines.Dequeue(); + var result = _parser.Parse(currentLine); - if (result.CommandResult != null && - _processors.TryGetValue(result.CommandResult.Command, out var processor)) - { - await _parser.InvokeAsync(result); - codeSubmission.Code = string.Join("\n", lines); - var newSubmission = await processor.ProcessAsync(codeSubmission); - lines = new Queue(newSubmission.Code.Split(new[] {"\r\n", "\n"}, - StringSplitOptions.None)); - } - else - { - unhandledLines.Enqueue(currentLine); - } + if (result.CommandResult != null && + _processors.TryGetValue(result.CommandResult.Command, out var processor)) + { + await _parser.InvokeAsync(result); + submitCode.Code = string.Join("\n", lines); + var newSubmission = await processor.ProcessAsync(submitCode); + lines = new Queue(newSubmission.Code.Split(new[] { "\r\n", "\n" }, + StringSplitOptions.None)); + } + else + { + unhandledLines.Enqueue(currentLine); } - - codeSubmission.Code = string.Join("\n", unhandledLines); - return codeSubmission; - } - catch (Exception e) - { - throw new CodeSubmissionProcessorException(e, codeSubmission); } + + submitCode.Code = string.Join("\n", unhandledLines); + + return submitCode; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CodeSubmissionReceived.cs b/WorkspaceServer/Kernel/CodeSubmissionReceived.cs index 1827ab887..e9bd0625c 100644 --- a/WorkspaceServer/Kernel/CodeSubmissionReceived.cs +++ b/WorkspaceServer/Kernel/CodeSubmissionReceived.cs @@ -7,11 +7,11 @@ namespace WorkspaceServer.Kernel { public class CodeSubmissionReceived : KernelEventBase { - public string Value { get; } - - public CodeSubmissionReceived(Guid parentId, string value) : base(parentId) + public CodeSubmissionReceived(string value, SubmitCode submitCode) : base(submitCode) { Value = value ?? throw new ArgumentNullException(nameof(value)); } + + public string Value { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CompleteCodeSubmissionReceived.cs b/WorkspaceServer/Kernel/CompleteCodeSubmissionReceived.cs index 28bebe6bd..5fd825f19 100644 --- a/WorkspaceServer/Kernel/CompleteCodeSubmissionReceived.cs +++ b/WorkspaceServer/Kernel/CompleteCodeSubmissionReceived.cs @@ -7,7 +7,7 @@ namespace WorkspaceServer.Kernel { public class CompleteCodeSubmissionReceived : KernelEventBase { - public CompleteCodeSubmissionReceived(Guid parentId) : base(parentId) + public CompleteCodeSubmissionReceived(SubmitCode submitCode) : base(submitCode) { } } diff --git a/WorkspaceServer/Kernel/CompletionReceived.cs b/WorkspaceServer/Kernel/CompletionReceived.cs index afef8c1fa..488b23fc1 100644 --- a/WorkspaceServer/Kernel/CompletionReceived.cs +++ b/WorkspaceServer/Kernel/CompletionReceived.cs @@ -7,7 +7,7 @@ namespace WorkspaceServer.Kernel { public class CompletionReceived : KernelEventBase { - public CompletionReceived(Guid parentId) : base(parentId) + public CompletionReceived(IKernelCommand command) : base(command) { } } diff --git a/WorkspaceServer/Kernel/CompositeKernel.cs b/WorkspaceServer/Kernel/CompositeKernel.cs index 55c4c38b6..51ed7ee19 100644 --- a/WorkspaceServer/Kernel/CompositeKernel.cs +++ b/WorkspaceServer/Kernel/CompositeKernel.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Linq; +using System.Linq; using System.Threading.Tasks; namespace WorkspaceServer.Kernel @@ -12,24 +12,48 @@ namespace WorkspaceServer.Kernel public class CompositeKernel : KernelBase { private readonly IReadOnlyList _kernels; - public CompositeKernel(IReadOnlyList kernels) { _kernels = kernels ?? throw new ArgumentNullException(nameof(kernels)); - AddDisposable( kernels.Select(k => k.KernelEvents).Merge().Subscribe(PublishEvent)); + + Pipeline.AddMiddleware(ChooseKernel); + + AddDisposable( + kernels.Select(k => k.KernelEvents) + .Merge() + .Subscribe(PublishEvent)); } - protected internal override async Task HandleAsync(KernelCommandContext context) + private Task ChooseKernel( + IKernelCommand command, + KernelCommandContext context, + KernelCommandPipelineContinuation next) { - foreach (var kernel in _kernels.OfType()) + if (context.Kernel == null) { - await kernel.Pipeline.InvokeAsync(context); - if (context.Result != null) + if (_kernels.Count == 1) { - return; + context.Kernel = _kernels[0]; } } + + return next(command, context); + } + + protected internal override async Task HandleAsync( + IKernelCommand command, + KernelCommandContext context) + { + var kernel = context.Kernel; + + if (kernel is KernelBase kernelBase) + { + await kernelBase.SendOnContextAsync(command, context); + return; + } + + throw new NoSuitableKernelException(); } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/DiagnosticsReceived.cs b/WorkspaceServer/Kernel/DiagnosticsReceived.cs index 2cf0baba1..44cef6bfc 100644 --- a/WorkspaceServer/Kernel/DiagnosticsReceived.cs +++ b/WorkspaceServer/Kernel/DiagnosticsReceived.cs @@ -1,17 +1,11 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class DiagnosticsReceived : KernelEventBase { - public DiagnosticsReceived(IKernelCommand command) : this(command.Id) - { - } - - public DiagnosticsReceived(Guid parentId) : base(parentId) + public DiagnosticsReceived(IKernelCommand command) : base(command) { } } diff --git a/WorkspaceServer/Kernel/DocumentationReceived.cs b/WorkspaceServer/Kernel/DocumentationReceived.cs index 8ecb58c25..13925fa48 100644 --- a/WorkspaceServer/Kernel/DocumentationReceived.cs +++ b/WorkspaceServer/Kernel/DocumentationReceived.cs @@ -1,17 +1,11 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class DocumentationReceived : KernelEventBase { - public DocumentationReceived(IKernelCommand command) : this(command.Id) - { - } - - public DocumentationReceived(Guid parentId) : base(parentId) + public DocumentationReceived(IKernelCommand command) : base(command) { } } diff --git a/WorkspaceServer/Kernel/IKernel.cs b/WorkspaceServer/Kernel/IKernel.cs index 6e5ad6875..6e69e3d52 100644 --- a/WorkspaceServer/Kernel/IKernel.cs +++ b/WorkspaceServer/Kernel/IKernel.cs @@ -7,9 +7,10 @@ namespace WorkspaceServer.Kernel { - public interface IKernel: IDisposable + public interface IKernel : IDisposable { IObservable KernelEvents { get; } + Task SendAsync(IKernelCommand command, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/IKernelCommand.cs b/WorkspaceServer/Kernel/IKernelCommand.cs index b29a3096e..fc7b26a7f 100644 --- a/WorkspaceServer/Kernel/IKernelCommand.cs +++ b/WorkspaceServer/Kernel/IKernelCommand.cs @@ -1,34 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public interface IKernelCommand { - Guid ParentId { get; } - Guid Id { get; } - } - - public abstract class KernelCommandBase : IKernelCommand - { - protected KernelCommandBase() : this(Guid.NewGuid(), Guid.Empty) - { - - } - - protected KernelCommandBase(Guid id) : this(id,Guid.Empty) - { - } - - protected KernelCommandBase(Guid id, Guid parentId) - { - Id = id; - ParentId = parentId; - } - - public Guid ParentId { get; } - public Guid Id { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/IKernelCommandResult.cs b/WorkspaceServer/Kernel/IKernelCommandResult.cs index fae1cfbba..054160203 100644 --- a/WorkspaceServer/Kernel/IKernelCommandResult.cs +++ b/WorkspaceServer/Kernel/IKernelCommandResult.cs @@ -7,6 +7,6 @@ namespace WorkspaceServer.Kernel { public interface IKernelCommandResult { - IObservable Events { get; } + IObservable KernelEvents { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/IKernelEvent.cs b/WorkspaceServer/Kernel/IKernelEvent.cs index 06cf4275a..96dc6d414 100644 --- a/WorkspaceServer/Kernel/IKernelEvent.cs +++ b/WorkspaceServer/Kernel/IKernelEvent.cs @@ -1,69 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public interface IKernelEvent { - Guid ParentId { get; } - Guid Id { get; } - } - - public abstract class KernelEventBase : IKernelEvent - { - public Guid ParentId { get; } - public Guid Id { get; } - - protected KernelEventBase(Guid id, IKernelCommand command) : this(id, command.Id) - { - - } - - protected KernelEventBase(IKernelCommand command) : this(Guid.NewGuid(), command) - { - - } - - protected KernelEventBase(Guid parentId) : this(Guid.NewGuid(), parentId) - { - } - - protected KernelEventBase(Guid id, Guid parentId) - { - ParentId = parentId; - Id = id; - } - } - - - public class SendStandardInput : KernelCommandBase - { - } - - - /// - /// add a packages to the execution - /// - public class AddPackage : KernelCommandBase - { - } - - public class RequestCompletion : KernelCommandBase - { - } - - public class RequestDiagnostics : KernelCommandBase - { + IKernelCommand Command { get; } } - - public class RequestSignatureHelp : KernelCommandBase - { - } - - public class RequestDocumentation : KernelCommandBase - { - } - } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/IncompleteCodeSubmissionReceived.cs b/WorkspaceServer/Kernel/IncompleteCodeSubmissionReceived.cs index 0145e1673..943652b84 100644 --- a/WorkspaceServer/Kernel/IncompleteCodeSubmissionReceived.cs +++ b/WorkspaceServer/Kernel/IncompleteCodeSubmissionReceived.cs @@ -1,16 +1,12 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; - namespace WorkspaceServer.Kernel { public class IncompleteCodeSubmissionReceived : KernelEventBase { - public IncompleteCodeSubmissionReceived(Guid parentId): base(parentId) + public IncompleteCodeSubmissionReceived(SubmitCode submitCode) : base(submitCode) { - } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelBase.cs b/WorkspaceServer/Kernel/KernelBase.cs index 7f6b80536..90612d2d6 100644 --- a/WorkspaceServer/Kernel/KernelBase.cs +++ b/WorkspaceServer/Kernel/KernelBase.cs @@ -9,7 +9,7 @@ namespace WorkspaceServer.Kernel { - public abstract class KernelBase: IKernel + public abstract class KernelBase : IKernel { public KernelCommandPipeline Pipeline { get; } @@ -23,38 +23,53 @@ protected KernelBase() _disposables = new CompositeDisposable(); } - public async Task SendAsync(IKernelCommand command, CancellationToken cancellationToken) + public async Task SendAsync( + IKernelCommand command, + CancellationToken cancellationToken) { - if (command == null) { + if (command == null) + { throw new ArgumentNullException(nameof(command)); - } - var invocationContext = new KernelCommandContext(command, cancellationToken); - await Pipeline.InvokeAsync(invocationContext); + + var invocationContext = new KernelCommandContext(cancellationToken); + + await SendOnContextAsync(command, invocationContext); + return invocationContext.Result; } + public async Task SendOnContextAsync( + IKernelCommand command, + KernelCommandContext invocationContext) + { + await Pipeline.InvokeAsync(command, invocationContext); + } + protected void PublishEvent(IKernelEvent kernelEvent) { if (kernelEvent == null) { throw new ArgumentNullException(nameof(kernelEvent)); } + _channel.OnNext(kernelEvent); } + protected void AddDisposable(IDisposable disposable) { if (disposable == null) { throw new ArgumentNullException(nameof(disposable)); } + _disposables.Add(disposable); } - protected internal abstract Task HandleAsync(KernelCommandContext context); - public void Dispose() - { - _disposables.Dispose(); - } + protected internal abstract Task HandleAsync( + IKernelCommand command, + KernelCommandContext context); + + public void Dispose() => _disposables.Dispose(); } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandBase.cs b/WorkspaceServer/Kernel/KernelCommandBase.cs new file mode 100644 index 000000000..1bab8c528 --- /dev/null +++ b/WorkspaceServer/Kernel/KernelCommandBase.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace WorkspaceServer.Kernel +{ + public abstract class KernelCommandBase : IKernelCommand + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandContext.cs b/WorkspaceServer/Kernel/KernelCommandContext.cs index cb1d65de2..cb3e23f6f 100644 --- a/WorkspaceServer/Kernel/KernelCommandContext.cs +++ b/WorkspaceServer/Kernel/KernelCommandContext.cs @@ -7,14 +7,15 @@ namespace WorkspaceServer.Kernel { public class KernelCommandContext { - public IKernelCommandResult Result { get; set; } - public IKernelCommand Command { get; } - public CancellationToken CancellationToken { get; } - - public KernelCommandContext(IKernelCommand command, CancellationToken cancellationToken) + public KernelCommandContext(CancellationToken cancellationToken) { - Command = command; CancellationToken = cancellationToken; } + + public IKernelCommandResult Result { get; set; } + + public CancellationToken CancellationToken { get; } + + internal IKernel Kernel { get; set; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandPipeline.cs b/WorkspaceServer/Kernel/KernelCommandPipeline.cs index 1d1db546a..b46a4f18a 100644 --- a/WorkspaceServer/Kernel/KernelCommandPipeline.cs +++ b/WorkspaceServer/Kernel/KernelCommandPipeline.cs @@ -8,41 +8,55 @@ namespace WorkspaceServer.Kernel { - public class KernelCommandPipeline { + public class KernelCommandPipeline + { private readonly KernelBase _kernel; private readonly List _invocations = new List(); + private KernelCommandPipelineMiddleware _invocationChain; public KernelCommandPipeline(KernelBase kernel) { _kernel = kernel ?? throw new ArgumentNullException(nameof(kernel)); } - public async Task InvokeAsync(KernelCommandContext context) + private void EnsureMiddlewarePipelineIsInitialized() { - var invocationChain = BuildInvocationChain(); + if (_invocationChain == null) + { + _invocationChain = BuildInvocationChain(); + } + } + + public async Task InvokeAsync( + IKernelCommand command, + KernelCommandContext context) + { + EnsureMiddlewarePipelineIsInitialized(); - await invocationChain(context, invocationContext => Task.CompletedTask); + await _invocationChain(command, context, (_, __) => Task.CompletedTask); } private KernelCommandPipelineMiddleware BuildInvocationChain() { var invocations = new List(_invocations); - invocations.Add(async (invocationContext, _) => + invocations.Add(async (command, context, _) => { - await _kernel.HandleAsync(invocationContext); + await _kernel.HandleAsync(command, context); }); return invocations.Aggregate( (function, continuation) => - (ctx, next) => - function(ctx, c => continuation(c, next))); + (cmd1, ctx1, next) => + function(cmd1, ctx1, (cmd2, ctx2) => + continuation(cmd2, ctx2, next))); } public void AddMiddleware(KernelCommandPipelineMiddleware middleware) { _invocations.Add(middleware); + _invocationChain = null; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs b/WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs new file mode 100644 index 000000000..10c6ee349 --- /dev/null +++ b/WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; + +namespace WorkspaceServer.Kernel +{ + public delegate Task KernelCommandPipelineContinuation( + IKernelCommand command, + KernelCommandContext context); +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs b/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs index 56817974a..6f02379d2 100644 --- a/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs +++ b/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs @@ -7,6 +7,7 @@ namespace WorkspaceServer.Kernel { public delegate Task KernelCommandPipelineMiddleware( + IKernelCommand command, KernelCommandContext context, - Func next); + KernelCommandPipelineContinuation next); } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandResult.cs b/WorkspaceServer/Kernel/KernelCommandResult.cs index 741745893..5a5f1c46b 100644 --- a/WorkspaceServer/Kernel/KernelCommandResult.cs +++ b/WorkspaceServer/Kernel/KernelCommandResult.cs @@ -9,15 +9,15 @@ namespace WorkspaceServer.Kernel internal class KernelCommandResult : IKernelCommandResult, IObserver { private readonly ReplaySubject _events; - private Action _eventRelay; + private Action _publishGlobally; - public KernelCommandResult() + public KernelCommandResult(Action publishGlobally) { _events = new ReplaySubject(); + _publishGlobally = publishGlobally; } - public IObservable Events => _events; - + public IObservable KernelEvents => _events; public void OnCompleted() { @@ -32,12 +32,7 @@ public void OnError(Exception error) public void OnNext(IKernelEvent kernelEvent) { _events.OnNext(kernelEvent); - _eventRelay?.Invoke(kernelEvent); - } - - public void RelayEventsOn(Action eventRelay) - { - _eventRelay = eventRelay; + _publishGlobally?.Invoke(kernelEvent); } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelEventBase.cs b/WorkspaceServer/Kernel/KernelEventBase.cs new file mode 100644 index 000000000..de72ad54d --- /dev/null +++ b/WorkspaceServer/Kernel/KernelEventBase.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace WorkspaceServer.Kernel +{ + public abstract class KernelEventBase : IKernelEvent + { + protected KernelEventBase(IKernelCommand command) + { + Command = command ?? throw new ArgumentNullException(nameof(command)); + } + + protected KernelEventBase() + { + } + + public IKernelCommand Command { get; } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/NoSuitableKernelException.cs b/WorkspaceServer/Kernel/NoSuitableKernelException.cs new file mode 100644 index 000000000..5cd075892 --- /dev/null +++ b/WorkspaceServer/Kernel/NoSuitableKernelException.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace WorkspaceServer.Kernel +{ + public class NoSuitableKernelException : Exception + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/PackageAdded.cs b/WorkspaceServer/Kernel/NuGetPackageAdded.cs similarity index 56% rename from WorkspaceServer/Kernel/PackageAdded.cs rename to WorkspaceServer/Kernel/NuGetPackageAdded.cs index ec53aefb7..b56760783 100644 --- a/WorkspaceServer/Kernel/PackageAdded.cs +++ b/WorkspaceServer/Kernel/NuGetPackageAdded.cs @@ -5,13 +5,9 @@ namespace WorkspaceServer.Kernel { - public class PackageAdded : KernelEventBase + public class NuGetPackageAdded : KernelEventBase { - public PackageAdded(IKernelCommand command) : this(command.Id) - { - } - - public PackageAdded(Guid parentId) : base(parentId) + public NuGetPackageAdded(IKernelCommand command) : base(command) { } } diff --git a/WorkspaceServer/Kernel/NugetPackageReference.cs b/WorkspaceServer/Kernel/NugetPackageReference.cs new file mode 100644 index 000000000..5bcd8c8d4 --- /dev/null +++ b/WorkspaceServer/Kernel/NugetPackageReference.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text.RegularExpressions; + +namespace WorkspaceServer.Kernel +{ + public class NugetPackageReference + { + private static Regex _regex = new Regex( + @"nuget:\s*(?[\w\.]+)(\s*,\s*(?[\w\.\-]+))?", + RegexOptions.Compiled | + RegexOptions.CultureInvariant | + RegexOptions.Singleline); + + public NugetPackageReference(string packageName, string packageVersion = null) + { + if (string.IsNullOrWhiteSpace(packageName)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(packageName)); + } + + PackageName = packageName; + PackageVersion = packageVersion ?? string.Empty; + } + + public string PackageName { get; } + + public string PackageVersion { get; } + + public static bool TryParse(string value, out NugetPackageReference reference) + { + var result = _regex.Match(value); + + if (!result.Success) + { + reference = null; + return false; + } + + var packageName = result.Groups["packageName"].Value; + var packageVersion = result.Groups["packageVersion"].Value; + + reference = new NugetPackageReference(packageName, packageVersion); + + return true; + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/RequestCompletion.cs b/WorkspaceServer/Kernel/RequestCompletion.cs new file mode 100644 index 000000000..98ec176e9 --- /dev/null +++ b/WorkspaceServer/Kernel/RequestCompletion.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace WorkspaceServer.Kernel +{ + public class RequestCompletion : KernelCommandBase + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/RequestDiagnostics.cs b/WorkspaceServer/Kernel/RequestDiagnostics.cs new file mode 100644 index 000000000..705a4f6a8 --- /dev/null +++ b/WorkspaceServer/Kernel/RequestDiagnostics.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace WorkspaceServer.Kernel +{ + public class RequestDiagnostics : KernelCommandBase + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/RequestDocumentation.cs b/WorkspaceServer/Kernel/RequestDocumentation.cs new file mode 100644 index 000000000..aa055c1c9 --- /dev/null +++ b/WorkspaceServer/Kernel/RequestDocumentation.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace WorkspaceServer.Kernel +{ + public class RequestDocumentation : KernelCommandBase + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/RequestSignatureHelp.cs b/WorkspaceServer/Kernel/RequestSignatureHelp.cs new file mode 100644 index 000000000..292beb6af --- /dev/null +++ b/WorkspaceServer/Kernel/RequestSignatureHelp.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace WorkspaceServer.Kernel +{ + public class RequestSignatureHelp : KernelCommandBase + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/SendStandardInput.cs b/WorkspaceServer/Kernel/SendStandardInput.cs new file mode 100644 index 000000000..fc3d58917 --- /dev/null +++ b/WorkspaceServer/Kernel/SendStandardInput.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace WorkspaceServer.Kernel +{ + public class SendStandardInput : KernelCommandBase + { + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/SignatureHelpReceived.cs b/WorkspaceServer/Kernel/SignatureHelpReceived.cs index 768f0f0ff..9b05042e3 100644 --- a/WorkspaceServer/Kernel/SignatureHelpReceived.cs +++ b/WorkspaceServer/Kernel/SignatureHelpReceived.cs @@ -7,12 +7,9 @@ namespace WorkspaceServer.Kernel { public class SignatureHelpReceived : KernelEventBase { - public SignatureHelpReceived(IKernelCommand command) : this(command.Id) + public SignatureHelpReceived(IKernelCommand command) : base(command) { } - public SignatureHelpReceived(Guid parentId) : base(parentId) - { - } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/StandardErrorReceived.cs b/WorkspaceServer/Kernel/StandardErrorReceived.cs index a07ef3be8..4e60939c2 100644 --- a/WorkspaceServer/Kernel/StandardErrorReceived.cs +++ b/WorkspaceServer/Kernel/StandardErrorReceived.cs @@ -1,17 +1,15 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class StandardErrorReceived : KernelEventBase { - public string Content { get; } - - public StandardErrorReceived( string content) : base(Guid.NewGuid(), Guid.Empty) + public StandardErrorReceived(string content) { - Content = content; + Content = content ?? string.Empty; } + + public string Content { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/StandardInputReceived.cs b/WorkspaceServer/Kernel/StandardInputReceived.cs index 3e7ce8fec..3b22a7870 100644 --- a/WorkspaceServer/Kernel/StandardInputReceived.cs +++ b/WorkspaceServer/Kernel/StandardInputReceived.cs @@ -1,21 +1,15 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class StandardInputReceived : KernelEventBase { - public string Content { get; } - - public StandardInputReceived(IKernelCommand command, string content) : this(command.Id, content) + public StandardInputReceived(string content) { + Content = content ?? string.Empty; } - public StandardInputReceived(Guid parentId, string content) : base(parentId) - { - Content = content; - } + public string Content { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/StandardOutputReceived.cs b/WorkspaceServer/Kernel/StandardOutputReceived.cs index 2cf7cf1a2..d1474cdcd 100644 --- a/WorkspaceServer/Kernel/StandardOutputReceived.cs +++ b/WorkspaceServer/Kernel/StandardOutputReceived.cs @@ -1,17 +1,15 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class StandardOutputReceived : KernelEventBase { - public string Content { get; } - - public StandardOutputReceived(string content) : base(Guid.NewGuid(), Guid.Empty) + public StandardOutputReceived(string content) { - Content = content; + Content = content ?? string.Empty; } + + public string Content { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/Started.cs b/WorkspaceServer/Kernel/Started.cs index ed0cc894c..edb949e33 100644 --- a/WorkspaceServer/Kernel/Started.cs +++ b/WorkspaceServer/Kernel/Started.cs @@ -1,14 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class Started : KernelEventBase { - public Started():base(Guid.NewGuid(), Guid.Empty) - { - } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/Stopped.cs b/WorkspaceServer/Kernel/Stopped.cs index 5e9b12175..821b34fb2 100644 --- a/WorkspaceServer/Kernel/Stopped.cs +++ b/WorkspaceServer/Kernel/Stopped.cs @@ -1,14 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class Stopped : KernelEventBase { - public Stopped() : base(Guid.NewGuid(), Guid.Empty) - { - } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/ValueProduced.cs b/WorkspaceServer/Kernel/ValueProduced.cs index 0ebb5f3ef..aeff6ae05 100644 --- a/WorkspaceServer/Kernel/ValueProduced.cs +++ b/WorkspaceServer/Kernel/ValueProduced.cs @@ -1,17 +1,15 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class ValueProduced: KernelEventBase { - public object Value { get; } - - public ValueProduced(Guid parentId, object value) : base(parentId) + public ValueProduced(object value, SubmitCode submitCode) : base(submitCode) { Value = value; } + + public object Value { get; } } } \ No newline at end of file From 7e8bacfd6852dd483be9f5241617f5bc3eafdf41 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 11 Jul 2019 10:14:07 -0700 Subject: [PATCH 2/4] split out pipeline from invocation contexts, aggregate command-specific events into result --- .../ExecuteRequestHandlerTests.cs | 4 +- .../Kernel/KernelCommandPipelineTests.cs | 10 ++++ WorkspaceServer/Kernel/CSharpRepl.cs | 54 ++++++------------ .../Kernel/CSharpReplExtensions.cs | 14 +++-- WorkspaceServer/Kernel/CompositeKernel.cs | 6 +- WorkspaceServer/Kernel/KernelBase.cs | 12 ++-- .../Kernel/KernelCommandContext.cs | 21 ------- .../Kernel/KernelCommandPipeline.cs | 21 +++---- .../Kernel/KernelCommandPipelineMiddleware.cs | 8 ++- WorkspaceServer/Kernel/KernelCommandResult.cs | 29 ++-------- WorkspaceServer/Kernel/KernelExtensions.cs | 4 +- .../Kernel/KernelInvocationContext.cs | 52 +++++++++++++++++ .../Kernel/KernelPipelineContext.cs | 57 +++++++++++++++++++ ...ation.cs => KernelPipelineContinuation.cs} | 4 +- 14 files changed, 182 insertions(+), 114 deletions(-) delete mode 100644 WorkspaceServer/Kernel/KernelCommandContext.cs create mode 100644 WorkspaceServer/Kernel/KernelInvocationContext.cs create mode 100644 WorkspaceServer/Kernel/KernelPipelineContext.cs rename WorkspaceServer/Kernel/{KernelCommandPipelineContinuation.cs => KernelPipelineContinuation.cs} (73%) diff --git a/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs b/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs index 7601b448a..2011c34b8 100644 --- a/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs +++ b/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs @@ -48,7 +48,7 @@ public async Task handles_executeRequest() } [Fact] - public async Task sends_executeReply_message_on_codeSubmissionEvaluated() + public async Task sends_ExecuteReply_message_on_codeSubmissionEvaluated() { var kernel = new CSharpRepl(); @@ -83,7 +83,7 @@ public async Task sends_executeReply_with_error_message_on_codeSubmissionEvaluat } [Fact] - public async Task sends_executeResult_message_on_valueProduced() + public async Task sends_ExecuteReply_message_on_ValueProduced() { var kernel = new CSharpRepl(); diff --git a/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs b/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs index b60c4b5df..67a98159d 100644 --- a/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs +++ b/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs @@ -4,15 +4,25 @@ using System; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Threading.Tasks; using FluentAssertions; +using FluentAssertions.Extensions; using WorkspaceServer.Kernel; using Xunit; +using Xunit.Abstractions; namespace WorkspaceServer.Tests.Kernel { public class KernelCommandPipelineTests { + private ITestOutputHelper _output; + + public KernelCommandPipelineTests(ITestOutputHelper output) + { + _output = output; + } + [Fact(Skip = "WIP")] public void When_SubmitCode_command_adds_packages_to_fsharp_kernel_then_the_submission_is_passed_to_fsi() { diff --git a/WorkspaceServer/Kernel/CSharpRepl.cs b/WorkspaceServer/Kernel/CSharpRepl.cs index 534b39e9a..5f2617941 100644 --- a/WorkspaceServer/Kernel/CSharpRepl.cs +++ b/WorkspaceServer/Kernel/CSharpRepl.cs @@ -65,53 +65,37 @@ private void SetupScriptOptions() protected internal override async Task HandleAsync( IKernelCommand command, - KernelCommandContext context) + KernelPipelineContext context) { switch (command) { case SubmitCode submitCode: if (submitCode.Language == "csharp") { - await HandleSubmitCode(submitCode, context); + context.OnExecute(async invocationContext => + { + await HandleSubmitCode(submitCode, invocationContext); + }); } - break; - - case AddNuGetPackage addPackage: - - await HandleAddNugetPackage(addPackage, context); - break; } - } - - private async Task HandleAddNugetPackage( - AddNuGetPackage addPackage, - KernelCommandContext context) - { - // FIX: (HandleAddNugetPackage) - - - - - } private async Task HandleSubmitCode( SubmitCode codeSubmission, - KernelCommandContext context) + KernelInvocationContext context) { - var commandResult = new KernelCommandResult(PublishEvent); - context.Result = commandResult; - commandResult.OnNext(new CodeSubmissionReceived( - codeSubmission.Code, - codeSubmission)); + var codeSubmissionReceived = new CodeSubmissionReceived( + codeSubmission.Code, + codeSubmission); + context.OnNext(codeSubmissionReceived); var (shouldExecute, code) = IsBufferACompleteSubmission(codeSubmission.Code); if (shouldExecute) { - commandResult.OnNext(new CompleteCodeSubmissionReceived(codeSubmission)); + context.OnNext(new CompleteCodeSubmissionReceived(codeSubmission)); Exception exception = null; try { @@ -145,7 +129,7 @@ private async Task HandleSubmitCode( if (hasReturnValue) { - commandResult.OnNext(new ValueProduced(_scriptState.ReturnValue, codeSubmission)); + context.OnNext(new ValueProduced(_scriptState.ReturnValue, codeSubmission)); } if (exception != null) @@ -153,21 +137,19 @@ private async Task HandleSubmitCode( var message = string.Join("\n", (_scriptState?.Script?.GetDiagnostics() ?? Enumerable.Empty()).Select(d => d.GetMessage())); - commandResult.OnNext(new CodeSubmissionEvaluationFailed(exception, message, codeSubmission)); - - - commandResult.OnError(exception); + context.OnNext(new CodeSubmissionEvaluationFailed(exception, message, codeSubmission)); + context.OnError(exception); } else { - commandResult.OnNext(new CodeSubmissionEvaluated(codeSubmission)); - commandResult.OnCompleted(); + context.OnNext(new CodeSubmissionEvaluated(codeSubmission)); + context.OnCompleted(); } } else { - commandResult.OnNext(new IncompleteCodeSubmissionReceived(codeSubmission)); - commandResult.OnCompleted(); + context.OnNext(new IncompleteCodeSubmissionReceived(codeSubmission)); + context.OnCompleted(); } } } diff --git a/WorkspaceServer/Kernel/CSharpReplExtensions.cs b/WorkspaceServer/Kernel/CSharpReplExtensions.cs index 8eaebe7cd..b8bf94f33 100644 --- a/WorkspaceServer/Kernel/CSharpReplExtensions.cs +++ b/WorkspaceServer/Kernel/CSharpReplExtensions.cs @@ -22,7 +22,7 @@ public static CSharpRepl UseNugetDirective(this CSharpRepl repl) packageRefArg }; - repl.Pipeline.AddMiddleware(async (command, context, next) => + repl.Pipeline.AddMiddleware(async (command, pipelineContext, next) => { switch (command) { @@ -43,11 +43,13 @@ public static CSharpRepl UseNugetDirective(this CSharpRepl repl) { var nugetReference = parseResult.FindResultFor(packageRefArg).GetValueOrDefault(); - var addNuGetPackage = new AddNuGetPackage(nugetReference); + pipelineContext.OnExecute(async invocationContext => + { + var addNuGetPackage = new AddNuGetPackage(nugetReference); - await repl.SendAsync( - addNuGetPackage, - context.CancellationToken); + invocationContext.OnNext(new NuGetPackageAdded(addNuGetPackage)); + invocationContext.OnCompleted(); + }); } else { @@ -60,7 +62,7 @@ await repl.SendAsync( break; } - await next(command, context); + await next(command, pipelineContext); }); return repl; diff --git a/WorkspaceServer/Kernel/CompositeKernel.cs b/WorkspaceServer/Kernel/CompositeKernel.cs index 51ed7ee19..e527b3b68 100644 --- a/WorkspaceServer/Kernel/CompositeKernel.cs +++ b/WorkspaceServer/Kernel/CompositeKernel.cs @@ -27,8 +27,8 @@ public CompositeKernel(IReadOnlyList kernels) private Task ChooseKernel( IKernelCommand command, - KernelCommandContext context, - KernelCommandPipelineContinuation next) + KernelPipelineContext context, + KernelPipelineContinuation next) { if (context.Kernel == null) { @@ -43,7 +43,7 @@ private Task ChooseKernel( protected internal override async Task HandleAsync( IKernelCommand command, - KernelCommandContext context) + KernelPipelineContext context) { var kernel = context.Kernel; diff --git a/WorkspaceServer/Kernel/KernelBase.cs b/WorkspaceServer/Kernel/KernelBase.cs index 90612d2d6..23c5bf41b 100644 --- a/WorkspaceServer/Kernel/KernelBase.cs +++ b/WorkspaceServer/Kernel/KernelBase.cs @@ -32,16 +32,18 @@ public async Task SendAsync( throw new ArgumentNullException(nameof(command)); } - var invocationContext = new KernelCommandContext(cancellationToken); + var pipelineContext = new KernelPipelineContext( + PublishEvent, + cancellationToken); - await SendOnContextAsync(command, invocationContext); + await SendOnContextAsync(command, pipelineContext); - return invocationContext.Result; + return await pipelineContext.InvokeAsync(); } public async Task SendOnContextAsync( IKernelCommand command, - KernelCommandContext invocationContext) + KernelPipelineContext invocationContext) { await Pipeline.InvokeAsync(command, invocationContext); } @@ -68,7 +70,7 @@ protected void AddDisposable(IDisposable disposable) protected internal abstract Task HandleAsync( IKernelCommand command, - KernelCommandContext context); + KernelPipelineContext context); public void Dispose() => _disposables.Dispose(); } diff --git a/WorkspaceServer/Kernel/KernelCommandContext.cs b/WorkspaceServer/Kernel/KernelCommandContext.cs deleted file mode 100644 index cb3e23f6f..000000000 --- a/WorkspaceServer/Kernel/KernelCommandContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Threading; - -namespace WorkspaceServer.Kernel -{ - public class KernelCommandContext - { - public KernelCommandContext(CancellationToken cancellationToken) - { - CancellationToken = cancellationToken; - } - - public IKernelCommandResult Result { get; set; } - - public CancellationToken CancellationToken { get; } - - internal IKernel Kernel { get; set; } - } -} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandPipeline.cs b/WorkspaceServer/Kernel/KernelCommandPipeline.cs index b46a4f18a..dcf75d222 100644 --- a/WorkspaceServer/Kernel/KernelCommandPipeline.cs +++ b/WorkspaceServer/Kernel/KernelCommandPipeline.cs @@ -12,8 +12,9 @@ public class KernelCommandPipeline { private readonly KernelBase _kernel; - private readonly List _invocations = new List(); - private KernelCommandPipelineMiddleware _invocationChain; + private readonly List _middlewares = new List(); + + private KernelCommandPipelineMiddleware _pipeline; public KernelCommandPipeline(KernelBase kernel) { @@ -22,24 +23,24 @@ public KernelCommandPipeline(KernelBase kernel) private void EnsureMiddlewarePipelineIsInitialized() { - if (_invocationChain == null) + if (_pipeline == null) { - _invocationChain = BuildInvocationChain(); + _pipeline = BuildPipeline(); } } public async Task InvokeAsync( IKernelCommand command, - KernelCommandContext context) + KernelPipelineContext context) { EnsureMiddlewarePipelineIsInitialized(); - await _invocationChain(command, context, (_, __) => Task.CompletedTask); + await _pipeline(command, context, (_, __) => Task.CompletedTask); } - private KernelCommandPipelineMiddleware BuildInvocationChain() + private KernelCommandPipelineMiddleware BuildPipeline() { - var invocations = new List(_invocations); + var invocations = new List(_middlewares); invocations.Add(async (command, context, _) => { @@ -55,8 +56,8 @@ private KernelCommandPipelineMiddleware BuildInvocationChain() public void AddMiddleware(KernelCommandPipelineMiddleware middleware) { - _invocations.Add(middleware); - _invocationChain = null; + _middlewares.Add(middleware); + _pipeline = null; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs b/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs index 6f02379d2..54e2cc3c0 100644 --- a/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs +++ b/WorkspaceServer/Kernel/KernelCommandPipelineMiddleware.cs @@ -1,13 +1,15 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; using System.Threading.Tasks; namespace WorkspaceServer.Kernel { public delegate Task KernelCommandPipelineMiddleware( IKernelCommand command, - KernelCommandContext context, - KernelCommandPipelineContinuation next); + KernelPipelineContext context, + KernelPipelineContinuation next); + + public delegate Task KernelCommandInvocation( + KernelInvocationContext context); } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandResult.cs b/WorkspaceServer/Kernel/KernelCommandResult.cs index 5a5f1c46b..93980b677 100644 --- a/WorkspaceServer/Kernel/KernelCommandResult.cs +++ b/WorkspaceServer/Kernel/KernelCommandResult.cs @@ -2,37 +2,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Reactive.Subjects; namespace WorkspaceServer.Kernel { - internal class KernelCommandResult : IKernelCommandResult, IObserver + internal class KernelCommandResult : IKernelCommandResult { - private readonly ReplaySubject _events; - private Action _publishGlobally; - - public KernelCommandResult(Action publishGlobally) - { - _events = new ReplaySubject(); - _publishGlobally = publishGlobally; - } - - public IObservable KernelEvents => _events; - - public void OnCompleted() + public KernelCommandResult(IObservable events) { - _events.OnCompleted(); + KernelEvents = events ?? throw new ArgumentNullException(nameof(events)); } - public void OnError(Exception error) - { - _events.OnError(error); - } - - public void OnNext(IKernelEvent kernelEvent) - { - _events.OnNext(kernelEvent); - _publishGlobally?.Invoke(kernelEvent); - } + public IObservable KernelEvents { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelExtensions.cs b/WorkspaceServer/Kernel/KernelExtensions.cs index 10780c2ec..3e87f13a8 100644 --- a/WorkspaceServer/Kernel/KernelExtensions.cs +++ b/WorkspaceServer/Kernel/KernelExtensions.cs @@ -8,7 +8,9 @@ namespace WorkspaceServer.Kernel { public static class KernelExtensions { - public static Task SendAsync(this IKernel kernel, IKernelCommand command) + public static Task SendAsync( + this IKernel kernel, + IKernelCommand command) { return kernel.SendAsync(command, CancellationToken.None); } diff --git a/WorkspaceServer/Kernel/KernelInvocationContext.cs b/WorkspaceServer/Kernel/KernelInvocationContext.cs new file mode 100644 index 000000000..e6ad79fad --- /dev/null +++ b/WorkspaceServer/Kernel/KernelInvocationContext.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; + +namespace WorkspaceServer.Kernel +{ + public class KernelInvocationContext : IObserver + { + private readonly KernelCommandInvocation _invocation; + private readonly Action _publishEvent; + private readonly ReplaySubject _events = new ReplaySubject(); + + public KernelInvocationContext( + KernelCommandInvocation invocation, + Action publishEvent, + CancellationToken cancellationToken) + { + _invocation = invocation; + _publishEvent = publishEvent; + CancellationToken = cancellationToken; + } + + public CancellationToken CancellationToken { get; } + + public void OnCompleted() + { + _events.OnCompleted(); + } + + public void OnError(Exception exception) + { + _events.OnError(exception); + } + + public void OnNext(IKernelEvent @event) + { + _events.OnNext(@event); + _publishEvent(@event); + } + + internal IObservable KernelEvents => _events; + + public async Task InvokeAsync() + { + await _invocation(this); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelPipelineContext.cs b/WorkspaceServer/Kernel/KernelPipelineContext.cs new file mode 100644 index 000000000..ab508a47b --- /dev/null +++ b/WorkspaceServer/Kernel/KernelPipelineContext.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation and contributors. 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.Reactive.Linq; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace WorkspaceServer.Kernel +{ + public class KernelPipelineContext + { + private readonly Action _publishEvent; + private readonly List _invocations = new List(); + + public KernelPipelineContext( + Action publishEvent, + CancellationToken cancellationToken) + { + _publishEvent = publishEvent; + CancellationToken = cancellationToken; + } + + public CancellationToken CancellationToken { get; } + + internal IKernel Kernel { get; set; } + + public void OnExecute(KernelCommandInvocation invocation) + { + if (invocation == null) + { + throw new ArgumentNullException(nameof(invocation)); + } + + _invocations.Add(new KernelInvocationContext( + invocation, + _publishEvent, + CancellationToken)); + } + + internal async Task InvokeAsync() + { + var invocationContexts = _invocations.ToArray(); + + var observable = invocationContexts.Select(i => i.KernelEvents).Merge(); + + foreach (var invocation in invocationContexts) + { + await invocation.InvokeAsync(); + } + + return new KernelCommandResult(observable); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs b/WorkspaceServer/Kernel/KernelPipelineContinuation.cs similarity index 73% rename from WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs rename to WorkspaceServer/Kernel/KernelPipelineContinuation.cs index 10c6ee349..6da3787bd 100644 --- a/WorkspaceServer/Kernel/KernelCommandPipelineContinuation.cs +++ b/WorkspaceServer/Kernel/KernelPipelineContinuation.cs @@ -5,7 +5,7 @@ namespace WorkspaceServer.Kernel { - public delegate Task KernelCommandPipelineContinuation( + public delegate Task KernelPipelineContinuation( IKernelCommand command, - KernelCommandContext context); + KernelPipelineContext context); } \ No newline at end of file From 293d781681f5a61836d30c8116c996a8de7e70b1 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 11 Jul 2019 10:24:55 -0700 Subject: [PATCH 3/4] make CompositeKernel mutable --- ...pelineTests.cs => CompositeKernelTests.cs} | 20 ++++++----- .../Kernel/CSharpReplExtensions.cs | 4 ++- WorkspaceServer/Kernel/CompositeKernel.cs | 33 ++++++++++++------- 3 files changed, 36 insertions(+), 21 deletions(-) rename WorkspaceServer.Tests/Kernel/{KernelCommandPipelineTests.cs => CompositeKernelTests.cs} (85%) diff --git a/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs b/WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs similarity index 85% rename from WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs rename to WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs index 67a98159d..b4545a87c 100644 --- a/WorkspaceServer.Tests/Kernel/KernelCommandPipelineTests.cs +++ b/WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs @@ -2,23 +2,21 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Threading.Tasks; using FluentAssertions; -using FluentAssertions.Extensions; +using System.Linq; +using System.Threading.Tasks; using WorkspaceServer.Kernel; using Xunit; using Xunit.Abstractions; namespace WorkspaceServer.Tests.Kernel { - public class KernelCommandPipelineTests + public class CompositeKernelTests { private ITestOutputHelper _output; - public KernelCommandPipelineTests(ITestOutputHelper output) + public CompositeKernelTests(ITestOutputHelper output) { _output = output; } @@ -38,7 +36,10 @@ public void When_SubmitCode_command_adds_packages_to_fsharp_kernel_then_PackageA [Fact] public async Task When_SubmitCode_command_adds_packages_to_csharp_kernel_then_the_submission_is_not_passed_to_csharpScript() { - var kernel = new CompositeKernel(new[] { new CSharpRepl().UseNugetDirective() }); + var kernel = new CompositeKernel + { + new CSharpRepl().UseNugetDirective() + }; var command = new SubmitCode("#r \"nuget:PocketLogger, 1.2.3\" \nvar a = new List();", "csharp"); await kernel.SendAsync(command); @@ -49,7 +50,10 @@ public async Task When_SubmitCode_command_adds_packages_to_csharp_kernel_then_th [Fact] public async Task When_SubmitCode_command_adds_packages_to_csharp_kernel_then_PackageAdded_event_is_raised() { - var kernel = new CompositeKernel(new[] { new CSharpRepl().UseNugetDirective() }); + var kernel = new CompositeKernel + { + new CSharpRepl().UseNugetDirective() + }; var command = new SubmitCode("#r \"nuget:PocketLogger, 1.2.3\" \nvar a = new List();", "csharp"); diff --git a/WorkspaceServer/Kernel/CSharpReplExtensions.cs b/WorkspaceServer/Kernel/CSharpReplExtensions.cs index b8bf94f33..28310b420 100644 --- a/WorkspaceServer/Kernel/CSharpReplExtensions.cs +++ b/WorkspaceServer/Kernel/CSharpReplExtensions.cs @@ -41,7 +41,9 @@ public static CSharpRepl UseNugetDirective(this CSharpRepl repl) if (parseResult.Errors.Count == 0) { - var nugetReference = parseResult.FindResultFor(packageRefArg).GetValueOrDefault(); + var nugetReference = + parseResult.FindResultFor(packageRefArg) + .GetValueOrDefault(); pipelineContext.OnExecute(async invocationContext => { diff --git a/WorkspaceServer/Kernel/CompositeKernel.cs b/WorkspaceServer/Kernel/CompositeKernel.cs index e527b3b68..17ebacf22 100644 --- a/WorkspaceServer/Kernel/CompositeKernel.cs +++ b/WorkspaceServer/Kernel/CompositeKernel.cs @@ -2,32 +2,37 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; -using System.Reactive.Linq; using System.Linq; using System.Threading.Tasks; namespace WorkspaceServer.Kernel { - public class CompositeKernel : KernelBase + public class CompositeKernel : KernelBase, IEnumerable { - private readonly IReadOnlyList _kernels; + private readonly List _kernels = new List(); - public CompositeKernel(IReadOnlyList kernels) + public CompositeKernel() { - _kernels = kernels ?? throw new ArgumentNullException(nameof(kernels)); - Pipeline.AddMiddleware(ChooseKernel); + } + + public void Add(IKernel kernel) + { + if (kernel == null) + { + throw new ArgumentNullException(nameof(kernel)); + } + + _kernels.Add(kernel); - AddDisposable( - kernels.Select(k => k.KernelEvents) - .Merge() - .Subscribe(PublishEvent)); + AddDisposable(kernel.KernelEvents.Subscribe(PublishEvent)); } private Task ChooseKernel( - IKernelCommand command, - KernelPipelineContext context, + IKernelCommand command, + KernelPipelineContext context, KernelPipelineContinuation next) { if (context.Kernel == null) @@ -55,5 +60,9 @@ protected internal override async Task HandleAsync( throw new NoSuitableKernelException(); } + + public IEnumerator GetEnumerator() => _kernels.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } \ No newline at end of file From 0efef9070da835d4e1a3cac9795ef8da6a8471d6 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 11 Jul 2019 14:37:33 -0700 Subject: [PATCH 4/4] kernel chooser directive --- .../ExecuteRequestHandlerTests.cs | 17 +++ .../ExecuteRequestHandler.cs | 4 - .../Kernel/CSharpReplTests.cs | 36 ++++- .../Kernel/CodeSubmissionProcessorTests.cs | 131 ------------------ .../Kernel/CompositeKernelTests.cs | 53 ++++++- WorkspaceServer/Kernel/AddNuGetPackage.cs | 17 --- WorkspaceServer/Kernel/CSharpRepl.cs | 28 ++-- .../Kernel/CSharpReplExtensions.cs | 52 ++----- .../CodeSubmissionProcessorException.cs | 17 --- .../Kernel/CodeSubmissionProcessors.cs | 64 --------- WorkspaceServer/Kernel/CompositeKernel.cs | 31 ++++- WorkspaceServer/Kernel/EmitProcessors.cs | 64 --------- .../Kernel/ICodeSubmissionPreProcessor.cs | 14 -- WorkspaceServer/Kernel/IKernel.cs | 2 + WorkspaceServer/Kernel/KernelBase.cs | 108 +++++++++++++-- .../Kernel/KernelInvocationContext.cs | 7 +- .../Kernel/KernelPipelineContext.cs | 11 +- WorkspaceServer/Kernel/NuGetPackageAdded.cs | 7 +- WorkspaceServer/Kernel/SubmitCode.cs | 13 +- 19 files changed, 267 insertions(+), 409 deletions(-) delete mode 100644 WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs delete mode 100644 WorkspaceServer/Kernel/AddNuGetPackage.cs delete mode 100644 WorkspaceServer/Kernel/CodeSubmissionProcessorException.cs delete mode 100644 WorkspaceServer/Kernel/CodeSubmissionProcessors.cs delete mode 100644 WorkspaceServer/Kernel/EmitProcessors.cs delete mode 100644 WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs diff --git a/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs b/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs index 2011c34b8..5151a163b 100644 --- a/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs +++ b/Microsoft.DotNet.Try.Jupyter.Tests/ExecuteRequestHandlerTests.cs @@ -99,5 +99,22 @@ public async Task sends_ExecuteReply_message_on_ValueProduced() .Should().Contain(message => message.Contains(MessageTypeValues.ExecuteResult)); } + + [Fact] + public async Task sends_ExecuteReply_message_when_submission_contains_only_a_directive() + { + var kernel = new CompositeKernel + { + new CSharpRepl() + }; + + var handler = new ExecuteRequestHandler(kernel); + var request = Message.Create(new ExecuteRequest("#kernel csharp"), null); + await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + _serverRecordingSocket.DecodedMessages + .Should().Contain(message => + message.Contains(MessageTypeValues.ExecuteReply)); + } } } diff --git a/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs b/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs index b04dcb0d7..a86dc20b0 100644 --- a/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs +++ b/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs @@ -20,7 +20,6 @@ public class ExecuteRequestHandler : IDisposable private readonly RenderingEngine _renderingEngine; private readonly ConcurrentDictionary _openRequests = new ConcurrentDictionary(); private int _executionCount; - private readonly CodeSubmissionProcessors _processors; private readonly CompositeDisposable _disposables = new CompositeDisposable(); private class OpenRequest : IDisposable @@ -34,7 +33,6 @@ private class OpenRequest : IDisposable public OpenRequest(JupyterRequestContext context, ExecuteRequest executeRequest, int executionCount, Guid id, Dictionary transient) { - Context = context; ExecuteRequest = executeRequest; ExecutionCount = executionCount; @@ -61,7 +59,6 @@ public ExecuteRequestHandler(IKernel kernel) _renderingEngine.RegisterRenderer(typeof(IDictionary), new DictionaryRenderer()); _renderingEngine.RegisterRenderer(typeof(IList), new ListRenderer()); _renderingEngine.RegisterRenderer(typeof(IEnumerable), new SequenceRenderer()); - _processors = new CodeSubmissionProcessors(); } public async Task Handle(JupyterRequestContext context) @@ -73,7 +70,6 @@ public async Task Handle(JupyterRequestContext context) try { var command = new SubmitCode(executeRequest.Code, "csharp"); - command = await _processors.ProcessAsync(command); var id = Guid.NewGuid(); diff --git a/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs b/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs index 1efb64528..14774d90b 100644 --- a/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs +++ b/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs @@ -2,9 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.IO; using FluentAssertions; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json; +using Recipes; using WorkspaceServer.Kernel; using Xunit; @@ -118,9 +121,6 @@ public async Task it_returns_the_result_of_a_null_expression() KernelEvents.OfType() .Last() - .Should() - .BeOfType() - .Which .Value .Should() .BeNull(); @@ -149,12 +149,36 @@ public async Task it_aggregates_multiple_submissions() KernelEvents.OfType() .Last() - .Should() - .BeOfType() - .Which .Value .Should() .Be(3); } + + [Fact] + public async Task it_can_load_assembly_references_using_r_directive() + { + var kernel = await CreateKernelAsync(); + + var dll = new FileInfo(typeof(JsonConvert).Assembly.Location).FullName; + + await kernel.SendAsync( + new SubmitCode($"#r \"{dll}\"")); + await kernel.SendAsync( + new SubmitCode(@" +using Newtonsoft.Json; + +var json = JsonConvert.SerializeObject(new { value = ""hello"" }); + +json +")); + + KernelEvents.Should() + .ContainSingle(e => e is ValueProduced); + KernelEvents.OfType() + .Single() + .Value + .Should() + .Be(new { value = "hello" }.ToJson()); + } } } \ No newline at end of file diff --git a/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs b/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs deleted file mode 100644 index 408b2177d..000000000 --- a/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.Threading.Tasks; -using FluentAssertions; -using WorkspaceServer.Kernel; -using Xunit; - -namespace WorkspaceServer.Tests.Kernel -{ - public class CodeSubmissionProcessorTests - { - private readonly CodeSubmissionProcessors _processors; - - public CodeSubmissionProcessorTests() - { - _processors = new CodeSubmissionProcessors(); - } - [Fact] - public void can_register_processorHandlers() - { - var action = new Action(() => _processors.Add(new ReplaceAllProcessor())); - action.Should().NotThrow(); - _processors.ProcessorsCount.Should().BeGreaterThan(0); - } - - [Fact] - public async Task processing_code_submission_removes_processors() - { - _processors.Add(new PassThroughProcessor()); - var submission = new SubmitCode("#pass\nthis should remain"); - submission = await _processors.ProcessAsync(submission); - submission.Code.Should().NotContain("#pass") - .And.Contain("this should remain"); - } - - [Fact] - public async Task processing_code_submission_leaves_unprocessed_directives() - { - _processors.Add(new PassThroughProcessor()); - var submission = new SubmitCode("#pass\n#region code\nthis should remain\n#endregion"); - submission = await _processors.ProcessAsync(submission); - submission.Code.Should().NotContain("#pass") - .And.Match("*#region code\nthis should remain\n#endregion*"); - } - - - [Fact] - public async Task processing_code_submission_respect_directive_order() - { - _processors.Add(new AppendProcessor()); - var submission = new SubmitCode("#append --value PART1\n#append --value PART2\n#region code\nthis should remain\n#endregion"); - submission = await _processors.ProcessAsync(submission); - submission.Code.Should().NotContain("#pass") - .And.Match("*#region code\nthis should remain\n#endregion\nPART1\nPART2*"); - } - - private class ReplaceAllProcessor : ICodeSubmissionProcessor - { - public ReplaceAllProcessor() - { - Command = new Command("#replace", "replace submission with empty string"); - } - - public Command Command { get; } - - public Task ProcessAsync(SubmitCode codeSubmission) - { - codeSubmission.Code = string.Empty; - return Task.FromResult(codeSubmission); - } - } - - private class PassThroughProcessor : ICodeSubmissionProcessor - { - public PassThroughProcessor() - { - Command = new Command("#pass", "pass all code"); - } - - public Command Command { get; } - - public Task ProcessAsync(SubmitCode codeSubmission) - { - return Task.FromResult(codeSubmission); - } - } - - - private class AppendProcessor : ICodeSubmissionProcessor - { - private string _valueToAppend; - - private class AppendProcessorOptions - { - public string Value { get; } - - public AppendProcessorOptions(string value) - { - Value = value; - } - } - - public AppendProcessor() - { - Command = new Command("#append"); - var valueOption = new Option("--value") - { - Argument = new Argument() - }; - Command.AddOption(valueOption); - - Command.Handler = CommandHandler.Create((options) => - { - _valueToAppend = options.Value; - }); - } - - public Command Command { get; } - - public Task ProcessAsync(SubmitCode codeSubmission) - { - codeSubmission.Code = codeSubmission.Code + $"\n{_valueToAppend}"; - return Task.FromResult(codeSubmission); - } - } - } -} diff --git a/WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs b/WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs index b4545a87c..6a19bcb6c 100644 --- a/WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs +++ b/WorkspaceServer.Tests/Kernel/CompositeKernelTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reactive.Linq; using FluentAssertions; using System.Linq; @@ -69,11 +70,57 @@ public async Task When_SubmitCode_command_adds_packages_to_csharp_kernel_then_Pa events.OfType() .Single() - .Command - .As() - .NugetReference + .PackageReference .Should() .BeEquivalentTo(new NugetPackageReference("PocketLogger", "1.2.3")); } + + [Fact] + public async Task Kernel_can_be_chosen_by_specifying_kernel_name() + { + var receivedOnFakeRepl = new List(); + + var kernel = new CompositeKernel + { + new CSharpRepl(), + new FakeRepl("fake") + { + Handle = (command, context) => + { + receivedOnFakeRepl.Add(command); + return Task.CompletedTask; + } + } + }; + + await kernel.SendAsync(new SubmitCode("#kernel csharp")); + await kernel.SendAsync(new SubmitCode("var x = 123;")); + await kernel.SendAsync(new SubmitCode("#kernel fake")); + await kernel.SendAsync(new SubmitCode("hello!")); + await kernel.SendAsync(new SubmitCode("#kernel csharp")); + await kernel.SendAsync(new SubmitCode("x")); + + receivedOnFakeRepl.Should() + .BeEquivalentTo(new SubmitCode("hello!")); + } + } + + public class FakeRepl : KernelBase + { + private readonly string _name; + + public FakeRepl(string name) + { + _name = name; + } + + public override string Name => _name; + + public Func Handle { get; set; } + + protected override Task HandleAsync(IKernelCommand command, KernelPipelineContext context) + { + return Handle(command, context); + } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/AddNuGetPackage.cs b/WorkspaceServer/Kernel/AddNuGetPackage.cs deleted file mode 100644 index f489323e6..000000000 --- a/WorkspaceServer/Kernel/AddNuGetPackage.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; - -namespace WorkspaceServer.Kernel -{ - public class AddNuGetPackage : KernelCommandBase - { - public AddNuGetPackage(NugetPackageReference nugetReference) - { - NugetReference = nugetReference ?? throw new ArgumentNullException(nameof(nugetReference)); - } - - public NugetPackageReference NugetReference { get; } - } -} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CSharpRepl.cs b/WorkspaceServer/Kernel/CSharpRepl.cs index 5f2617941..d3edd31cc 100644 --- a/WorkspaceServer/Kernel/CSharpRepl.cs +++ b/WorkspaceServer/Kernel/CSharpRepl.cs @@ -16,6 +16,8 @@ namespace WorkspaceServer.Kernel { public class CSharpRepl : KernelBase { + internal const string KernelName = "csharp"; + private static readonly MethodInfo _hasReturnValueMethod = typeof(Script) .GetMethod("HasReturnValue", BindingFlags.Instance | BindingFlags.NonPublic); @@ -31,6 +33,8 @@ public CSharpRepl() SetupScriptOptions(); } + public override string Name => KernelName; + private void SetupScriptOptions() { ScriptOptions = ScriptOptions.Default @@ -70,13 +74,10 @@ protected internal override async Task HandleAsync( switch (command) { case SubmitCode submitCode: - if (submitCode.Language == "csharp") + context.OnExecute(async invocationContext => { - context.OnExecute(async invocationContext => - { - await HandleSubmitCode(submitCode, invocationContext); - }); - } + await HandleSubmitCode(submitCode, invocationContext); + }); break; } @@ -103,8 +104,7 @@ private async Task HandleSubmitCode( { _scriptState = await CSharpScript.RunAsync( code, - ScriptOptions, - cancellationToken: context.CancellationToken); + ScriptOptions); } else { @@ -115,8 +115,7 @@ private async Task HandleSubmitCode( { exception = e; return true; - }, - context.CancellationToken); + }); } } catch (Exception e) @@ -124,10 +123,7 @@ private async Task HandleSubmitCode( exception = e; } - var hasReturnValue = _scriptState != null && - (bool) _hasReturnValueMethod.Invoke(_scriptState.Script, null); - - if (hasReturnValue) + if (HasReturnValue) { context.OnNext(new ValueProduced(_scriptState.ReturnValue, codeSubmission)); } @@ -152,5 +148,9 @@ private async Task HandleSubmitCode( context.OnCompleted(); } } + + private bool HasReturnValue => + _scriptState != null && + (bool) _hasReturnValueMethod.Invoke(_scriptState.Script, null); } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CSharpReplExtensions.cs b/WorkspaceServer/Kernel/CSharpReplExtensions.cs index 28310b420..30ea82c6a 100644 --- a/WorkspaceServer/Kernel/CSharpReplExtensions.cs +++ b/WorkspaceServer/Kernel/CSharpReplExtensions.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.CommandLine; +using System.CommandLine.Invocation; namespace WorkspaceServer.Kernel { @@ -17,56 +17,22 @@ public static CSharpRepl UseNugetDirective(this CSharpRepl repl) Name = "package" }; - var parser = new Command("#r") + var r = new Command("#r") { packageRefArg }; - repl.Pipeline.AddMiddleware(async (command, pipelineContext, next) => + r.Handler = CommandHandler.Create(async (package, pipelineContext) => { - switch (command) + pipelineContext.OnExecute(async invocationContext => { - case SubmitCode submitCode: - - var lines = new Queue( - submitCode.Code.Split(new[] { "\r\n", "\n" }, - StringSplitOptions.None)); - - var unhandledLines = new List(); - - while (lines.Count > 0) - { - var currentLine = lines.Dequeue(); - var parseResult = parser.Parse(currentLine); - - if (parseResult.Errors.Count == 0) - { - var nugetReference = - parseResult.FindResultFor(packageRefArg) - .GetValueOrDefault(); - - pipelineContext.OnExecute(async invocationContext => - { - var addNuGetPackage = new AddNuGetPackage(nugetReference); - - invocationContext.OnNext(new NuGetPackageAdded(addNuGetPackage)); - invocationContext.OnCompleted(); - }); - } - else - { - unhandledLines.Add(currentLine); - } - } - - submitCode.Code = string.Join("\n", unhandledLines); - - break; - } - - await next(command, pipelineContext); + invocationContext.OnNext(new NuGetPackageAdded(package)); + invocationContext.OnCompleted(); + }); }); + repl.AddDirective(r); + return repl; } } diff --git a/WorkspaceServer/Kernel/CodeSubmissionProcessorException.cs b/WorkspaceServer/Kernel/CodeSubmissionProcessorException.cs deleted file mode 100644 index e2594cabf..000000000 --- a/WorkspaceServer/Kernel/CodeSubmissionProcessorException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; - -namespace WorkspaceServer.Kernel -{ - public class CodeSubmissionProcessorException : Exception - { - public SubmitCode CodeSubmission { get; } - - public CodeSubmissionProcessorException(Exception exception, SubmitCode codeSubmission) : base("CodeSubmission processing failed", exception) - { - CodeSubmission = codeSubmission; - } - } -} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs b/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs deleted file mode 100644 index e6bf4f90a..000000000 --- a/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. 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.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; -using System.Threading.Tasks; - -namespace WorkspaceServer.Kernel -{ - public class CodeSubmissionProcessors - { - private readonly RootCommand _rootCommand; - private readonly Dictionary _processors = new Dictionary(); - private Parser _parser; - - public int ProcessorsCount => _processors.Count; - - public CodeSubmissionProcessors() - { - _rootCommand = new RootCommand(); - _parser = new CommandLineBuilder(_rootCommand).Build(); - } - - public void Add(ICodeSubmissionProcessor processor) - { - _processors.Add(processor.Command, processor); - _rootCommand.AddCommand(processor.Command); - _parser = new CommandLineBuilder(_rootCommand).Build(); - } - - public async Task ProcessAsync(SubmitCode submitCode) - { - var lines = new Queue(submitCode.Code.Split(new[] { "\r\n", "\n" }, - StringSplitOptions.None)); - var unhandledLines = new Queue(); - while (lines.Count > 0) - { - var currentLine = lines.Dequeue(); - var result = _parser.Parse(currentLine); - - if (result.CommandResult != null && - _processors.TryGetValue(result.CommandResult.Command, out var processor)) - { - await _parser.InvokeAsync(result); - submitCode.Code = string.Join("\n", lines); - var newSubmission = await processor.ProcessAsync(submitCode); - lines = new Queue(newSubmission.Code.Split(new[] { "\r\n", "\n" }, - StringSplitOptions.None)); - } - else - { - unhandledLines.Enqueue(currentLine); - } - } - - submitCode.Code = string.Join("\n", unhandledLines); - - return submitCode; - } - } -} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CompositeKernel.cs b/WorkspaceServer/Kernel/CompositeKernel.cs index 17ebacf22..e1c5e2c26 100644 --- a/WorkspaceServer/Kernel/CompositeKernel.cs +++ b/WorkspaceServer/Kernel/CompositeKernel.cs @@ -4,6 +4,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; using System.Linq; using System.Threading.Tasks; @@ -12,12 +15,32 @@ namespace WorkspaceServer.Kernel public class CompositeKernel : KernelBase, IEnumerable { private readonly List _kernels = new List(); + private readonly Argument _kernelNameArgument; public CompositeKernel() { Pipeline.AddMiddleware(ChooseKernel); + + _kernelNameArgument = new Argument("kernelName"); + + var chooseKernelCommand = new Command("#kernel") + { + _kernelNameArgument + }; + + chooseKernelCommand.Handler = + CommandHandler.Create((kernelName, context) => + { + DefaultKernel = this.Single(k => k.Name == kernelName); + }); + + AddDirective(chooseKernelCommand); } + public IKernel DefaultKernel { get; set; } + + public override string Name => nameof(CompositeKernel); + public void Add(IKernel kernel) { if (kernel == null) @@ -27,6 +50,8 @@ public void Add(IKernel kernel) _kernels.Add(kernel); + _kernelNameArgument.FromAmong(kernel.Name); + AddDisposable(kernel.KernelEvents.Subscribe(PublishEvent)); } @@ -37,7 +62,11 @@ private Task ChooseKernel( { if (context.Kernel == null) { - if (_kernels.Count == 1) + if (DefaultKernel != null) + { + context.Kernel = DefaultKernel; + } + else if (_kernels.Count == 1) { context.Kernel = _kernels[0]; } diff --git a/WorkspaceServer/Kernel/EmitProcessors.cs b/WorkspaceServer/Kernel/EmitProcessors.cs deleted file mode 100644 index 8cd13830f..000000000 --- a/WorkspaceServer/Kernel/EmitProcessors.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.IO; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Scripting; - -namespace WorkspaceServer.Kernel -{ - public class EmitProcessors : ICodeSubmissionProcessor - { - private readonly Func _getScriptState; - - public EmitProcessors(Func getScriptState) - { - _getScriptState = getScriptState; - Command = new Command("emit"); - var outputOption = new Option("--output") - { - Argument = new Argument() - }; - - Command.AddOption(outputOption); - Command.Handler = CommandHandler.Create((options) => - { - if (!options.Output.Exists) - { - options.Output.Create(); - } - - var state = getScriptState(); - - var codeFile = new FileInfo(Path.Combine(options.Output.FullName, "code.cs")); - - using (var destination = codeFile.OpenWrite()) - using (var textWriter = new StreamWriter(destination)) - { - var source = state?.Script?.Code ?? string.Empty; - textWriter.Write($"// generated code\n{source}"); - } - - }); - } - public Task ProcessAsync(SubmitCode codeSubmission) - { - return Task.FromResult(codeSubmission); - } - - public Command Command { get; } - - private class EmitProcessorsOptions - { - public DirectoryInfo Output { get; } - - public EmitProcessorsOptions(DirectoryInfo output) - { - Output = output; - } - } - } -} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs b/WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs deleted file mode 100644 index bb7b1e4f1..000000000 --- a/WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.CommandLine; -using System.Threading.Tasks; - -namespace WorkspaceServer.Kernel -{ - public interface ICodeSubmissionProcessor - { - Task ProcessAsync(SubmitCode codeSubmission); - Command Command { get; } - } -} diff --git a/WorkspaceServer/Kernel/IKernel.cs b/WorkspaceServer/Kernel/IKernel.cs index 6e69e3d52..a9a6fc4c8 100644 --- a/WorkspaceServer/Kernel/IKernel.cs +++ b/WorkspaceServer/Kernel/IKernel.cs @@ -9,6 +9,8 @@ namespace WorkspaceServer.Kernel { public interface IKernel : IDisposable { + string Name { get; } + IObservable KernelEvents { get; } Task SendAsync(IKernelCommand command, CancellationToken cancellationToken); diff --git a/WorkspaceServer/Kernel/KernelBase.cs b/WorkspaceServer/Kernel/KernelBase.cs index 23c5bf41b..c450832c1 100644 --- a/WorkspaceServer/Kernel/KernelBase.cs +++ b/WorkspaceServer/Kernel/KernelBase.cs @@ -2,6 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; using System.Reactive.Disposables; using System.Reactive.Subjects; using System.Threading; @@ -15,12 +19,100 @@ public abstract class KernelBase : IKernel private readonly Subject _channel = new Subject(); private readonly CompositeDisposable _disposables; - public IObservable KernelEvents => _channel; + private readonly List _directiveCommands = new List(); protected KernelBase() { - Pipeline = new KernelCommandPipeline(this); _disposables = new CompositeDisposable(); + + Pipeline = new KernelCommandPipeline(this); + + Pipeline.AddMiddleware(async (command, pipelineContext, next) => + { + switch (command) + { + case SubmitCode submitCode: + + var modified = false; + + var directiveParser = BuildDirectiveParser(pipelineContext); + + var lines = new Queue( + submitCode.Code.Split(new[] { "\r\n", "\n" }, + StringSplitOptions.None)); + + var unhandledLines = new List(); + + while (lines.Count > 0) + { + var currentLine = lines.Dequeue(); + + var parseResult = directiveParser.Parse(currentLine); + + if (parseResult.Errors.Count == 0) + { + modified = true; + await directiveParser.InvokeAsync(parseResult); + } + else + { + unhandledLines.Add(currentLine); + } + } + + var code = string.Join("\n", unhandledLines); + + if (modified) + { + if (string.IsNullOrWhiteSpace(code)) + { + pipelineContext.OnExecute(context => + { + context.OnNext(new CodeSubmissionEvaluated(submitCode)); + context.OnCompleted(); + return Task.CompletedTask; + }); + + return; + } + else + { + submitCode.Code = code; + } + } + + break; + } + + await next(command, pipelineContext); + }); + } + + protected Parser BuildDirectiveParser(KernelPipelineContext pipelineContext) + { + var root = new RootCommand(); + + foreach (var c in _directiveCommands) + { + root.Add(c); + } + + return new CommandLineBuilder(root) + .UseMiddleware( + context => context.BindingContext + .AddService( + typeof(KernelPipelineContext), + () => pipelineContext)) + .Build(); + } + + public IObservable KernelEvents => _channel; + + public abstract string Name { get; } + + public void AddDirective(Command command) + { + _directiveCommands.Add(command); } public async Task SendAsync( @@ -32,17 +124,17 @@ public async Task SendAsync( throw new ArgumentNullException(nameof(command)); } - var pipelineContext = new KernelPipelineContext( - PublishEvent, - cancellationToken); + var pipelineContext = new KernelPipelineContext(PublishEvent); await SendOnContextAsync(command, pipelineContext); - return await pipelineContext.InvokeAsync(); + var result = await pipelineContext.InvokeAsync(); + + return result; } public async Task SendOnContextAsync( - IKernelCommand command, + IKernelCommand command, KernelPipelineContext invocationContext) { await Pipeline.InvokeAsync(command, invocationContext); @@ -69,7 +161,7 @@ protected void AddDisposable(IDisposable disposable) } protected internal abstract Task HandleAsync( - IKernelCommand command, + IKernelCommand command, KernelPipelineContext context); public void Dispose() => _disposables.Dispose(); diff --git a/WorkspaceServer/Kernel/KernelInvocationContext.cs b/WorkspaceServer/Kernel/KernelInvocationContext.cs index e6ad79fad..bc775c9f8 100644 --- a/WorkspaceServer/Kernel/KernelInvocationContext.cs +++ b/WorkspaceServer/Kernel/KernelInvocationContext.cs @@ -3,7 +3,6 @@ using System; using System.Reactive.Subjects; -using System.Threading; using System.Threading.Tasks; namespace WorkspaceServer.Kernel @@ -16,16 +15,12 @@ public class KernelInvocationContext : IObserver public KernelInvocationContext( KernelCommandInvocation invocation, - Action publishEvent, - CancellationToken cancellationToken) + Action publishEvent) { _invocation = invocation; _publishEvent = publishEvent; - CancellationToken = cancellationToken; } - public CancellationToken CancellationToken { get; } - public void OnCompleted() { _events.OnCompleted(); diff --git a/WorkspaceServer/Kernel/KernelPipelineContext.cs b/WorkspaceServer/Kernel/KernelPipelineContext.cs index ab508a47b..d08a1aa90 100644 --- a/WorkspaceServer/Kernel/KernelPipelineContext.cs +++ b/WorkspaceServer/Kernel/KernelPipelineContext.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Reactive.Linq; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace WorkspaceServer.Kernel @@ -15,16 +14,11 @@ public class KernelPipelineContext private readonly Action _publishEvent; private readonly List _invocations = new List(); - public KernelPipelineContext( - Action publishEvent, - CancellationToken cancellationToken) + public KernelPipelineContext(Action publishEvent) { _publishEvent = publishEvent; - CancellationToken = cancellationToken; } - public CancellationToken CancellationToken { get; } - internal IKernel Kernel { get; set; } public void OnExecute(KernelCommandInvocation invocation) @@ -36,8 +30,7 @@ public void OnExecute(KernelCommandInvocation invocation) _invocations.Add(new KernelInvocationContext( invocation, - _publishEvent, - CancellationToken)); + _publishEvent)); } internal async Task InvokeAsync() diff --git a/WorkspaceServer/Kernel/NuGetPackageAdded.cs b/WorkspaceServer/Kernel/NuGetPackageAdded.cs index b56760783..1b2dea7f0 100644 --- a/WorkspaceServer/Kernel/NuGetPackageAdded.cs +++ b/WorkspaceServer/Kernel/NuGetPackageAdded.cs @@ -1,14 +1,15 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; - namespace WorkspaceServer.Kernel { public class NuGetPackageAdded : KernelEventBase { - public NuGetPackageAdded(IKernelCommand command) : base(command) + public NuGetPackageAdded(NugetPackageReference packageReference) { + PackageReference = packageReference; } + + public NugetPackageReference PackageReference { get; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/SubmitCode.cs b/WorkspaceServer/Kernel/SubmitCode.cs index 186d219e8..2dc88a2d8 100644 --- a/WorkspaceServer/Kernel/SubmitCode.cs +++ b/WorkspaceServer/Kernel/SubmitCode.cs @@ -7,13 +7,16 @@ namespace WorkspaceServer.Kernel { public class SubmitCode : KernelCommandBase { - public string Code { get; set; } - public string Language { get; set; } - - public SubmitCode(string code, string language = null) + public SubmitCode( + string code, + string targetKernelName = null) { Code = code ?? throw new ArgumentNullException(nameof(code)); - Language = language; + TargetKernelName = targetKernelName; } + + public string Code { get; set; } + + public string TargetKernelName { get; set; } } } \ No newline at end of file