diff --git a/MLS.Agent/Properties/launchSettings.json b/MLS.Agent/Properties/launchSettings.json index 3135a219e..69def8824 100644 --- a/MLS.Agent/Properties/launchSettings.json +++ b/MLS.Agent/Properties/launchSettings.json @@ -31,6 +31,10 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "Jupyter": { + "commandName": "Project", + "commandLineArgs": "jupyter ../MLS.Agent.Tests/kernel_connection_file.json" } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Try.Jupyter.Tests/RenderingTests.cs b/Microsoft.DotNet.Try.Jupyter.Tests/RenderingTests.cs index 802a6b70c..3129e633f 100644 --- a/Microsoft.DotNet.Try.Jupyter.Tests/RenderingTests.cs +++ b/Microsoft.DotNet.Try.Jupyter.Tests/RenderingTests.cs @@ -23,13 +23,21 @@ private struct PriorityElement public RenderingTests() { - _engine = new RenderingEngine(new DefaultRenderer()); + _engine = new RenderingEngine(new DefaultRenderer(), new PlainTextRendering("")); _engine.RegisterRenderer(new DefaultRenderer()); _engine.RegisterRenderer(typeof(IDictionary), new DictionaryRenderer()); _engine.RegisterRenderer(typeof(IList), new ListRenderer()); _engine.RegisterRenderer(typeof(IEnumerable), new SequenceRenderer()); } + [Fact] + public void renders_null() + { + var rendering = _engine.Render(null); + rendering.Mime.Should().Be("text/plain"); + rendering.Content.Should().Be(""); + } + [Fact] public void objects_are_rendered_as_table() { diff --git a/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs b/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs index 1eacd097f..ee46fb38b 100644 --- a/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs +++ b/Microsoft.DotNet.Try.Jupyter/ExecuteRequestHandler.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.Try.Jupyter.Protocol; @@ -42,8 +43,7 @@ public OpenRequest(JupyterRequestContext context, ExecuteRequest executeRequest, public ExecuteRequestHandler(IKernel kernel) { _kernel = kernel; - _renderingEngine = new RenderingEngine(new DefaultRenderer()); - _renderingEngine = new RenderingEngine(new DefaultRenderer()); + _renderingEngine = new RenderingEngine(new DefaultRenderer(), new PlainTextRendering("")); _renderingEngine.RegisterRenderer(new DefaultRenderer()); _renderingEngine.RegisterRenderer(typeof(IDictionary), new DictionaryRenderer()); _renderingEngine.RegisterRenderer(typeof(IList), new ListRenderer()); @@ -96,13 +96,13 @@ void IObserver.OnNext(IKernelEvent value) } } - private void OnCodeSubmissionEvaluatedFailed(CodeSubmissionEvaluationFailed codeSubmissionEvaluationFailed, ConcurrentDictionary openRequests) + private static void OnCodeSubmissionEvaluatedFailed(CodeSubmissionEvaluationFailed codeSubmissionEvaluationFailed, ConcurrentDictionary openRequests) { var openRequest = openRequests[codeSubmissionEvaluationFailed.ParentId]; var errorContent = new Error( eName: "Unhandled Exception", - eValue: $"{codeSubmissionEvaluationFailed.Error}" + eValue: $"{codeSubmissionEvaluationFailed.Message}" ); if (!openRequest.ExecuteRequest.Silent) @@ -122,7 +122,7 @@ private void OnCodeSubmissionEvaluatedFailed(CodeSubmissionEvaluationFailed code } // reply Error - var executeReplyPayload = new ExecuteReplyError(errorContent, executionCount: _executionCount); + var executeReplyPayload = new ExecuteReplyError(errorContent, executionCount: openRequest.ExecutionCount); // send to server var executeReply = Message.CreateResponse( diff --git a/Microsoft.DotNet.Try.Jupyter/Rendering/DefaultRenderer.cs b/Microsoft.DotNet.Try.Jupyter/Rendering/DefaultRenderer.cs index 15e90d3dc..f6aad9d9a 100644 --- a/Microsoft.DotNet.Try.Jupyter/Rendering/DefaultRenderer.cs +++ b/Microsoft.DotNet.Try.Jupyter/Rendering/DefaultRenderer.cs @@ -28,19 +28,24 @@ public IRendering Render(object source, IRenderingEngine engine = null) public IRendering RenderObject(object source, IRenderingEngine engine = null) { - - var rows = CreateRows(source, engine); - var table = $@" + try + { + var rows = CreateRows(source, engine); + var table = $@"
{rows}
"; - return new HtmlRendering(table); + return new HtmlRendering(table); + } + catch (Exception) + { + return new PlainTextRendering(source?.ToString()); + } } private string CreateRows(object source, IRenderingEngine engine) { - - var props = source.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); + var props = source.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); var rows = new StringBuilder(); foreach (var propertyInfo in props) { @@ -51,7 +56,7 @@ private string CreateRows(object source, IRenderingEngine engine) rows.AppendLine(row); } - var fields = source.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); + var fields = source.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (var fieldInfo in fields) { diff --git a/Microsoft.DotNet.Try.Jupyter/Rendering/RendererUtilities.cs b/Microsoft.DotNet.Try.Jupyter/Rendering/RendererUtilities.cs index f19012385..a314fe6cd 100644 --- a/Microsoft.DotNet.Try.Jupyter/Rendering/RendererUtilities.cs +++ b/Microsoft.DotNet.Try.Jupyter/Rendering/RendererUtilities.cs @@ -46,8 +46,8 @@ public static IEnumerable GetAccessors(Type sourceType) { if (IsStructured(sourceType)) { - var props = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).OfType(); - var fields = sourceType.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).OfType(); + var props = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OfType(); + var fields = sourceType.GetFields(BindingFlags.Public | BindingFlags.Instance).OfType(); return props.Concat(fields); } diff --git a/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs b/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs index 17ee21358..ace30d0ed 100644 --- a/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs +++ b/WorkspaceServer.Tests/Kernel/CSharpReplTests.cs @@ -48,7 +48,8 @@ public async Task it_returns_exceptions_thrown_in_user_code() { var repl = await CreateKernelAsync(); - await repl.SendAsync(new SubmitCode("throw new System.NotImplementedException();")); + await repl.SendAsync(new SubmitCode("using System;")); + await repl.SendAsync(new SubmitCode("throw new NotImplementedException();")); KernelEvents.Last() .Should() @@ -59,6 +60,23 @@ public async Task it_returns_exceptions_thrown_in_user_code() .BeOfType(); } + [Fact] + public async Task it_returns_diagnostics() + { + var repl = await CreateKernelAsync(); + + await repl.SendAsync(new SubmitCode("using System;")); + await repl.SendAsync(new SubmitCode("aaaadd")); + + KernelEvents.Last() + .Should() + .BeOfType() + .Which + .Message + .Should() + .Be("(1,1): error CS0103: The name 'aaaadd' does not exist in the current context"); + } + [Fact] public async Task it_notifies_when_submission_is_complete() { @@ -125,8 +143,9 @@ public async Task it_aggregates_multiple_submissions() { var repl = await CreateKernelAsync(); - await repl.SendAsync(new SubmitCode("var x = 123;")); - await repl.SendAsync(new SubmitCode("x")); + await repl.SendAsync(new SubmitCode("var x = new List{1,2};")); + await repl.SendAsync(new SubmitCode("x.Add(3);")); + await repl.SendAsync(new SubmitCode("x.Max()")); KernelEvents.OfType() .Last() @@ -135,7 +154,7 @@ public async Task it_aggregates_multiple_submissions() .Which .Value .Should() - .Be(123); + .Be(3); } } } \ No newline at end of file diff --git a/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs b/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs new file mode 100644 index 000000000..91b7fa5bc --- /dev/null +++ b/WorkspaceServer.Tests/Kernel/CodeSubmissionProcessorTests.cs @@ -0,0 +1,129 @@ +// 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.Register(new ReplaceAllProcessor())); + action.Should().NotThrow(); + _processors.ProcessorsCount.Should().BeGreaterThan(0); + } + + [Fact] + public async Task processing_code_submission_removes_processors() + { + _processors.Register(new PassThroughAllProcessor()); + var submission = new SubmitCode("#pass\nthis should remain"); + submission = await _processors.ProcessAsync(submission); + submission.Value.Should().NotContain("#pass") + .And.Contain("this should remain"); + } + + [Fact] + public async Task processing_code_submission_leaves_unprocessed_directives() + { + _processors.Register(new PassThroughAllProcessor()); + var submission = new SubmitCode("#pass\n#region code\nthis should remain\n#endregion"); + submission = await _processors.ProcessAsync(submission); + submission.Value.Should().NotContain("#pass") + .And.Match("*#region code\nthis should remain\n#endregion*"); + } + + + [Fact] + public async Task processing_code_submission_respect_directive_order() + { + _processors.Register(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.Value.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) + { + return Task.FromResult(new SubmitCode(string.Empty, codeSubmission.Id, codeSubmission.ParentId)); + } + } + + private class PassThroughAllProcessor : ICodeSubmissionProcessor + { + public PassThroughAllProcessor() + { + 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) + { + return Task.FromResult(new SubmitCode(codeSubmission.Value + $"\n{_valueToAppend}" , codeSubmission.Id, codeSubmission.ParentId)); + } + } + } +} diff --git a/WorkspaceServer/Kernel/CSharpRepl.cs b/WorkspaceServer/Kernel/CSharpRepl.cs index fd992951e..e11cce6f7 100644 --- a/WorkspaceServer/Kernel/CSharpRepl.cs +++ b/WorkspaceServer/Kernel/CSharpRepl.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.Collections.Generic; +using System.Linq; using System.Reactive.Subjects; using System.Reflection; using System.Text; @@ -23,6 +25,8 @@ public class CSharpRepl : IKernel private ScriptState _scriptState; protected CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Latest, kind: SourceCodeKind.Script); + protected ScriptOptions ScriptOptions; + protected StringBuilder _inputBuffer = new StringBuilder(); public IObservable KernelEvents => _channel; @@ -30,6 +34,23 @@ public class CSharpRepl : IKernel public CSharpRepl() { _channel = new Subject(); + SetupScriptOptions(); + } + + private void SetupScriptOptions() + { + ScriptOptions = ScriptOptions.Default + .AddImports( + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic", + "System.Threading.Tasks", + "System.Linq") + .AddReferences( + typeof(Enumerable).GetTypeInfo().Assembly, + typeof(IEnumerable<>).GetTypeInfo().Assembly, + typeof(Task<>).GetTypeInfo().Assembly); } public async Task SendAsync(SubmitCode submitCode, CancellationToken cancellationToken) @@ -46,11 +67,22 @@ public async Task SendAsync(SubmitCode submitCode, CancellationToken cancellatio { if (_scriptState == null) { - _scriptState = await CSharpScript.RunAsync(code, cancellationToken: cancellationToken); + _scriptState = await CSharpScript.RunAsync( + code, + ScriptOptions, + cancellationToken: cancellationToken); } else { - _scriptState = await _scriptState.ContinueWithAsync(code, cancellationToken: cancellationToken); + _scriptState = await _scriptState.ContinueWithAsync( + code, + ScriptOptions, + e => + { + exception = e; + return true; + }, + cancellationToken); } } catch (Exception e) @@ -66,7 +98,17 @@ public async Task SendAsync(SubmitCode submitCode, CancellationToken cancellatio } if (exception != null) { - _channel.OnNext(new CodeSubmissionEvaluationFailed(submitCode.Id, exception)); + var diagnostics = _scriptState?.Script?.GetDiagnostics() ?? Enumerable.Empty(); + if (diagnostics.Any()) + { + var message = string.Join("\n", diagnostics.Select(d => d.GetMessage())); + + _channel.OnNext(new CodeSubmissionEvaluationFailed(submitCode.Id, exception, message)); + } + else + { + _channel.OnNext(new CodeSubmissionEvaluationFailed(submitCode.Id, exception)); + } } else { diff --git a/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs b/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs index e19a0c871..ef9b1a2a0 100644 --- a/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs +++ b/WorkspaceServer/Kernel/CodeSubmissionEvaluationFailed.cs @@ -9,10 +9,14 @@ namespace WorkspaceServer.Kernel public class CodeSubmissionEvaluationFailed : KernelEventBase { public object Error { get; } + public string Message { get; } - public CodeSubmissionEvaluationFailed(Guid parentId, object error): base(parentId) + public CodeSubmissionEvaluationFailed(Guid parentId, object error, string message = null): base(parentId) { Error = error; + Message = string.IsNullOrWhiteSpace(message) + ? error is Exception exception ? exception.Message : error.ToString() + : message; } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs b/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs new file mode 100644 index 000000000..55b157bd5 --- /dev/null +++ b/WorkspaceServer/Kernel/CodeSubmissionProcessors.cs @@ -0,0 +1,56 @@ +// 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(); + } + + public void Register(ICodeSubmissionProcessor processor) + { + _processors[processor.Command] = processor; + _rootCommand.AddCommand(processor.Command); + _parser = new CommandLineBuilder(_rootCommand).Build(); + } + + public async Task ProcessAsync(SubmitCode codeSubmission) + { + var lines = new Queue( codeSubmission.Value.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); + var newSubmission = await processor.ProcessAsync(new SubmitCode(string.Join("\n", lines), codeSubmission.Id, codeSubmission.ParentId)); + lines = new Queue(newSubmission.Value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)); + } + else + { + unhandledLines.Enqueue(currentLine); + } + } + return new SubmitCode(string.Join("\n", unhandledLines), codeSubmission.Id, codeSubmission.ParentId); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/EnumerableExtensions.cs b/WorkspaceServer/Kernel/EnumerableExtensions.cs new file mode 100644 index 000000000..19bf7291a --- /dev/null +++ b/WorkspaceServer/Kernel/EnumerableExtensions.cs @@ -0,0 +1,18 @@ +// 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.Linq; + +namespace WorkspaceServer.Kernel +{ + internal static class EnumerableExtensions + { + public static IOrderedEnumerable OrderBy(this IEnumerable source, Comparison compare) + { + var comparer = Comparer.Create(compare); + return source.OrderBy(t => t, comparer); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs b/WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs new file mode 100644 index 000000000..bb7b1e4f1 --- /dev/null +++ b/WorkspaceServer/Kernel/ICodeSubmissionPreProcessor.cs @@ -0,0 +1,14 @@ +// 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/IKernelEvent.cs b/WorkspaceServer/Kernel/IKernelEvent.cs index dfff438ce..06cf4275a 100644 --- a/WorkspaceServer/Kernel/IKernelEvent.cs +++ b/WorkspaceServer/Kernel/IKernelEvent.cs @@ -50,16 +50,6 @@ public class AddPackage : KernelCommandBase { } - public class SubmitCode : KernelCommandBase - { - public string Value { get; } - - public SubmitCode(string value) : base() - { - Value = value ?? throw new ArgumentNullException(nameof(value)); - } - } - public class RequestCompletion : KernelCommandBase { } diff --git a/WorkspaceServer/Kernel/RenderingEngine.cs b/WorkspaceServer/Kernel/RenderingEngine.cs index 230e85378..efefad776 100644 --- a/WorkspaceServer/Kernel/RenderingEngine.cs +++ b/WorkspaceServer/Kernel/RenderingEngine.cs @@ -11,17 +11,19 @@ namespace WorkspaceServer.Kernel public class RenderingEngine : IRenderingEngine { private readonly IRenderer _defaultRenderer; + private readonly IRendering _nullRendering; private readonly Dictionary _rendererRegistry = new Dictionary(); - public RenderingEngine(IRenderer defaultRenderer) + public RenderingEngine(IRenderer defaultRenderer, IRendering nullRendering) { _defaultRenderer = defaultRenderer; + _nullRendering = nullRendering; } public IRendering Render(object source) { if (source == null) { - throw new ArgumentNullException(nameof(source)); + return _nullRendering; } var renderer = FindRenderer(source.GetType()); return renderer.Render(source, this); diff --git a/WorkspaceServer/Kernel/ScriptExtensions.cs b/WorkspaceServer/Kernel/ScriptExtensions.cs new file mode 100644 index 000000000..7653c6c2c --- /dev/null +++ b/WorkspaceServer/Kernel/ScriptExtensions.cs @@ -0,0 +1,30 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Scripting; + +namespace WorkspaceServer.Kernel +{ + internal static class ScriptExtensions + { + public static IEnumerable GetDiagnostics(this Script script) + { + if(script == null) + { + return Enumerable.Empty(); + } + + var compilation = script.GetCompilation(); + var orderedDiagnostics = compilation.GetDiagnostics().OrderBy((d1, d2) => + { + var severityDiff = (int)d2.Severity - (int)d1.Severity; + return severityDiff != 0 ? severityDiff : d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start; + }); + + return orderedDiagnostics; + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Kernel/SubmitCode.cs b/WorkspaceServer/Kernel/SubmitCode.cs new file mode 100644 index 000000000..5a17e4974 --- /dev/null +++ b/WorkspaceServer/Kernel/SubmitCode.cs @@ -0,0 +1,22 @@ +// 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 SubmitCode : KernelCommandBase + { + public string Value { get; } + + public SubmitCode(string value) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public SubmitCode(string value, Guid id, Guid parentId) : base(id, parentId) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + } +} \ No newline at end of file