Skip to content

Commit

Permalink
add support for multiple inputs using output cell HTML forms
Browse files Browse the repository at this point in the history
  • Loading branch information
jonsequitur committed Sep 24, 2024
1 parent f1f4d3b commit ad23b21
Show file tree
Hide file tree
Showing 29 changed files with 824 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Microsoft.DotNet.Interactive
public static System.Void CSS(System.String content)
public static DisplayedValue display(System.Object value, System.String[] mimeTypes)
public static System.Threading.Tasks.Task<System.String> GetInputAsync(System.String prompt = , System.String typeHint = text)
public static System.Threading.Tasks.Task<System.Collections.Generic.Dictionary<System.String,System.String>> GetInputsAsync(Microsoft.DotNet.Interactive.Commands.InputDescription[] inputDescriptions)
public static System.Threading.Tasks.Task<PasswordString> GetPasswordAsync(System.String prompt = )
public static Microsoft.AspNetCore.Html.IHtmlContent HTML(System.String content)
public static System.Void Javascript(System.String scriptContent)
Expand Down Expand Up @@ -171,6 +172,7 @@ Microsoft.DotNet.Interactive
public static System.Collections.Generic.IEnumerable<Kernel> FindKernels(System.Func<Kernel,System.Boolean> predicate)
public static System.Threading.Tasks.Task LoadAndRunInteractiveDocument(System.IO.FileInfo file, Microsoft.DotNet.Interactive.Commands.KernelCommand parentCommand = null)
public static System.Threading.Tasks.Task<KernelCommandResult> SubmitCodeAsync(System.String code)
public static TKernel UseFormsForMultipleInputs<TKernel>()
public static TKernel UseImportMagicCommand<TKernel>()
public static T UseQuitCommand<T>(System.Func<System.Threading.Tasks.Task> onQuitAsync = null)
public static T UseValueSharing<T>()
Expand Down Expand Up @@ -366,6 +368,12 @@ Microsoft.DotNet.Interactive.Commands
public class ImportDocument : KernelCommand, System.IEquatable<KernelCommand>
.ctor(System.String filePath, System.String targetKernelName = null)
public System.String FilePath { get;}
public class InputDescription
.ctor(System.String name, System.String prompt = null)
public System.String Name { get;}
public System.String Prompt { get;}
public System.String SaveAs { get; set;}
public System.String TypeHint { get; set;}
public abstract class KernelCommand, System.IEquatable<KernelCommand>
public System.Uri DestinationUri { get; set;}
public Microsoft.DotNet.Interactive.KernelCommandInvocation Handler { get; set;}
Expand Down Expand Up @@ -396,11 +404,15 @@ Microsoft.DotNet.Interactive.Commands
public class RequestHoverText : LanguageServiceCommand, System.IEquatable<KernelCommand>
.ctor(System.String code, Microsoft.DotNet.Interactive.LinePosition linePosition, System.String targetKernelName = null)
public class RequestInput : KernelCommand, System.IEquatable<KernelCommand>
.ctor(System.String prompt, System.String targetKernelName = null, System.String inputTypeHint = null)
.ctor(System.String prompt, System.String inputTypeHint = null)
public System.String InputTypeHint { get; set;}
public System.Boolean IsPassword { get;}
public System.String ParameterName { get; set;}
public System.String Prompt { get;}
public System.String SaveAs { get; set;}
public class RequestInputs : KernelCommand, System.IEquatable<KernelCommand>
.ctor()
public System.Collections.Generic.List<InputDescription> Inputs { get; set;}
public class RequestKernelInfo : KernelCommand, System.IEquatable<KernelCommand>
.ctor(System.String targetKernelName = null)
.ctor(System.Uri destinationUri)
Expand Down Expand Up @@ -622,6 +634,9 @@ Microsoft.DotNet.Interactive.Events
public class InputProduced : KernelEvent
.ctor(System.String value, Microsoft.DotNet.Interactive.Commands.RequestInput command)
public System.String Value { get;}
public class InputsProduced : KernelEvent
.ctor(System.Collections.Generic.Dictionary<System.String,System.String> values, Microsoft.DotNet.Interactive.Commands.RequestInputs command)
public System.Collections.Generic.Dictionary<System.String,System.String> Values { get;}
public enum InsertTextFormat : System.Enum, System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable
PlainText=1
Snippet=2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +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;
Expand Down Expand Up @@ -111,10 +111,10 @@ public async Task When_receiver_is_disposed_then_no_further_reads_occur()

for (int i = 0; i < 2; i++)
{
_messageQueue.Add(new CommandOrEvent(new SubmitCode("")));
_messageQueue.Add(new CommandOrEvent(new SubmitCode(i.ToString())));
}

