Skip to content

Commit

Permalink
implement %whos magic command for F# (dotnet#730)
Browse files Browse the repository at this point in the history
* implement `%whos` magic command for F#

* get rid of unused constructor

* convert potentially expensive property getter into a method

* move formatter to separate class

* add null check
  • Loading branch information
brettfo authored Jan 8, 2020
1 parent 0ea984e commit a77f759
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 107 deletions.
35 changes: 3 additions & 32 deletions Microsoft.DotNet.Interactive.CSharp/CSharpKernelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.DotNet.Interactive.Commands;
using Microsoft.DotNet.Interactive.Events;
Expand Down Expand Up @@ -268,35 +267,7 @@ static string InstallingPackageMessage(PackageReference package)
public static CSharpKernel UseWho(this CSharpKernel kernel)
{
kernel.AddDirective(who_and_whos());

Formatter<CurrentVariables>.Register((variables, writer) =>
{
PocketView output = null;

if (variables.Detailed)
{
output = table(
thead(
tr(
th("Variable"),
th("Type"),
th("Value"))),
tbody(
variables.Select(v =>
tr(
td(v.Name),
td(v.Type),
td(v.Value.ToDisplayString())
))));
}
else
{
output = div(variables.Select(v => v.Name + "\t "));
}

output.WriteTo(writer, HtmlEncoder.Default);
}, "text/html");

Formatter.Register(new CurrentVariablesFormatter());
return kernel;
}

Expand All @@ -313,10 +284,10 @@ private static Command who_and_whos()
if (context.Command is SubmitCode &&
context.HandlingKernel is CSharpKernel kernel)
{
var variables = kernel.ScriptState.Variables;
var variables = kernel.ScriptState.Variables.Select(v => new CurrentVariable(v.Name, v.Type, v.Value));

var currentVariables = new CurrentVariables(
variables,
variables,
detailed);

var html = currentVariables
Expand Down
24 changes: 0 additions & 24 deletions Microsoft.DotNet.Interactive.CSharp/CurrentVariable.cs

This file was deleted.

30 changes: 0 additions & 30 deletions Microsoft.DotNet.Interactive.CSharp/CurrentVariables.cs

This file was deleted.

21 changes: 20 additions & 1 deletion Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ type FSharpKernel() =
let resolvedAssemblies = List<string>()
static let lockObj = Object();
let script = lock lockObj (fun () -> new FSharpScript(additionalArgs=[|"/langversion:preview"|]))
do base.RegisterForDisposal(script)
let mutable cancellationTokenSource = new CancellationTokenSource()
let variables = HashSet<string>()

let valueBoundHandler = new Handler<(obj * Type * string)>(fun _ (_, _, name) -> variables.Add(name) |> ignore)
do script.ValueBound.AddHandler valueBoundHandler
do base.RegisterForDisposal(fun () -> script.ValueBound.RemoveHandler valueBoundHandler)

let handler = new Handler<string> (fun o s -> resolvedAssemblies.Add(s))
do script.AssemblyReferenceAdded.AddHandler handler
do base.RegisterForDisposal(fun () -> do script.AssemblyReferenceAdded.RemoveHandler handler)
do base.RegisterForDisposal(script)

let messageMap = Dictionary<string, string>()

Expand Down Expand Up @@ -204,6 +209,20 @@ type FSharpKernel() =
context.Publish(CurrentCommandCancelled(cancelCurrentCommand))
}

member _.GetCurrentVariables() =
// `ValueBound` event will make a copy of value types, so to ensure we always get the current value, we re-evaluate each variable
variables
|> Seq.filter (fun v -> v <> "it") // don't report special variable `it`
|> Seq.choose (fun v ->
let result, _errors =
try
script.Eval("``" + v + "``")
with
| ex -> Error(ex), [||]
match result with
| Ok(Some(value)) -> Some (CurrentVariable(v, value.ReflectionType, value.ReflectionValue))
| _ -> None)

