diff --git a/Microsoft.DotNet.Interactive.CSharp.Tests/CSharpKernelTests.cs b/Microsoft.DotNet.Interactive.CSharp.Tests/CSharpKernelTests.cs new file mode 100644 index 0000000000..d5f18ecc74 --- /dev/null +++ b/Microsoft.DotNet.Interactive.CSharp.Tests/CSharpKernelTests.cs @@ -0,0 +1,42 @@ +// 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; +using FluentAssertions; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.Interactive.CSharp.Tests +{ + public class CSharpKernelTests : LanguageKernelTestBase + { + public CSharpKernelTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task Script_state_is_available_within_middleware_pipeline() + { + var variableCountBeforeEvaluation = 0; + var variableCountAfterEvaluation = 0; + + using var kernel = new CSharpKernel(); + + kernel.AddMiddleware(async (command, context, next) => + { + var k = context.HandlingKernel as CSharpKernel; + + await next(command, context); + + variableCountAfterEvaluation = k.ScriptState.Variables.Length; + }); + + await kernel.SendAsync(new SubmitCode("var x = 1;")); + + variableCountBeforeEvaluation.Should().Be(0); + variableCountAfterEvaluation.Should().Be(1); + } + } +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.CSharp.Tests/SubmissionParsingTests.cs b/Microsoft.DotNet.Interactive.CSharp.Tests/SubmissionParsingTests.cs index d3337f5cc4..bbef507340 100644 --- a/Microsoft.DotNet.Interactive.CSharp.Tests/SubmissionParsingTests.cs +++ b/Microsoft.DotNet.Interactive.CSharp.Tests/SubmissionParsingTests.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.DotNet.Interactive.Commands; -using Microsoft.DotNet.Interactive.Tests; using Microsoft.DotNet.Interactive.Tests.Utility; using Xunit; diff --git a/Microsoft.DotNet.Interactive.CSharp/CSharpCodeGeneration.cs b/Microsoft.DotNet.Interactive.CSharp/CSharpCodeGeneration.cs new file mode 100644 index 0000000000..ee05be9121 --- /dev/null +++ b/Microsoft.DotNet.Interactive.CSharp/CSharpCodeGeneration.cs @@ -0,0 +1,40 @@ +// 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.IO; + +namespace Microsoft.DotNet.Interactive.CSharp +{ + internal static partial class TypeExtensions + { + public static void WriteCSharpDeclarationTo( + this Type type, + TextWriter writer) + { + var typeName = type.FullName ?? type.Name; + + if (typeName.Contains("`")) + { + writer.Write(typeName.Remove(typeName.IndexOf('`'))); + writer.Write("<"); + var genericArguments = type.GetGenericArguments(); + + for (var i = 0; i < genericArguments.Length; i++) + { + genericArguments[i].WriteCSharpDeclarationTo(writer); + if (i < genericArguments.Length - 1) + { + writer.Write(","); + } + } + + writer.Write(">"); + } + else + { + writer.Write(typeName); + } + } + } +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs b/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs index a7caa114c8..5163bfd975 100644 --- a/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs +++ b/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Interactive.DependencyManager; using Microsoft.CodeAnalysis; @@ -25,12 +25,11 @@ using Newtonsoft.Json.Linq; using XPlot.Plotly; using Task = System.Threading.Tasks.Task; -using System.ComponentModel; namespace Microsoft.DotNet.Interactive.CSharp { public class CSharpKernel : - KernelBase, + DotNetLanguageKernel, IExtensibleKernel, ISupportNuget { @@ -43,9 +42,9 @@ public class CSharpKernel : new CSharpParseOptions(LanguageVersion.Default, kind: SourceCodeKind.Script); private WorkspaceFixture _fixture; - private AssemblyResolutionProbe _assemblyProbingPaths; private NativeResolutionProbe _nativeProbingRoots; + private readonly Lazy _dependencies; internal ScriptOptions ScriptOptions = ScriptOptions.Default @@ -74,7 +73,6 @@ public CSharpKernel() : base(DefaultKernelName) RegisterForDisposal(() => { ScriptState = null; - (_dependencies as IDisposable)?.Dispose(); }); } @@ -86,14 +84,14 @@ public Task IsCompleteSubmissionAsync(string code) return Task.FromResult(SyntaxFactory.IsCompleteSubmission(syntaxTree)); } - public override bool TryGetVariable( + public override bool TryGetVariable( string name, - out object value) + out T value) { if (ScriptState?.Variables .LastOrDefault(v => v.Name == name) is { } variable) { - value = variable.Value; + value = (T) variable.Value; return true; } @@ -101,6 +99,21 @@ public override bool TryGetVariable( return false; } + public override async Task SetVariableAsync(string name, object value) + { + var csharpTypeDeclaration = new StringWriter(); + + value.GetType().WriteCSharpDeclarationTo(csharpTypeDeclaration); + + + + await RunAsync($"{csharpTypeDeclaration} {name} = default;"); + + var scriptVariable = ScriptState.GetVariable(name); + + scriptVariable.Value = value; + } + public override Task LspMethod(string methodName, JObject request) { LspResponse result; @@ -121,13 +134,13 @@ public override Task LspMethod(string methodName, JObject request) public TextDocumentHoverResponse TextDocumentHover(HoverParams hoverParams) { - return new TextDocumentHoverResponse() + return new TextDocumentHoverResponse { - Contents = new MarkupContent() + Contents = new MarkupContent { Kind = MarkupKind.Markdown, Value = $"textDocument/hover at position ({hoverParams.Position.Line}, {hoverParams.Position.Character}) with `markdown`", - }, + } }; } @@ -160,33 +173,16 @@ protected override async Task HandleSubmitCode( if (!context.CancellationToken.IsCancellationRequested) { - ScriptOptions = ScriptOptions.WithMetadataResolver( - ScriptMetadataResolver.Default.WithBaseDirectory( - Directory.GetCurrentDirectory())); - try { - if (ScriptState == null) - { - ScriptState = await CSharpScript.RunAsync( - code, - ScriptOptions, - cancellationToken: context.CancellationToken) - .UntilCancelled(context.CancellationToken); - } - else - { - ScriptState = await ScriptState.ContinueWithAsync( - code, - ScriptOptions, - catchException: e => - { - exception = e; - return true; - }, - cancellationToken: context.CancellationToken) - .UntilCancelled(context.CancellationToken); - } + await RunAsync( + code, + context.CancellationToken, + e => + { + exception = e; + return true; + }); } catch (CompilationErrorException cpe) { @@ -232,6 +228,34 @@ protected override async Task HandleSubmitCode( } } + private async Task RunAsync( + string code, + CancellationToken cancellationToken = default, + Func catchException = default) + { + ScriptOptions = ScriptOptions.WithMetadataResolver( + ScriptMetadataResolver.Default.WithBaseDirectory( + Directory.GetCurrentDirectory())); + + if (ScriptState == null) + { + ScriptState = await CSharpScript.RunAsync( + code, + ScriptOptions, + cancellationToken: cancellationToken) + .UntilCancelled(cancellationToken); + } + else + { + ScriptState = await ScriptState.ContinueWithAsync( + code, + ScriptOptions, + catchException: catchException, + cancellationToken: cancellationToken) + .UntilCancelled(cancellationToken); + } + } + protected override async Task HandleRequestCompletion( RequestCompletion requestCompletion, KernelInvocationContext context) @@ -308,7 +332,6 @@ await _extensionLoader.LoadFromDirectoryAsync( context); } - private Lazy _dependencies; private DependencyProvider GetDependencyProvider() { @@ -323,7 +346,13 @@ private DependencyProvider GetDependencyProvider() throw new ArgumentNullException(nameof(_nativeProbingRoots)); } - return new DependencyProvider(_assemblyProbingPaths, _nativeProbingRoots); + var dependencyProvider = new DependencyProvider( + _assemblyProbingPaths, + _nativeProbingRoots); + + RegisterForDisposal(dependencyProvider); + + return dependencyProvider; } // Set assemblyProbingPaths, nativeProbingRoots for Kernel. @@ -331,19 +360,11 @@ private DependencyProvider GetDependencyProvider() // They are used by the dependecymanager for Assembly and Native dll resolving void ISupportNuget.Initialize(AssemblyResolutionProbe assemblyProbingPaths, NativeResolutionProbe nativeProbingRoots) { - if(assemblyProbingPaths == null) - { - throw new ArgumentNullException(nameof(assemblyProbingPaths)); - } - if (nativeProbingRoots == null) - { - throw new ArgumentNullException(nameof(nativeProbingRoots)); - } - _assemblyProbingPaths = assemblyProbingPaths; - _nativeProbingRoots = nativeProbingRoots; + _assemblyProbingPaths = assemblyProbingPaths ?? throw new ArgumentNullException(nameof(assemblyProbingPaths)); + _nativeProbingRoots = nativeProbingRoots ?? throw new ArgumentNullException(nameof(nativeProbingRoots)); } - void ISupportNuget.RegisterNugetResolvedPackageReferences(IReadOnlyList resolvedReferences) + void ISupportNuget.RegisterResolvedPackageReferences(IReadOnlyList resolvedReferences) { var references = resolvedReferences .SelectMany(r => r.AssemblyPaths) diff --git a/Microsoft.DotNet.Interactive.CSharp/CSharpKernelExtensions.cs b/Microsoft.DotNet.Interactive.CSharp/CSharpKernelExtensions.cs index d95e6d0655..661ef4a98d 100644 --- a/Microsoft.DotNet.Interactive.CSharp/CSharpKernelExtensions.cs +++ b/Microsoft.DotNet.Interactive.CSharp/CSharpKernelExtensions.cs @@ -1,20 +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; -using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; using System.CommandLine.Parsing; -using System.CommandLine.Rendering; -using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Html; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Formatting; -using static Microsoft.DotNet.Interactive.Formatting.PocketViewTags; namespace Microsoft.DotNet.Interactive.CSharp { diff --git a/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs b/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs index 18104d037c..8e5886d086 100644 --- a/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs +++ b/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs @@ -14,7 +14,6 @@ open System.Threading.Tasks open Microsoft.CodeAnalysis.Tags open Microsoft.DotNet.Interactive -open Microsoft.DotNet.Interactive open Microsoft.DotNet.Interactive.Commands open Microsoft.DotNet.Interactive.Events open Microsoft.DotNet.Interactive.Extensions @@ -26,7 +25,7 @@ open FSharp.Compiler.Scripting open FSharp.Compiler.SourceCodeServices type FSharpKernel() as this = - inherit KernelBase("fsharp") + inherit DotNetLanguageKernel("fsharp") static let lockObj = Object(); @@ -40,14 +39,12 @@ type FSharpKernel() as this = do registerForDisposal(fun () -> script.ValueBound.RemoveHandler valueBoundHandler) script - let extensionLoader: AssemblyBasedExtensionLoader = new AssemblyBasedExtensionLoader() + let extensionLoader: AssemblyBasedExtensionLoader = AssemblyBasedExtensionLoader() let script = lazy createScript this.RegisterForDisposal do base.RegisterForDisposal(fun () -> if script.IsValueCreated then (script.Value :> IDisposable).Dispose()) let mutable cancellationTokenSource = new CancellationTokenSource() - let messageMap = Dictionary() - let getLineAndColumn (text: string) offset = let rec getLineAndColumn' i l c = if i >= offset then l, c @@ -149,10 +146,10 @@ type FSharpKernel() as this = // ISupportNuget.Initialize must be invoked prior to creating the DependencyManager // With non null funcs if isNull _assemblyProbingPaths then - raise (new ArgumentNullException("_assemblyProbingPaths")) + raise (new InvalidOperationException("_assemblyProbingPaths is null")) if isNull _nativeProbingRoots then - raise (new ArgumentNullException("_nativeProbingRoots")) + raise (new InvalidOperationException("_nativeProbingRoots is null")) new DependencyProvider(_assemblyProbingPaths, _nativeProbingRoots) lazy (createDependencyProvider ()) @@ -178,28 +175,31 @@ type FSharpKernel() as this = override _.HandleRequestCompletion(command: RequestCompletion, context: KernelInvocationContext): Task = handleRequestCompletion command context |> Async.StartAsTask :> Task - override this.TryGetVariable(name: string, [] value: Object byref) = + override _.TryGetVariable<'a>(name: string, [] value: 'a byref) = match this.GetCurrentVariable(name) with | Some(cv) -> - value <- cv.Value + value <- cv.Value :?> 'a true | None -> false + override _.SetVariableAsync(name: string, value: Object) : Task = + raise (NotImplementedException()) + interface ISupportNuget with member this.Initialize (assemblyProbingPaths: AssemblyResolutionProbe, nativeProbingRoots: NativeResolutionProbe) = // These may not be set to null, if they are it is a product coding error // ISupportNuget.Initialize must be invoked prior to creating the DependencyManager // With non null funcs if isNull assemblyProbingPaths then - raise (new ArgumentNullException("assemblyProbingPaths")) + raise (ArgumentNullException("assemblyProbingPaths")) if isNull nativeProbingRoots then - raise (new ArgumentNullException("nativeProbingRoots")) + raise (ArgumentNullException("nativeProbingRoots")) _assemblyProbingPaths <- assemblyProbingPaths _nativeProbingRoots <- nativeProbingRoots - member this.RegisterNugetResolvedPackageReferences (packageReferences: IReadOnlyList) = + member this.RegisterResolvedPackageReferences (packageReferences: IReadOnlyList) = // Generate #r and #I from packageReferences let sb = StringBuilder() let hashset = HashSet() diff --git a/Microsoft.DotNet.Interactive.Formatting/DefaultPlainTextFormatterSet.cs b/Microsoft.DotNet.Interactive.Formatting/DefaultPlainTextFormatterSet.cs index 8b1eaf92f3..7489d3f248 100644 --- a/Microsoft.DotNet.Interactive.Formatting/DefaultPlainTextFormatterSet.cs +++ b/Microsoft.DotNet.Interactive.Formatting/DefaultPlainTextFormatterSet.cs @@ -8,6 +8,7 @@ using System.Dynamic; using System.Linq; using System.Text.Encodings.Web; +using Microsoft.DotNet.Interactive.CSharp; namespace Microsoft.DotNet.Interactive.Formatting { @@ -105,29 +106,7 @@ private static ConcurrentDictionary DefaultFormatters() [typeof(Type)] = new PlainTextFormatter((type, writer) => { - var typeName = type.FullName ?? type.Name; - - if (typeName.Contains("`") && !type.IsAnonymous()) - { - writer.Write(typeName.Remove(typeName.IndexOf('`'))); - writer.Write("<"); - var genericArguments = type.GetGenericArguments(); - - for (var i = 0; i < genericArguments.Length; i++) - { - Formatter.FormatTo(genericArguments[i], writer); - if (i < genericArguments.Length - 1) - { - writer.Write(","); - } - } - - writer.Write(">"); - } - else - { - writer.Write(typeName); - } + type.WriteCSharpDeclarationTo(writer); }), [typeof(DateTime)] = new PlainTextFormatter((value, writer) => writer.Write(value.ToString("u"))), diff --git a/Microsoft.DotNet.Interactive.Formatting/Microsoft.DotNet.Interactive.Formatting.csproj b/Microsoft.DotNet.Interactive.Formatting/Microsoft.DotNet.Interactive.Formatting.csproj index c29492413e..924b32781d 100644 --- a/Microsoft.DotNet.Interactive.Formatting/Microsoft.DotNet.Interactive.Formatting.csproj +++ b/Microsoft.DotNet.Interactive.Formatting/Microsoft.DotNet.Interactive.Formatting.csproj @@ -11,10 +11,14 @@ interactive formatting Jupyter mime + + + + - + diff --git a/Microsoft.DotNet.Interactive.Formatting/TypeExtensions.cs b/Microsoft.DotNet.Interactive.Formatting/TypeExtensions.cs index a4e3e108a6..1e62bd10b2 100644 --- a/Microsoft.DotNet.Interactive.Formatting/TypeExtensions.cs +++ b/Microsoft.DotNet.Interactive.Formatting/TypeExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -10,7 +11,7 @@ namespace Microsoft.DotNet.Interactive.Formatting { - internal static class TypeExtensions + internal static partial class TypeExtensions { internal static bool CanBeInstantiated(this Type type) { diff --git a/Microsoft.DotNet.Interactive.PowerShell.Tests/Microsoft.DotNet.Interactive.PowerShell.Tests.csproj b/Microsoft.DotNet.Interactive.PowerShell.Tests/Microsoft.DotNet.Interactive.PowerShell.Tests.csproj index 1664055ffb..e2cf03eeeb 100644 --- a/Microsoft.DotNet.Interactive.PowerShell.Tests/Microsoft.DotNet.Interactive.PowerShell.Tests.csproj +++ b/Microsoft.DotNet.Interactive.PowerShell.Tests/Microsoft.DotNet.Interactive.PowerShell.Tests.csproj @@ -3,6 +3,7 @@ netcoreapp3.1 latest + $(NoWarn);8002;CS8002 false @@ -15,6 +16,7 @@ + diff --git a/Microsoft.DotNet.Interactive.PowerShell.Tests/PowerShellKernelTests.cs b/Microsoft.DotNet.Interactive.PowerShell.Tests/PowerShellKernelTests.cs index e4b1931be7..4f1cbefc41 100644 --- a/Microsoft.DotNet.Interactive.PowerShell.Tests/PowerShellKernelTests.cs +++ b/Microsoft.DotNet.Interactive.PowerShell.Tests/PowerShellKernelTests.cs @@ -6,14 +6,24 @@ using System.Management.Automation; using System.Threading.Tasks; using FluentAssertions; +using System.Linq; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.Events; +using Microsoft.DotNet.Interactive.Tests; +using XPlot.Plotly; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Interactive.PowerShell.Tests { - public class PowerShellKernelTests + public class PowerShellKernelTests : LanguageKernelTestBase { private readonly string _allUsersCurrentHostProfilePath = Path.Combine(Path.GetDirectoryName(typeof(PSObject).Assembly.Location), "Microsoft.dotnet-interactive_profile.ps1"); + public PowerShellKernelTests(ITestOutputHelper output) : base(output) + { + } + [Theory] [InlineData(@"$x = New-Object -TypeName System.IO.FileInfo -ArgumentList c:\temp\some.txt", typeof(FileInfo))] [InlineData("$x = \"hello!\"", typeof(string))] @@ -23,11 +33,127 @@ public async Task TryGetVariable_unwraps_PowerShell_object(string code, Type exp await kernel.SubmitCodeAsync(code); - kernel.TryGetVariable("x", out var fi).Should().BeTrue(); + kernel.TryGetVariable("x", out object fi).Should().BeTrue(); fi.Should().BeOfType(expectedType); } + [Fact] + public async Task PowerShell_progress_sends_updated_display_values() + { + var kernel = CreateKernel(Language.PowerShell); + var command = new SubmitCode(@" +for ($j = 0; $j -le 4; $j += 4 ) { + $p = $j * 25 + Write-Progress -Id 1 -Activity 'Search in Progress' -Status ""$p% Complete"" -PercentComplete $p + Start-Sleep -Milliseconds 300 +} +"); + await kernel.SendAsync(command); + + Assert.Collection(KernelEvents, + e => e.Should().BeOfType(), + e => e.Should().BeOfType(), + e => e.Should().BeOfType().Which + .Value.Should().BeOfType().Which + .Should().Match("* Search in Progress* 0% Complete* [ * ] *"), + e => e.Should().BeOfType().Which + .Value.Should().BeOfType().Which + .Should().Match("* Search in Progress* 100% Complete* [ooo*ooo] *"), + e => e.Should().BeOfType().Which + .Value.Should().BeOfType().Which + .Should().Be(string.Empty), + e => e.Should().BeOfType()); + } + + [Fact] + public void PowerShell_type_accelerators_present() + { + CreateKernel(Language.PowerShell); + + var accelerator = typeof(PSObject).Assembly.GetType("System.Management.Automation.TypeAccelerators"); + dynamic typeAccelerators = accelerator.GetProperty("Get").GetValue(null); + Assert.Equal(typeAccelerators["Graph.Scatter"].FullName, $"{typeof(Graph).FullName}+Scatter"); + Assert.Equal(typeAccelerators["Layout"].FullName, $"{typeof(Layout).FullName}+Layout"); + Assert.Equal(typeAccelerators["Chart"].FullName, typeof(Chart).FullName); + } + + [Fact] + public async Task PowerShell_token_variables_work() + { + var kernel = CreateKernel(Language.PowerShell); + + await kernel.SendAsync(new SubmitCode("echo /this/is/a/path")); + await kernel.SendAsync(new SubmitCode("$$; $^")); + + Assert.Collection(KernelEvents, + e => e.Should() + .BeOfType() + .Which.Code + .Should().Be("echo /this/is/a/path"), + e => e.Should() + .BeOfType() + .Which.Code + .Should().Be("echo /this/is/a/path"), + e => e.Should() + .BeOfType() + .Which.Value.ToString() + .Should().Be("/this/is/a/path" + Environment.NewLine), + e => e.Should().BeOfType(), + e => e.Should() + .BeOfType() + .Which.Code + .Should().Be("$$; $^"), + e => e.Should() + .BeOfType() + .Which.Code + .Should().Be("$$; $^"), + e => e.Should() + .BeOfType() + .Which.Value.ToString() + .Should().Be("/this/is/a/path" + Environment.NewLine), + e => e.Should() + .BeOfType() + .Which.Value.ToString() + .Should().Be("echo" + Environment.NewLine), + e => e.Should().BeOfType()); + } + + [Fact] + public async Task PowerShell_get_history_should_work() + { + var kernel = CreateKernel(Language.PowerShell); + + await kernel.SendAsync(new SubmitCode("Get-Verb > $null")); + await kernel.SendAsync(new SubmitCode("echo bar > $null")); + await kernel.SendAsync(new SubmitCode("Get-History | % CommandLine")); + + var outputs = KernelEvents.OfType(); + outputs.Should().HaveCount(2); + Assert.Collection(outputs, + e => e.Value.As().Should().Be("Get-Verb > $null" + Environment.NewLine), + e => e.Value.As().Should().Be("echo bar > $null" + Environment.NewLine)); + } + + [Fact] + public async Task PowerShell_native_executable_output_is_collected() + { + var kernel = CreateKernel(Language.PowerShell); + + var command = Platform.IsWindows + ? new SubmitCode("ping.exe -n 1 localhost") + : new SubmitCode("ping -c 1 localhost"); + + await kernel.SendAsync(command); + + var outputs = KernelEvents.OfType(); + outputs.Should().HaveCountGreaterThan(1); + outputs.Select(e => e.Value.ToString()) + .First(s => s.Trim().Length > 0) + .ToLowerInvariant() + .Should().Match("*ping*data*"); + } + [Fact] public async Task GetCorrectProfilePaths() { @@ -37,7 +163,7 @@ public async Task GetCorrectProfilePaths() await kernel.SubmitCodeAsync("$currentUserCurrentHost = $PROFILE.CurrentUserCurrentHost"); await kernel.SubmitCodeAsync("$allUsersCurrentHost = $PROFILE.AllUsersCurrentHost"); - kernel.TryGetVariable("currentUserCurrentHost", out var profileObj).Should().BeTrue(); + kernel.TryGetVariable("currentUserCurrentHost", out object profileObj).Should().BeTrue(); profileObj.Should().BeOfType(); string currentUserCurrentHost = profileObj.As(); @@ -73,7 +199,7 @@ public async Task VerifyAllUsersProfileRuns() // trigger first time setup. await kernel.SubmitCodeAsync("Get-Date"); - kernel.TryGetVariable(randomVariableName, out var profileObj).Should().BeTrue(); + kernel.TryGetVariable(randomVariableName, out object profileObj).Should().BeTrue(); profileObj.Should().BeOfType(); profileObj.As().Should().BeTrue(); diff --git a/Microsoft.DotNet.Interactive.PowerShell/Microsoft.DotNet.Interactive.PowerShell.csproj b/Microsoft.DotNet.Interactive.PowerShell/Microsoft.DotNet.Interactive.PowerShell.csproj index b9eb41f12c..f350dcf380 100644 --- a/Microsoft.DotNet.Interactive.PowerShell/Microsoft.DotNet.Interactive.PowerShell.csproj +++ b/Microsoft.DotNet.Interactive.PowerShell/Microsoft.DotNet.Interactive.PowerShell.csproj @@ -33,7 +33,7 @@ PackageCopyToOutput="true" CopyToOutputDirectory="PreserveNewest" /> - + - - +--> diff --git a/Microsoft.DotNet.Interactive.PowerShell/PowerShellKernel.cs b/Microsoft.DotNet.Interactive.PowerShell/PowerShellKernel.cs index d3138d3e5d..094da50fbd 100644 --- a/Microsoft.DotNet.Interactive.PowerShell/PowerShellKernel.cs +++ b/Microsoft.DotNet.Interactive.PowerShell/PowerShellKernel.cs @@ -19,7 +19,8 @@ namespace Microsoft.DotNet.Interactive.PowerShell { using System.Management.Automation; - public class PowerShellKernel : KernelBase + public class PowerShellKernel : + DotNetLanguageKernel { internal const string DefaultKernelName = "powershell"; @@ -112,7 +113,7 @@ private PowerShell CreatePowerShell() return pwsh; } - public override bool TryGetVariable(string name, out object value) + public override bool TryGetVariable(string name, out T value) { var variable = pwsh.Runspace.SessionStateProxy.PSVariable.Get(name); @@ -121,20 +122,26 @@ public override bool TryGetVariable(string name, out object value) switch (variable.Value) { case PSObject psobject: - value = psobject.BaseObject; + value = (T) psobject.BaseObject; break; default: - value = variable.Value; + value = (T) variable.Value; break; } return true; } - value = null; + value = default; return false; } + public override Task SetVariableAsync(string name, object value) + { + _lazyPwsh.Value.Runspace.SessionStateProxy.PSVariable.Set(name, value); + return Task.CompletedTask; + } + protected override async Task HandleSubmitCode( SubmitCode submitCode, KernelInvocationContext context) diff --git a/Microsoft.DotNet.Interactive.Telemetry/Microsoft.DotNet.Interactive.Telemetry.csproj b/Microsoft.DotNet.Interactive.Telemetry/Microsoft.DotNet.Interactive.Telemetry.csproj index fbad516926..b257ff61ca 100644 --- a/Microsoft.DotNet.Interactive.Telemetry/Microsoft.DotNet.Interactive.Telemetry.csproj +++ b/Microsoft.DotNet.Interactive.Telemetry/Microsoft.DotNet.Interactive.Telemetry.csproj @@ -9,6 +9,6 @@ - + diff --git a/Microsoft.DotNet.Interactive.Tests/KernelExtensionsTests.cs b/Microsoft.DotNet.Interactive.Tests/KernelExtensionsTests.cs index 2a41c2deb7..a662a134f0 100644 --- a/Microsoft.DotNet.Interactive.Tests/KernelExtensionsTests.cs +++ b/Microsoft.DotNet.Interactive.Tests/KernelExtensionsTests.cs @@ -5,12 +5,61 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.DotNet.Interactive.Tests.Utility; using Xunit; namespace Microsoft.DotNet.Interactive.Tests { public class KernelExtensionsTests { + [Fact] + public void FindKernel_finds_a_subkernel_of_a_composite_kernel_by_name() + { + var one = new FakeKernel("one"); + var two = new FakeKernel("two"); + using var compositeKernel = new CompositeKernel + { + one, + two, + }; + + var found = compositeKernel.FindKernel("two"); + + found.Should().BeSameAs(two); + } + + [Fact] + public void FindKernel_finds_a_subkernel_of_a_parent_composite_kernel_by_name() + { + var one = new FakeKernel("one"); + var two = new FakeKernel("two"); + using var compositeKernel = new CompositeKernel + { + one, + two, + }; + + var found = one.FindKernel("two"); + + found.Should().BeSameAs(two); + } + + [Fact] + public void FindKernel_returns_null_for_unknown_kernel() + { + var one = new FakeKernel("one"); + var two = new FakeKernel("two"); + using var compositeKernel = new CompositeKernel + { + one, + two, + }; + + var found = compositeKernel.FindKernel("three"); + + found.Should().BeNull(); + } + [Fact] public void VisitSubkernels_does_not_recurse_by_default() { diff --git a/Microsoft.DotNet.Interactive.Tests/LanguageKernelTestBase.cs b/Microsoft.DotNet.Interactive.Tests/LanguageKernelTestBase.cs index e463600407..44279e1101 100644 --- a/Microsoft.DotNet.Interactive.Tests/LanguageKernelTestBase.cs +++ b/Microsoft.DotNet.Interactive.Tests/LanguageKernelTestBase.cs @@ -74,7 +74,7 @@ public void Dispose() _lockReleaser.Dispose(); } - protected KernelBase CreateKernel(Language language) + protected CompositeKernel CreateKernel(Language language) { var kernelBase = language switch { diff --git a/Microsoft.DotNet.Interactive.Tests/LanguageKernelTests.cs b/Microsoft.DotNet.Interactive.Tests/LanguageKernelTests.cs index a187231f2b..bdd716fff4 100644 --- a/Microsoft.DotNet.Interactive.Tests/LanguageKernelTests.cs +++ b/Microsoft.DotNet.Interactive.Tests/LanguageKernelTests.cs @@ -3,23 +3,21 @@ using System; using System.Linq; -using System.Management.Automation; using System.Reactive.Linq; using System.Threading.Tasks; using FluentAssertions; +using FluentAssertions.Execution; using FluentAssertions.Extensions; using Microsoft.DotNet.Interactive.Commands; -using Microsoft.DotNet.Interactive.CSharp; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Tests.Utility; -using XPlot.Plotly; using Xunit; using Xunit.Abstractions; #pragma warning disable 8509 namespace Microsoft.DotNet.Interactive.Tests { - public class LanguageKernelTests : LanguageKernelTestBase + public sealed class LanguageKernelTests : LanguageKernelTestBase { public LanguageKernelTests(ITestOutputHelper output) : base(output) { @@ -730,7 +728,7 @@ public async Task it_returns_completion_list_for_types(Language language, string .Contain(i => i.DisplayText == expectedCompletion); } - [Theory(Timeout = 45000)] + [Theory] [InlineData(Language.CSharp)] [InlineData(Language.FSharp)] [InlineData(Language.PowerShell)] @@ -761,30 +759,7 @@ public async Task it_returns_completion_list_for_previously_declared_items(Langu .Contain(i => i.DisplayText == "alpha"); } - [Fact(Timeout = 45000)] - public async Task Script_state_is_available_within_middleware_pipeline() - { - var variableCountBeforeEvaluation = 0; - var variableCountAfterEvaluation = 0; - - using var kernel = new CSharpKernel(); - - kernel.AddMiddleware(async (command, context, next) => - { - var k = context.HandlingKernel as CSharpKernel; - - await next(command, context); - - variableCountAfterEvaluation = k.ScriptState.Variables.Length; - }); - - await kernel.SendAsync(new SubmitCode("var x = 1;")); - - variableCountBeforeEvaluation.Should().Be(0); - variableCountAfterEvaluation.Should().Be(1); - } - - [Fact(Timeout = 45000)] + [Fact] public async Task When_submission_is_split_then_CommandHandled_is_published_only_for_the_root_command() { var kernel = CreateKernel(Language.CSharp); @@ -802,165 +777,93 @@ public async Task When_submission_is_split_then_CommandHandled_is_published_only .Be(command); } - [Fact()] - public async Task PowerShell_streams_handled_in_correct_order() + [Theory] + [InlineData(Language.CSharp)] + [InlineData(Language.FSharp)] + [InlineData(Language.PowerShell)] + public async Task TryGetVariable_returns_defined_variable(Language language) { - var kernel = CreateKernel(Language.PowerShell); + var codeToSetVariable = language switch + { + Language.CSharp => "var x = 123;", + Language.FSharp => "let x = 123", + Language.PowerShell => "$x = 123" + }; - const string yellow_foreground = "\u001b[93m"; - const string red_foreground = "\u001b[91m"; - const string reset = "\u001b[0m"; + var kernel = CreateKernel(language); - const string warningMessage = "I am a warning message"; - const string verboseMessage = "I am a verbose message"; - const string outputMessage = "I am output"; - const string debugMessage = "I am a debug message"; - const string hostMessage = "I am a message written to host"; - const string errorMessage = "I am a non-terminating error"; + await kernel.SubmitCodeAsync(codeToSetVariable); - var command = new SubmitCode($@" -Write-Warning '{warningMessage}' -Write-Verbose '{verboseMessage}' -Verbose -'{outputMessage}' -Write-Debug '{debugMessage}' -Debug -Write-Host '{hostMessage}' -NoNewline -Write-Error '{errorMessage}' -"); + var languageKernel = kernel.ChildKernels.OfType().Single(); - await kernel.SendAsync(command); + var succeeded = languageKernel.TryGetVariable("x", out int x); - Assert.Collection(KernelEvents, - e => e.Should().BeOfType(), - e => e.Should().BeOfType(), - e => e.Should().BeOfType().Which - .Value.ToString().Should().Contain($"{yellow_foreground}WARNING: {warningMessage}{reset}"), - e => e.Should().BeOfType().Which - .Value.ToString().Should().Contain($"{yellow_foreground}VERBOSE: {verboseMessage}{reset}"), - e => e.Should().BeOfType().Which - .Value.ToString().Should().Be(outputMessage + Environment.NewLine), - e => e.Should().BeOfType().Which - .Value.ToString().Should().Contain($"{yellow_foreground}DEBUG: {debugMessage}{reset}"), - e => e.Should().BeOfType().Which - .Value.ToString().Should().Be(hostMessage), - e => e.Should().BeOfType().Which - .Value.ToString().Should().Contain($"{red_foreground}Write-Error: {red_foreground}{errorMessage}{reset}"), - e => e.Should().BeOfType()); - } - - [Fact()] - public async Task PowerShell_progress_sends_updated_display_values() - { - var kernel = CreateKernel(Language.PowerShell); - var command = new SubmitCode(@" -for ($j = 0; $j -le 4; $j += 4 ) { - $p = $j * 25 - Write-Progress -Id 1 -Activity 'Search in Progress' -Status ""$p% Complete"" -PercentComplete $p - Start-Sleep -Milliseconds 300 -} -"); - await kernel.SendAsync(command); + using var _ = new AssertionScope(); - Assert.Collection(KernelEvents, - e => e.Should().BeOfType(), - e => e.Should().BeOfType(), - e => e.Should().BeOfType().Which - .Value.Should().BeOfType().Which - .Should().Match("* Search in Progress* 0% Complete* [ * ] *"), - e => e.Should().BeOfType().Which - .Value.Should().BeOfType().Which - .Should().Match("* Search in Progress* 100% Complete* [ooo*ooo] *"), - e => e.Should().BeOfType().Which - .Value.Should().BeOfType().Which - .Should().Be(string.Empty), - e => e.Should().BeOfType()); - } - - [Fact()] - public void PowerShell_type_accelerators_present() - { - var kernel = CreateKernel(Language.PowerShell); - - var accelerator = typeof(PSObject).Assembly.GetType("System.Management.Automation.TypeAccelerators"); - dynamic typeAccelerators = accelerator.GetProperty("Get").GetValue(null); - Assert.Equal(typeAccelerators["Graph.Scatter"].FullName, $"{typeof(Graph).FullName}+Scatter"); - Assert.Equal(typeAccelerators["Layout"].FullName, $"{typeof(Layout).FullName}+Layout"); - Assert.Equal(typeAccelerators["Chart"].FullName, typeof(Chart).FullName); - } - - [Fact()] - public async Task PowerShell_token_variables_work() - { - var kernel = CreateKernel(Language.PowerShell); - - await kernel.SendAsync(new SubmitCode("echo /this/is/a/path")); - await kernel.SendAsync(new SubmitCode("$$; $^")); - - Assert.Collection(KernelEvents, - e => e.Should() - .BeOfType() - .Which.Code - .Should().Be("echo /this/is/a/path"), - e => e.Should() - .BeOfType() - .Which.Code - .Should().Be("echo /this/is/a/path"), - e => e.Should() - .BeOfType() - .Which.Value.ToString() - .Should().Be("/this/is/a/path" + Environment.NewLine), - e => e.Should().BeOfType(), - e => e.Should() - .BeOfType() - .Which.Code - .Should().Be("$$; $^"), - e => e.Should() - .BeOfType() - .Which.Code - .Should().Be("$$; $^"), - e => e.Should() - .BeOfType() - .Which.Value.ToString() - .Should().Be("/this/is/a/path" + Environment.NewLine), - e => e.Should() - .BeOfType() - .Which.Value.ToString() - .Should().Be("echo" + Environment.NewLine), - e => e.Should().BeOfType()); - } - - [Fact()] - public async Task PowerShell_get_history_should_work() - { - var kernel = CreateKernel(Language.PowerShell); - - await kernel.SendAsync(new SubmitCode("Get-Verb > $null")); - await kernel.SendAsync(new SubmitCode("echo bar > $null")); - await kernel.SendAsync(new SubmitCode("Get-History | % CommandLine")); - - var outputs = KernelEvents.OfType(); - outputs.Should().HaveCount(2); - Assert.Collection(outputs, - e => e.Value.As().Should().Be("Get-Verb > $null" + Environment.NewLine), - e => e.Value.As().Should().Be("echo bar > $null" + Environment.NewLine)); - } - - [Fact()] - public async Task PowerShell_native_executable_output_is_collected() - { - var kernel = CreateKernel(Language.PowerShell); - - var command = Platform.IsWindows - ? new SubmitCode("ping.exe -n 1 localhost") - : new SubmitCode("ping -c 1 localhost"); + succeeded.Should().BeTrue(); + x.Should().Be(123); + } - await kernel.SendAsync(command); + [Theory] + [InlineData(Language.CSharp)] + [InlineData(Language.FSharp, Skip = "Requires FSI API changes")] + [InlineData(Language.PowerShell)] + public async Task SetVariableAsync_declares_the_specified_variable(Language language) + { + var kernel = CreateKernel(language); + + var languageKernel = kernel.ChildKernels.OfType().Single(); + + await languageKernel.SetVariableAsync("x", 123); + + var succeeded = languageKernel.TryGetVariable("x", out int x); + + using var _ = new AssertionScope(); + + succeeded.Should().BeTrue(); + x.Should().Be(123); + } + + [Theory] + [InlineData(Language.CSharp)] + [InlineData(Language.FSharp, Skip = "Requires FSI API changes")] + [InlineData(Language.PowerShell)] + public async Task SetVariableAsync_overwrites_an_existing_variable_of_the_same_type(Language language) + { + var kernel = CreateKernel(language); + + var languageKernel = kernel.ChildKernels.OfType().Single(); + + await languageKernel.SetVariableAsync("x", 123); + await languageKernel.SetVariableAsync("x", 456); + + var succeeded = languageKernel.TryGetVariable("x", out int x); + + using var _ = new AssertionScope(); + + succeeded.Should().BeTrue(); + x.Should().Be(456); + } + + [Theory] + [InlineData(Language.CSharp)] + [InlineData(Language.FSharp, Skip = "Requires FSI API changes")] + [InlineData(Language.PowerShell)] + public async Task SetVariableAsync_can_redeclare_an_existing_variable_and_change_its_type(Language language) + { + var kernel = CreateKernel(language); + + var languageKernel = kernel.ChildKernels.OfType().Single(); + + await languageKernel.SetVariableAsync("x", 123); + await languageKernel.SetVariableAsync("x", "hello"); + + var succeeded = languageKernel.TryGetVariable("x", out string x); + + using var _ = new AssertionScope(); - var outputs = KernelEvents.OfType(); - outputs.Should().HaveCountGreaterThan(1); - outputs.Select(e => e.Value.ToString()) - .First(s => s.Trim().Length > 0) - .ToLowerInvariant() - .Should().Match("*ping*data*"); + succeeded.Should().BeTrue(); + x.Should().Be("hello"); } } } diff --git a/Microsoft.DotNet.Interactive.Tests/Utility/FakeKernel.cs b/Microsoft.DotNet.Interactive.Tests/Utility/FakeKernel.cs index eef427072b..049d97066a 100644 --- a/Microsoft.DotNet.Interactive.Tests/Utility/FakeKernel.cs +++ b/Microsoft.DotNet.Interactive.Tests/Utility/FakeKernel.cs @@ -16,12 +16,6 @@ public FakeKernel([CallerMemberName] string name = null) : base(name) public KernelCommandInvocation Handle { get; set; } - public override bool TryGetVariable(string name, out object value) - { - value = null; - return false; - } - protected override Task HandleSubmitCode(SubmitCode command, KernelInvocationContext context) { Handle(command, context); diff --git a/Microsoft.DotNet.Interactive.Tests/VariableSharingTests.cs b/Microsoft.DotNet.Interactive.Tests/VariableSharingTests.cs new file mode 100644 index 0000000000..299cd791f6 --- /dev/null +++ b/Microsoft.DotNet.Interactive.Tests/VariableSharingTests.cs @@ -0,0 +1,67 @@ +// 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; +using FluentAssertions; +using Microsoft.DotNet.Interactive.CSharp; +using Microsoft.DotNet.Interactive.Events; +using Microsoft.DotNet.Interactive.FSharp; +using Microsoft.DotNet.Interactive.PowerShell; +using Microsoft.DotNet.Interactive.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.Tests +{ + public class VariableSharingTests + { + [Theory] + [InlineData( + "#!fsharp", + "let x = 123", + "#!csharp", + "(GetKernel(\"fsharp\") as Microsoft.DotNet.Interactive.DotNetLanguageKernel).TryGetVariable(\"x\", out int x);\nx")] + [InlineData( + "#!fsharp", + "let x = 123", + "#!csharp", + "#!share --from fsharp x\nfsharp.TryGetVariable(\"x\", out int x);\nx", Skip = "WIP")] + public async Task Variables_can_be_read_from_other_kernels( + string fromLanguage, + string codeToWrite, + string toLanguage, + string codeToRead) + { + using var kernel = new CompositeKernel + { + new CSharpKernel() + .UseKernelHelpers() + .UseDotNetVariableSharing(), + new FSharpKernel() + .UseKernelHelpers() + .UseDotNetVariableSharing(), + new PowerShellKernel() + .UseDotNetVariableSharing() + }.LogEventsToPocketLogger(); + + using var events = kernel.KernelEvents.ToSubscribedList(); + + await kernel.SubmitCodeAsync($"{fromLanguage}\n{codeToWrite}"); + + await kernel.SubmitCodeAsync($"{toLanguage}\n{codeToRead}"); + + events.Should() + .ContainSingle() + .Which + .Value + .Should() + .Be(123); + } + + [Fact(Skip = "WIP")] + public void Internal_types_are_shared_as_their_most_public_supertype() + { + throw new NotImplementedException("test not written"); + } + } +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive/CompositeKernel.cs b/Microsoft.DotNet.Interactive/CompositeKernel.cs index f8ccb6b22f..1ea4ade58d 100644 --- a/Microsoft.DotNet.Interactive/CompositeKernel.cs +++ b/Microsoft.DotNet.Interactive/CompositeKernel.cs @@ -16,7 +16,7 @@ namespace Microsoft.DotNet.Interactive { public class CompositeKernel : - KernelBase, + KernelBase, IExtensibleKernel, IEnumerable { @@ -44,7 +44,7 @@ public string DefaultKernelName } } - public void Add(IKernel kernel, IEnumerable aliases = null) + public void Add(IKernel kernel, IReadOnlyCollection aliases = null) { if (kernel == null) { @@ -55,13 +55,15 @@ public void Add(IKernel kernel, IEnumerable aliases = null) { if (kernelBase.ParentKernel != null) { - throw new InvalidOperationException("Kernel already has a parent."); + throw new InvalidOperationException($"Kernel \"{kernelBase.Name}\" already has a parent: \"{kernelBase.ParentKernel.Name}\"."); } kernelBase.ParentKernel = this; kernelBase.AddMiddleware(LoadExtensions); } + AddChooseKernelDirective(kernel, aliases); + _childKernels.Add(kernel); if (_childKernels.Count == 1) @@ -69,6 +71,14 @@ public void Add(IKernel kernel, IEnumerable aliases = null) DefaultKernelName = kernel.Name; } + RegisterForDisposal(kernel.KernelEvents.Subscribe(PublishEvent)); + RegisterForDisposal(kernel); + } + + private void AddChooseKernelDirective( + IKernel kernel, + IEnumerable aliases) + { var chooseKernelCommand = new ChooseKernelDirective(kernel); if (aliases is { }) @@ -80,8 +90,6 @@ public void Add(IKernel kernel, IEnumerable aliases = null) } AddDirective(chooseKernelCommand); - RegisterForDisposal(kernel.KernelEvents.Subscribe(PublishEvent)); - RegisterForDisposal(kernel); } private async Task LoadExtensions( @@ -150,12 +158,6 @@ private IKernel GetHandlingKernel( return kernel ?? this; } - public override bool TryGetVariable(string name, out object value) - { - value = null; - return false; - } - internal override async Task HandleAsync( IKernelCommand command, KernelInvocationContext context) diff --git a/Microsoft.DotNet.Interactive/DotNetLanguageKernel.cs b/Microsoft.DotNet.Interactive/DotNetLanguageKernel.cs new file mode 100644 index 0000000000..9ce96bb23c --- /dev/null +++ b/Microsoft.DotNet.Interactive/DotNetLanguageKernel.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.Threading.Tasks; + +namespace Microsoft.DotNet.Interactive +{ + public abstract class DotNetLanguageKernel : KernelBase + { + protected DotNetLanguageKernel(string name) : base(name) + { + } + + public abstract bool TryGetVariable(string name, out T value); + + public abstract Task SetVariableAsync(string name, object value); + } +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive/Extensions/AssemblyBasedExtensionLoader.cs b/Microsoft.DotNet.Interactive/Extensions/AssemblyBasedExtensionLoader.cs index 842cab950e..7f0c1b8baf 100644 --- a/Microsoft.DotNet.Interactive/Extensions/AssemblyBasedExtensionLoader.cs +++ b/Microsoft.DotNet.Interactive/Extensions/AssemblyBasedExtensionLoader.cs @@ -112,6 +112,7 @@ private async Task LoadFromAssembly( if (loadExtensions) { var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFile.FullName); + var extensionTypes = assembly .ExportedTypes .Where(t => t.CanBeInstantiated() && typeof(IKernelExtension).IsAssignableFrom(t)) diff --git a/Microsoft.DotNet.Interactive/HtmlKernel.cs b/Microsoft.DotNet.Interactive/HtmlKernel.cs index 9f3c41df43..4389879faa 100644 --- a/Microsoft.DotNet.Interactive/HtmlKernel.cs +++ b/Microsoft.DotNet.Interactive/HtmlKernel.cs @@ -16,12 +16,6 @@ public HtmlKernel() : base(DefaultKernelName) { } - public override bool TryGetVariable(string name, out object value) - { - value = default; - return false; - } - protected override async Task HandleSubmitCode(SubmitCode command, KernelInvocationContext context) { await context.DisplayAsync( diff --git a/Microsoft.DotNet.Interactive/ISupportNuget.cs b/Microsoft.DotNet.Interactive/ISupportNuget.cs index 8f07e04b0f..dce0d596a8 100644 --- a/Microsoft.DotNet.Interactive/ISupportNuget.cs +++ b/Microsoft.DotNet.Interactive/ISupportNuget.cs @@ -12,12 +12,12 @@ public interface ISupportNuget // Set assemblyProbingPaths, nativeProbingRoots for Kernel. // These values are functions that return the list of discovered assemblies, and package roots // They are used by the dependecymanager for Assembly and Native dll resolving - public abstract void Initialize(AssemblyResolutionProbe assemblyProbingPaths, NativeResolutionProbe nativeProbingRoots); + public void Initialize(AssemblyResolutionProbe assemblyProbingPaths, NativeResolutionProbe nativeProbingRoots); - public abstract void RegisterNugetResolvedPackageReferences(IReadOnlyList packageReferences); + public void RegisterResolvedPackageReferences(IReadOnlyList packageReferences); // Summary: // Resolve reference for a list of package manager lines - public abstract IResolveDependenciesResult Resolve(IEnumerable packageManagerTextLines, string executionTfm, ResolvingErrorReport reportError); + public IResolveDependenciesResult Resolve(IEnumerable packageManagerTextLines, string executionTfm, ResolvingErrorReport reportError); } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive/JavaScriptKernel.cs b/Microsoft.DotNet.Interactive/JavaScriptKernel.cs index 8c6738187f..9d61d024b0 100644 --- a/Microsoft.DotNet.Interactive/JavaScriptKernel.cs +++ b/Microsoft.DotNet.Interactive/JavaScriptKernel.cs @@ -14,13 +14,6 @@ public class JavaScriptKernel : KernelBase public JavaScriptKernel() : base(DefaultKernelName) { } - - public override bool TryGetVariable(string name, out object value) - { - value = default; - return false; - } - protected override async Task HandleSubmitCode( SubmitCode command, KernelInvocationContext context) diff --git a/Microsoft.DotNet.Interactive/Kernel.cs b/Microsoft.DotNet.Interactive/Kernel.cs index 51bdabf9f6..22a83707d6 100644 --- a/Microsoft.DotNet.Interactive/Kernel.cs +++ b/Microsoft.DotNet.Interactive/Kernel.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.Threading.Tasks; using Microsoft.AspNetCore.Html; using Microsoft.DotNet.Interactive.Commands; @@ -42,5 +44,13 @@ public static void Javascript( kernel.SendAsync(new DisplayValue(value, formatted))) .Wait(); } + + public static IKernel GetKernel(string name) + { + var kernel = KernelInvocationContext.Current.HandlingKernel; + + return kernel.FindKernel(name) ?? + throw new KeyNotFoundException($"Kernel \"{name}\" was not found."); + } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive/KernelBase.cs b/Microsoft.DotNet.Interactive/KernelBase.cs index 5c582e1225..851670e35a 100644 --- a/Microsoft.DotNet.Interactive/KernelBase.cs +++ b/Microsoft.DotNet.Interactive/KernelBase.cs @@ -193,8 +193,6 @@ public FrontendEnvironment FrontendEnvironment public IReadOnlyCollection Directives => SubmissionParser.Directives; - public abstract bool TryGetVariable(string name, out object value); - public void AddDirective(Command command) => SubmissionParser.AddDirective(command); public virtual Task LspMethod(string methodName, JObject request) diff --git a/Microsoft.DotNet.Interactive/KernelExtensions.cs b/Microsoft.DotNet.Interactive/KernelExtensions.cs index 782ca7c59b..09a4e53541 100644 --- a/Microsoft.DotNet.Interactive/KernelExtensions.cs +++ b/Microsoft.DotNet.Interactive/KernelExtensions.cs @@ -6,11 +6,13 @@ using System.CommandLine.Invocation; using System.CommandLine.Rendering; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Formatting; +using Microsoft.DotNet.Interactive.Utility; using Pocket; using static System.CommandLine.Rendering.Ansi.Color; using CompositeDisposable = System.Reactive.Disposables.CompositeDisposable; @@ -21,6 +23,25 @@ public static class KernelExtensions { private static readonly TextSpanFormatter _textSpanFormatter = new TextSpanFormatter(); + public static IKernel FindKernel(this IKernel kernelBase, string name) + { + var root = kernelBase + .RecurseWhileNotNull(k => k switch + { + KernelBase kb => kb.ParentKernel, + _ => null + }) + .LastOrDefault(); + + return root switch + { + CompositeKernel c => c.ChildKernels + .SingleOrDefault(k => k.Name == name), + IKernel k when k.Name == name => k, + _ => null + }; + } + public static Task SendAsync( this IKernel kernel, IKernelCommand command) @@ -50,7 +71,6 @@ public static T UseLog(this T kernel) { var command = new Command("#!log", "Enables session logging."); - var logStarted = false; command.Handler = CommandHandler.Create(async context => @@ -102,6 +122,31 @@ public static T UseLog(this T kernel) return kernel; } + public static T UseDotNetVariableSharing(this T kernel) + where T : DotNetLanguageKernel + { + var share = new Command("#!share", "Share a .NET object between subkernels") + { + new Option("--from"), + new Argument("name") + }; + + share.Handler = CommandHandler.Create(async (from, name, context) => + { + if (kernel.FindKernel(from) is DotNetLanguageKernel fromKernel) + { + if (fromKernel.TryGetVariable(name, out object shared)) + { + await kernel.SetVariableAsync(name, shared); + } + } + }); + + kernel.AddDirective(share); + + return kernel; + } + internal static Task DisplayAnsi( this KernelInvocationContext context, FormattableString message) => diff --git a/Microsoft.DotNet.Interactive/KernelSupportsNugetExtensions.cs b/Microsoft.DotNet.Interactive/KernelSupportsNugetExtensions.cs index 232be7857e..a1d4f3d24c 100644 --- a/Microsoft.DotNet.Interactive/KernelSupportsNugetExtensions.cs +++ b/Microsoft.DotNet.Interactive/KernelSupportsNugetExtensions.cs @@ -212,7 +212,7 @@ internal static KernelCommandInvocation DoNugetRestore( if (result.Succeeded) { - (kernel as ISupportNuget)?.RegisterNugetResolvedPackageReferences(result.ResolvedReferences); + (kernel as ISupportNuget)?.RegisterResolvedPackageReferences(result.ResolvedReferences); foreach (var resolvedReference in result.ResolvedReferences) { diff --git a/Microsoft.DotNet.Interactive/Microsoft.DotNet.Interactive.csproj b/Microsoft.DotNet.Interactive/Microsoft.DotNet.Interactive.csproj index 54eb0ffcdc..78f14163a8 100644 --- a/Microsoft.DotNet.Interactive/Microsoft.DotNet.Interactive.csproj +++ b/Microsoft.DotNet.Interactive/Microsoft.DotNet.Interactive.csproj @@ -18,7 +18,7 @@ 2.9.6 4.3.0 4.3.2 - 2.0.0-beta1.20214.1 + 2.0.0-beta1.20219.3 diff --git a/Microsoft.DotNet.Interactive/TypeExtensions.cs b/Microsoft.DotNet.Interactive/TypeExtensions.cs index b67eaa1a90..5cda4ae103 100644 --- a/Microsoft.DotNet.Interactive/TypeExtensions.cs +++ b/Microsoft.DotNet.Interactive/TypeExtensions.cs @@ -14,4 +14,4 @@ internal static bool CanBeInstantiated(this Type type) && !type.IsInterface; } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive/Utility/EnumerableExtensions.cs b/Microsoft.DotNet.Interactive/Utility/EnumerableExtensions.cs index 3e0fd7f79c..628b087f6e 100644 --- a/Microsoft.DotNet.Interactive/Utility/EnumerableExtensions.cs +++ b/Microsoft.DotNet.Interactive/Utility/EnumerableExtensions.cs @@ -31,7 +31,7 @@ internal static IEnumerable FlattenBreadthFirst( yield return current; } } - + internal static IEnumerable FlattenDepthFirst( this IEnumerable source, Func> children) @@ -55,5 +55,18 @@ internal static IEnumerable FlattenDepthFirst( yield return current; } } + + internal static IEnumerable RecurseWhileNotNull( + this T source, + Func next) + where T : class + { + yield return source; + + while ((source = next(source)) != null) + { + yield return source; + } + } } } \ No newline at end of file diff --git a/NotebookExamples/Extensions.ipynb b/NotebookExamples/Extensions.ipynb index 80a2c9deb9..cf4893eede 100644 --- a/NotebookExamples/Extensions.ipynb +++ b/NotebookExamples/Extensions.ipynb @@ -2,9 +2,133 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\r\n", + "
\r\n", + " \r\n", + " \r\n", + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "What is the path to your dotnet/interactive repo?: c:\\dev\\interactive\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + " Directory: C:\\dev\\interactive\\samples\\extensions\\ClockExtension\n", + "\n", + "Mode LastWriteTime Length Name\n", + "---- ------------- ------ ----\n", + "-a--- 2/27/2020 12:25 PM 902 ClockExtension.csproj\n", + "-a--- 2/28/2020 4:53 PM 2191 ClockKernelExtension.cs\n", + "-a--- 2/23/2020 12:59 PM 5172 SvgClock.cs\n", + "\n" + ] + } + ], "source": [ "#!pwsh\n", "\n", @@ -15,9 +139,56 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Microsoft (R) Build Engine version 16.6.0-preview-20162-03+00781ad13 for .NET Core\n", + "Copyright (C) Microsoft Corporation. All rights reserved.\n", + "\n", + " Determining projects to restore...\n", + " Restored C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\ClockExtension.csproj (in 1.74 sec).\n", + " You are using a preview version of .NET Core. See: https://aka.ms/dotnet-core-preview\n", + " ClockExtension -> C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\bin\\Debug\\netcoreapp3.1\\ClockExtension.dll\n", + "\n", + "Build succeeded.\n", + " 0 Warning(s)\n", + " 0 Error(s)\n", + "\n", + "Time Elapsed 00:00:05.49\n", + "Microsoft (R) Build Engine version 16.6.0-preview-20162-03+00781ad13 for .NET Core\n", + "Copyright (C) Microsoft Corporation. All rights reserved.\n", + "\n", + " Determining projects to restore...\n", + " Restored C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\ClockExtension.csproj (in 455 ms).\n", + " You are using a preview version of .NET Core. See: https://aka.ms/dotnet-core-preview\n", + " ClockExtension -> C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\bin\\Debug\\netcoreapp3.1\\ClockExtension.dll\n", + " Successfully created package 'C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\bin\\Debug\\ClockExtension.1.2.8.nupkg'.\n", + "C:\\Program Files\\dotnet\\sdk\\3.1.300-preview-015095\\Sdks\\NuGet.Build.Tasks.Pack\\build\\NuGet.Build.Tasks.Pack.targets(198,5): warning NU5100: The assembly 'interactive-extensions\\dotnet\\ClockExtension.dll' is not inside the 'lib' folder and hence it won't be added as a reference when the package is installed into a project. Move it into the 'lib' folder if it needs to be referenced. [C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\ClockExtension.csproj]\n", + "C:\\Program Files\\dotnet\\sdk\\3.1.300-preview-015095\\Sdks\\NuGet.Build.Tasks.Pack\\build\\NuGet.Build.Tasks.Pack.targets(198,5): warning NU5104: A stable release of a package should not have a prerelease dependency. Either modify the version spec of dependency \"microsoft.dotnet.interactive [1.0.0-beta.20111.6, )\" or update the version field in the nuspec. [C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\ClockExtension.csproj]\n", + "\n", + "\n", + " Directory: C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\bin\\Debug\n", + "\n", + "Mode LastWriteTime Length Name\n", + "---- ------------- ------ ----\n", + "-a--- 4/21/2020 9:51 AM 17200 ClockExtension.1.2.8.nupkg\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "Wall time: 10121.251ms" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "#!pwsh\n", "#!time\n", @@ -28,9 +199,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
Restore sources
  • C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\bin\\Debug\\
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Installed package clockextension version 1.2.8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Loaded kernel extension \"ClockKernelExtension\" from assembly C:\\Users\\josequ\\.nuget\\packages\\clockextension\\1.2.8\\interactive-extensions\\dotnet\\ClockExtension.dll" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "`ClockExtension` is loaded. It adds visualizations for `System.DateTime` and `System.DateTimeOffset`. Try it by running: `display(DateTime.Now);` or `#!clock -h`" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "#i nuget:C:\\dev\\interactive\\samples\\extensions\\ClockExtension\\bin\\Debug\\\n", "#r nuget:clockextension" @@ -38,27 +246,253 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
.NET Interactive
" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "DateTime.UtcNow" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "#!clock:\n", + " Displays a clock showing the current or specified time.\n", + "\n", + "Usage:\n", + " #!clock [options]\n", + "\n", + "Options:\n", + " --hour, -o The position of the hour hand\n", + " -m, --minute The position of the minute hand\n", + " -s, --second The position of the second hand\n", + " -?, -h, --help Show help and usage information\n", + "\n" + ] + } + ], "source": [ "#!clock -h" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
.NET Interactive
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "#!clock --hour 1 -m 2 -s 3" ] diff --git a/dotnet-interactive.Tests/MagicCommandTests.about.cs b/dotnet-interactive.Tests/MagicCommandTests.about.cs index 249c5388b8..ad56b23cfb 100644 --- a/dotnet-interactive.Tests/MagicCommandTests.about.cs +++ b/dotnet-interactive.Tests/MagicCommandTests.about.cs @@ -1,12 +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.CommandLine; -using System.CommandLine.IO; using System.Threading.Tasks; using FluentAssertions; using Microsoft.DotNet.Interactive.Events; -using Microsoft.DotNet.Interactive.Tests; using Microsoft.DotNet.Interactive.Tests.Utility; using Xunit; @@ -38,7 +35,7 @@ public async Task it_shows_the_product_name_and_version_information() .Should() .ContainAll( ".NET Interactive", - "Version", + "Version", "https://github.com/dotnet/interactive"); } } diff --git a/dotnet-interactive/HttpRouting/VariableRouter.cs b/dotnet-interactive/HttpRouting/VariableRouter.cs index bf808eeea5..3aac6ac69f 100644 --- a/dotnet-interactive/HttpRouting/VariableRouter.cs +++ b/dotnet-interactive/HttpRouting/VariableRouter.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.DotNet.Interactive.Formatting; using Newtonsoft.Json; @@ -44,10 +43,10 @@ private async Task BatchVariableRequest(RouteContext context) { var segments = context.HttpContext - .Request - .Path - .Value - .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + .Request + .Path + .Value + .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (segments.Length == 1 && segments[0] == "variables") { @@ -61,11 +60,11 @@ private async Task BatchVariableRequest(RouteContext context) var propertyBag = new JObject(); response[kernelName] = propertyBag; var targetKernel = GetKernel(kernelName); - if (targetKernel is KernelBase kernelBase) + if (targetKernel is DotNetLanguageKernel languageKernel) { foreach (var variableName in kernelProperty.Value.Values()) { - if (kernelBase.TryGetVariable(variableName, out var value)) + if (languageKernel.TryGetVariable(variableName, out object value)) { if (value is string) { @@ -118,10 +117,10 @@ private void SingleVariableRequest(RouteContext context) { var segments = context.HttpContext - .Request - .Path - .Value - .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + .Request + .Path + .Value + .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (segments[0] == "variables") { @@ -130,15 +129,14 @@ private void SingleVariableRequest(RouteContext context) var targetKernel = GetKernel(kernelName); - if (targetKernel is KernelBase kernelBase) + if (targetKernel is DotNetLanguageKernel languageKernel) { - if (kernelBase.TryGetVariable(variableName, out var value)) + if (languageKernel.TryGetVariable(variableName, out object value)) { context.Handler = async httpContext => { await using (var writer = new StreamWriter(httpContext.Response.Body)) { - httpContext.Response.ContentType = JsonFormatter.MimeType; if (value is string) { diff --git a/dotnet-interactive/KernelExtensions.cs b/dotnet-interactive/KernelExtensions.cs index 65d3ece40a..277226dcb2 100644 --- a/dotnet-interactive/KernelExtensions.cs +++ b/dotnet-interactive/KernelExtensions.cs @@ -6,7 +6,6 @@ using System.CommandLine.Invocation; using Microsoft.DotNet.Interactive.App.CommandLine; using Microsoft.DotNet.Interactive.Commands; -using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Formatting; using Recipes; using XPlot.DotNet.Interactive.KernelExtensions; diff --git a/dotnet-interactive/dotnet-interactive.csproj b/dotnet-interactive/dotnet-interactive.csproj index fda3ab30ce..0923e92678 100644 --- a/dotnet-interactive/dotnet-interactive.csproj +++ b/dotnet-interactive/dotnet-interactive.csproj @@ -68,7 +68,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all