using var receiver = new KernelCommandAndEventReceiver(t =>
var receiver = new KernelCommandAndEventReceiver(t =>
{
var commandOrEvent = _messageQueue.Take(t);
readCount++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ IEnumerable<KernelCommand> commands()

yield return new RequestValue("a", mimeType: HtmlFormatter.MimeType, targetKernelName: "csharp");

yield return new RequestInput(prompt: "provide answer", inputTypeHint: "password", targetKernelName: "vscode");
yield return new RequestInput(prompt: "provide answer", inputTypeHint: "password");

yield return new SendValue(
"name",
Expand Down Expand Up @@ -454,7 +454,7 @@ [new FormattedValue(PlainTextFormatter.MimeType, diagnostic.ToString())],
"<span>raw value</span>"),
new RequestValue("a", mimeType: HtmlFormatter.MimeType, targetKernelName: "csharp"));

yield return new InputProduced("user input", new RequestInput(prompt: "What is the path to the log file?", inputTypeHint: "file", targetKernelName: "vscode"));
yield return new InputProduced("user input", new RequestInput(prompt: "What is the path to the log file?", inputTypeHint: "file"));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,15 @@ var x = 123; // with some intervening code
<NamedTestSelector>
<TestName>Microsoft.DotNet.Interactive.Tests.Connection.SerializationTests.Event_contract_has_not_been_broken</TestName>
</NamedTestSelector>
<FixtureTestSelector>
<FixtureName>Microsoft.DotNet.Interactive.Tests.RequestInputTests</FixtureName>
</FixtureTestSelector>
<NamedTestSelector>
<TestName>Microsoft.DotNet.Interactive.Tests.RequestInputTests.When_a_saved_value_is_used_then_the_user_is_notified</TestName>
</NamedTestSelector>
<NamedTestSelector>
<TestName>Microsoft.DotNet.Interactive.Tests.RequestInputTests.When_a_value_is_saved_then_the_user_is_notified</TestName>
</NamedTestSelector>
<NamedTestSelector>
<TestName>Microsoft.DotNet.Interactive.Tests.RequestInputTests.When_Save_is_specified_then_subsequent_requests_reuse_the_saved_value</TestName>
</NamedTestSelector>
</IgnoredTests>
</Settings>
</ProjectConfiguration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// 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.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.DotNet.Interactive.App;
using Microsoft.DotNet.Interactive.Commands;
using Microsoft.DotNet.Interactive.CSharp;
using Microsoft.DotNet.Interactive.Directives;
using Microsoft.DotNet.Interactive.Tests.Utility;
using Xunit;

namespace Microsoft.DotNet.Interactive.Tests;

public class MultipleInputsWithinMagicCommandsTests : IDisposable
{
private readonly CompositeKernel _kernel;
private readonly KernelActionDirective _shimCommand;
private readonly List<ShimCommand> _receivedShimCommands = new();

public MultipleInputsWithinMagicCommandsTests()
{
_kernel = CreateKernel();

_shimCommand = new("#!shim")
{
KernelCommandType = typeof(ShimCommand),
Parameters =
{
new("--name"),
new("--value")
{
AllowImplicitName = true
}
}
};

_kernel.FindKernelByName("csharp")
.AddDirective<ShimCommand>(_shimCommand, (command, _) =>
{
_receivedShimCommands.Add(command);
return Task.CompletedTask;
});
}

public class ShimCommand : KernelCommand
{
public string Name { get; set; }

public string Value { get; set; }
}

public void Dispose()
{
_kernel.Dispose();
}

[Fact]
public async Task Multiple_inputs_are_bound_within_a_single_magic_command_that_uses_JSON_binding()
{
_kernel.RespondToRequestInputsFormWith(new Dictionary<string, string>
{
["name"] = "age",
["value"] = "123"
});

var result = await _kernel.SendAsync(
new SubmitCode("""
#!shim --name @input --value @input:{"type": "date"}
""", "csharp"));

result.Events.Should().NotContainErrors();

using var _ = new AssertionScope();

var receivedCommand= _receivedShimCommands.Should().ContainSingle().Which;
receivedCommand.Name.Should().Be("age");
receivedCommand.Value.Should().Be("123");
}

[Fact]
public async Task Multiple_inputs_are_bound_within_a_single_magic_command_that_uses_custom_binding()
{
_kernel.RespondToRequestInputsFormWith(new Dictionary<string, string>
{
["name"] = "age",
["value"] = "123"
});

var result = await _kernel.SendAsync(
new SubmitCode("""
#!set --name @input:age --value @input:{"type": "number"}
""", "csharp"));

result.Events.Should().NotContainErrors();

_kernel.FindKernelByName("csharp").As<CSharpKernel>().TryGetValue<string>("age", out var boundValue);

boundValue.Should().Be("123");
}

[Fact]
public async Task When_multiple_inputs_are_enabled_then_RequestInput_is_not_sent_for_a_magic_command_that_uses_custom_binding()
{
_kernel.RespondToRequestInputsFormWith(new Dictionary<string, string>
{
["name"] = "fruit",
["value"] = "cherry"
});

RequestInput requestInputSent = null;
_kernel.AddMiddleware(async (command, context, next) =>
{
if (command is RequestInput requestInput)
{
requestInputSent = requestInput;
}

await next(command, context);
});

await _kernel.SendAsync(
new SubmitCode("""
#!set --name @input:name --value @input:value
""", "csharp"));

requestInputSent.Should().BeNull();
}

[Fact]
public async Task When_multiple_inputs_are_enabled_then_RequestInput_is_not_sent_for_a_magic_command_that_uses_JSON_binding()
{
_kernel.RespondToRequestInputsFormWith(new Dictionary<string, string>
{
["name"] = "fruit",
["value"] = "cherry"
});

RequestInput requestInputSent = null;
_kernel.AddMiddleware(async (command, context, next) =>
{
if (command is RequestInput requestInput)
{
requestInputSent = requestInput;
}

await next(command, context);
});

await _kernel.SendAsync(
new SubmitCode("""
#!shim --name @input --value @input
""", "csharp"));

requestInputSent.Should().BeNull();
}

[Theory]
[MemberData(nameof(LanguageServiceCommands))]
public async Task Language_service_commands_do_not_trigger_input_requests(KernelCommand command)
{
var result = await _kernel.SendAsync(command);

_kernel.RespondToRequestInputsFormWith(new Dictionary<string, string>
{
["name"] = "fruit",
["value"] = "cherry"
});

RequestInputs requestInputsSent = null;
_kernel.AddMiddleware(async (kernelCommand, context, next) =>
{
if (kernelCommand is RequestInputs requestInput)
{
requestInputsSent = requestInput;
}

await next(command, context);
});

result.Events.Should().NotContainErrors();

requestInputsSent.Should().BeNull();
}

public static IEnumerable<object[]> LanguageServiceCommands()
{
var code = "#!set --name @input:name --value @password:password";

yield return [new RequestCompletions(code, new LinePosition(0, code.Length), targetKernelName: "csharp")];
yield return [new RequestHoverText(code, new LinePosition(0, 3), targetKernelName: "csharp")];
yield return [new RequestDiagnostics(code, targetKernelName: "csharp")];
yield return [new RequestSignatureHelp(code, new LinePosition(0, 3), targetKernelName: "csharp")];

code = "#!shim --name @input:name --value @password:password";

yield return [new RequestCompletions(code, new LinePosition(0, code.Length), targetKernelName: "csharp")];
yield return [new RequestHoverText(code, new LinePosition(0, 3), targetKernelName: "csharp")];
yield return [new RequestDiagnostics(code, targetKernelName: "csharp")];
yield return [new RequestSignatureHelp(code, new LinePosition(0, 3), targetKernelName: "csharp")];
}

private static CompositeKernel CreateKernel() =>
new CompositeKernel
{
new CSharpKernel()
.UseNugetDirective()
.UseKernelHelpers()
.UseValueSharing(),
new KeyValueStoreKernel()
}.UseFormsForMultipleInputs();
}
43 changes: 43 additions & 0 deletions src/Microsoft.DotNet.Interactive.Tests/RequestInputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.Linq;
using System.Threading.Tasks;
using FluentAssertions;
Expand Down Expand Up @@ -108,6 +109,48 @@ public async Task When_a_saved_value_is_used_then_the_user_is_notified()
.Match($"Using previously saved value for `{saveAs}`.*To remove this value *, run the following command in a PowerShell cell:*");
}

[Fact]
public async Task Multiple_inputs_can_be_requested_together()
{
var requestInputs = new RequestInputs
{
Inputs =
[
new("Fruit"),
new("Tastiness"),
new("Color")
]
};

using var kernel = CreateKernel()
.UseFormsForMultipleInputs();

var formValues = new Dictionary<string, string>
{
["Fruit"] = "cherry",
["Tastiness"] = "9000",
["Color"] = "red"
};

kernel.RespondToRequestInputsFormWith(formValues);

var result = await kernel.SendAsync(requestInputs);

result.Events.Should().NotContainErrors();

result.Events.Should().ContainSingle<InputsProduced>()
.Which.Values.Should().BeEquivalentTo(formValues);
}

[Fact]
public void When_RequestInputs_is_not_supported_then_it_falls_back_to_sending_multiple_RequestInput_commands()
{


// TODO (When_RequestInputs_is_not_supported_then_multiple_RequestInput_commands_are_sent) write test
throw new NotImplementedException();
}

private static CompositeKernel CreateKernel()
{
var kernel = new CompositeKernel
Expand Down
Loading

0 comments on commit ad23b21

Please sign in to comment.