override __.HandleAsync(command: IKernelCommand, context: KernelInvocationContext): Task =
match command with
| :? SubmitCode as submitCode -> submitCode.Handler <- fun _ _ -> (handleSubmitCode submitCode context) |> Async.StartAsTask :> Task
Expand Down
29 changes: 28 additions & 1 deletion Microsoft.DotNet.Interactive.FSharp/FSharpKernelExtensions.fs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
namespace Microsoft.DotNet.Interactive.FSharp

open System
open System.CommandLine
open System.CommandLine.Invocation
open System.Runtime.CompilerServices
open System.Threading.Tasks
open Microsoft.AspNetCore.Html
open Microsoft.DotNet.Interactive
open Microsoft.DotNet.Interactive.Commands
open Microsoft.DotNet.Interactive.Events
open Microsoft.DotNet.Interactive.FSharp
open Microsoft.DotNet.Interactive.Formatting
open XPlot.Plotly
Expand Down Expand Up @@ -61,4 +65,27 @@ open System.Linq
return! kernel.SendAsync(SubmitCode code) |> Async.AwaitTask
}
Async.RunSynchronously t |> ignore
kernel
kernel

[<Extension>]
static member UseWho(kernel: FSharpKernel) =
let detailedName = "%whos"
let command = Command(detailedName)
command.Handler <- CommandHandler.Create(
fun (parseResult: ParseResult) (context: KernelInvocationContext) ->
let detailed = parseResult.CommandResult.Token.Value = detailedName
match context.Command with
| :? SubmitCode ->
match context.HandlingKernel with
| :? FSharpKernel as kernel ->
let kernelVariables = kernel.GetCurrentVariables()
let currentVariables = CurrentVariables(kernelVariables, detailed)
let html = currentVariables.ToDisplayString(HtmlFormatter.MimeType)
context.Publish(DisplayedValueProduced(html, context.Command, [| FormattedValue(HtmlFormatter.MimeType, html) |]))
| _ -> ()
| _ -> ()
Task.CompletedTask)
command.AddAlias("%who")
kernel.AddDirective(command)
Formatter.Register(CurrentVariablesFormatter())
kernel
89 changes: 70 additions & 19 deletions Microsoft.DotNet.Interactive.Jupyter.Tests/MagicCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Xunit.Abstractions;
using static Pocket.Logger;

#pragma warning disable 8509 // don't warn on incomplete pattern matches
namespace Microsoft.DotNet.Interactive.Jupyter.Tests
{
public class MagicCommandTests
Expand Down Expand Up @@ -221,22 +222,47 @@ await kernel.SendAsync(new SubmitCode(
.Contain(v => v.Value.Equals("done!"));
}

[Fact]
public async Task whos_lists_the_names_and_values_of_variables_in_scope()
[Theory]
[InlineData(Language.CSharp)]
[InlineData(Language.FSharp)]
public async Task whos_lists_the_names_and_values_of_variables_in_scope(Language language)
{
using var baseKernel = language switch
{
Language.CSharp => new CSharpKernel().UseWho() as KernelBase,
Language.FSharp => new FSharpKernel().UseWho(),
};
using var kernel = new CompositeKernel
{
new CSharpKernel().UseWho()
baseKernel
}
.LogEventsToPocketLogger();

using var events = kernel.KernelEvents.ToSubscribedList();

await kernel.SendAsync(new SubmitCode(@"using Microsoft.DotNet.Interactive;"));
await kernel.SendAsync(new SubmitCode(@"var x = 1;"));
await kernel.SendAsync(new SubmitCode(@"x = 2;"));
await kernel.SendAsync(new SubmitCode(@"var y = ""hi!"";"));
await kernel.SendAsync(new SubmitCode(@"var z = new object[] { x, y };"));
var commands = language switch
{
Language.CSharp => new[]
{
"var x = 1;",
"x = 2;",
"var y = \"hi!\";",
"var z = new object[] { x, y };",
},
Language.FSharp => new[]
{
"let mutable x = 1",
"x <- 2",
"let y = \"hi!\"",
"let z = [| x :> obj; y :> obj |]",
},
};

foreach (var command in commands)
{
await kernel.SendAsync(new SubmitCode(command));
}

await kernel.SendAsync(new SubmitCode(@"%whos"));

events.Should()
Expand All @@ -256,22 +282,47 @@ public async Task whos_lists_the_names_and_values_of_variables_in_scope()
"<td>z</td><td>System.Object[]</td><td>[ 2, hi! ]</td>");
}

[Fact]
public async Task who_lists_the_names_of_variables_in_scope()
[Theory]
[InlineData(Language.CSharp)]
[InlineData(Language.FSharp)]
public async Task who_lists_the_names_of_variables_in_scope(Language language)
{
using var baseKernel = language switch
{
Language.CSharp => new CSharpKernel().UseWho() as KernelBase,
Language.FSharp => new FSharpKernel().UseWho(),
};
using var kernel = new CompositeKernel
{
new CSharpKernel().UseWho()
}
.LogEventsToPocketLogger();
{
baseKernel
}
.LogEventsToPocketLogger();

using var events = kernel.KernelEvents.ToSubscribedList();

await kernel.SendAsync(new SubmitCode(@"using Microsoft.DotNet.Interactive;"));
await kernel.SendAsync(new SubmitCode(@"var x = 1;"));
await kernel.SendAsync(new SubmitCode(@"x = 2;"));
await kernel.SendAsync(new SubmitCode(@"var y = ""hi!"";"));
await kernel.SendAsync(new SubmitCode(@"var z = new object[] { x, y };"));
var commands = language switch
{
Language.CSharp => new[]
{
"var x = 1;",
"x = 2;",
"var y = \"hi!\";",
"var z = new object[] { x, y };",
},
Language.FSharp => new[]
{
"let mutable x = 1",
"x <- 2",
"let y = \"hi!\"",
"let z = [| x :> obj; y :> obj |]",
},
};

foreach (var command in commands)
{
await kernel.SendAsync(new SubmitCode(command));
}

await kernel.SendAsync(new SubmitCode(@"%who"));

events.Should()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ protected KernelBase CreateKernel(Language language)
Language.FSharp => new FSharpKernel()
.UseDefaultFormatting()
.UseKernelHelpers()
.UseWho()
.UseDefaultNamespaces() as KernelBase,
Language.CSharp => new CSharpKernel()
.UseDefaultFormatting()
Expand Down
20 changes: 20 additions & 0 deletions Microsoft.DotNet.Interactive/CurrentVariable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace Microsoft.DotNet.Interactive
{
public class CurrentVariable
{
public CurrentVariable(string name, Type type, object value)
{
Name = name;
Type = type;
Value = value;
}

public object Value { get; }

public Type Type { get; }

public string Name { get; }
}
}
37 changes: 37 additions & 0 deletions Microsoft.DotNet.Interactive/CurrentVariables.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.DotNet.Interactive
{
public class CurrentVariables : IEnumerable<CurrentVariable>
{
private readonly Dictionary<string, CurrentVariable> _variables = new Dictionary<string, CurrentVariable>();

public CurrentVariables(IEnumerable<CurrentVariable> variables, bool detailed)
: this(detailed)
{
if (variables == null)
{
throw new ArgumentNullException(nameof(variables));
}

foreach (var variable in variables.Where(v => v != null))
{
_variables[variable.Name] = variable;
}
}

private CurrentVariables(bool detailed)
{
Detailed = detailed;
}

public bool Detailed { get; }

public IEnumerator<CurrentVariable> GetEnumerator() => _variables.Values.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Loading

0 comments on commit a77f759

Please sign in to comment.